Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rule Template installation fixes #4591

Merged
merged 4 commits into from
Feb 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,29 @@
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;
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;
Expand All @@ -46,27 +52,48 @@
*
* @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);
Expand Down Expand Up @@ -98,55 +125,83 @@ protected RuleTemplateDTO toPersistableElement(RuleTemplate element) {
}

/**
* 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
* @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, ValidationException {
addTemplate(uid, json, Parser.FORMAT_JSON);
}

/**
* Adds a new rule template to persistent storage from its {@code YAML} representation.
*
* @throws ParsingException if the content cannot be parsed correctly
* @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 addTemplateAsJSON(String uid, String json) throws ParsingException {
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.
*
* @param uid the UID to be used for the template
* @param yaml the template content as a YAML string
* Validates that the parsed template is valid.
*
* @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");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ public void install(Addon addon) throws MarketplaceHandlerException {
}
} 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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public Path getLibraryPath() {
@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());
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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) {
}
}
}
}
Loading