Add a registry for transformation configurations and allow editing them (#2821)
Signed-off-by: Jan N. Klug <github@klug.nrw>pull/2910/head
parent
90f6a95251
commit
53dcf48a17
|
@ -238,6 +238,12 @@
|
|||
<version>${project.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.io.rest.transform</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.io.rest.ui</artifactId>
|
||||
|
|
|
@ -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
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.reactor.bundles</artifactId>
|
||||
<version>3.3.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.core.io.rest.transform</artifactId>
|
||||
|
||||
<name>openHAB Core :: Bundles :: Transformation REST Interface</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.transform</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.io.rest</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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<TransformationConfigurationDTO> 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;
|
||||
}
|
||||
}
|
|
@ -20,6 +20,12 @@
|
|||
<artifactId>org.openhab.core.config.core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.core.bundles</groupId>
|
||||
<artifactId>org.openhab.core.test</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<String> IGNORED_EXTENSIONS = Set.of("txt");
|
||||
private static final Pattern FILENAME_PATTERN = Pattern
|
||||
.compile("(?<filename>.+?)(_(?<language>[a-z]{2}))?\\.(?<extension>[^.]*)$");
|
||||
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<ProviderChangeListener<TransformationConfiguration>> listeners = ConcurrentHashMap.newKeySet();
|
||||
private final Map<Path, TransformationConfiguration> 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<TransformationConfiguration> listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeProviderChangeListener(ProviderChangeListener<TransformationConfiguration> listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<TransformationConfiguration> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<TransformationConfiguration, String, PersistedTransformationConfiguration>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String> {
|
||||
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 (<code>null</code> 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 + "'}";
|
||||
}
|
||||
}
|
|
@ -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<TransformationConfiguration> {
|
||||
|
||||
}
|
|
@ -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<TransformationConfiguration, String> {
|
||||
Pattern CONFIG_UID_PATTERN = Pattern.compile("config:(?<type>\\w+):(?<name>\\w+)(:(?<language>\\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 <code>null</code>)
|
||||
* @return the requested {@link TransformationConfiguration} (or <code>null</code> 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<TransformationConfiguration> getConfigurations(Collection<String> types);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<TransformationConfiguration, String, TransformationConfigurationProvider>
|
||||
implements TransformationConfigurationRegistry {
|
||||
private static final Pattern FILENAME_PATTERN = Pattern
|
||||
.compile("(?<filename>.+)(_(?<language>[a-z]{2}))?\\.(?<extension>[^.]*)$");
|
||||
|
||||
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<TransformationConfiguration> getConfigurations(Collection<String> 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<TransformationConfiguration> provider) {
|
||||
// overridden to make method available for testing
|
||||
super.addProvider(provider);
|
||||
}
|
||||
}
|
|
@ -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<String> 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<Path> 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());
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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."));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -67,6 +67,7 @@
|
|||
<module>org.openhab.core.io.rest.sitemap</module>
|
||||
<module>org.openhab.core.io.rest.sse</module>
|
||||
<module>org.openhab.core.io.rest.swagger</module>
|
||||
<module>org.openhab.core.io.rest.transform</module>
|
||||
<module>org.openhab.core.io.rest.ui</module>
|
||||
<module>org.openhab.core.io.rest.voice</module>
|
||||
<module>org.openhab.core.io.transport.mdns</module>
|
||||
|
|
|
@ -157,6 +157,11 @@
|
|||
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.io.rest.audio/${project.version}</bundle>
|
||||
</feature>
|
||||
|
||||
<feature name="openhab-core-io-rest-transform" version="${project.version}">
|
||||
<feature>openhab-core-base</feature>
|
||||
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.io.rest.transform/${project.version}</bundle>
|
||||
</feature>
|
||||
|
||||
<feature name="openhab-core-io-rest-voice" version="${project.version}">
|
||||
<feature>openhab-core-base</feature>
|
||||
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.io.rest.voice/${project.version}</bundle>
|
||||
|
@ -379,12 +384,13 @@
|
|||
<feature>openhab-core-automation-module-media</feature>
|
||||
<feature>openhab-core-io-console-karaf</feature>
|
||||
<feature>openhab-core-io-http-auth</feature>
|
||||
<feature>openhab-core-io-rest-auth</feature>
|
||||
<feature>openhab-core-io-rest-sitemap</feature>
|
||||
<feature>openhab-core-io-rest-audio</feature>
|
||||
<feature>openhab-core-io-rest-voice</feature>
|
||||
<feature>openhab-core-io-rest-swagger</feature>
|
||||
<feature>openhab-core-io-rest-auth</feature>
|
||||
<feature>openhab-core-io-rest-mdns</feature>
|
||||
<feature>openhab-core-io-rest-sitemap</feature>
|
||||
<feature>openhab-core-io-rest-swagger</feature>
|
||||
<feature>openhab-core-io-rest-transform</feature>
|
||||
<feature>openhab-core-io-rest-voice</feature>
|
||||
<feature>openhab-core-model-lsp</feature>
|
||||
<feature>openhab-core-model-item</feature>
|
||||
<feature>openhab-core-model-persistence</feature>
|
||||
|
|
Loading…
Reference in New Issue