Rule Template installation fixes (#4591)

* Fix file based rule templates
* Add YAML Template parser
* Refactor marketplace rule template parsing
* Prevent file system access for WatchService DELETE events

Trying to check if deleted files are hidden, are readable or are directories will result in IOExceptions on many file systems, so that no action will be taken for deletions.

Signed-off-by: Arne Seime <arne.seime@gmail.com>
pull/4561/head
Arne Seime 2025-02-15 17:28:46 +01:00 committed by GitHub
parent 3018a8b0f1
commit fe9af132aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 484 additions and 96 deletions

View File

@ -19,14 +19,20 @@ import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.Module;
import org.openhab.core.automation.dto.RuleTemplateDTO;
import org.openhab.core.automation.dto.RuleTemplateDTOMapper;
import org.openhab.core.automation.parser.Parser;
import org.openhab.core.automation.parser.ParsingException;
import org.openhab.core.automation.parser.ParsingNestedException;
import org.openhab.core.automation.parser.ValidationException;
import org.openhab.core.automation.parser.ValidationException.ObjectType;
import org.openhab.core.automation.template.RuleTemplate;
import org.openhab.core.automation.template.RuleTemplateProvider;
import org.openhab.core.common.registry.AbstractManagedProvider;
@ -34,8 +40,8 @@ import org.openhab.core.storage.StorageService;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
@ -46,27 +52,48 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
*
* @author Kai Kreuzer - Initial contribution and API
* @author Yannick Schaus - refactoring
*
* @author Arne Seime - refactored rule template parsing
*/
@NonNullByDefault
@Component(service = { MarketplaceRuleTemplateProvider.class, RuleTemplateProvider.class })
public class MarketplaceRuleTemplateProvider extends AbstractManagedProvider<RuleTemplate, String, RuleTemplateDTO>
implements RuleTemplateProvider {
private final Logger logger = LoggerFactory.getLogger(MarketplaceRuleTemplateProvider.class);
private final Parser<RuleTemplate> parser;
private final Map<String, Parser<RuleTemplate>> parsers = new ConcurrentHashMap<>();
ObjectMapper yamlMapper;
@Activate
public MarketplaceRuleTemplateProvider(final @Reference StorageService storageService,
final @Reference(target = "(&(format=json)(parser.type=parser.template))") Parser<RuleTemplate> parser) {
public MarketplaceRuleTemplateProvider(final @Reference StorageService storageService) {
super(storageService);
this.parser = parser;
this.yamlMapper = new ObjectMapper(new YAMLFactory());
yamlMapper.findAndRegisterModules();
}
/**
* Registers a {@link Parser}.
*
* @param parser the {@link Parser} service to register.
* @param properties the properties.
*/
@Reference(cardinality = ReferenceCardinality.AT_LEAST_ONE, policy = ReferencePolicy.DYNAMIC, target = "(parser.type=parser.template)")
public void addParser(Parser<RuleTemplate> parser, Map<String, String> properties) {
String parserType = properties.get(Parser.FORMAT);
parserType = parserType == null ? Parser.FORMAT_JSON : parserType;
parsers.put(parserType, parser);
}
/**
* Unregisters a {@link Parser}.
*
* @param parser the {@link Parser} service to unregister.
* @param properties the properties.
*/
public void removeParser(Parser<RuleTemplate> parser, Map<String, String> properties) {
String parserType = properties.get(Parser.FORMAT);
parserType = parserType == null ? Parser.FORMAT_JSON : parserType;
parsers.remove(parserType);
}
@Override
public @Nullable RuleTemplate getTemplate(String uid, @Nullable Locale locale) {
return get(uid);
@ -98,55 +125,83 @@ public class MarketplaceRuleTemplateProvider extends AbstractManagedProvider<Rul
}
/**
* This adds a new rule template to the persistent storage from its JSON representation.
* Adds a new rule template to persistent storage from its {@code JSON} representation.
*
* @param uid the UID to be used for the template
* @param json the template content as a JSON string
*
* @throws ParsingException if the content cannot be parsed correctly
* @param uid the marketplace UID to use.
* @param json the template content as a {@code JSON} string
* @throws ParsingException If the parsing fails.
* @throws ValidationException If the validation fails.
*/
public void addTemplateAsJSON(String uid, String json) throws ParsingException {
public void addTemplateAsJSON(String uid, String json) throws ParsingException, ValidationException {
addTemplate(uid, json, Parser.FORMAT_JSON);
}
/**
* Adds a new rule template to persistent storage from its {@code YAML} representation.
*
* @param uid the marketplace UID to use.
* @param yaml the template content as a {@code YAML} string
* @throws ParsingException If the parsing fails.
* @throws ValidationException If the validation fails.
*/
public void addTemplateAsYAML(String uid, String yaml) throws ParsingException, ValidationException {
addTemplate(uid, yaml, Parser.FORMAT_YAML);
}
/**
* Adds one or ore new {@link RuleTemplate}s parsed from the provided content using the specified parser.
*
* @param uid the marketplace UID to use.
* @param content the content to parse.
* @param format the format to parse.
* @throws ParsingException If the parsing fails.
* @throws ValidationException If the validation fails.
*/
protected void addTemplate(String uid, String content, String format) throws ParsingException, ValidationException {
Parser<RuleTemplate> parser = parsers.get(format);
// The parser might not have been registered yet
if (parser == null) {
throw new ParsingException(new ParsingNestedException(ParsingNestedException.TEMPLATE,
"No " + format.toUpperCase(Locale.ROOT) + " parser available", null));
}
try (InputStreamReader isr = new InputStreamReader(
new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)))) {
new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)))) {
Set<RuleTemplate> templates = parser.parse(isr);
if (templates.size() != 1) {
throw new IllegalArgumentException("JSON must contain exactly one template!");
} else {
RuleTemplate entry = templates.iterator().next();
// add a tag with the add-on ID to be able to identify the widget in the registry
entry.getTags().add(uid);
RuleTemplate template = new RuleTemplate(entry.getUID(), entry.getLabel(), entry.getDescription(),
entry.getTags(), entry.getTriggers(), entry.getConditions(), entry.getActions(),
entry.getConfigurationDescriptions(), entry.getVisibility());
add(template);
// Add a tag with the marketplace add-on ID to be able to identify the template in the registry
Set<String> tags;
for (RuleTemplate template : templates) {
validateTemplate(template);
tags = new HashSet<String>(template.getTags());
tags.add(uid);
add(new RuleTemplate(template.getUID(), template.getLabel(), template.getDescription(), tags,
template.getTriggers(), template.getConditions(), template.getActions(),
template.getConfigurationDescriptions(), template.getVisibility()));
}
} catch (IOException e) {
logger.error("Cannot close input stream.", e);
// Impossible for ByteArrayInputStream
}
}
/**
* This adds a new rule template to the persistent storage from its YAML representation.
* Validates that the parsed template is valid.
*
* @param uid the UID to be used for the template
* @param yaml the template content as a YAML string
*
* @throws ParsingException if the content cannot be parsed correctly
* @param template the {@link RuleTemplate} to validate.
* @throws ValidationException If the validation failed.
*/
public void addTemplateAsYAML(String uid, String yaml) throws ParsingException {
try {
RuleTemplateDTO dto = yamlMapper.readValue(yaml, RuleTemplateDTO.class);
// add a tag with the add-on ID to be able to identify the widget in the registry
dto.tags = new HashSet<@Nullable String>((dto.tags != null) ? dto.tags : new HashSet<>());
dto.tags.add(uid);
RuleTemplate entry = RuleTemplateDTOMapper.map(dto);
RuleTemplate template = new RuleTemplate(entry.getUID(), entry.getLabel(), entry.getDescription(),
entry.getTags(), entry.getTriggers(), entry.getConditions(), entry.getActions(),
entry.getConfigurationDescriptions(), entry.getVisibility());
add(template);
} catch (IOException e) {
logger.error("Unable to parse YAML: {}", e.getMessage());
throw new IllegalArgumentException("Unable to parse YAML");
@SuppressWarnings("null")
protected void validateTemplate(RuleTemplate template) throws ValidationException {
String s;
if ((s = template.getUID()) == null || s.isBlank()) {
throw new ValidationException(ObjectType.TEMPLATE, null, "UID cannot be blank");
}
if ((s = template.getLabel()) == null || s.isBlank()) {
throw new ValidationException(ObjectType.TEMPLATE, template.getUID(), "Label cannot be blank");
}
if (template.getModules(Module.class).isEmpty()) {
throw new ValidationException(ObjectType.TEMPLATE, template.getUID(), "There must be at least one module");
}
}
}

View File

@ -85,10 +85,10 @@ public class CommunityRuleTemplateAddonHandler implements MarketplaceAddonHandle
}
} catch (IOException e) {
logger.error("Rule template from marketplace cannot be downloaded: {}", e.getMessage());
throw new MarketplaceHandlerException("Template cannot be downloaded.", e);
throw new MarketplaceHandlerException("Rule template cannot be downloaded", e);
} catch (Exception e) {
logger.error("Rule template from marketplace is invalid: {}", e.getMessage());
throw new MarketplaceHandlerException("Template is not valid.", e);
logger.error("Failed to add rule template from the marketplace: {}", e.getMessage());
throw new MarketplaceHandlerException("Rule template is invalid", e);
}
}

View File

@ -80,7 +80,7 @@ public abstract class AbstractScriptDependencyTracker
@Override
public void processWatchEvent(WatchService.Kind kind, Path path) {
File file = libraryPath.resolve(path).toFile();
if (!file.isHidden() && (kind == DELETE || (file.canRead() && (kind == CREATE || kind == MODIFY)))) {
if (kind == DELETE || (!file.isHidden() && file.canRead() && (kind == CREATE || kind == MODIFY))) {
dependencyChanged(file.toString());
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.automation.internal.parser.jackson;
import java.io.OutputStreamWriter;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.automation.parser.Parser;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
/**
* Abstract class that can be used by YAML parsers for the different entity types.
*
* @param <T> the type of the entities to parse
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public abstract class AbstractJacksonYAMLParser<T> implements Parser<T> {
/** The YAML object mapper instance */
protected static final ObjectMapper yamlMapper;
static {
yamlMapper = new ObjectMapper(new YAMLFactory());
yamlMapper.findAndRegisterModules();
}
@Override
public void serialize(Set<T> dataObjects, OutputStreamWriter writer) throws Exception {
for (T dataObject : dataObjects) {
yamlMapper.writeValue(writer, dataObject);
}
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.automation.internal.parser.jackson;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.automation.dto.RuleTemplateDTO;
import org.openhab.core.automation.dto.RuleTemplateDTOMapper;
import org.openhab.core.automation.parser.Parser;
import org.openhab.core.automation.parser.ParsingException;
import org.openhab.core.automation.parser.ParsingNestedException;
import org.openhab.core.automation.template.Template;
import org.osgi.service.component.annotations.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
/**
* This class can parse and serialize sets of {@link Template}s.
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
@Component(immediate = true, service = Parser.class, property = { "parser.type=parser.template", "format=yaml" })
public class TemplateYAMLParser extends AbstractJacksonYAMLParser<Template> {
@Override
public Set<Template> parse(InputStreamReader reader) throws ParsingException {
try {
Set<Template> templates = new HashSet<>();
JsonNode rootNode = yamlMapper.readTree(reader);
if (rootNode.isArray()) {
List<RuleTemplateDTO> templateDtos = yamlMapper.convertValue(rootNode,
new TypeReference<List<RuleTemplateDTO>>() {
});
for (RuleTemplateDTO templateDTO : templateDtos) {
templates.add(RuleTemplateDTOMapper.map(templateDTO));
}
} else {
RuleTemplateDTO templateDto = yamlMapper.convertValue(rootNode, new TypeReference<RuleTemplateDTO>() {
});
templates.add(RuleTemplateDTOMapper.map(templateDto));
}
return templates;
} catch (Exception e) {
throw new ParsingException(new ParsingNestedException(ParsingNestedException.TEMPLATE, null, e));
} finally {
try {
reader.close();
} catch (IOException e) {
}
}
}
}

View File

@ -33,6 +33,7 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.OpenHAB;
import org.openhab.core.automation.parser.Parser;
import org.openhab.core.automation.parser.ParsingException;
import org.openhab.core.automation.parser.ValidationException;
import org.openhab.core.automation.template.Template;
import org.openhab.core.automation.template.TemplateProvider;
import org.openhab.core.automation.type.ModuleType;
@ -50,6 +51,7 @@ import org.slf4j.LoggerFactory;
* {@link ProviderChangeListener}s for adding, updating and removing the {@link ModuleType}s or {@link Template}s.
*
* @author Ana Dimova - Initial contribution
* @author Arne Seime - Added object validation support
*/
@NonNullByDefault
public abstract class AbstractFileProvider<@NonNull E> implements Provider<E> {
@ -260,19 +262,42 @@ public abstract class AbstractFileProvider<@NonNull E> implements Provider<E> {
}
}
@SuppressWarnings("null")
protected void updateProvidedObjectsHolder(URL url, Set<E> providedObjects) {
if (providedObjects != null && !providedObjects.isEmpty()) {
List<String> uids = new ArrayList<>();
for (E providedObject : providedObjects) {
try {
validateObject(providedObject);
} catch (ValidationException e) {
logger.warn("Rejecting \"{}\" because the validation failed: {}", url, e.getMessage());
logger.trace("", e);
continue;
}
String uid = getUID(providedObject);
if (providerPortfolio.entrySet().stream().filter(e -> !url.equals(e.getKey()))
.flatMap(e -> e.getValue().stream()).anyMatch(u -> uid.equals(u))) {
logger.warn("Rejecting \"{}\" from \"{}\" because the UID is already registered", uid, url);
continue;
}
uids.add(uid);
final @Nullable E oldProvidedObject = providedObjectsHolder.put(uid, providedObject);
notifyListeners(oldProvidedObject, providedObject);
}
providerPortfolio.put(url, uids);
if (!uids.isEmpty()) {
providerPortfolio.put(url, uids);
}
}
}
/**
* Validates that the parsed object is valid. For no validation, create an empty method.
*
* @param object the object to validate.
* @throws ValidationException If the validation failed.
*/
protected abstract void validateObject(E object) throws ValidationException;
protected void removeElements(@Nullable List<String> objectsForRemove) {
if (objectsForRemove != null) {
for (String removedObject : objectsForRemove) {

View File

@ -12,11 +12,14 @@
*/
package org.openhab.core.automation.internal.provider.file;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.service.WatchService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class is an implementation of {@link WatchService.WatchEventListener} which is responsible for tracking file
@ -25,6 +28,7 @@ import org.openhab.core.service.WatchService;
* It provides functionality for tracking {@link #watchingDir} changes to import or remove the automation objects.
*
* @author Ana Dimova - Initial contribution
* @author Arne Seime - Fixed watch event handling
*/
@SuppressWarnings("rawtypes")
@NonNullByDefault
@ -33,6 +37,7 @@ public class AutomationWatchService implements WatchService.WatchEventListener {
private final WatchService watchService;
private final Path watchingDir;
private AbstractFileProvider provider;
private final Logger logger = LoggerFactory.getLogger(AutomationWatchService.class);
public AutomationWatchService(AbstractFileProvider provider, WatchService watchService, String watchingDir) {
this.watchService = watchService;
@ -54,13 +59,17 @@ public class AutomationWatchService implements WatchService.WatchEventListener {
@Override
public void processWatchEvent(WatchService.Kind kind, Path path) {
File file = path.toFile();
if (!file.isHidden()) {
Path fullPath = watchingDir.resolve(path);
try {
if (kind == WatchService.Kind.DELETE) {
provider.removeResources(file);
} else if (file.canRead()) {
provider.importResources(file);
provider.removeResources(fullPath.toFile());
} else if (!Files.isHidden(fullPath)
&& (kind == WatchService.Kind.CREATE || kind == WatchService.Kind.MODIFY)) {
provider.importResources(fullPath.toFile());
}
} catch (IOException e) {
logger.error("Failed to process automation watch event {} for \"{}\": {}", kind, fullPath, e.getMessage());
logger.trace("", e);
}
}
}

View File

@ -16,6 +16,8 @@ import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.automation.parser.Parser;
import org.openhab.core.automation.parser.ValidationException;
import org.openhab.core.automation.parser.ValidationException.ObjectType;
import org.openhab.core.automation.type.ModuleType;
import org.openhab.core.automation.type.ModuleTypeProvider;
import org.openhab.core.service.WatchService;
@ -29,6 +31,7 @@ import org.osgi.service.component.annotations.ReferencePolicy;
* This class is a wrapper of {@link ModuleTypeProvider}, responsible for initializing the WatchService.
*
* @author Ana Dimova - Initial contribution
* @author Arne Seime - Added module validation support
*/
@NonNullByDefault
@Component(immediate = true, service = ModuleTypeProvider.class)
@ -62,4 +65,16 @@ public class ModuleTypeFileProviderWatcher extends ModuleTypeFileProvider {
public void removeParser(Parser<ModuleType> parser, Map<String, String> properties) {
super.removeParser(parser, properties);
}
@SuppressWarnings("null")
@Override
protected void validateObject(ModuleType moduleType) throws ValidationException {
String s;
if ((s = moduleType.getUID()) == null || s.isBlank()) {
throw new ValidationException(ObjectType.MODULE_TYPE, null, "UID cannot be blank");
}
if ((s = moduleType.getLabel()) == null || s.isBlank()) {
throw new ValidationException(ObjectType.MODULE_TYPE, moduleType.getUID(), "Label cannot be blank");
}
}
}

View File

@ -15,7 +15,10 @@ package org.openhab.core.automation.internal.provider.file;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.automation.Module;
import org.openhab.core.automation.parser.Parser;
import org.openhab.core.automation.parser.ValidationException;
import org.openhab.core.automation.parser.ValidationException.ObjectType;
import org.openhab.core.automation.template.RuleTemplate;
import org.openhab.core.automation.template.RuleTemplateProvider;
import org.openhab.core.automation.template.TemplateProvider;
@ -30,6 +33,7 @@ import org.osgi.service.component.annotations.ReferencePolicy;
* This class is a wrapper of multiple {@link TemplateProvider}s, responsible for initializing the WatchService.
*
* @author Ana Dimova - Initial contribution
* @author Arne Seime - Added template validation support
*/
@NonNullByDefault
@Component(immediate = true, service = RuleTemplateProvider.class)
@ -63,4 +67,19 @@ public class TemplateFileProviderWatcher extends TemplateFileProvider {
public void removeParser(Parser<RuleTemplate> parser, Map<String, String> properties) {
super.removeParser(parser, properties);
}
@SuppressWarnings("null")
@Override
protected void validateObject(RuleTemplate template) throws ValidationException {
String s;
if ((s = template.getUID()) == null || s.isBlank()) {
throw new ValidationException(ObjectType.TEMPLATE, null, "UID cannot be blank");
}
if ((s = template.getLabel()) == null || s.isBlank()) {
throw new ValidationException(ObjectType.TEMPLATE, template.getUID(), "Label cannot be blank");
}
if (template.getModules(Module.class).isEmpty()) {
throw new ValidationException(ObjectType.TEMPLATE, template.getUID(), "There must be at least one module");
}
}
}

View File

@ -58,10 +58,15 @@ public interface Parser<T> {
String FORMAT = "format";
/**
* Defines the possible value of property {@link #FORMAT}. It means that the parser supports json format.
* Defines a possible value of property {@link #FORMAT}. It means that the parser supports {@code JSON} format.
*/
String FORMAT_JSON = "json";
/**
* Defines a possible value of property {@link #FORMAT}. It means that the parser supports {@code YAML} format.
*/
String FORMAT_YAML = "yaml";
/**
* Loads a file with some particular format and parse it to the corresponding automation objects.
*

View File

@ -48,7 +48,7 @@ public class ParsingNestedException extends Exception {
* @param msg is the additional message with additional information about the parsing process.
* @param t is the exception thrown during the parsing.
*/
public ParsingNestedException(int type, @Nullable String id, String msg, Throwable t) {
public ParsingNestedException(int type, @Nullable String id, String msg, @Nullable Throwable t) {
super(msg, t);
this.id = id;
this.type = type;
@ -62,7 +62,7 @@ public class ParsingNestedException extends Exception {
* @param id is the UID of the automation object for parsing.
* @param t is the exception thrown during the parsing.
*/
public ParsingNestedException(int type, @Nullable String id, Throwable t) {
public ParsingNestedException(int type, @Nullable String id, @Nullable Throwable t) {
super(t);
this.id = id;
this.type = type;

View File

@ -0,0 +1,124 @@
/*
* Copyright (c) 2010-2025 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.automation.parser;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* This is an {@link Exception} implementation for automation objects that retain some additional information.
*
* @author Arne Seime - Initial contribution
*/
@NonNullByDefault
public class ValidationException extends Exception {
private static final long serialVersionUID = 1L;
/**
* Keeps information about the type of the automation object for validation - module type, template or rule.
*/
private final ObjectType type;
/**
* Keeps information about the UID of the automation object for validation - module type, template or rule.
*/
private final @Nullable String uid;
/**
* Creates a new instance with the specified type, UID and message.
*
* @param type the {@link ObjectType} to use.
* @param uid the UID to use, if any.
* @param message The detail message.
*/
public ValidationException(ObjectType type, @Nullable String uid, @Nullable String message) {
super(message);
this.type = type;
this.uid = uid;
}
/**
* Creates a new instance with the specified type, UID and cause.
*
* @param type the {@link ObjectType} to use.
* @param uid the UID to use, if any.
* @param cause the {@link Throwable} that caused this {@link Exception}.
*/
public ValidationException(ObjectType type, @Nullable String uid, @Nullable Throwable cause) {
super(cause);
this.type = type;
this.uid = uid;
}
/**
* Creates a new instance with the specified type, UID, message and cause.
*
* @param type the {@link ObjectType} to use.
* @param uid the UID to use, if any.
* @param message The detail message.
* @param cause the {@link Throwable} that caused this {@link Exception}.
*/
public ValidationException(ObjectType type, @Nullable String uid, @Nullable String message,
@Nullable Throwable cause) {
super(message, cause);
this.type = type;
this.uid = uid;
}
/**
* Creates a new instance with the specified type, UID, message and cause.
*
* @param type the {@link ObjectType} to use.
* @param uid the UID to use, if any.
* @param message The detail message.
* @param cause the {@link Throwable} that caused this {@link Exception}.
* @param enableSuppression whether or not suppression is enabled or disabled.
* @param writableStackTrace whether or not the stack trace should be writable.
*/
public ValidationException(ObjectType type, @Nullable String uid, @Nullable String message,
@Nullable Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
this.type = type;
this.uid = uid;
}
@Override
public @Nullable String getMessage() {
StringBuilder sb = new StringBuilder();
switch (type) {
case MODULE_TYPE:
sb.append("[Module Type");
break;
case TEMPLATE:
sb.append("[Template");
break;
case RULE:
sb.append("[Rule");
break;
default:
break;
}
if (uid != null) {
sb.append(' ').append(uid);
}
sb.append("] ").append(super.getMessage());
return sb.toString();
}
public enum ObjectType {
MODULE_TYPE,
TEMPLATE,
RULE;
}
}

View File

@ -73,7 +73,7 @@ public class ConfigDispatcherFileWatcher implements WatchService.WatchEventListe
} else if (kind == WatchService.Kind.DELETE) {
// Detect if a service specific configuration file was removed. We want to
// notify the service in this case with an updated empty configuration.
if (Files.isHidden(fullPath) || Files.isDirectory(fullPath) || !fullPath.toString().endsWith(".cfg")) {
if (!fullPath.toString().endsWith(".cfg")) {
return;
}
configDispatcher.fileRemoved(fullPath.toString());

View File

@ -135,8 +135,7 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
public synchronized void processWatchEvent(Kind kind, Path path) {
Path fullPath = watchPath.resolve(path);
String pathString = path.toString();
if (!Files.isReadable(fullPath) || Files.isDirectory(fullPath) || path.startsWith("automation")
|| !pathString.endsWith(".yaml") || fullPath.toFile().isHidden()) {
if (path.startsWith("automation") || !pathString.endsWith(".yaml")) {
logger.trace("Ignored {}", fullPath);
return;
}
@ -144,29 +143,29 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
// strip extension for model name
String modelName = pathString.substring(0, pathString.lastIndexOf("."));
if (kind == WatchService.Kind.DELETE) {
logger.info("Removing YAML model {}", modelName);
YamlModelWrapper removedModel = modelCache.remove(modelName);
if (removedModel == null) {
return;
}
for (Map.Entry<String, List<JsonNode>> modelEntry : removedModel.getNodes().entrySet()) {
String elementName = modelEntry.getKey();
List<JsonNode> removedNodes = modelEntry.getValue();
if (!removedNodes.isEmpty()) {
getElementListeners(elementName).forEach(listener -> {
List removedElements = parseJsonNodes(removedNodes, listener.getElementClass());
listener.removedModel(modelName, removedElements);
});
try {
if (kind == WatchService.Kind.DELETE) {
logger.info("Removing YAML model {}", modelName);
YamlModelWrapper removedModel = modelCache.remove(modelName);
if (removedModel == null) {
return;
}
for (Map.Entry<String, List<JsonNode>> modelEntry : removedModel.getNodes().entrySet()) {
String elementName = modelEntry.getKey();
List<JsonNode> removedNodes = modelEntry.getValue();
if (!removedNodes.isEmpty()) {
getElementListeners(elementName).forEach(listener -> {
List removedElements = parseJsonNodes(removedNodes, listener.getElementClass());
listener.removedModel(modelName, removedElements);
});
}
}
} else if (!Files.isHidden(fullPath) && Files.isReadable(fullPath) && !Files.isDirectory(fullPath)) {
if (kind == Kind.CREATE) {
logger.info("Adding YAML model {}", modelName);
} else {
logger.info("Updating YAML model {}", modelName);
}
}
} else {
if (kind == Kind.CREATE) {
logger.info("Adding YAML model {}", modelName);
} else {
logger.info("Updating YAML model {}", modelName);
}
try {
JsonNode fileContent = objectMapper.readTree(fullPath.toFile());
// check version
@ -233,9 +232,11 @@ public class YamlModelRepositoryImpl implements WatchService.WatchEventListener,
// replace cache
model.getNodes().put(elementName, newNodeElements);
}
} catch (IOException e) {
logger.warn("Failed to read {}: {}", modelName, e.getMessage());
} else {
logger.trace("Ignored {}", fullPath);
}
} catch (IOException e) {
logger.warn("Failed to process model {}: {}", modelName, e.getMessage());
}
}

View File

@ -81,4 +81,6 @@ Fragment-Host: org.openhab.core.automation
org.osgi.service.cm;version='[1.6.0,1.6.1)',\
de.focus_shift.jollyday-core;version='[1.4.0,1.4.1)',\
de.focus_shift.jollyday-jackson;version='[1.4.0,1.4.1)',\
org.ops4j.pax.logging.pax-logging-api;version='[2.2.8,2.2.9)'
org.ops4j.pax.logging.pax-logging-api;version='[2.2.8,2.2.9)',\
com.fasterxml.jackson.dataformat.jackson-dataformat-yaml;version='[2.18.2,2.18.3)',\
org.yaml.snakeyaml;version='[2.3.0,2.3.1)'

View File

@ -81,4 +81,6 @@ Fragment-Host: org.openhab.core.automation
org.osgi.service.cm;version='[1.6.0,1.6.1)',\
de.focus_shift.jollyday-core;version='[1.4.0,1.4.1)',\
de.focus_shift.jollyday-jackson;version='[1.4.0,1.4.1)',\
org.ops4j.pax.logging.pax-logging-api;version='[2.2.8,2.2.9)'
org.ops4j.pax.logging.pax-logging-api;version='[2.2.8,2.2.9)',\
com.fasterxml.jackson.dataformat.jackson-dataformat-yaml;version='[2.18.2,2.18.3)',\
org.yaml.snakeyaml;version='[2.3.0,2.3.1)'

View File

@ -78,4 +78,6 @@ Fragment-Host: org.openhab.core.automation.module.script
org.osgi.service.cm;version='[1.6.0,1.6.1)',\
de.focus_shift.jollyday-core;version='[1.4.0,1.4.1)',\
de.focus_shift.jollyday-jackson;version='[1.4.0,1.4.1)',\
org.ops4j.pax.logging.pax-logging-api;version='[2.2.8,2.2.9)'
org.ops4j.pax.logging.pax-logging-api;version='[2.2.8,2.2.9)',\
com.fasterxml.jackson.dataformat.jackson-dataformat-yaml;version='[2.18.2,2.18.3)',\
org.yaml.snakeyaml;version='[2.3.0,2.3.1)'

View File

@ -81,4 +81,6 @@ Fragment-Host: org.openhab.core.automation
org.osgi.service.cm;version='[1.6.0,1.6.1)',\
de.focus_shift.jollyday-core;version='[1.4.0,1.4.1)',\
de.focus_shift.jollyday-jackson;version='[1.4.0,1.4.1)',\
org.ops4j.pax.logging.pax-logging-api;version='[2.2.8,2.2.9)'
org.ops4j.pax.logging.pax-logging-api;version='[2.2.8,2.2.9)',\
com.fasterxml.jackson.dataformat.jackson-dataformat-yaml;version='[2.18.2,2.18.3)',\
org.yaml.snakeyaml;version='[2.3.0,2.3.1)'

View File

@ -81,4 +81,6 @@ Fragment-Host: org.openhab.core.automation
org.osgi.service.cm;version='[1.6.0,1.6.1)',\
de.focus_shift.jollyday-core;version='[1.4.0,1.4.1)',\
de.focus_shift.jollyday-jackson;version='[1.4.0,1.4.1)',\
org.ops4j.pax.logging.pax-logging-api;version='[2.2.8,2.2.9)'
org.ops4j.pax.logging.pax-logging-api;version='[2.2.8,2.2.9)',\
com.fasterxml.jackson.dataformat.jackson-dataformat-yaml;version='[2.18.2,2.18.3)',\
org.yaml.snakeyaml;version='[2.3.0,2.3.1)'

View File

@ -128,4 +128,6 @@ Fragment-Host: org.openhab.core.model.item
org.openhab.core.model.rule.runtime;version='[5.0.0,5.0.1)',\
de.focus_shift.jollyday-core;version='[1.4.0,1.4.1)',\
de.focus_shift.jollyday-jackson;version='[1.4.0,1.4.1)',\
org.ops4j.pax.logging.pax-logging-api;version='[2.2.8,2.2.9)'
org.ops4j.pax.logging.pax-logging-api;version='[2.2.8,2.2.9)',\
com.fasterxml.jackson.dataformat.jackson-dataformat-yaml;version='[2.18.2,2.18.3)',\
org.yaml.snakeyaml;version='[2.3.0,2.3.1)'

View File

@ -130,4 +130,6 @@ Fragment-Host: org.openhab.core.model.rule.runtime
org.osgi.service.cm;version='[1.6.0,1.6.1)',\
de.focus_shift.jollyday-core;version='[1.4.0,1.4.1)',\
de.focus_shift.jollyday-jackson;version='[1.4.0,1.4.1)',\
org.ops4j.pax.logging.pax-logging-api;version='[2.2.8,2.2.9)'
org.ops4j.pax.logging.pax-logging-api;version='[2.2.8,2.2.9)',\
com.fasterxml.jackson.dataformat.jackson-dataformat-yaml;version='[2.18.2,2.18.3)',\
org.yaml.snakeyaml;version='[2.3.0,2.3.1)'

View File

@ -134,4 +134,6 @@ Fragment-Host: org.openhab.core.model.script
org.openhab.core.model.rule.runtime;version='[5.0.0,5.0.1)',\
de.focus_shift.jollyday-core;version='[1.4.0,1.4.1)',\
de.focus_shift.jollyday-jackson;version='[1.4.0,1.4.1)',\
org.ops4j.pax.logging.pax-logging-api;version='[2.2.8,2.2.9)'
org.ops4j.pax.logging.pax-logging-api;version='[2.2.8,2.2.9)',\
com.fasterxml.jackson.dataformat.jackson-dataformat-yaml;version='[2.18.2,2.18.3)',\
org.yaml.snakeyaml;version='[2.3.0,2.3.1)'

View File

@ -137,4 +137,6 @@ Fragment-Host: org.openhab.core.model.thing
org.openhab.core.model.rule.runtime;version='[5.0.0,5.0.1)',\
de.focus_shift.jollyday-core;version='[1.4.0,1.4.1)',\
de.focus_shift.jollyday-jackson;version='[1.4.0,1.4.1)',\
org.ops4j.pax.logging.pax-logging-api;version='[2.2.8,2.2.9)'
org.ops4j.pax.logging.pax-logging-api;version='[2.2.8,2.2.9)',\
com.fasterxml.jackson.dataformat.jackson-dataformat-yaml;version='[2.18.2,2.18.3)',\
org.yaml.snakeyaml;version='[2.3.0,2.3.1)'