diff --git a/bom/openhab-core/pom.xml b/bom/openhab-core/pom.xml index 74e15de369..b4113644a7 100644 --- a/bom/openhab-core/pom.xml +++ b/bom/openhab-core/pom.xml @@ -238,6 +238,12 @@ ${project.version} compile + + org.openhab.core.bundles + org.openhab.core.io.rest.transform + ${project.version} + compile + org.openhab.core.bundles org.openhab.core.io.rest.ui diff --git a/bundles/org.openhab.core.io.rest.transform/NOTICE b/bundles/org.openhab.core.io.rest.transform/NOTICE new file mode 100644 index 0000000000..6c17d0d8a4 --- /dev/null +++ b/bundles/org.openhab.core.io.rest.transform/NOTICE @@ -0,0 +1,14 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-core + diff --git a/bundles/org.openhab.core.io.rest.transform/pom.xml b/bundles/org.openhab.core.io.rest.transform/pom.xml new file mode 100644 index 0000000000..353d36e372 --- /dev/null +++ b/bundles/org.openhab.core.io.rest.transform/pom.xml @@ -0,0 +1,30 @@ + + + + 4.0.0 + + + org.openhab.core.bundles + org.openhab.core.reactor.bundles + 3.3.0-SNAPSHOT + + + org.openhab.core.io.rest.transform + + openHAB Core :: Bundles :: Transformation REST Interface + + + + org.openhab.core.bundles + org.openhab.core.transform + ${project.version} + + + org.openhab.core.bundles + org.openhab.core.io.rest + ${project.version} + + + + diff --git a/bundles/org.openhab.core.io.rest.transform/src/main/java/org/openhab/core/io/rest/transform/TransformationConfigurationDTO.java b/bundles/org.openhab.core.io.rest.transform/src/main/java/org/openhab/core/io/rest/transform/TransformationConfigurationDTO.java new file mode 100644 index 0000000000..d692586ba3 --- /dev/null +++ b/bundles/org.openhab.core.io.rest.transform/src/main/java/org/openhab/core/io/rest/transform/TransformationConfigurationDTO.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2022 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.io.rest.transform; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.transform.TransformationConfiguration; + +/** + * The {@link TransformationConfigurationDTO} wraps a {@link TransformationConfiguration} + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class TransformationConfigurationDTO { + public String uid; + public String label; + public String type; + public @Nullable String language; + public String content; + public boolean editable = false; + + public TransformationConfigurationDTO(TransformationConfiguration transformationConfiguration) { + this.uid = transformationConfiguration.getUID(); + this.label = transformationConfiguration.getLabel(); + this.type = transformationConfiguration.getType(); + this.content = transformationConfiguration.getContent(); + this.language = transformationConfiguration.getLanguage(); + } +} diff --git a/bundles/org.openhab.core.io.rest.transform/src/main/java/org/openhab/core/io/rest/transform/internal/TransformationConfigurationResource.java b/bundles/org.openhab.core.io.rest.transform/src/main/java/org/openhab/core/io/rest/transform/internal/TransformationConfigurationResource.java new file mode 100644 index 0000000000..66d211ad84 --- /dev/null +++ b/bundles/org.openhab.core.io.rest.transform/src/main/java/org/openhab/core/io/rest/transform/internal/TransformationConfigurationResource.java @@ -0,0 +1,192 @@ +/** + * Copyright (c) 2010-2022 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.io.rest.transform.internal; + +import java.util.Objects; +import java.util.stream.Stream; + +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.auth.Role; +import org.openhab.core.io.rest.RESTConstants; +import org.openhab.core.io.rest.RESTResource; +import org.openhab.core.io.rest.Stream2JSONInputStream; +import org.openhab.core.io.rest.transform.TransformationConfigurationDTO; +import org.openhab.core.transform.ManagedTransformationConfigurationProvider; +import org.openhab.core.transform.TransformationConfiguration; +import org.openhab.core.transform.TransformationConfigurationRegistry; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants; +import org.osgi.service.jaxrs.whiteboard.propertytypes.JSONRequired; +import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect; +import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsName; +import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; + +/** + * The {@link TransformationConfigurationResource} is a REST resource for handling transformation configurations + * + * @author Jan N. Klug - Initial contribution + */ +@Component(immediate = true) +@JaxrsResource +@JaxrsName(TransformationConfigurationResource.PATH_TRANSFORMATIONS) +@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")") +@JSONRequired +@Path(TransformationConfigurationResource.PATH_TRANSFORMATIONS) +@RolesAllowed({ Role.ADMIN }) +@SecurityRequirement(name = "oauth2", scopes = { "admin" }) +@Tag(name = TransformationConfigurationResource.PATH_TRANSFORMATIONS) +@NonNullByDefault +public class TransformationConfigurationResource implements RESTResource { + public static final String PATH_TRANSFORMATIONS = "transformations"; + + private final Logger logger = LoggerFactory.getLogger(TransformationConfigurationResource.class); + private final TransformationConfigurationRegistry transformationConfigurationRegistry; + private final ManagedTransformationConfigurationProvider managedTransformationConfigurationProvider; + private @Context @NonNullByDefault({}) UriInfo uriInfo; + + @Activate + public TransformationConfigurationResource( + final @Reference TransformationConfigurationRegistry transformationConfigurationRegistry, + final @Reference ManagedTransformationConfigurationProvider managedTransformationConfigurationProvider) { + this.transformationConfigurationRegistry = transformationConfigurationRegistry; + this.managedTransformationConfigurationProvider = managedTransformationConfigurationProvider; + } + + @GET + @Path("configurations") + @Produces(MediaType.APPLICATION_JSON) + @Operation(operationId = "getTransformationConfigurations", summary = "Get a list of all transformation configurations", responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = TransformationConfigurationDTO.class)))) }) + public Response getTransformationConfigurations() { + logger.debug("Received HTTP GET request at '{}'", uriInfo.getPath()); + Stream stream = transformationConfigurationRegistry.stream() + .map(TransformationConfigurationDTO::new).peek(c -> c.editable = isEditable(c.uid)); + return Response.ok(new Stream2JSONInputStream(stream)).build(); + } + + @GET + @Path("configurations/{uid}") + @Produces(MediaType.APPLICATION_JSON) + @Operation(operationId = "getTransformationConfiguration", summary = "Get a single transformation configuration", responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = TransformationConfiguration.class))), + @ApiResponse(responseCode = "404", description = "Not found") }) + public Response getTransformationConfiguration( + @PathParam("uid") @Parameter(description = "Configuration UID") String uid) { + logger.debug("Received HTTP GET request at '{}'", uriInfo.getPath()); + + TransformationConfiguration configuration = transformationConfigurationRegistry.get(uid); + if (configuration == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(configuration).build(); + } + + @PUT + @Path("configurations/{uid}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.TEXT_PLAIN) + @Operation(operationId = "putTransformationConfiguration", summary = "Get a single transformation configuration", responses = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "400", description = "Bad Request (content missing or invalid)"), + @ApiResponse(responseCode = "405", description = "Configuration not editable") }) + public Response putTransformationConfiguration( + @PathParam("uid") @Parameter(description = "Configuration UID") String uid, + @Parameter(description = "configuration", required = true) @Nullable TransformationConfigurationDTO newConfiguration) { + logger.debug("Received HTTP PUT request at '{}'", uriInfo.getPath()); + + TransformationConfiguration oldConfiguration = transformationConfigurationRegistry.get(uid); + if (oldConfiguration != null && !isEditable(uid)) { + return Response.status(Response.Status.METHOD_NOT_ALLOWED).build(); + } + + if (newConfiguration == null) { + return Response.status(Response.Status.BAD_REQUEST).entity("Content missing.").build(); + } + + if (!uid.equals(newConfiguration.uid)) { + return Response.status(Response.Status.BAD_REQUEST).entity("UID of configuration and path not matching.") + .build(); + } + + TransformationConfiguration transformationConfiguration = new TransformationConfiguration(newConfiguration.uid, + newConfiguration.label, newConfiguration.type, newConfiguration.language, newConfiguration.content); + try { + if (oldConfiguration != null) { + managedTransformationConfigurationProvider.update(transformationConfiguration); + } else { + managedTransformationConfigurationProvider.add(transformationConfiguration); + } + } catch (IllegalArgumentException e) { + return Response.status(Response.Status.BAD_REQUEST).entity(Objects.requireNonNullElse(e.getMessage(), "")) + .build(); + } + + return Response.ok().build(); + } + + @DELETE + @Path("configurations/{uid}") + @Produces(MediaType.TEXT_PLAIN) + @Operation(operationId = "deleteTransformationConfiguration", summary = "Get a single transformation configuration", responses = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "404", description = "UID not found"), + @ApiResponse(responseCode = "405", description = "Configuration not editable") }) + public Response deleteTransformationConfiguration( + @PathParam("uid") @Parameter(description = "Configuration UID") String uid) { + logger.debug("Received HTTP DELETE request at '{}'", uriInfo.getPath()); + + TransformationConfiguration oldConfiguration = transformationConfigurationRegistry.get(uid); + if (oldConfiguration == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + if (!isEditable(uid)) { + return Response.status(Response.Status.METHOD_NOT_ALLOWED).build(); + } + + managedTransformationConfigurationProvider.remove(uid); + + return Response.ok().build(); + } + + private boolean isEditable(String uid) { + return managedTransformationConfigurationProvider.get(uid) != null; + } +} diff --git a/bundles/org.openhab.core.transform/pom.xml b/bundles/org.openhab.core.transform/pom.xml index 5b70829f4d..0cee63b00d 100644 --- a/bundles/org.openhab.core.transform/pom.xml +++ b/bundles/org.openhab.core.transform/pom.xml @@ -20,6 +20,12 @@ org.openhab.core.config.core ${project.version} + + org.openhab.core.bundles + org.openhab.core.test + ${project.version} + test + diff --git a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/AbstractFileTransformationService.java b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/AbstractFileTransformationService.java index 9f7907a2a0..d522e330b2 100644 --- a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/AbstractFileTransformationService.java +++ b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/AbstractFileTransformationService.java @@ -49,6 +49,8 @@ import org.slf4j.LoggerFactory; * under the 'transform' folder within the configuration path. To organize the various * transformations one might use subfolders. * + * @deprecated use the {@link TransformationConfigurationRegistry} instead + * * @author Gaƫl L'hopital - Initial contribution * @author Kai Kreuzer - File caching mechanism * @author Markus Rathgeb - Add locale provider support diff --git a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/FileTransformationConfigurationProvider.java b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/FileTransformationConfigurationProvider.java new file mode 100644 index 0000000000..2dd45c2afa --- /dev/null +++ b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/FileTransformationConfigurationProvider.java @@ -0,0 +1,156 @@ +/** + * Copyright (c) 2010-2022 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.transform; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.OpenHAB; +import org.openhab.core.common.registry.ProviderChangeListener; +import org.openhab.core.service.AbstractWatchService; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link FileTransformationConfigurationProvider} implements a {@link TransformationConfigurationProvider} for + * supporting configurations stored in configuration files + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@Component(service = TransformationConfigurationProvider.class, immediate = true) +public class FileTransformationConfigurationProvider extends AbstractWatchService + implements TransformationConfigurationProvider { + private static final WatchEvent.Kind[] WATCH_EVENTS = { StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY }; + private static final Set IGNORED_EXTENSIONS = Set.of("txt"); + private static final Pattern FILENAME_PATTERN = Pattern + .compile("(?.+?)(_(?[a-z]{2}))?\\.(?[^.]*)$"); + private static final Path TRANSFORMATION_PATH = Path.of(OpenHAB.getConfigFolder(), + TransformationService.TRANSFORM_FOLDER_NAME); + + private final Logger logger = LoggerFactory.getLogger(FileTransformationConfigurationProvider.class); + + private final Set> listeners = ConcurrentHashMap.newKeySet(); + private final Map transformationConfigurations = new ConcurrentHashMap<>(); + private final Path transformationPath; + + public FileTransformationConfigurationProvider() { + this(TRANSFORMATION_PATH); + } + + // constructor package private used for testing + FileTransformationConfigurationProvider(Path transformationPath) { + super(transformationPath.toString()); + this.transformationPath = transformationPath; + + // read initial contents + try { + Files.walk(transformationPath).filter(Files::isRegularFile) + .forEach(f -> processPath(StandardWatchEventKinds.ENTRY_CREATE, f)); + } catch (IOException e) { + logger.warn("Could not list files in '{}', transformation configurations might be missing: {}", + transformationPath, e.getMessage()); + } + } + + @Override + public void addProviderChangeListener(ProviderChangeListener listener) { + listeners.add(listener); + } + + @Override + public void removeProviderChangeListener(ProviderChangeListener listener) { + listeners.remove(listener); + } + + @Override + public Collection getAll() { + return transformationConfigurations.values(); + } + + @Override + protected boolean watchSubDirectories() { + return true; + } + + @Override + protected WatchEvent.Kind @Nullable [] getWatchEventKinds(Path directory) { + return WATCH_EVENTS; + } + + @Override + protected void processWatchEvent(WatchEvent event, WatchEvent.Kind kind, Path path) { + processPath(kind, path); + } + + private void processPath(WatchEvent.Kind kind, Path path) { + if (!Files.isRegularFile(path)) { + logger.trace("Skipping {} event for '{}' - not a regular file", kind, path); + return; + } + if (StandardWatchEventKinds.ENTRY_DELETE.equals(kind)) { + TransformationConfiguration oldElement = transformationConfigurations.remove(path); + if (oldElement != null) { + logger.trace("Removed configuration from file '{}", path); + listeners.forEach(listener -> listener.removed(this, oldElement)); + } + } else if (StandardWatchEventKinds.ENTRY_CREATE.equals(kind) + || StandardWatchEventKinds.ENTRY_MODIFY.equals(kind)) { + try { + String fileName = path.getFileName().toString(); + Matcher m = FILENAME_PATTERN.matcher(fileName); + if (!m.matches()) { + logger.debug("Skipping {} event for '{}' - no file extensions found or remaining filename empty", + kind, path); + return; + } + + String fileExtension = m.group("extension"); + if (IGNORED_EXTENSIONS.contains(fileExtension)) { + logger.debug("Skipping {} event for '{}' - file extension '{}' is ignored", kind, path, + fileExtension); + return; + } + + String content = new String(Files.readAllBytes(path)); + String uid = transformationPath.relativize(path).toString(); + + TransformationConfiguration newElement = new TransformationConfiguration(uid, uid, fileExtension, + m.group("language"), content); + TransformationConfiguration oldElement = transformationConfigurations.put(path, newElement); + if (oldElement == null) { + logger.trace("Added new configuration from file '{}'", path); + listeners.forEach(listener -> listener.added(this, newElement)); + } else { + logger.trace("Updated new configuration from file '{}'", path); + listeners.forEach(listener -> listener.updated(this, oldElement, newElement)); + } + } catch (IOException e) { + logger.warn("Skipping {} event for '{}' - failed to read content: {}", kind, path, e.getMessage()); + } + } + } +} diff --git a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/ManagedTransformationConfigurationProvider.java b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/ManagedTransformationConfigurationProvider.java new file mode 100644 index 0000000000..35efc67c57 --- /dev/null +++ b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/ManagedTransformationConfigurationProvider.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2010-2022 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.transform; + +import java.util.Objects; +import java.util.regex.Matcher; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.common.registry.AbstractManagedProvider; +import org.openhab.core.storage.StorageService; +import org.openhab.core.transform.ManagedTransformationConfigurationProvider.PersistedTransformationConfiguration; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link ManagedTransformationConfigurationProvider} implements a {@link TransformationConfigurationProvider} for + * managed configurations stored in a JSON database + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@Component(service = { TransformationConfigurationProvider.class, + ManagedTransformationConfigurationProvider.class }, immediate = true) +public class ManagedTransformationConfigurationProvider + extends AbstractManagedProvider + implements TransformationConfigurationProvider { + + @Activate + public ManagedTransformationConfigurationProvider(final @Reference StorageService storageService) { + super(storageService); + } + + @Override + protected String getStorageName() { + return TransformationConfiguration.class.getName(); + } + + @Override + protected String keyToString(String key) { + return key; + } + + @Override + protected @Nullable TransformationConfiguration toElement(String key, + PersistedTransformationConfiguration persistableElement) { + return new TransformationConfiguration(persistableElement.uid, persistableElement.label, + persistableElement.type, persistableElement.language, persistableElement.content); + } + + @Override + protected PersistedTransformationConfiguration toPersistableElement(TransformationConfiguration element) { + return new PersistedTransformationConfiguration(element); + } + + @Override + public void add(TransformationConfiguration element) { + checkConfiguration(element); + super.add(element); + } + + @Override + public @Nullable TransformationConfiguration update(TransformationConfiguration element) { + checkConfiguration(element); + return super.update(element); + } + + private static void checkConfiguration(TransformationConfiguration element) { + Matcher matcher = TransformationConfigurationRegistry.CONFIG_UID_PATTERN.matcher(element.getUID()); + if (!matcher.matches()) { + throw new IllegalArgumentException( + "The transformation configuration UID '" + element.getUID() + "' is invalid."); + } + if (!Objects.equals(element.getLanguage(), matcher.group("language"))) { + throw new IllegalArgumentException("The transformation configuration UID '" + element.getUID() + + "' contains(misses) a language, but it is not set (set)."); + } + if (!Objects.equals(element.getType(), matcher.group("type"))) { + throw new IllegalArgumentException("The transformation configuration UID '" + element.getUID() + + "' is not matching the type '" + element.getType() + "'."); + } + } + + public static class PersistedTransformationConfiguration { + public @NonNullByDefault({}) String uid; + public @NonNullByDefault({}) String label; + public @NonNullByDefault({}) String type; + public @Nullable String language; + public @NonNullByDefault({}) String content; + + protected PersistedTransformationConfiguration() { + // default constructor for deserialization + } + + public PersistedTransformationConfiguration(TransformationConfiguration configuration) { + this.uid = configuration.getUID(); + this.label = configuration.getLabel(); + this.type = configuration.getType(); + this.language = configuration.getLanguage(); + this.content = configuration.getContent(); + } + } +} diff --git a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationConfiguration.java b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationConfiguration.java new file mode 100644 index 0000000000..67f664b4e7 --- /dev/null +++ b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationConfiguration.java @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2010-2022 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.transform; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.common.registry.Identifiable; + +/** + * The {@link TransformationConfiguration} encapsulates a transformation configuration + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public class TransformationConfiguration implements Identifiable { + private final String uid; + private final String label; + private final String type; + private final @Nullable String language; + private final String content; + + /** + * @param uid the configuration UID. The format is config:<type>:<name>[:<locale>]. For backward + * compatibility also filenames are allowed. + * @param type the type of the configuration (file extension for file-based providers) + * @param language the language of this configuration (null if not set) + * @param content the content of this configuration + */ + public TransformationConfiguration(String uid, String label, String type, @Nullable String language, + String content) { + this.uid = uid; + this.label = label; + this.type = type; + this.content = content; + this.language = language; + } + + @Override + public String getUID() { + return uid; + } + + public String getLabel() { + return label; + } + + public String getType() { + return type; + } + + public @Nullable String getLanguage() { + return language; + } + + public String getContent() { + return content; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TransformationConfiguration that = (TransformationConfiguration) o; + return uid.equals(that.uid) && label.equals(that.label) && type.equals(that.type) + && Objects.equals(language, that.language) && content.equals(that.content); + } + + @Override + public int hashCode() { + return Objects.hash(uid, label, type, language, content); + } + + @Override + public String toString() { + return "TransformationConfiguration{uid='" + uid + "', label='" + label + "', type='" + type + "', language='" + + language + "', content='" + content + "'}"; + } +} diff --git a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationConfigurationProvider.java b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationConfigurationProvider.java new file mode 100644 index 0000000000..a3f9ec3390 --- /dev/null +++ b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationConfigurationProvider.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2022 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.transform; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.common.registry.Provider; + +/** + * The {@link TransformationConfigurationProvider} is implemented by providers for transformation configurations + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public interface TransformationConfigurationProvider extends Provider { + +} diff --git a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationConfigurationRegistry.java b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationConfigurationRegistry.java new file mode 100644 index 0000000000..1ae5cc3406 --- /dev/null +++ b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/TransformationConfigurationRegistry.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2022 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.transform; + +import java.util.Collection; +import java.util.Locale; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.common.registry.Registry; + +/** + * The {@link TransformationConfigurationRegistry} is the interface for the transformation configuration registry + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public interface TransformationConfigurationRegistry extends Registry { + Pattern CONFIG_UID_PATTERN = Pattern.compile("config:(?\\w+):(?\\w+)(:(?\\w+))?"); + + /** + * Get a localized version of the configuration for a given UID + * + * @param uid the configuration UID + * @param locale a locale (system locale is used if null) + * @return the requested {@link TransformationConfiguration} (or null if not found). + */ + @Nullable + TransformationConfiguration get(String uid, @Nullable Locale locale); + + /** + * Get all configurations which match the given types + * + * @param types a {@link Collection} of configuration types + * @return a {@link Collection} of {@link TransformationConfiguration}s + */ + Collection getConfigurations(Collection types); +} diff --git a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/internal/TransformationActivator.java b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/internal/TransformationActivator.java index a1f11be4b2..e052661bfc 100644 --- a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/internal/TransformationActivator.java +++ b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/internal/TransformationActivator.java @@ -12,6 +12,8 @@ */ package org.openhab.core.transform.internal; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; import org.slf4j.Logger; @@ -23,17 +25,18 @@ import org.slf4j.LoggerFactory; * @author Thomas Eichstaedt-Engelen - Initial contribution * @author Kai Kreuzer - Initial contribution */ +@NonNullByDefault public final class TransformationActivator implements BundleActivator { private final Logger logger = LoggerFactory.getLogger(TransformationActivator.class); - private static BundleContext context; + private static @Nullable BundleContext context; /** * Called whenever the OSGi framework starts our bundle */ @Override - public void start(BundleContext bc) throws Exception { + public void start(@Nullable BundleContext bc) throws Exception { context = bc; logger.debug("Transformation Service has been started."); } @@ -42,7 +45,7 @@ public final class TransformationActivator implements BundleActivator { * Called whenever the OSGi framework stops our bundle */ @Override - public void stop(BundleContext bc) throws Exception { + public void stop(@Nullable BundleContext bc) throws Exception { context = null; logger.debug("Transformation Service has been stopped."); } @@ -52,7 +55,7 @@ public final class TransformationActivator implements BundleActivator { * * @return the bundle context */ - public static BundleContext getContext() { + public static @Nullable BundleContext getContext() { return context; } } diff --git a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/internal/TransformationConfigurationRegistryImpl.java b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/internal/TransformationConfigurationRegistryImpl.java new file mode 100644 index 0000000000..d86d5431e6 --- /dev/null +++ b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/internal/TransformationConfigurationRegistryImpl.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2010-2022 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.transform.internal; + +import java.util.Collection; +import java.util.Locale; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.common.registry.AbstractRegistry; +import org.openhab.core.common.registry.Provider; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.transform.ManagedTransformationConfigurationProvider; +import org.openhab.core.transform.TransformationConfiguration; +import org.openhab.core.transform.TransformationConfigurationProvider; +import org.openhab.core.transform.TransformationConfigurationRegistry; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; + +/** + * The {@link TransformationConfigurationRegistryImpl} implements the {@link TransformationConfigurationRegistry} + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true) +public class TransformationConfigurationRegistryImpl + extends AbstractRegistry + implements TransformationConfigurationRegistry { + private static final Pattern FILENAME_PATTERN = Pattern + .compile("(?.+)(_(?[a-z]{2}))?\\.(?[^.]*)$"); + + private final LocaleProvider localeProvider; + + @Activate + public TransformationConfigurationRegistryImpl(@Reference LocaleProvider localeProvider) { + super(TransformationConfigurationProvider.class); + + this.localeProvider = localeProvider; + } + + @Override + public @Nullable TransformationConfiguration get(String uid, @Nullable Locale locale) { + TransformationConfiguration configuration = null; + + String language = Objects.requireNonNullElse(locale, localeProvider.getLocale()).getLanguage(); + Matcher uidMatcher = CONFIG_UID_PATTERN.matcher(uid); + if (uidMatcher.matches()) { + // try to get localized version of the uid if no locale information is present + if (uidMatcher.group("language") == null) { + configuration = get(uid + ":" + language); + } + } else { + // check if legacy configuration and try to get localized version + uidMatcher = FILENAME_PATTERN.matcher(uid); + if (uidMatcher.matches() && uidMatcher.group("language") == null) { + // try to get a localized version + String localizedUid = uidMatcher.group("filename") + "_" + language + "." + + uidMatcher.group("extension"); + configuration = get(localizedUid); + } + } + + return (configuration != null) ? configuration : get(uid); + } + + @Override + public Collection getConfigurations(Collection types) { + return getAll().stream().filter(e -> types.contains(e.getType())).collect(Collectors.toList()); + } + + @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC) + protected void setManagedProvider(ManagedTransformationConfigurationProvider provider) { + super.setManagedProvider(provider); + } + + protected void unsetManagedProvider(ManagedTransformationConfigurationProvider provider) { + super.unsetManagedProvider(provider); + } + + @Override + protected void addProvider(Provider provider) { + // overridden to make method available for testing + super.addProvider(provider); + } +} diff --git a/bundles/org.openhab.core.transform/src/test/java/org/openhab/core/transform/FileTransformationConfigurationProviderTest.java b/bundles/org.openhab.core.transform/src/test/java/org/openhab/core/transform/FileTransformationConfigurationProviderTest.java new file mode 100644 index 0000000000..bd26fe9b0d --- /dev/null +++ b/bundles/org.openhab.core.transform/src/test/java/org/openhab/core/transform/FileTransformationConfigurationProviderTest.java @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2010-2022 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.transform; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.not; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.core.common.registry.ProviderChangeListener; + +/** + * The {@link FileTransformationConfigurationProviderTest} includes tests for the + * {@link FileTransformationConfigurationProvider} + * + * @author Jan N. Klug - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@NonNullByDefault +public class FileTransformationConfigurationProviderTest { + private static final String FOO_TYPE = "foo"; + private static final String INITIAL_CONTENT = "initial"; + private static final String INITIAL_FILENAME = INITIAL_CONTENT + "." + FOO_TYPE; + private static final TransformationConfiguration INITIAL_CONFIGURATION = new TransformationConfiguration( + INITIAL_FILENAME, INITIAL_FILENAME, FOO_TYPE, null, INITIAL_CONTENT); + private static final String ADDED_CONTENT = "added"; + private static final String ADDED_FILENAME = ADDED_CONTENT + "." + FOO_TYPE; + + private @Mock @NonNullByDefault({}) WatchEvent watchEventMock; + private @Mock @NonNullByDefault({}) ProviderChangeListener<@NonNull TransformationConfiguration> listenerMock; + + private @NonNullByDefault({}) FileTransformationConfigurationProvider provider; + private @NonNullByDefault({}) Path targetPath; + + @BeforeEach + public void setup() throws IOException { + // create directory + targetPath = Files.createTempDirectory("fileTest"); + // set initial content + Files.write(targetPath.resolve(INITIAL_FILENAME), INITIAL_CONTENT.getBytes(StandardCharsets.UTF_8)); + + provider = new FileTransformationConfigurationProvider(targetPath); + provider.addProviderChangeListener(listenerMock); + } + + @AfterEach + public void tearDown() throws IOException { + try (Stream walk = Files.walk(targetPath)) { + walk.map(Path::toFile).forEach(File::delete); + } + Files.deleteIfExists(targetPath); + } + + @Test + public void testInitialConfigurationIsPresent() { + // assert that initial configuration is present + assertThat(provider.getAll(), contains(INITIAL_CONFIGURATION)); + } + + @Test + public void testAddingConfigurationIsPropagated() throws IOException { + Path path = targetPath.resolve(ADDED_FILENAME); + + Files.write(path, ADDED_CONTENT.getBytes(StandardCharsets.UTF_8)); + TransformationConfiguration addedConfiguration = new TransformationConfiguration(ADDED_FILENAME, ADDED_FILENAME, + FOO_TYPE, null, ADDED_CONTENT); + + provider.processWatchEvent(watchEventMock, StandardWatchEventKinds.ENTRY_CREATE, path); + + // assert registry is notified and internal cache updated + Mockito.verify(listenerMock).added(provider, addedConfiguration); + assertThat(provider.getAll(), hasItem(addedConfiguration)); + } + + @Test + public void testUpdatingConfigurationIsPropagated() throws IOException { + Path path = targetPath.resolve(INITIAL_FILENAME); + Files.write(path, "updated".getBytes(StandardCharsets.UTF_8)); + TransformationConfiguration updatedConfiguration = new TransformationConfiguration(INITIAL_FILENAME, + INITIAL_FILENAME, FOO_TYPE, null, "updated"); + + provider.processWatchEvent(watchEventMock, StandardWatchEventKinds.ENTRY_MODIFY, path); + + Mockito.verify(listenerMock).updated(provider, INITIAL_CONFIGURATION, updatedConfiguration); + assertThat(provider.getAll(), contains(updatedConfiguration)); + assertThat(provider.getAll(), not(contains(INITIAL_CONFIGURATION))); + } + + @Test + public void testDeletingConfigurationIsPropagated() { + Path path = targetPath.resolve(INITIAL_FILENAME); + + provider.processWatchEvent(watchEventMock, StandardWatchEventKinds.ENTRY_DELETE, path); + + Mockito.verify(listenerMock).removed(provider, INITIAL_CONFIGURATION); + assertThat(provider.getAll(), not(contains(INITIAL_CONFIGURATION))); + } + + @Test + public void testLanguageIsProperlyParsed() throws IOException { + String fileName = "test_de." + FOO_TYPE; + Path path = targetPath.resolve(fileName); + + Files.write(path, INITIAL_CONTENT.getBytes(StandardCharsets.UTF_8)); + + TransformationConfiguration expected = new TransformationConfiguration(fileName, fileName, FOO_TYPE, "de", + INITIAL_CONTENT); + + provider.processWatchEvent(watchEventMock, StandardWatchEventKinds.ENTRY_CREATE, path); + assertThat(provider.getAll(), hasItem(expected)); + } + + @Test + public void testMissingExtensionIsIgnored() throws IOException { + Path path = targetPath.resolve("extensionMissing"); + Files.write(path, INITIAL_CONTENT.getBytes(StandardCharsets.UTF_8)); + provider.processWatchEvent(watchEventMock, StandardWatchEventKinds.ENTRY_CREATE, path); + provider.processWatchEvent(watchEventMock, StandardWatchEventKinds.ENTRY_MODIFY, path); + + Mockito.verify(listenerMock, never()).added(any(), any()); + Mockito.verify(listenerMock, never()).updated(any(), any(), any()); + } + + @Test + public void testIgnoredExtensionIsIgnored() throws IOException { + Path path = targetPath.resolve("extensionIgnore.txt"); + Files.write(path, INITIAL_CONTENT.getBytes(StandardCharsets.UTF_8)); + provider.processWatchEvent(watchEventMock, StandardWatchEventKinds.ENTRY_CREATE, path); + provider.processWatchEvent(watchEventMock, StandardWatchEventKinds.ENTRY_MODIFY, path); + + Mockito.verify(listenerMock, never()).added(any(), any()); + Mockito.verify(listenerMock, never()).updated(any(), any(), any()); + } +} diff --git a/bundles/org.openhab.core.transform/src/test/java/org/openhab/core/transform/ManagedTransformationConfigurationProviderTest.java b/bundles/org.openhab.core.transform/src/test/java/org/openhab/core/transform/ManagedTransformationConfigurationProviderTest.java new file mode 100644 index 0000000000..2c15e77037 --- /dev/null +++ b/bundles/org.openhab.core.transform/src/test/java/org/openhab/core/transform/ManagedTransformationConfigurationProviderTest.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2010-2022 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.transform; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.core.common.registry.ProviderChangeListener; +import org.openhab.core.test.storage.VolatileStorageService; + +/** + * The {@link ManagedTransformationConfigurationProviderTest} includes tests for the + * {@link org.openhab.core.transform.ManagedTransformationConfigurationProvider} + * + * @author Jan N. Klug - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@NonNullByDefault +public class ManagedTransformationConfigurationProviderTest { + + private @Mock @NonNullByDefault({}) ProviderChangeListener<@NonNull TransformationConfiguration> listenerMock; + + private @NonNullByDefault({}) ManagedTransformationConfigurationProvider provider; + + @BeforeEach + public void setup() { + VolatileStorageService storageService = new VolatileStorageService(); + provider = new ManagedTransformationConfigurationProvider(storageService); + provider.addProviderChangeListener(listenerMock); + } + + @Test + public void testValidConfigurationsAreAdded() { + TransformationConfiguration withoutLanguage = new TransformationConfiguration("config:foo:identifier", "", + "foo", null, "content"); + provider.add(withoutLanguage); + + TransformationConfiguration withLanguage = new TransformationConfiguration("config:foo:identifier:de", "", + "foo", "de", "content"); + provider.add(withLanguage); + + Mockito.verify(listenerMock).added(provider, withoutLanguage); + Mockito.verify(listenerMock).added(provider, withLanguage); + } + + @Test + public void testValidConfigurationsIsUpdated() { + TransformationConfiguration configuration = new TransformationConfiguration("config:foo:identifier", "", "foo", + null, "content"); + TransformationConfiguration updatedConfiguration = new TransformationConfiguration("config:foo:identifier", "", + "foo", null, "updated"); + + provider.add(configuration); + provider.update(updatedConfiguration); + + Mockito.verify(listenerMock).added(provider, configuration); + Mockito.verify(listenerMock).updated(provider, configuration, updatedConfiguration); + } + + @Test + public void testUidFormatValidation() { + TransformationConfiguration inValidUid = new TransformationConfiguration("invalid:foo:identifier", "", "foo", + null, "content"); + + assertThrows(IllegalArgumentException.class, () -> provider.add(inValidUid)); + } + + @Test + public void testLanguageValidations() { + TransformationConfiguration languageMissingInUid = new TransformationConfiguration("config:foo:identifier", "", + "foo", "de", "content"); + + assertThrows(IllegalArgumentException.class, () -> provider.add(languageMissingInUid)); + + TransformationConfiguration languageMissingInConfiguration = new TransformationConfiguration( + "config:foo:identifier:de", "", "foo", null, "content"); + + assertThrows(IllegalArgumentException.class, () -> provider.add(languageMissingInConfiguration)); + + TransformationConfiguration languageNotMatching = new TransformationConfiguration("config:foo:identifier:en", + "", "foo", "de", "content"); + + assertThrows(IllegalArgumentException.class, () -> provider.add(languageNotMatching)); + } + + @Test + public void testTypeValidation() { + TransformationConfiguration typeNotMatching = new TransformationConfiguration("config:foo:identifier", "", + "bar", null, "content"); + + assertThrows(IllegalArgumentException.class, () -> provider.add(typeNotMatching)); + } + + @Test + public void testSerializationDeserializationResultsInSameConfiguration() { + TransformationConfiguration configuration = new TransformationConfiguration("config:foo:identifier", "", "foo", + null, "content"); + provider.add(configuration); + + TransformationConfiguration configuration1 = provider.get("config:foo:identifier"); + + assertThat(configuration, is(configuration1)); + } +} diff --git a/bundles/org.openhab.core.transform/src/test/java/org/openhab/core/transform/actions/TransformationTest.java b/bundles/org.openhab.core.transform/src/test/java/org/openhab/core/transform/actions/TransformationTest.java index c5e02bce9d..beccca3217 100644 --- a/bundles/org.openhab.core.transform/src/test/java/org/openhab/core/transform/actions/TransformationTest.java +++ b/bundles/org.openhab.core.transform/src/test/java/org/openhab/core/transform/actions/TransformationTest.java @@ -12,7 +12,9 @@ */ package org.openhab.core.transform.actions; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; @@ -27,15 +29,13 @@ public class TransformationTest { @Test public void testTransform() { String result = Transformation.transform("UnknownTransformation", "function", "test"); - assertEquals("test", result); + assertThat(result, is("test")); } @Test public void testTransformRaw() { - try { - Transformation.transformRaw("UnknownTransformation", "function", "test"); - } catch (TransformationException e) { - assertEquals("No transformation service 'UnknownTransformation' could be found.", e.getMessage()); - } + TransformationException e = assertThrows(TransformationException.class, + () -> Transformation.transformRaw("UnknownTransformation", "function", "test")); + assertThat(e.getMessage(), is("No transformation service 'UnknownTransformation' could be found.")); } } diff --git a/bundles/org.openhab.core.transform/src/test/java/org/openhab/core/transform/internal/TransformationConfigurationRegistryImplTest.java b/bundles/org.openhab.core.transform/src/test/java/org/openhab/core/transform/internal/TransformationConfigurationRegistryImplTest.java new file mode 100644 index 0000000000..937458ad71 --- /dev/null +++ b/bundles/org.openhab.core.transform/src/test/java/org/openhab/core/transform/internal/TransformationConfigurationRegistryImplTest.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2010-2022 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.transform.internal; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.transform.ManagedTransformationConfigurationProvider; +import org.openhab.core.transform.TransformationConfiguration; + +/** + * The {@link TransformationConfigurationRegistryImplTest} includes tests for the + * {@link TransformationConfigurationRegistryImpl} + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class TransformationConfigurationRegistryImplTest { + private static final String SERVICE = "foo"; + + private static final String MANAGED_WITHOUT_LANGUAGE_UID = "config:" + SERVICE + ":managed"; + private static final String MANAGED_WITH_EN_LANGUAGE_UID = "config:" + SERVICE + ":managed:en"; + private static final String MANAGED_WITH_DE_LANGUAGE_UID = "config:" + SERVICE + ":managed:de"; + + private static final TransformationConfiguration MANAGED_WITHOUT_LANGUAGE = new TransformationConfiguration( + MANAGED_WITHOUT_LANGUAGE_UID, "", SERVICE, null, MANAGED_WITHOUT_LANGUAGE_UID); + private static final TransformationConfiguration MANAGED_WITH_EN_LANGUAGE = new TransformationConfiguration( + MANAGED_WITH_EN_LANGUAGE_UID, "", SERVICE, "en", MANAGED_WITH_EN_LANGUAGE_UID); + private static final TransformationConfiguration MANAGED_WITH_DE_LANGUAGE = new TransformationConfiguration( + MANAGED_WITH_DE_LANGUAGE_UID, "", SERVICE, "de", MANAGED_WITH_DE_LANGUAGE_UID); + + private static final String FILE_WITHOUT_LANGUAGE_UID = "foo/FILE." + SERVICE; + private static final String FILE_WITH_EN_LANGUAGE_UID = "foo/FILE_en." + SERVICE; + private static final String FILE_WITH_DE_LANGUAGE_UID = "foo/FILE_de." + SERVICE; + + private static final TransformationConfiguration FILE_WITHOUT_LANGUAGE = new TransformationConfiguration( + FILE_WITHOUT_LANGUAGE_UID, "", SERVICE, null, FILE_WITHOUT_LANGUAGE_UID); + private static final TransformationConfiguration FILE_WITH_EN_LANGUAGE = new TransformationConfiguration( + FILE_WITH_EN_LANGUAGE_UID, "", SERVICE, "en", FILE_WITH_EN_LANGUAGE_UID); + private static final TransformationConfiguration FILE_WITH_DE_LANGUAGE = new TransformationConfiguration( + FILE_WITH_DE_LANGUAGE_UID, "", SERVICE, "de", FILE_WITH_DE_LANGUAGE_UID); + + private @Mock @NonNullByDefault({}) LocaleProvider localeProviderMock; + + private @Mock @NonNullByDefault({}) ManagedTransformationConfigurationProvider providerMock; + + private @NonNullByDefault({}) TransformationConfigurationRegistryImpl registry; + + @BeforeEach + public void setup() { + Mockito.when(localeProviderMock.getLocale()).thenReturn(Locale.US); + + registry = new TransformationConfigurationRegistryImpl(localeProviderMock); + registry.addProvider(providerMock); + registry.added(providerMock, MANAGED_WITHOUT_LANGUAGE); + registry.added(providerMock, MANAGED_WITH_EN_LANGUAGE); + registry.added(providerMock, MANAGED_WITH_DE_LANGUAGE); + registry.added(providerMock, FILE_WITHOUT_LANGUAGE); + registry.added(providerMock, FILE_WITH_EN_LANGUAGE); + registry.added(providerMock, FILE_WITH_DE_LANGUAGE); + } + + @Test + public void testManagedReturnsCorrectLanguage() { + // language contained in uid, default requested (explicit uid takes precedence) + assertThat(registry.get(MANAGED_WITH_DE_LANGUAGE_UID, null), is(MANAGED_WITH_DE_LANGUAGE)); + // language contained in uid, other requested (explicit uid takes precedence) + assertThat(registry.get(MANAGED_WITH_DE_LANGUAGE_UID, Locale.FRANCE), is(MANAGED_WITH_DE_LANGUAGE)); + // no language in uid, default requested + assertThat(registry.get(MANAGED_WITHOUT_LANGUAGE_UID, null), is(MANAGED_WITH_EN_LANGUAGE)); + // no language in uid, other requested + assertThat(registry.get(MANAGED_WITHOUT_LANGUAGE_UID, Locale.GERMANY), is(MANAGED_WITH_DE_LANGUAGE)); + // no language in uid, unknown requested + assertThat(registry.get(MANAGED_WITHOUT_LANGUAGE_UID, Locale.FRANCE), is(MANAGED_WITHOUT_LANGUAGE)); + } + + @Test + public void testFileReturnsCorrectLanguage() { + // language contained in uid, default requested (explicit uid takes precedence) + assertThat(registry.get(FILE_WITH_DE_LANGUAGE_UID, null), is(FILE_WITH_DE_LANGUAGE)); + // language contained in uid, other requested (explicit uid takes precedence) + assertThat(registry.get(FILE_WITH_DE_LANGUAGE_UID, Locale.FRANCE), is(FILE_WITH_DE_LANGUAGE)); + // no language in uid, default requested + assertThat(registry.get(FILE_WITHOUT_LANGUAGE_UID, null), is(FILE_WITH_EN_LANGUAGE)); + // no language in uid, other requested + assertThat(registry.get(FILE_WITHOUT_LANGUAGE_UID, Locale.GERMANY), is(FILE_WITH_DE_LANGUAGE)); + // no language in uid, unknown requested + assertThat(registry.get(FILE_WITHOUT_LANGUAGE_UID, Locale.FRANCE), is(FILE_WITHOUT_LANGUAGE)); + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index 776814bdb6..69fe3d2968 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -67,6 +67,7 @@ org.openhab.core.io.rest.sitemap org.openhab.core.io.rest.sse org.openhab.core.io.rest.swagger + org.openhab.core.io.rest.transform org.openhab.core.io.rest.ui org.openhab.core.io.rest.voice org.openhab.core.io.transport.mdns diff --git a/features/karaf/openhab-core/src/main/feature/feature.xml b/features/karaf/openhab-core/src/main/feature/feature.xml index 727a8dae8e..2883d6b269 100644 --- a/features/karaf/openhab-core/src/main/feature/feature.xml +++ b/features/karaf/openhab-core/src/main/feature/feature.xml @@ -157,6 +157,11 @@ mvn:org.openhab.core.bundles/org.openhab.core.io.rest.audio/${project.version} + + openhab-core-base + mvn:org.openhab.core.bundles/org.openhab.core.io.rest.transform/${project.version} + + openhab-core-base mvn:org.openhab.core.bundles/org.openhab.core.io.rest.voice/${project.version} @@ -379,12 +384,13 @@ openhab-core-automation-module-media openhab-core-io-console-karaf openhab-core-io-http-auth - openhab-core-io-rest-auth - openhab-core-io-rest-sitemap openhab-core-io-rest-audio - openhab-core-io-rest-voice - openhab-core-io-rest-swagger + openhab-core-io-rest-auth openhab-core-io-rest-mdns + openhab-core-io-rest-sitemap + openhab-core-io-rest-swagger + openhab-core-io-rest-transform + openhab-core-io-rest-voice openhab-core-model-lsp openhab-core-model-item openhab-core-model-persistence