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
parent
3018a8b0f1
commit
fe9af132aa
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)'
|
||||
|
|
|
@ -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)'
|
||||
|
|
|
@ -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)'
|
||||
|
|
|
@ -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)'
|
||||
|
|
|
@ -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)'
|
||||
|
|
|
@ -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)'
|
||||
|
|
|
@ -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)'
|
||||
|
|
|
@ -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)'
|
||||
|
|
|
@ -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)'
|
||||
|
|
Loading…
Reference in New Issue