Migrate to JAX-RS Whiteboard (#1443)

* Migrate to JAX-RS Whiteboard
* Upgrade Jackson and Swagger TP feature dependencies

The upstream JAX-RS Whiteboard feature is not used because the version ranges currently do not work with Karaf on Windows.
See: https://issues.apache.org/jira/browse/KARAF-6536

* Update Felix logback to 1.0.2

This version adds the jul-to-slf4j bridge (FELIX-6027).
JUL is used by CXF and using the bridge we can use logback to reduce the verbosity of the info logging whenever org.apache.cxf.endpoint.ServerImpl creates endpoints.

* Update Aries Whiteboard to 1.0.8
* Removes the hated default web application (ARIES-1931)
* Improves performance
* Uses CXF 3.2.12
* Don't reuse SSE event builders

SSE event builders are not thread-safe.
It also causes the wrong default values being used.

Also-by: Markus Rathgeb <maggu2810@gmail.com>
Signed-off-by: Wouter Born <github@maindrain.net>
This commit is contained in:
Wouter Born
2020-05-14 22:32:35 +02:00
committed by GitHub
parent 583ea02429
commit 859da2e1c7
92 changed files with 2060 additions and 1400 deletions
+13 -18
View File
@@ -88,14 +88,6 @@
<version>2.0.0</version>
</dependency>
<!-- EclipseSource JAX RS -->
<dependency>
<groupId>com.eclipsesource.jaxrs</groupId>
<artifactId>publisher</artifactId>
<version>5.3.1</version>
<scope>compile</scope>
</dependency>
<!-- Gson -->
<dependency>
<groupId>org.eclipse.orbit.bundles</groupId>
@@ -236,11 +228,17 @@
<scope>compile</scope>
</dependency>
<!-- Servlet API -->
<!-- Servlet and JAX-RS API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<groupId>org.apache.felix</groupId>
<artifactId>org.apache.felix.http.servlet-api</artifactId>
<version>1.1.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.aries.spec</groupId>
<artifactId>org.apache.aries.javax.jax.rs-api</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
@@ -251,13 +249,10 @@
<version>1.5.5</version>
<scope>compile</scope>
</dependency>
<!-- WR RS API -->
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
<version>2.0.1</version>
<scope>compile</scope>
<groupId>de.maggu2810.jaxrswb.bundles</groupId>
<artifactId>jaxrswb-swagger1-gen</artifactId>
<version>0.0.4</version>
</dependency>
<!-- XStream -->
+6 -6
View File
@@ -238,12 +238,6 @@
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.rest.optimize</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.rest.sitemap</artifactId>
@@ -256,6 +250,12 @@
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.rest.swagger</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.rest.ui</artifactId>
+126 -12
View File
@@ -16,8 +16,10 @@
<name>openHAB Core :: BOM :: Runtime</name>
<properties>
<pax.web.version>7.2.11</pax.web.version>
<jackson.version>2.10.3</jackson.version>
<jetty.version>9.4.20.v20190813</jetty.version>
<pax.web.version>7.2.11</pax.web.version>
<swagger.version>1.6.1</swagger.version>
</properties>
<dependencies>
@@ -113,8 +115,18 @@
<dependency>
<groupId>org.apache.aries.jax.rs</groupId>
<artifactId>org.apache.aries.jax.rs.whiteboard</artifactId>
<version>1.0.1</version>
<version>1.0.8</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>org.apache.geronimo.specs</groupId>
<artifactId>geronimo-annotation_1.3_spec</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.geronimo.specs</groupId>
<artifactId>geronimo-jaxrs_2.1_spec</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- JAX-RS 2.1 API with contract -->
@@ -138,7 +150,7 @@
<version>2.2.11_1</version>
</dependency>
<!-- Java-Activation 1.1 API -->
<!-- Activation 1.1 API -->
<dependency>
<groupId>org.apache.servicemix.specs</groupId>
<artifactId>org.apache.servicemix.specs.activation-api-1.1</artifactId>
@@ -146,6 +158,13 @@
<scope>compile</scope>
</dependency>
<!-- Annotation API -->
<dependency>
<groupId>org.apache.servicemix.specs</groupId>
<artifactId>org.apache.servicemix.specs.annotation-api-1.3</artifactId>
<version>1.3_1</version>
</dependency>
<!-- Stax 1.2 API -->
<dependency>
<groupId>org.apache.servicemix.specs</groupId>
@@ -256,6 +275,13 @@
<scope>compile</scope>
</dependency>
<!-- OSGi ServiceLoader -->
<dependency>
<groupId>org.apache.aries.spifly</groupId>
<artifactId>org.apache.aries.spifly.dynamic.bundle</artifactId>
<version>1.2.3</version>
</dependency>
<!-- Several implementations need to log using SLF4J -->
<dependency>
<groupId>org.slf4j</groupId>
@@ -342,7 +368,7 @@
<dependency>
<groupId>org.apache.felix</groupId>
<artifactId>org.apache.felix.logback</artifactId>
<version>1.0.0</version>
<version>1.0.2</version>
<scope>compile</scope>
</dependency>
<!-- END: logging -->
@@ -868,14 +894,6 @@
<scope>compile</scope>
</dependency>
<!-- ... -->
<dependency>
<groupId>com.eclipsesource.jaxrs</groupId>
<artifactId>publisher</artifactId>
<version>5.3.1</version>
<scope>compile</scope>
</dependency>
<!-- jollyday -->
<dependency>
<groupId>de.jollyday</groupId>
@@ -894,6 +912,102 @@
<scope>compile</scope>
</dependency>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>${jackson.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${jackson.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
<version>${jackson.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-base</artifactId>
<version>${jackson.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
<version>${jackson.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-jaxb-annotations</artifactId>
<version>${jackson.version}</version>
<scope>compile</scope>
</dependency>
<!-- Swagger -->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${swagger.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-core</artifactId>
<version>${swagger.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-jaxrs</artifactId>
<version>${swagger.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>${swagger.version}</version>
<scope>compile</scope>
</dependency>
<!-- JAX-RS Whiteboard Swagger 1 generator -->
<dependency>
<groupId>de.maggu2810.jaxrswb.bundles</groupId>
<artifactId>jaxrswb-swagger1-gen</artifactId>
<version>0.0.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>de.maggu2810.thirdparty.modified.org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.9.10.v20160429-1435</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
+15
View File
@@ -41,6 +41,21 @@
<artifactId>json-path</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.aries.jax.rs</groupId>
<artifactId>org.apache.aries.jax.rs.whiteboard</artifactId>
<version>1.0.8</version>
<exclusions>
<exclusion>
<groupId>org.apache.geronimo.specs</groupId>
<artifactId>geronimo-annotation_1.3_spec</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.geronimo.specs</groupId>
<artifactId>geronimo-jaxrs_2.1_spec</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>
@@ -43,11 +43,17 @@ import org.openhab.core.automation.type.ModuleType;
import org.openhab.core.automation.type.ModuleTypeRegistry;
import org.openhab.core.automation.type.TriggerType;
import org.openhab.core.io.rest.LocaleService;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
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;
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 io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -61,13 +67,21 @@ import io.swagger.annotations.ApiResponses;
* @author Kai Kreuzer - Initial contribution
* @author Markus Rathgeb - Use DTOs
* @author Ana Dimova - extends Module type DTOs with composites
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Path("module-types")
@Api("module-types")
@NonNullByDefault
@Component
@JaxrsResource
@JaxrsName(ModuleTypeResource.PATH_MODULE_TYPES)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(ModuleTypeResource.PATH_MODULE_TYPES)
@Api(ModuleTypeResource.PATH_MODULE_TYPES)
@NonNullByDefault
public class ModuleTypeResource implements RESTResource {
/** The URI path to this resource */
public static final String PATH_MODULE_TYPES = "module-types";
private @NonNullByDefault({}) ModuleTypeRegistry moduleTypeRegistry;
private @NonNullByDefault({}) LocaleService localeService;
@@ -62,11 +62,17 @@ import org.openhab.core.automation.util.RuleBuilder;
import org.openhab.core.config.core.ConfigUtil;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.rest.JSONResponse;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
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;
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;
@@ -82,13 +88,21 @@ import io.swagger.annotations.ResponseHeader;
*
* @author Kai Kreuzer - Initial contribution
* @author Markus Rathgeb - Use DTOs
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Path("rules")
@Api("rules")
@Component
@JaxrsResource
@JaxrsName(RuleResource.PATH_RULES)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(RuleResource.PATH_RULES)
@Api(RuleResource.PATH_RULES)
@RolesAllowed({ Role.ADMIN })
public class RuleResource implements RESTResource {
/** The URI path to this resource */
public static final String PATH_RULES = "rules";
private final Logger logger = LoggerFactory.getLogger(RuleResource.class);
private RuleRegistry ruleRegistry;
@@ -35,11 +35,17 @@ import org.openhab.core.automation.template.RuleTemplate;
import org.openhab.core.automation.template.Template;
import org.openhab.core.automation.template.TemplateRegistry;
import org.openhab.core.io.rest.LocaleService;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
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;
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 io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -51,13 +57,21 @@ import io.swagger.annotations.ApiResponses;
* This class acts as a REST resource for templates and is registered with the Jersey servlet.
*
* @author Kai Kreuzer - Initial contribution
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Path("templates")
@Api("templates")
@NonNullByDefault
@Component
@JaxrsResource
@JaxrsName(TemplateResource.PATH_TEMPLATES)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(TemplateResource.PATH_TEMPLATES)
@Api(TemplateResource.PATH_TEMPLATES)
@NonNullByDefault
public class TemplateResource implements RESTResource {
/** The URI path to this resource */
public static final String PATH_TEMPLATES = "templates";
private @NonNullByDefault({}) TemplateRegistry<@NonNull RuleTemplate> templateRegistry;
private @NonNullByDefault({}) LocaleService localeService;
@@ -21,8 +21,13 @@ import javax.ws.rs.core.Response;
import org.openhab.core.auth.Role;
import org.openhab.core.id.InstanceUUID;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
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 io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -33,10 +38,14 @@ import io.swagger.annotations.ApiResponses;
* This class acts as a REST resource for accessing the UUID of the instance
*
* @author Kai Kreuzer - Initial contribution
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Component
@JaxrsResource
@JaxrsName(UUIDResource.PATH_UUID)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@Path(UUIDResource.PATH_UUID)
@Api(value = UUIDResource.PATH_UUID)
@Api(UUIDResource.PATH_UUID)
@RolesAllowed({ Role.ADMIN })
public class UUIDResource implements RESTResource {
@@ -35,11 +35,17 @@ import org.openhab.core.audio.AudioSource;
import org.openhab.core.auth.Role;
import org.openhab.core.io.rest.JSONResponse;
import org.openhab.core.io.rest.LocaleService;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
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;
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 io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -51,15 +57,20 @@ import io.swagger.annotations.ApiResponses;
* This class acts as a REST resource for audio features.
*
* @author Laurent Garnier - Initial contribution
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Component
@Path(AudioResource.PATH_SITEMAPS)
@JaxrsResource
@JaxrsName(AudioResource.PATH_AUDIO)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(AudioResource.PATH_AUDIO)
@RolesAllowed({ Role.USER, Role.ADMIN })
@Api(value = AudioResource.PATH_SITEMAPS)
@Api(AudioResource.PATH_AUDIO)
@NonNullByDefault
public class AudioResource implements RESTResource {
static final String PATH_SITEMAPS = "audio";
static final String PATH_AUDIO = "audio";
private @Nullable AudioManager audioManager;
private @Nullable LocaleService localeService;
@@ -6,6 +6,13 @@
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
@@ -16,12 +23,5 @@
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>
@@ -24,7 +24,7 @@ import org.osgi.framework.ServiceRegistration;
public class Activator implements BundleActivator {
private static Activator instance;
private ServiceRegistration rolesAllowedDynamicFeatureRegistration;
private ServiceRegistration<?> rolesAllowedDynamicFeatureRegistration;
public static Activator getInstance() {
return instance;
@@ -14,6 +14,7 @@ package org.openhab.core.io.rest.auth.internal;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Optional;
@@ -31,8 +32,6 @@ import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.FeatureContext;
import javax.ws.rs.ext.Provider;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;
import org.glassfish.jersey.server.model.AnnotatedMethod;
import org.openhab.core.auth.Role;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -54,7 +53,7 @@ public class RolesAllowedDynamicFeatureImpl implements DynamicFeature {
@Override
public void configure(ResourceInfo resourceInfo, FeatureContext configuration) {
final AnnotatedMethod am = new AnnotatedMethod(resourceInfo.getResourceMethod());
final Method am = resourceInfo.getResourceMethod();
try {
// DenyAll on the method take precedence over RolesAllowed and PermitAll
if (am.isAnnotationPresent(DenyAll.class)) {
@@ -1,55 +0,0 @@
/**
* Copyright (c) 2010-2020 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.auth.internal;
import java.util.Dictionary;
import java.util.Hashtable;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.http.HttpContext;
import org.osgi.service.http.HttpService;
import com.eclipsesource.jaxrs.publisher.ServletConfiguration;
/**
* Custom servlet configuration for jaxrs handler.
*
* This extension doesn't do much, just forces usage of http context created somewhere else.
*
* @author Łukasz Dywicki - Initial contribution
*/
@Component
public class SmartHomeRestServletConfig implements ServletConfiguration {
private HttpContext httpContext;
@Override
public HttpContext getHttpContext(HttpService httpService, String rootPath) {
return httpContext;
}
@Override
public Dictionary<String, String> getInitParams(HttpService httpService, String rootPath) {
return new Hashtable<>();
}
@Reference
public void setHttpContext(HttpContext httpContext) {
this.httpContext = httpContext;
}
public void unsetHttpContext(HttpContext httpContext) {
this.httpContext = null;
}
}
@@ -46,12 +46,18 @@ import org.openhab.core.auth.PendingToken;
import org.openhab.core.auth.User;
import org.openhab.core.auth.UserRegistry;
import org.openhab.core.auth.UserSession;
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.auth.internal.TokenEndpointException.ErrorType;
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;
@@ -64,10 +70,15 @@ import io.swagger.annotations.ApiResponses;
* This class is used to issue JWT tokens to clients.
*
* @author Yannick Schaus - Initial contribution
* @author Wouter Born - Migrated to JAX-RS Whiteboard Specification
*/
@Path(TokenResource.PATH_AUTH)
@Api(value = TokenResource.PATH_AUTH)
@Component(service = { RESTResource.class, TokenResource.class })
@JaxrsResource
@JaxrsName(TokenResource.PATH_AUTH)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(TokenResource.PATH_AUTH)
@Api(TokenResource.PATH_AUTH)
public class TokenResource implements RESTResource {
private final Logger logger = LoggerFactory.getLogger(TokenResource.class);
@@ -1,2 +0,0 @@
Bundle-SymbolicName: ${project.artifactId}
Bundle-Activator: org.openhab.core.io.rest.core.internal.RESTCoreActivator
@@ -0,0 +1,61 @@
/**
* Copyright (c) 2010-2020 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.core.internal;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import com.google.gson.Gson;
/**
* A message body reader for JSON using GSON.
*
* @author Markus Rathgeb - Initial contribution
*/
public class GsonMessageBodyReader<T> implements MessageBodyReader<T> {
private final Gson gson;
/**
* Constructor.
*
* @param gson the GSON object to use
*/
public GsonMessageBodyReader(final Gson gson) {
this.gson = gson;
}
@Override
public boolean isReadable(final Class<?> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType) {
return true;
}
@Override
public T readFrom(final Class<T> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType, final MultivaluedMap<String, String> httpHeaders, final InputStream entityStream)
throws IOException, WebApplicationException {
try (InputStreamReader reader = new InputStreamReader(entityStream, StandardCharsets.UTF_8)) {
return gson.fromJson(reader, type);
}
}
}
@@ -0,0 +1,92 @@
/**
* Copyright (c) 2010-2020 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.core.internal;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.ext.MessageBodyWriter;
import org.openhab.core.io.rest.JSONInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* A message body writer for JSON using GSON.
*
* @author Markus Rathgeb - Initial contribution
*/
public class GsonMessageBodyWriter<T> implements MessageBodyWriter<T> {
private final Logger logger = LoggerFactory.getLogger(GsonMessageBodyWriter.class);
private final Gson gson;
/**
* Constructor.
*
* @param gson the GSON object to use
*/
public GsonMessageBodyWriter(final Gson gson) {
this.gson = gson;
}
@Override
public long getSize(final T object, final Class<?> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType) {
return -1;
}
@Override
public boolean isWriteable(final Class<?> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType) {
return true;
}
@Override
public void writeTo(final T object, final Class<?> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType, final MultivaluedMap<String, Object> httpHeaders,
final OutputStream entityStream) throws IOException, WebApplicationException {
if (object == null) {
return;
}
// Log a message if a response builder is received.
if (object instanceof ResponseBuilder) {
logger.error(
"A REST endpoint returns a ResponseBuilder object. This is mostly wrong and the call to \"build()\" is missing. Please report or fix it. Got: {}",
object);
}
if (object instanceof InputStream && object instanceof JSONInputStream) {
((InputStream) object).transferTo(entityStream);
} else {
entityStream.write(gson.toJson(object).getBytes(StandardCharsets.UTF_8));
}
// Flush the stream.
// Keep this code as it has been present before,
// but I don't think this needs to be done in the message body writer itself.
entityStream.flush();
}
}
@@ -1,89 +0,0 @@
/**
* Copyright (c) 2010-2020 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.core.internal;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import javax.ws.rs.Consumes;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import org.osgi.service.component.annotations.Component;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* JSON reader/writer for Jersey using GSON.
*
* @author Simon Kaufmann - Initial contribution
*
* @param <T>
*/
@Provider
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Component(immediate = true)
public class GsonProvider<T> implements MessageBodyReader<T>, MessageBodyWriter<T> {
private final Gson gson;
public GsonProvider() {
gson = new GsonBuilder().create();
}
@Override
public long getSize(T t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return -1;
}
@Override
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return true;
}
@Override
public void writeTo(T object, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream)
throws IOException, WebApplicationException {
try (OutputStream stream = entityStream) {
entityStream.write(gson.toJson(object).getBytes(StandardCharsets.UTF_8));
entityStream.flush();
}
}
@Override
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return true;
}
@Override
public T readFrom(Class<T> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
throws IOException, WebApplicationException {
try (InputStreamReader reader = new InputStreamReader(entityStream, StandardCharsets.UTF_8)) {
return gson.fromJson(reader, type);
}
}
}
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2020 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.core.internal;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.rest.JSONResponse;
import org.openhab.core.io.rest.RESTConstants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsExtension;
/**
* Trap exceptions.
*
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Component
@JaxrsExtension
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@NonNullByDefault
public class JSONResponseExceptionMapper implements ExceptionMapper<@NonNull Exception> {
private final ExceptionMapper<Exception> delegate = new JSONResponse.ExceptionMapper();
@Override
public Response toResponse(Exception e) {
return delegate.toResponse(e);
}
}
@@ -0,0 +1,108 @@
/**
* Copyright (c) 2010-2020 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.core.internal;
import static org.osgi.service.component.annotations.ServiceScope.PROTOTYPE;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import org.openhab.core.io.rest.RESTConstants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsExtension;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsMediaType;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* A media type extension for all supported media types.
*
* @author Markus Rathgeb - Initial contribution
*/
@Component(scope = PROTOTYPE)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JaxrsExtension
@JaxrsMediaType({ MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN })
public class MediaTypeExtension<T> implements MessageBodyReader<T>, MessageBodyWriter<T> {
private static String mediaTypeWithoutParams(final MediaType mediaType) {
return mediaType.getType() + "/" + mediaType.getSubtype();
}
private final Map<String, MessageBodyReader<T>> readers = new HashMap<>();
private final Map<String, MessageBodyWriter<T>> writers = new HashMap<>();
/**
* Constructor.
*/
public MediaTypeExtension() {
final Gson gson = new GsonBuilder().create();
readers.put(mediaTypeWithoutParams(MediaType.APPLICATION_JSON_TYPE), new GsonMessageBodyReader<>(gson));
readers.put(mediaTypeWithoutParams(MediaType.TEXT_PLAIN_TYPE), new PlainMessageBodyReader<>());
writers.put(mediaTypeWithoutParams(MediaType.APPLICATION_JSON_TYPE), new GsonMessageBodyWriter<>(gson));
writers.put(mediaTypeWithoutParams(MediaType.TEXT_PLAIN_TYPE), new PlainMessageBodyWriter<>());
}
@Override
public boolean isWriteable(final Class<?> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType) {
final MessageBodyWriter<T> writer = writers.get(mediaTypeWithoutParams(mediaType));
return writer != null && writer.isWriteable(type, genericType, annotations, mediaType);
}
@Override
public void writeTo(final T object, final Class<?> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType, final MultivaluedMap<String, Object> httpHeaders,
final OutputStream entityStream) throws IOException, WebApplicationException {
final MessageBodyWriter<T> writer = writers.get(mediaTypeWithoutParams(mediaType));
if (writer != null) {
writer.writeTo(object, type, genericType, annotations, mediaType, httpHeaders, entityStream);
} else {
throw new InternalServerErrorException("unsupported media type");
}
}
@Override
public boolean isReadable(final Class<?> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType) {
final MessageBodyReader<T> reader = readers.get(mediaTypeWithoutParams(mediaType));
return reader != null && reader.isReadable(type, genericType, annotations, mediaType);
}
@Override
public T readFrom(final Class<T> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType, final MultivaluedMap<String, String> httpHeaders, final InputStream entityStream)
throws IOException, WebApplicationException {
final MessageBodyReader<T> reader = readers.get(mediaTypeWithoutParams(mediaType));
if (reader != null) {
return reader.readFrom(type, genericType, annotations, mediaType, httpHeaders, entityStream);
} else {
throw new InternalServerErrorException("unsupported media type");
}
}
}
@@ -0,0 +1,78 @@
/**
* Copyright (c) 2010-2020 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.core.internal;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A message body reader for plain text.
*
* @author Markus Rathgeb - Initial contribution
*/
public class PlainMessageBodyReader<T> implements MessageBodyReader<T> {
private final Logger logger = LoggerFactory.getLogger(PlainMessageBodyReader.class);
@Override
public boolean isReadable(final Class<?> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType) {
return true;
}
@Override
public T readFrom(final Class<T> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType, final MultivaluedMap<String, String> httpHeaders, final InputStream entityStream)
throws IOException, WebApplicationException {
final byte[] data = data(entityStream);
logger.debug("Received: type={}, genericType={}, annotations={}, mediaType={}, httpHeaders={}", type,
genericType, annotations, mediaType, httpHeaders);
if (type.equals(String.class) || genericType.equals(String.class)) {
return (T) new String(data, StandardCharsets.UTF_8);
} else if (type.equals(byte[].class) || genericType.equals(byte[].class)) {
return (T) data;
} else if (type.equals(Byte[].class) || genericType.equals(Byte[].class)) {
final Byte[] dataB = new Byte[data.length];
for (int i = 0; i < data.length; ++i) {
dataB[i] = data[i];
}
return (T) dataB;
} else {
throw new InternalServerErrorException(
String.format("Cannot assign text plain to type \"%s\", generic type: \"%s\".", type, genericType));
}
}
private static byte[] data(final InputStream is) throws IOException {
final ByteArrayOutputStream outputBytes = new ByteArrayOutputStream();
int read;
final byte[] buffer = new byte[1024];
while ((read = is.read(buffer)) != -1) {
outputBytes.write(buffer, 0, read);
}
return outputBytes.toByteArray();
}
}
@@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2020 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.core.internal;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyWriter;
/**
* A message body writer for plain text.
*
* @author Markus Rathgeb - Initial contribution
*/
public class PlainMessageBodyWriter<T> implements MessageBodyWriter<T> {
@Override
public long getSize(final T object, final Class<?> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType) {
return -1;
}
@Override
public boolean isWriteable(final Class<?> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType) {
return true;
}
@Override
public void writeTo(final T object, final Class<?> type, final Type genericType, final Annotation[] annotations,
final MediaType mediaType, final MultivaluedMap<String, Object> httpHeaders,
final OutputStream entityStream) throws IOException, WebApplicationException {
if (object == null) {
return;
}
entityStream.write(object.toString().getBytes(StandardCharsets.UTF_8));
}
}
@@ -1,54 +0,0 @@
/**
* Copyright (c) 2010-2020 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.core.internal;
import org.openhab.core.io.rest.JSONResponse;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
/**
* Extension of the default OSGi bundle activator
*
* @author Kai Kreuzer - Initial contribution
* @author Benedikt Niehues - made serviceRegistration of ExceptionMapper compatible with older OSGI versions
*/
public class RESTCoreActivator implements BundleActivator {
private static BundleContext context;
@SuppressWarnings("rawtypes")
private ServiceRegistration mExcMapper;
/**
* Called whenever the OSGi framework starts our bundle
*/
@Override
public void start(BundleContext bc) throws Exception {
context = bc;
mExcMapper = bc.registerService(JSONResponse.ExceptionMapper.class.getName(),
new JSONResponse.ExceptionMapper(), null);
}
/**
* Called whenever the OSGi framework stops our bundle
*/
@Override
public void stop(BundleContext context) throws Exception {
RESTCoreActivator.context = null;
mExcMapper.unregister();
}
public static BundleContext getBundleContext() {
return context;
}
}
@@ -43,6 +43,7 @@ import org.openhab.core.config.core.ConfigDescriptionRegistry;
import org.openhab.core.config.core.ConfigUtil;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.rest.LocaleService;
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.core.config.ConfigurationService;
@@ -50,6 +51,11 @@ 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;
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;
@@ -67,11 +73,16 @@ import io.swagger.annotations.ApiResponses;
* @author Kai Kreuzer - refactored for using the OSGi JAX-RS connector
* @author Yordan Zhelev - Added Swagger annotations
* @author Franck Dechavanne - Added DTOs to ApiResponses
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Component
@JaxrsResource
@JaxrsName(BindingResource.PATH_BINDINGS)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(BindingResource.PATH_BINDINGS)
@RolesAllowed({ Role.ADMIN })
@Api(value = BindingResource.PATH_BINDINGS)
@Component
@Api(BindingResource.PATH_BINDINGS)
public class BindingResource implements RESTResource {
/** The URI path to this resource */
@@ -41,6 +41,7 @@ import org.openhab.core.config.core.dto.ConfigDescriptionDTOMapper;
import org.openhab.core.config.core.dto.ConfigDescriptionParameterDTO;
import org.openhab.core.config.core.dto.ConfigDescriptionParameterGroupDTO;
import org.openhab.core.io.rest.LocaleService;
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.thing.dto.ChannelTypeDTO;
@@ -55,6 +56,11 @@ 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;
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 io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -68,11 +74,16 @@ import io.swagger.annotations.ApiResponses;
* @author Chris Jackson - Initial contribution
* @author Franck Dechavanne - Added DTOs to ApiResponses
* @author Yannick Schaus - Added filter to getAll
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Component
@JaxrsResource
@JaxrsName(ChannelTypeResource.PATH_CHANNEL_TYPES)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(ChannelTypeResource.PATH_CHANNEL_TYPES)
@RolesAllowed({ Role.ADMIN })
@Api(value = ChannelTypeResource.PATH_CHANNEL_TYPES)
@Component
@Api(ChannelTypeResource.PATH_CHANNEL_TYPES)
public class ChannelTypeResource implements RESTResource {
/** The URI path to this resource */
@@ -36,6 +36,7 @@ import org.openhab.core.config.core.ConfigDescriptionRegistry;
import org.openhab.core.config.core.dto.ConfigDescriptionDTO;
import org.openhab.core.io.rest.JSONResponse;
import org.openhab.core.io.rest.LocaleService;
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.core.config.EnrichedConfigDescriptionDTOMapper;
@@ -43,6 +44,11 @@ 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;
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 io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -56,11 +62,16 @@ import io.swagger.annotations.ApiResponses;
* @author Dennis Nobel - Initial contribution
* @author Chris Jackson - Modify response to use JSONResponse
* @author Franck Dechavanne - Added DTOs to ApiResponses
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Component
@JaxrsResource
@JaxrsName(ConfigDescriptionResource.PATH_CONFIG_DESCRIPTIONS)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(ConfigDescriptionResource.PATH_CONFIG_DESCRIPTIONS)
@RolesAllowed({ Role.ADMIN })
@Api(value = ConfigDescriptionResource.PATH_CONFIG_DESCRIPTIONS)
@Component
@Api(ConfigDescriptionResource.PATH_CONFIG_DESCRIPTIONS)
@NonNullByDefault
public class ConfigDescriptionResource implements RESTResource {
@@ -29,11 +29,17 @@ import javax.ws.rs.core.UriInfo;
import org.openhab.core.auth.Role;
import org.openhab.core.config.discovery.DiscoveryServiceRegistry;
import org.openhab.core.config.discovery.ScanListener;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
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;
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;
@@ -52,11 +58,16 @@ import io.swagger.annotations.ApiResponses;
* @author Yordan Zhelev - Added Swagger annotations
* @author Ivaylo Ivanov - Added payload to the response of <code>scan</code>
* @author Franck Dechavanne - Added DTOs to ApiResponses
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Component(service = { RESTResource.class, DiscoveryResource.class })
@JaxrsResource
@JaxrsName(DiscoveryResource.PATH_DISCOVERY)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(DiscoveryResource.PATH_DISCOVERY)
@RolesAllowed({ Role.ADMIN })
@Api(value = DiscoveryResource.PATH_DISCOVERY)
@Component(service = { RESTResource.class, DiscoveryResource.class })
@Api(DiscoveryResource.PATH_DISCOVERY)
public class DiscoveryResource implements RESTResource {
/** The URI path to this resource */
@@ -36,6 +36,7 @@ import org.openhab.core.config.discovery.dto.DiscoveryResultDTO;
import org.openhab.core.config.discovery.dto.DiscoveryResultDTOMapper;
import org.openhab.core.config.discovery.inbox.Inbox;
import org.openhab.core.io.rest.JSONResponse;
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.thing.Thing;
@@ -44,6 +45,11 @@ 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;
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;
@@ -62,11 +68,16 @@ import io.swagger.annotations.ApiResponses;
* @author Yordan Zhelev - Added Swagger annotations
* @author Chris Jackson - Updated to use JSONResponse. Fixed null response from approve. Improved error reporting.
* @author Franck Dechavanne - Added DTOs to ApiResponses
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Component(service = { RESTResource.class, InboxResource.class })
@JaxrsResource
@JaxrsName(InboxResource.PATH_INBOX)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(InboxResource.PATH_INBOX)
@RolesAllowed({ Role.ADMIN })
@Api(value = InboxResource.PATH_INBOX)
@Component(service = { RESTResource.class, InboxResource.class })
@Api(InboxResource.PATH_INBOX)
public class InboxResource implements RESTResource {
private final Logger logger = LoggerFactory.getLogger(InboxResource.class);
@@ -45,12 +45,18 @@ import org.openhab.core.extension.ExtensionService;
import org.openhab.core.extension.ExtensionType;
import org.openhab.core.io.rest.JSONResponse;
import org.openhab.core.io.rest.LocaleService;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
import org.openhab.core.io.rest.Stream2JSONInputStream;
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;
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;
@@ -65,11 +71,16 @@ import io.swagger.annotations.ApiResponses;
*
* @author Kai Kreuzer - Initial contribution
* @author Franck Dechavanne - Added DTOs to ApiResponses
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Component
@JaxrsResource
@JaxrsName(ExtensionResource.PATH_EXTENSIONS)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(ExtensionResource.PATH_EXTENSIONS)
@RolesAllowed({ Role.ADMIN })
@Api(value = ExtensionResource.PATH_EXTENSIONS)
@Component
@Api(ExtensionResource.PATH_EXTENSIONS)
public class ExtensionResource implements RESTResource {
private static final String THREAD_POOL_NAME = "extensionService";
@@ -194,6 +205,7 @@ public class ExtensionResource implements RESTResource {
@POST
@Path("/{extensionId: [a-zA-Z_0-9-:]+}/uninstall")
@ApiOperation(value = "Uninstalls the extension with the given ID.")
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
public Response uninstallExtension(
final @PathParam("extensionId") @ApiParam(value = "extension ID", required = true) String extensionId) {
@@ -18,6 +18,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Stream;
@@ -41,6 +42,7 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -50,6 +52,7 @@ import org.openhab.core.events.EventPublisher;
import org.openhab.core.io.rest.DTOMapper;
import org.openhab.core.io.rest.JSONResponse;
import org.openhab.core.io.rest.LocaleService;
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.core.item.EnrichedGroupItemDTO;
@@ -80,6 +83,11 @@ 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;
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;
@@ -111,21 +119,39 @@ import io.swagger.annotations.ApiResponses;
* @author Jörg Plewe - refactoring, error handling
* @author Franck Dechavanne - Added DTOs to ApiResponses
* @author Stefan Triller - Added bulk item add method
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@NonNullByDefault
@Path(ItemResource.PATH_ITEMS)
@Api(value = ItemResource.PATH_ITEMS)
@Component
@JaxrsResource
@JaxrsName(ItemResource.PATH_ITEMS)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(ItemResource.PATH_ITEMS)
@Api(ItemResource.PATH_ITEMS)
@NonNullByDefault
public class ItemResource implements RESTResource {
private final Logger logger = LoggerFactory.getLogger(ItemResource.class);
/** The URI path to this resource */
public static final String PATH_ITEMS = "items";
@Context
@NonNullByDefault({})
UriInfo uriInfo;
/**
* Replaces part of the URI builder by forwarded headers.
*
* @param uriBuilder the URI builder
* @param httpHeaders the HTTP headers
*/
private static void respectForwarded(final UriBuilder uriBuilder, final @Context HttpHeaders httpHeaders) {
Optional.ofNullable(httpHeaders.getHeaderString("X-Forwarded-Host")).ifPresent(host -> {
final String[] parts = host.split(":");
uriBuilder.host(parts[0]);
if (parts.length > 1) {
uriBuilder.port(Integer.parseInt(parts[1]));
}
});
Optional.ofNullable(httpHeaders.getHeaderString("X-Forwarded-Proto")).ifPresent(uriBuilder::scheme);
}
private final Logger logger = LoggerFactory.getLogger(ItemResource.class);
private @NonNullByDefault({}) ItemRegistry itemRegistry;
private @NonNullByDefault({}) MetadataRegistry metadataRegistry;
@@ -208,13 +234,20 @@ public class ItemResource implements RESTResource {
this.itemBuilderFactory = null;
}
private UriBuilder uriBuilder(final UriInfo uriInfo, final HttpHeaders httpHeaders) {
final UriBuilder uriBuilder = uriInfo.getAbsolutePathBuilder();
respectForwarded(uriBuilder, httpHeaders);
uriBuilder.path("{itemName}");
return uriBuilder;
}
@GET
@RolesAllowed({ Role.USER, Role.ADMIN })
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Get all available items.", response = EnrichedItemDTO.class, responseContainer = "List")
@ApiResponses(value = {
@ApiResponse(code = 200, message = "OK", response = EnrichedItemDTO.class, responseContainer = "List") })
public Response getItems(
public Response getItems(final @Context UriInfo uriInfo, final @Context HttpHeaders httpHeaders,
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @ApiParam(value = "language") @Nullable String language,
@QueryParam("type") @ApiParam(value = "item type filter", required = false) @Nullable String type,
@QueryParam("tags") @ApiParam(value = "item tag filter", required = false) @Nullable String tags,
@@ -223,10 +256,11 @@ public class ItemResource implements RESTResource {
@QueryParam("fields") @ApiParam(value = "limit output to the given fields (comma separated)", required = false) @Nullable String fields) {
final Locale locale = localeService.getLocale(language);
final Set<String> namespaces = splitAndFilterNamespaces(namespaceSelector, locale);
logger.debug("Received HTTP GET request at '{}'", uriInfo.getPath());
final UriBuilder uriBuilder = uriBuilder(uriInfo, httpHeaders);
Stream<EnrichedItemDTO> itemStream = getItems(type, tags).stream() //
.map(item -> EnrichedItemDTOMapper.map(item, recursive, null, uriInfo.getBaseUri(), locale)) //
.map(item -> EnrichedItemDTOMapper.map(item, recursive, null, uriBuilder, locale)) //
.peek(dto -> addMetadata(dto, namespaces, null)) //
.peek(dto -> dto.editable = isEditable(dto.name));
itemStream = dtoMapper.limitToFields(itemStream, fields);
@@ -240,26 +274,23 @@ public class ItemResource implements RESTResource {
@ApiOperation(value = "Gets a single item.", response = EnrichedItemDTO.class)
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK", response = EnrichedItemDTO.class),
@ApiResponse(code = 404, message = "Item not found") })
public Response getItemData(
public Response getItemData(final @Context UriInfo uriInfo, final @Context HttpHeaders httpHeaders,
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @ApiParam(value = "language") @Nullable String language,
@QueryParam("metadata") @ApiParam(value = "metadata selector", required = false) @Nullable String namespaceSelector,
@PathParam("itemname") @ApiParam(value = "item name", required = true) String itemname) {
final Locale locale = localeService.getLocale(language);
final Set<String> namespaces = splitAndFilterNamespaces(namespaceSelector, locale);
logger.debug("Received HTTP GET request at '{}'", uriInfo.getPath());
// get item
Item item = getItem(itemname);
// if it exists
if (item != null) {
logger.debug("Received HTTP GET request at '{}'.", uriInfo.getPath());
EnrichedItemDTO dto = EnrichedItemDTOMapper.map(item, true, null, uriInfo.getBaseUri(), locale);
EnrichedItemDTO dto = EnrichedItemDTOMapper.map(item, true, null, uriBuilder(uriInfo, httpHeaders), locale);
addMetadata(dto, namespaces, null);
dto.editable = isEditable(dto.name);
return JSONResponse.createResponse(Status.OK, dto, null);
} else {
logger.info("Received HTTP GET request at '{}' for the unknown item '{}'.", uriInfo.getPath(), itemname);
return getItemNotFoundResponse(itemname);
}
}
@@ -287,13 +318,10 @@ public class ItemResource implements RESTResource {
// if it exists
if (item != null) {
logger.debug("Received HTTP GET request at '{}'.", uriInfo.getPath());
// we cannot use JSONResponse.createResponse() bc. MediaType.TEXT_PLAIN
// return JSONResponse.createResponse(Status.OK, item.getState().toString(), null);
return Response.ok(item.getState().toFullString()).build();
} else {
logger.info("Received HTTP GET request at '{}' for the unknown item '{}'.", uriInfo.getPath(), itemname);
return getItemNotFoundResponse(itemname);
}
}
@@ -322,18 +350,14 @@ public class ItemResource implements RESTResource {
if (state != null) {
// set State and report OK
logger.debug("Received HTTP PUT request at '{}' with value '{}'.", uriInfo.getPath(), value);
eventPublisher.post(ItemEventFactory.createStateEvent(itemname, state));
return getItemResponse(Status.ACCEPTED, null, locale, null);
return getItemResponse(null, Status.ACCEPTED, null, locale, null);
} else {
// State could not be parsed
logger.warn("Received HTTP PUT request at '{}' with an invalid status value '{}'.", uriInfo.getPath(),
value);
return JSONResponse.createErrorResponse(Status.BAD_REQUEST, "State could not be parsed: " + value);
}
} else {
// Item does not exist
logger.info("Received HTTP PUT request at '{}' for the unknown item '{}'.", uriInfo.getPath(), itemname);
return getItemNotFoundResponse(itemname);
}
}
@@ -369,18 +393,14 @@ public class ItemResource implements RESTResource {
command = TypeParser.parseCommand(item.getAcceptedCommandTypes(), value);
}
if (command != null) {
logger.debug("Received HTTP POST request at '{}' with value '{}'.", uriInfo.getPath(), value);
eventPublisher.post(ItemEventFactory.createCommandEvent(itemname, command));
ResponseBuilder resbuilder = Response.ok();
resbuilder.type(MediaType.TEXT_PLAIN);
return resbuilder.build();
} else {
logger.warn("Received HTTP POST request at '{}' with an invalid status value '{}'.", uriInfo.getPath(),
value);
return Response.status(Status.BAD_REQUEST).build();
}
} else {
logger.info("Received HTTP POST request at '{}' for the unknown item '{}'.", uriInfo.getPath(), itemname);
throw new WebApplicationException(404);
}
}
@@ -469,7 +489,6 @@ public class ItemResource implements RESTResource {
@ApiResponse(code = 404, message = "Item not found or item is not editable.") })
public Response removeItem(@PathParam("itemname") @ApiParam(value = "item name", required = true) String itemname) {
if (managedItemProvider.remove(itemname) == null) {
logger.info("Received HTTP DELETE request at '{}' for the unknown item '{}'.", uriInfo.getPath(), itemname);
return Response.status(Status.NOT_FOUND).build();
}
return Response.ok(null, MediaType.TEXT_PLAIN).build();
@@ -487,7 +506,6 @@ public class ItemResource implements RESTResource {
Item item = getItem(itemname);
if (item == null) {
logger.info("Received HTTP PUT request at '{}' for the unknown item '{}'.", uriInfo.getPath(), itemname);
return Response.status(Status.NOT_FOUND).build();
}
@@ -513,7 +531,6 @@ public class ItemResource implements RESTResource {
Item item = getItem(itemname);
if (item == null) {
logger.info("Received HTTP DELETE request at '{}' for the unknown item '{}'.", uriInfo.getPath(), itemname);
return Response.status(Status.NOT_FOUND).build();
}
@@ -544,14 +561,11 @@ public class ItemResource implements RESTResource {
Item item = getItem(itemname);
if (item == null) {
logger.info("Received HTTP PUT request at '{}' for the unknown item '{}'.", uriInfo.getPath(), itemname);
return Response.status(Status.NOT_FOUND).build();
}
String value = metadata.value;
if (value == null || value.isEmpty()) {
logger.info("Received HTTP PUT request at '{}' for item '{}' with empty metadata.", uriInfo.getPath(),
itemname);
return Response.status(Status.BAD_REQUEST).build();
}
@@ -579,20 +593,15 @@ public class ItemResource implements RESTResource {
Item item = getItem(itemname);
if (item == null) {
logger.info("Received HTTP DELETE request at '{}' for the unknown item '{}'.", uriInfo.getPath(), itemname);
return Response.status(Status.NOT_FOUND).build();
}
MetadataKey key = new MetadataKey(namespace, itemname);
if (metadataRegistry.get(key) != null) {
if (metadataRegistry.remove(key) == null) {
logger.info("Received HTTP DELETE request at '{}' for unmanaged item meta-data '{}'.",
uriInfo.getPath(), key);
return Response.status(Status.CONFLICT).build();
}
} else {
logger.info("Received HTTP DELETE request at '{}' for unknown item meta-data '{}'.", uriInfo.getPath(),
key);
return Response.status(Status.NOT_FOUND).build();
}
@@ -615,7 +624,7 @@ public class ItemResource implements RESTResource {
@ApiResponse(code = 201, message = "Item created."), @ApiResponse(code = 400, message = "Item null."),
@ApiResponse(code = 404, message = "Item not found."),
@ApiResponse(code = 405, message = "Item not editable.") })
public Response createOrUpdateItem(
public Response createOrUpdateItem(final @Context UriInfo uriInfo, final @Context HttpHeaders httpHeaders,
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @ApiParam(value = "language") String language,
@PathParam("itemname") @ApiParam(value = "item name", required = true) String itemname,
@ApiParam(value = "item data", required = true) @Nullable GroupItemDTO item) {
@@ -637,11 +646,13 @@ public class ItemResource implements RESTResource {
if (getItem(itemname) == null) {
// item does not yet exist, create it
managedItemProvider.add(newItem);
return getItemResponse(Status.CREATED, itemRegistry.get(itemname), locale, null);
return getItemResponse(uriBuilder(uriInfo, httpHeaders), Status.CREATED, itemRegistry.get(itemname), locale,
null);
} else if (managedItemProvider.get(itemname) != null) {
// item already exists as a managed item, update it
managedItemProvider.update(newItem);
return getItemResponse(Status.OK, itemRegistry.get(itemname), locale, null);
return getItemResponse(uriBuilder(uriInfo, httpHeaders), Status.OK, itemRegistry.get(itemname), locale,
null);
} else {
// Item exists but cannot be updated
logger.warn("Cannot update existing item '{}', because is not managed.", itemname);
@@ -708,8 +719,8 @@ public class ItemResource implements RESTResource {
List<JsonObject> responseList = new ArrayList<>();
for (GroupItemDTO item : wrongTypes) {
responseList.add(buildStatusObject(item.name, "error", "Received HTTP PUT request at '" + uriInfo.getPath()
+ "' with an invalid item type '" + item.type + "'."));
responseList.add(buildStatusObject(item.name, "error",
"Received HTTP PUT request with an invalid item type '" + item.type + "'."));
}
for (Item item : failedItems) {
responseList.add(buildStatusObject(item.getName(), "error", "Cannot update non-managed item"));
@@ -746,14 +757,16 @@ public class ItemResource implements RESTResource {
/**
* Prepare a response representing the Item depending in the status.
*
* @param uriBuilder the URI builder
* @param status
* @param item can be null
* @param locale the locale
* @param errormessage optional message in case of error
* @return Response configured to represent the Item in depending on the status
*/
private Response getItemResponse(Status status, @Nullable Item item, Locale locale, @Nullable String errormessage) {
Object entity = null != item ? EnrichedItemDTOMapper.map(item, true, null, uriInfo.getBaseUri(), locale) : null;
private Response getItemResponse(final @Nullable UriBuilder uriBuilder, Status status, @Nullable Item item,
Locale locale, @Nullable String errormessage) {
Object entity = null != item ? EnrichedItemDTOMapper.map(item, true, null, uriBuilder, locale) : null;
return JSONResponse.createResponse(status, entity, errormessage);
}
@@ -34,6 +34,7 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.auth.Role;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.rest.JSONResponse;
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.thing.ChannelUID;
@@ -45,6 +46,11 @@ 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;
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 io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -60,10 +66,16 @@ import io.swagger.annotations.ApiResponses;
* @author Kai Kreuzer - Removed Thing links and added auto link url
* @author Franck Dechavanne - Added DTOs to ApiResponses
* @author Yannick Schaus - Added filters to getAll
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Path(ItemChannelLinkResource.PATH_LINKS)
@Api(value = ItemChannelLinkResource.PATH_LINKS)
@Component(service = { RESTResource.class, ItemChannelLinkResource.class })
@JaxrsResource
@JaxrsName(ItemChannelLinkResource.PATH_LINKS)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(ItemChannelLinkResource.PATH_LINKS)
@RolesAllowed({ Role.ADMIN })
@Api(ItemChannelLinkResource.PATH_LINKS)
public class ItemChannelLinkResource implements RESTResource {
/** The URI path to this resource */
@@ -39,6 +39,7 @@ import org.openhab.core.auth.Role;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.rest.JSONResponse;
import org.openhab.core.io.rest.LocaleService;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException;
@@ -61,6 +62,11 @@ 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;
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;
@@ -79,10 +85,15 @@ import io.swagger.annotations.ApiResponses;
* @author Franck Dechavanne - Added DTOs to ApiResponses
* @author Erdoan Hadzhiyusein - Adapted the convertTime() method to work with the new DateTimeType
* @author Lyubomir Papazov - Change java.util.Date references to be of type java.time.ZonedDateTime
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Path(PersistenceResource.PATH)
@Api(value = PersistenceResource.PATH)
@Component
@JaxrsResource
@JaxrsName(PersistenceResource.PATH_PERSISTENCE)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(PersistenceResource.PATH_PERSISTENCE)
@Api(PersistenceResource.PATH_PERSISTENCE)
public class PersistenceResource implements RESTResource {
private final Logger logger = LoggerFactory.getLogger(PersistenceResource.class);
@@ -92,7 +103,7 @@ public class PersistenceResource implements RESTResource {
private static final String STANDARD = "Standard";
// The URI path to this resource
public static final String PATH = "persistence";
public static final String PATH_PERSISTENCE = "persistence";
private ItemRegistry itemRegistry;
private PersistenceServiceRegistry persistenceServiceRegistry;
@@ -30,6 +30,7 @@ import javax.ws.rs.core.Response;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.auth.Role;
import org.openhab.core.io.rest.LocaleService;
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.items.ItemUtil;
@@ -46,6 +47,11 @@ 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;
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;
@@ -59,11 +65,16 @@ import io.swagger.annotations.ApiResponses;
* REST resource to obtain profile-types
*
* @author Stefan Triller - Initial contribution
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Component
@JaxrsResource
@JaxrsName(ProfileTypeResource.PATH_PROFILE_TYPES)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(ProfileTypeResource.PATH_PROFILE_TYPES)
@RolesAllowed({ Role.ADMIN })
@Api(value = ProfileTypeResource.PATH_PROFILE_TYPES)
@Component
@Api(ProfileTypeResource.PATH_PROFILE_TYPES)
public class ProfileTypeResource implements RESTResource {
/** The URI path to this resource */
@@ -42,19 +42,26 @@ import org.openhab.core.config.core.ConfigDescriptionRegistry;
import org.openhab.core.config.core.ConfigUtil;
import org.openhab.core.config.core.ConfigurableService;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
import org.openhab.core.io.rest.core.config.ConfigurationService;
import org.openhab.core.io.rest.core.internal.RESTCoreActivator;
import org.openhab.core.io.rest.core.service.ConfigurableServiceDTO;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.component.ComponentConstants;
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;
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;
@@ -70,11 +77,16 @@ import io.swagger.annotations.ApiResponses;
*
* @author Dennis Nobel - Initial contribution
* @author Franck Dechavanne - Added DTOs to ApiResponses
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Component(service = { RESTResource.class, ConfigurableServiceResource.class })
@JaxrsResource
@JaxrsName(ConfigurableServiceResource.PATH_SERVICES)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(ConfigurableServiceResource.PATH_SERVICES)
@RolesAllowed({ Role.ADMIN })
@Api(value = ConfigurableServiceResource.PATH_SERVICES)
@Component(service = { RESTResource.class, ConfigurableServiceResource.class })
@Api(ConfigurableServiceResource.PATH_SERVICES)
public class ConfigurableServiceResource implements RESTResource {
/** The URI path to this resource */
@@ -91,9 +103,16 @@ public class ConfigurableServiceResource implements RESTResource {
private final Logger logger = LoggerFactory.getLogger(ConfigurableServiceResource.class);
private final BundleContext bundleContext;
private ConfigurationService configurationService;
private ConfigDescriptionRegistry configDescRegistry;
@Activate
public ConfigurableServiceResource(final BundleContext bundleContext) {
this.bundleContext = bundleContext;
}
@GET
@Produces({ MediaType.APPLICATION_JSON })
@ApiOperation(value = "Get all configurable services.")
@@ -250,7 +269,7 @@ public class ConfigurableServiceResource implements RESTResource {
List<ConfigurableServiceDTO> services = new ArrayList<>();
ServiceReference<?>[] serviceReferences = null;
try {
serviceReferences = RESTCoreActivator.getBundleContext().getServiceReferences((String) null, filter);
serviceReferences = bundleContext.getServiceReferences((String) null, filter);
} catch (InvalidSyntaxException ex) {
logger.error("Cannot get service references because the syntax of the filter '{}' is invalid.", filter);
}
@@ -287,8 +306,7 @@ public class ConfigurableServiceResource implements RESTResource {
String filter = "(" + Constants.SERVICE_PID + "=" + factoryPid + ")";
try {
ServiceReference<?>[] refs = RESTCoreActivator.getBundleContext().getServiceReferences((String) null,
filter);
ServiceReference<?>[] refs = bundleContext.getServiceReferences((String) null, filter);
if (refs != null && refs.length > 0) {
configDescriptionURI = (String) refs[0]
@@ -56,6 +56,7 @@ import org.openhab.core.config.core.status.ConfigStatusService;
import org.openhab.core.config.core.validation.ConfigValidationException;
import org.openhab.core.io.rest.JSONResponse;
import org.openhab.core.io.rest.LocaleService;
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.core.thing.EnrichedThingDTO;
@@ -97,6 +98,11 @@ 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;
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;
@@ -121,10 +127,15 @@ import io.swagger.annotations.ApiResponses;
* add editable flag to thing responses
* @author Franck Dechavanne - Added DTOs to ApiResponses
* @author Dimitar Ivanov - replaced Firmware UID with thing UID and firmware version
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Path(ThingResource.PATH_THINGS)
@Api(value = ThingResource.PATH_THINGS)
@Component
@JaxrsResource
@JaxrsName(ThingResource.PATH_THINGS)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(ThingResource.PATH_THINGS)
@Api(ThingResource.PATH_THINGS)
public class ThingResource implements RESTResource {
private final Logger logger = LoggerFactory.getLogger(ThingResource.class);
@@ -37,6 +37,7 @@ import org.openhab.core.config.core.dto.ConfigDescriptionDTOMapper;
import org.openhab.core.config.core.dto.ConfigDescriptionParameterDTO;
import org.openhab.core.config.core.dto.ConfigDescriptionParameterGroupDTO;
import org.openhab.core.io.rest.LocaleService;
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.thing.ThingTypeUID;
@@ -58,6 +59,11 @@ 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;
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;
@@ -79,14 +85,19 @@ import io.swagger.annotations.ApiResponses;
* @author Miki Jankov - Introducing StrippedThingTypeDTO
* @author Franck Dechavanne - Added DTOs to ApiResponses
* @author Yannick Schaus - Added filter to getAll
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Path(ThingTypeResource.PATH_THINGS_TYPES)
@Api(value = ThingTypeResource.PATH_THINGS_TYPES)
@Component
@JaxrsResource
@JaxrsName(ThingTypeResource.PATH_THING_TYPES)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(ThingTypeResource.PATH_THING_TYPES)
@Api(ThingTypeResource.PATH_THING_TYPES)
public class ThingTypeResource implements RESTResource {
/** The URI path to this resource */
public static final String PATH_THINGS_TYPES = "thing-types";
public static final String PATH_THING_TYPES = "thing-types";
private final Logger logger = LoggerFactory.getLogger(ThingTypeResource.class);
@@ -12,16 +12,15 @@
*/
package org.openhab.core.io.rest.core.item;
import java.net.URI;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.function.Predicate;
import javax.ws.rs.core.UriBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.rest.core.internal.RESTCoreActivator;
import org.openhab.core.io.rest.core.internal.item.ItemResource;
import org.openhab.core.items.GroupItem;
import org.openhab.core.items.Item;
import org.openhab.core.items.dto.ItemDTO;
@@ -30,6 +29,7 @@ import org.openhab.core.transform.TransformationException;
import org.openhab.core.transform.TransformationHelper;
import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -51,17 +51,18 @@ public class EnrichedItemDTOMapper {
* @param drillDown defines whether the whole tree should be traversed or only direct members are considered
* @param itemFilter a predicate that filters items while traversing the tree (true means that an item is
* considered, can be null)
* @param uri the uri (can be null)
* @param uriBuilder if present the URI builder contains one template that will be replaced by the specific item
* name
* @param locale locale (can be null)
* @return item DTO object
*/
public static EnrichedItemDTO map(Item item, boolean drillDown, @Nullable Predicate<Item> itemFilter,
@Nullable URI uri, @Nullable Locale locale) {
@Nullable UriBuilder uriBuilder, @Nullable Locale locale) {
ItemDTO itemDTO = ItemDTOMapper.map(item);
return map(item, itemDTO, uri, drillDown, itemFilter, locale);
return map(item, itemDTO, uriBuilder, drillDown, itemFilter, locale);
}
private static EnrichedItemDTO map(Item item, ItemDTO itemDTO, @Nullable URI uri, boolean drillDown,
private static EnrichedItemDTO map(Item item, ItemDTO itemDTO, @Nullable UriBuilder uriBuilder, boolean drillDown,
@Nullable Predicate<Item> itemFilter, @Nullable Locale locale) {
String state = item.getState().toFullString();
String transformedState = considerTransformation(state, item, locale);
@@ -69,7 +70,13 @@ public class EnrichedItemDTOMapper {
transformedState = null;
}
StateDescription stateDescription = considerTransformation(item.getStateDescription(locale));
String link = null != uri ? uri.toASCIIString() + ItemResource.PATH_ITEMS + "/" + itemDTO.name : null;
final String link;
if (uriBuilder != null) {
link = uriBuilder.build(itemDTO.name).toASCIIString();
} else {
link = null;
}
EnrichedItemDTO enrichedItemDTO = null;
@@ -80,7 +87,7 @@ public class EnrichedItemDTOMapper {
Collection<EnrichedItemDTO> members = new LinkedHashSet<>();
for (Item member : groupItem.getMembers()) {
if (itemFilter == null || itemFilter.test(member)) {
members.add(map(member, drillDown, itemFilter, uri, locale));
members.add(map(member, drillDown, itemFilter, uriBuilder, locale));
}
}
memberDTOs = members.toArray(new EnrichedItemDTO[members.size()]);
@@ -121,7 +128,8 @@ public class EnrichedItemDTOMapper {
String pattern = stateDescription.getPattern();
if (pattern != null) {
try {
return TransformationHelper.transform(RESTCoreActivator.getBundleContext(), pattern, state);
return TransformationHelper.transform(
FrameworkUtil.getBundle(EnrichedItemDTOMapper.class).getBundleContext(), pattern, state);
} catch (NoClassDefFoundError ex) {
// TransformationHelper is optional dependency, so ignore if class not found
// return state as it is without transformation
@@ -15,8 +15,6 @@ package org.openhab.core.io.rest.core.item;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.net.URI;
import org.junit.Before;
import org.junit.Test;
import org.openhab.core.items.GenericItem;
@@ -51,32 +49,31 @@ public class EnrichedItemDTOMapperTest extends JavaTest {
subGroup.addMember(stringItem);
}
EnrichedGroupItemDTO dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, false, null, URI.create(""),
null);
EnrichedGroupItemDTO dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, false, null, null, null);
assertThat(dto.members.length, is(0));
dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, true, null, URI.create(""), null);
dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, true, null, null, null);
assertThat(dto.members.length, is(3));
assertThat(((EnrichedGroupItemDTO) dto.members[0]).members.length, is(1));
dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, true,
i -> CoreItemFactory.NUMBER.equals(i.getType()), URI.create(""), null);
i -> CoreItemFactory.NUMBER.equals(i.getType()), null, null);
assertThat(dto.members.length, is(1));
dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, true,
i -> CoreItemFactory.NUMBER.equals(i.getType()) || i instanceof GroupItem, URI.create(""), null);
i -> CoreItemFactory.NUMBER.equals(i.getType()) || i instanceof GroupItem, null, null);
assertThat(dto.members.length, is(2));
assertThat(((EnrichedGroupItemDTO) dto.members[0]).members.length, is(0));
dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, true,
i -> CoreItemFactory.NUMBER.equals(i.getType()) || i instanceof GroupItem, URI.create(""), null);
i -> CoreItemFactory.NUMBER.equals(i.getType()) || i instanceof GroupItem, null, null);
assertThat(dto.members.length, is(2));
assertThat(((EnrichedGroupItemDTO) dto.members[0]).members.length, is(0));
dto = (EnrichedGroupItemDTO) EnrichedItemDTOMapper.map(group, true,
i -> CoreItemFactory.NUMBER.equals(i.getType()) || i.getType().equals(CoreItemFactory.STRING)
|| i instanceof GroupItem,
URI.create(""), null);
null, null);
assertThat(dto.members.length, is(2));
assertThat(((EnrichedGroupItemDTO) dto.members[0]).members.length, is(1));
}
@@ -32,8 +32,14 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
import org.osgi.service.component.annotations.Component;
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;
@@ -46,10 +52,15 @@ import io.swagger.annotations.ApiResponses;
/**
*
* @author Sebastian Janzen - Initial contribution
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Component
@Path("/log")
@Api(value = LogHandler.PATH_LOG)
@JaxrsResource
@JaxrsName(LogHandler.PATH_LOG)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(LogHandler.PATH_LOG)
@Api(LogHandler.PATH_LOG)
@Produces(MediaType.APPLICATION_JSON)
public class LogHandler implements RESTResource {
@@ -1,14 +0,0 @@
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
@@ -1,2 +0,0 @@
Bundle-SymbolicName: ${project.artifactId}
Bundle-Activator: org.openhab.core.io.rest.optimize.internal.Activator
@@ -1,55 +0,0 @@
/**
* Copyright (c) 2010-2020 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.optimize.internal;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceRegistration;
import com.eclipsesource.jaxrs.publisher.ResourceFilter;
/**
* Activator
*
* It registers a {@link ResourceFilter} in order to prevent the JAX-RS implementation to
* enforce starting all services once they are registered.
*
* @author Simon Kaufmann - Initial contribution
*/
public class Activator implements BundleActivator {
private ServiceRegistration<?> resourceFilterRegistration;
@Override
public void start(BundleContext context) throws Exception {
registerResourceFilter(context);
}
@Override
public void stop(BundleContext context) throws Exception {
unregisterResourceFilter();
}
private void registerResourceFilter(BundleContext context) throws InvalidSyntaxException {
resourceFilterRegistration = context.registerService(ResourceFilter.class.getName(), new ResourceFilterImpl(),
null);
}
private void unregisterResourceFilter() {
if (resourceFilterRegistration != null) {
resourceFilterRegistration.unregister();
resourceFilterRegistration = null;
}
}
}
@@ -1,143 +0,0 @@
/**
* Copyright (c) 2010-2020 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.optimize.internal;
import static com.eclipsesource.jaxrs.publisher.ServiceProperties.PUBLISH;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import org.osgi.framework.Constants;
import org.osgi.framework.Filter;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.eclipsesource.jaxrs.publisher.ResourceFilter;
/**
* Provides a filter for all classes/interfaces which are relevant in the context of JAX-RS.
*
* By default, this filter will allow every service outside of the org.openhab.core.**-Namespace to be parsed by
* the JAXR-RS implementation. To further optimize this, install a fragment which adds a "/res/whitelist.txt" file,
* containing one service interface or class per line like in the following example:
*
* <pre>
* {@code
* # My Custom Services
* org.example.foo
* org.example.bar
* # Another one
* org.example.test
* }
* </pre>
*
* If this file is present, no other services will be scanned and hence won't be available.
*
* @author Simon Kaufmann - Initial contribution
*/
public class ResourceFilterImpl implements ResourceFilter {
private final Logger logger = LoggerFactory.getLogger(ResourceFilterImpl.class);
/**
* All classes and interfaces which are considered to be relevant for JAX-RS.
*/
private static final String[] WHITELIST = new String[] {
// JAX-RS
"javax.ws.rs.ext.MessageBodyReader", "javax.ws.rs.ext.MessageBodyWriter",
// openHAB
"org.openhab.core.io.rest.internal.filter.ProxyFilter",
"org.openhab.core.io.rest.internal.resources.RootResource",
"org.openhab.core.io.rest.JSONResponse$ExceptionMapper", "org.openhab.core.io.rest.RESTResource",
"org.openhab.core.io.rest.sse.internal.async.BlockingAsyncFeature",
"org.openhab.core.io.rest.sse.SseResource",
// SSE
"org.glassfish.jersey.media.sse.SseFeature",
"org.glassfish.jersey.server.monitoring.ApplicationEventListener" };
@Override
public Filter getFilter() {
String filterString = createFilter(WHITELIST);
try {
return FrameworkUtil.createFilter(filterString);
} catch (InvalidSyntaxException e) {
logger.error("Error creating RESTResource filter", e);
}
return null;
}
/**
* @param interfaces interface or class names
* @return filter string which matches if the class implements one of the interfaces or the name of the class is
* contained in interfaces
*/
private String createFilter(String[] interfaces) {
StringBuilder builder = new StringBuilder();
builder.append("(&");
builder.append("(|");
List<String> whitelist = loadWhitelistExtension();
if (whitelist == null) {
logger.debug("No /res/whitelist.txt file found - scanning all unknown services");
builder.append("(!(" + Constants.OBJECTCLASS + "=org.openhab.core.*))");
} else {
logger.debug("Whitelist /res/whitelist.txt file found - restricting scanning of services");
whitelist.forEach(entry -> {
builder.append("(" + Constants.OBJECTCLASS + "=" + entry + ")");
});
}
for (String clazz : interfaces) {
builder.append("(" + Constants.OBJECTCLASS + "=" + clazz + ")");
}
builder.append(")");
builder.append("(!(" + PUBLISH + "=false)))");
return builder.toString();
}
private List<String> loadWhitelistExtension() {
Enumeration<URL> entries = FrameworkUtil.getBundle(this.getClass()).findEntries("res", "whitelist.txt", false);
if (entries != null && entries.hasMoreElements()) {
URL url = entries.nextElement();
try (InputStream is = url.openStream()) {
return readWhitelistEntries(is);
} catch (IOException e) {
logger.warn("Error reading REST extension whitelist from {}", url, e);
return null;
}
} else {
return null;
}
}
private List<String> readWhitelistEntries(InputStream stream) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
List<String> ret = new LinkedList<>();
String line = reader.readLine();
while (line != null) {
String trimmed = line.trim();
if (!trimmed.isEmpty() && !trimmed.startsWith("#")) {
ret.add(trimmed);
}
line = reader.readLine();
}
return ret;
}
}
@@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2020 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.sitemap.internal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.openhab.core.io.rest.sitemap.internal.JerseyResponseBuilderUtils.JerseyResponseBuilderDTO.ContextDTO;
import org.openhab.core.io.rest.sitemap.internal.JerseyResponseBuilderUtils.JerseyResponseBuilderDTO.ContextDTO.StreamInfoDTO;
import org.osgi.dto.DTO;
/**
* Data transfer object until UIs are fixed.
*
* <p>
* {@link https://github.com/openhab/openhab-core/issues/1216}
*
* @author Markus Rathgeb - Initial contribution
*/
public class JerseyResponseBuilderUtils extends DTO {
public static class JerseyResponseBuilderDTO extends DTO {
public static class ContextDTO extends DTO {
public static class StreamInfoDTO extends DTO {
int bufferSize;
boolean directWrite;
boolean isCommitted;
boolean isClosed;
}
public Map<String, List<Object>> headers;
public StreamInfoDTO committingOutputStream;
public List<String> entityAnnotations;
public StreamInfoDTO entityStream;
}
public String status;
public ContextDTO context;
}
public static DTO created(final String location) {
JerseyResponseBuilderDTO jrbDTO = new JerseyResponseBuilderDTO();
jrbDTO.status = "CREATED";
jrbDTO.context = new ContextDTO();
jrbDTO.context.headers = new HashMap<>();
jrbDTO.context.headers.put("Location", Arrays.asList(location));
jrbDTO.context.committingOutputStream = new StreamInfoDTO();
jrbDTO.context.committingOutputStream.bufferSize = 0;
jrbDTO.context.committingOutputStream.directWrite = true;
jrbDTO.context.committingOutputStream.isCommitted = false;
jrbDTO.context.committingOutputStream.isClosed = false;
jrbDTO.context.entityAnnotations = new ArrayList<>(0);
jrbDTO.context.entityStream = new StreamInfoDTO();
jrbDTO.context.entityStream.bufferSize = 0;
jrbDTO.context.entityStream.directWrite = true;
jrbDTO.context.entityStream.isCommitted = false;
jrbDTO.context.entityStream.isClosed = false;
return jrbDTO;
}
}
@@ -1,70 +0,0 @@
/**
* Copyright (c) 2010-2020 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.sitemap.internal;
import java.io.IOException;
import org.glassfish.jersey.media.sse.EventOutput;
import org.glassfish.jersey.media.sse.OutboundEvent;
import org.openhab.core.io.rest.sitemap.SitemapSubscriptionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link EventOutput} implementation that takes a subscription id parameter and only writes out events that match the
* page of this subscription.
* Should only be used when the {@link OutboundEvent}s sent through this {@link EventOutput} contain a data object of
* type {@link SitemapEvent}
*
* @author Kai Kreuzer - Initial contribution
*/
public class SitemapEventOutput extends EventOutput {
private final Logger logger = LoggerFactory.getLogger(SitemapEventOutput.class);
private final String subscriptionId;
private final SitemapSubscriptionService subscriptions;
public SitemapEventOutput(SitemapSubscriptionService subscriptions, String subscriptionId) {
super();
this.subscriptions = subscriptions;
this.subscriptionId = subscriptionId;
}
@Override
public void write(OutboundEvent chunk) throws IOException {
if (chunk.getName().equals("subscriptionId") && chunk.getData().equals(subscriptionId)) {
super.write(chunk);
} else {
SitemapEvent event = (SitemapEvent) chunk.getData();
String sitemapName = event.sitemapName;
String pageId = event.pageId;
if (sitemapName != null && sitemapName.equals(subscriptions.getSitemapName(subscriptionId))
&& pageId != null && pageId.equals(subscriptions.getPageId(subscriptionId))) {
super.write(chunk);
if (logger.isDebugEnabled()) {
if (event instanceof SitemapWidgetEvent) {
logger.debug("Sent sitemap event for widget {} to subscription {}.",
((SitemapWidgetEvent) event).widgetId, subscriptionId);
} else if (event instanceof ServerAliveEvent) {
logger.debug("Sent alive event to subscription {}.", subscriptionId);
}
}
}
}
}
public String getSubscriptionId() {
return subscriptionId;
}
}
@@ -45,20 +45,21 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.sse.OutboundSseEvent;
import javax.ws.rs.sse.Sse;
import javax.ws.rs.sse.SseEventSink;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.glassfish.jersey.media.sse.EventOutput;
import org.glassfish.jersey.media.sse.OutboundEvent;
import org.glassfish.jersey.media.sse.SseBroadcaster;
import org.glassfish.jersey.media.sse.SseFeature;
import org.glassfish.jersey.server.BroadcasterListener;
import org.glassfish.jersey.server.ChunkedOutput;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.auth.Role;
import org.openhab.core.common.ThreadPoolManager;
import org.openhab.core.io.rest.JSONResponse;
import org.openhab.core.io.rest.LocaleService;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
import org.openhab.core.io.rest.SseBroadcaster;
import org.openhab.core.io.rest.core.item.EnrichedItemDTOMapper;
import org.openhab.core.io.rest.sitemap.SitemapSubscriptionService;
import org.openhab.core.io.rest.sitemap.SitemapSubscriptionService.SitemapSubscriptionCallback;
@@ -93,6 +94,11 @@ import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
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;
@@ -112,12 +118,18 @@ import io.swagger.annotations.ApiResponses;
* @author Kai Kreuzer - Initial contribution
* @author Chris Jackson - Initial contribution
* @author Yordan Zhelev - Added Swagger annotations
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Component(service = RESTResource.class)
@JaxrsResource
@JaxrsName(SitemapResource.PATH_SITEMAPS)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(SitemapResource.PATH_SITEMAPS)
@RolesAllowed({ Role.USER, Role.ADMIN })
@Api(value = SitemapResource.PATH_SITEMAPS)
public class SitemapResource implements RESTResource, SitemapSubscriptionCallback, BroadcasterListener<OutboundEvent> {
@Api(SitemapResource.PATH_SITEMAPS)
public class SitemapResource
implements RESTResource, SitemapSubscriptionCallback, SseBroadcaster.Listener<SseSinkInfo> {
private final Logger logger = LoggerFactory.getLogger(SitemapResource.class);
@@ -127,7 +139,9 @@ public class SitemapResource implements RESTResource, SitemapSubscriptionCallbac
private static final long TIMEOUT_IN_MS = 30000;
private SseBroadcaster broadcaster;
private @NonNullByDefault({}) Sse sse;
private SseBroadcaster<@NonNull SseSinkInfo> broadcaster;
@Context
UriInfo uriInfo;
@@ -146,7 +160,7 @@ public class SitemapResource implements RESTResource, SitemapSubscriptionCallbac
private final java.util.List<SitemapProvider> sitemapProviders = new ArrayList<>();
private final Map<String, EventOutput> eventOutputs = new MapMaker().weakValues().makeMap();
private final Map<String, SseSinkInfo> knownSubscriptions = new MapMaker().weakValues().makeMap();
private final ScheduledExecutorService scheduler = ThreadPoolManager
.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON);
@@ -155,8 +169,7 @@ public class SitemapResource implements RESTResource, SitemapSubscriptionCallbac
@Activate
protected void activate() {
broadcaster = new SseBroadcaster();
broadcaster.add(this);
broadcaster = new SseBroadcaster<>();
// The clean SSE subscriptions job sends an ALIVE event to all subscribers. This will trigger
// an exception when the subscriber is dead, leading to the release of the SSE subscription
@@ -179,8 +192,12 @@ public class SitemapResource implements RESTResource, SitemapSubscriptionCallbac
cleanSubscriptionsJob.cancel(true);
cleanSubscriptionsJob = null;
}
broadcaster.remove(this);
broadcaster = null;
broadcaster.close();
}
@Context
public void setSse(final Sse sse) {
this.sse = sse;
}
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
@@ -292,6 +309,7 @@ public class SitemapResource implements RESTResource, SitemapSubscriptionCallbac
*/
@POST
@Path(SEGMENT_EVENTS + "/subscribe")
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Creates a sitemap event subscription.")
@ApiResponses(value = { @ApiResponse(code = 201, message = "Subscription created."),
@ApiResponse(code = 503, message = "Subscriptions limit reached.") })
@@ -301,13 +319,15 @@ public class SitemapResource implements RESTResource, SitemapSubscriptionCallbac
return JSONResponse.createResponse(Status.SERVICE_UNAVAILABLE, null,
"Max number of subscriptions is reached.");
}
final EventOutput eventOutput = new SitemapEventOutput(subscriptions, subscriptionId);
broadcaster.add(eventOutput);
eventOutputs.put(subscriptionId, eventOutput);
final SseSinkInfo sinkInfo = new SseSinkInfo(subscriptionId, subscriptions);
knownSubscriptions.put(subscriptionId, sinkInfo);
URI uri = uriInfo.getBaseUriBuilder().path(PATH_SITEMAPS).path(SEGMENT_EVENTS).path(subscriptionId).build();
logger.debug("Client from IP {} requested new subscription => got id {}.", request.getRemoteAddr(),
subscriptionId);
return Response.created(uri);
// See https://github.com/openhab/openhab-core/issues/1216
// return Response.created(uri).build();
return JerseyResponseBuilderUtils.created(uri.toASCIIString());
}
/**
@@ -317,28 +337,31 @@ public class SitemapResource implements RESTResource, SitemapSubscriptionCallbac
* connection.
*/
@GET
@Path(SEGMENT_EVENTS + "/{subscriptionid: [a-zA-Z_0-9-]+}/")
@Produces(SseFeature.SERVER_SENT_EVENTS)
@ApiOperation(value = "Get sitemap events.", response = EventOutput.class)
@Path(SEGMENT_EVENTS + "/{subscriptionid: [a-zA-Z_0-9-]+}")
@Produces(MediaType.SERVER_SENT_EVENTS)
@ApiOperation(value = "Get sitemap events.")
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 400, message = "Page not linked to the subscription."),
@ApiResponse(code = 404, message = "Subscription not found.") })
public Object getSitemapEvents(
public void getSitemapEvents(@Context final SseEventSink sseEventSink, @Context final HttpServletResponse response,
@PathParam("subscriptionid") @ApiParam(value = "subscription id") String subscriptionId,
@QueryParam("sitemap") @ApiParam(value = "sitemap name", required = false) String sitemapname,
@QueryParam("pageid") @ApiParam(value = "page id", required = false) String pageId) {
EventOutput eventOutput = eventOutputs.get(subscriptionId);
if (!subscriptions.exists(subscriptionId) || eventOutput == null) {
return JSONResponse.createResponse(Status.NOT_FOUND, null,
"Subscription id " + subscriptionId + " does not exist.");
final SseSinkInfo sinkInfo = knownSubscriptions.get(subscriptionId);
if (sinkInfo == null) {
logger.debug("Subscription id {} does not exist.", subscriptionId);
response.setStatus(Status.NOT_FOUND.getStatusCode());
return;
}
if (sitemapname != null && pageId != null) {
subscriptions.setPageId(subscriptionId, sitemapname, pageId);
}
if (subscriptions.getSitemapName(subscriptionId) == null || subscriptions.getPageId(subscriptionId) == null) {
return JSONResponse.createResponse(Status.BAD_REQUEST, null,
"Subscription id " + subscriptionId + " is not yet linked to a sitemap/page.");
logger.debug("Subscription id {} is not yet linked to a sitemap/page.", subscriptionId);
response.setStatus(Status.BAD_REQUEST.getStatusCode());
return;
}
logger.debug("Client from IP {} requested sitemap event stream for subscription {}.", request.getRemoteAddr(),
subscriptionId);
@@ -346,7 +369,7 @@ public class SitemapResource implements RESTResource, SitemapSubscriptionCallbac
// This allows you to not disable proxy buffering in nginx and still have working sse
response.addHeader(X_ACCEL_BUFFERING_HEADER, "no");
return eventOutput;
broadcaster.add(sseEventSink, sinkInfo);
}
private PageDTO getPageBean(String sitemapName, String pageId, URI uri, Locale locale, boolean timeout,
@@ -491,8 +514,8 @@ public class SitemapResource implements RESTResource, SitemapSubscriptionCallbac
.substring(widget.eClass().getInstanceTypeName().lastIndexOf(".") + 1);
boolean isMapview = "mapview".equalsIgnoreCase(widgetTypeName);
Predicate<Item> itemFilter = (i -> CoreItemFactory.LOCATION.equals(i.getType()));
bean.item = EnrichedItemDTOMapper.map(item, isMapview, itemFilter, UriBuilder.fromUri(uri).build(),
locale);
bean.item = EnrichedItemDTOMapper.map(item, isMapview, itemFilter,
UriBuilder.fromUri(uri).path("{itemName}"), locale);
bean.state = itemUIRegistry.getState(widget).toFullString();
// In case the widget state is identical to the item state, its value is set to null.
if (bean.state != null && bean.state.equals(bean.item.state)) {
@@ -801,42 +824,49 @@ public class SitemapResource implements RESTResource, SitemapSubscriptionCallbac
@Override
public void onEvent(SitemapEvent event) {
OutboundEvent.Builder eventBuilder = new OutboundEvent.Builder();
OutboundEvent outboundEvent = eventBuilder.name("event").mediaType(MediaType.APPLICATION_JSON_TYPE).data(event)
.build();
broadcaster.broadcast(outboundEvent);
final Sse sse = this.sse;
if (sse == null) {
logger.trace("broadcast skipped (no one listened since activation)");
return;
}
final OutboundSseEvent outboundSseEvent = sse.newEventBuilder().name("event")
.mediaType(MediaType.APPLICATION_JSON_TYPE).data(event).build();
broadcaster.sendIf(outboundSseEvent, info -> {
String sitemapName = event.sitemapName;
String pageId = event.pageId;
if (sitemapName != null && sitemapName.equals(subscriptions.getSitemapName(info.subscriptionId))
&& pageId != null && pageId.equals(subscriptions.getPageId(info.subscriptionId))) {
if (logger.isDebugEnabled()) {
if (event instanceof SitemapWidgetEvent) {
logger.debug("Sent sitemap event for widget {} to subscription {}.",
((SitemapWidgetEvent) event).widgetId, info.subscriptionId);
} else if (event instanceof ServerAliveEvent) {
logger.debug("Sent alive event to subscription {}.", info.subscriptionId);
}
}
return true;
}
return false;
});
}
@Override
public void onRelease(String subscriptionId) {
logger.debug("SSE connection for subscription {} has been released.", subscriptionId);
EventOutput eventOutput = eventOutputs.remove(subscriptionId);
if (eventOutput != null) {
broadcaster.remove(eventOutput);
}
}
@Override
public void onClose(ChunkedOutput<OutboundEvent> event) {
if (event instanceof SitemapEventOutput) {
SitemapEventOutput sitemapEvent = (SitemapEventOutput) event;
logger.debug("SSE connection for subscription {} has been closed.", sitemapEvent.getSubscriptionId());
subscriptions.removeSubscription(sitemapEvent.getSubscriptionId());
EventOutput eventOutput = eventOutputs.remove(sitemapEvent.getSubscriptionId());
if (eventOutput != null) {
broadcaster.remove(eventOutput);
}
}
}
@Override
public void onException(ChunkedOutput<OutboundEvent> event, Exception e) {
// the exception is usually "null" and onClose() is automatically called afterwards
// - so let's don't do anything in this method.
broadcaster.closeAndRemoveIf(info -> info.subscriptionId.equals(subscriptionId));
knownSubscriptions.remove(subscriptionId);
}
@Override
public boolean isSatisfied() {
return itemUIRegistry != null && subscriptions != null && localeService != null;
}
@Override
public void sseEventSinkRemoved(SseEventSink sink, SseSinkInfo info) {
logger.debug("SSE connection for subscription {} has been closed.", info.subscriptionId);
subscriptions.removeSubscription(info.subscriptionId);
knownSubscriptions.remove(info.subscriptionId);
}
}
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 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.sitemap.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.rest.sitemap.SitemapSubscriptionService;
/**
* The specific information we need to hold for a SSE sink.
*
* @author Markus Rathgeb - Initial contribution
*/
@NonNullByDefault
public class SseSinkInfo {
public final String subscriptionId;
public final SitemapSubscriptionService subscriptions;
public SseSinkInfo(String subscriptionId, SitemapSubscriptionService subscriptions) {
this.subscriptionId = subscriptionId;
this.subscriptions = subscriptions;
}
}
@@ -1,2 +0,0 @@
Bundle-SymbolicName: ${project.artifactId}
Bundle-Activator: org.openhab.core.io.rest.sse.internal.SseActivator
+1 -1
View File
@@ -17,7 +17,7 @@
<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core</artifactId>
<artifactId>org.openhab.core.io.rest</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
@@ -12,14 +12,17 @@
*/
package org.openhab.core.io.rest.sse;
import static org.openhab.core.io.rest.sse.internal.SseSinkItemInfo.*;
import static org.openhab.core.io.rest.sse.internal.SseSinkTopicInfo.matchesTopic;
import java.io.IOException;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.annotation.security.RolesAllowed;
import javax.inject.Singleton;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
@@ -29,29 +32,37 @@ import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.sse.OutboundSseEvent;
import javax.ws.rs.sse.Sse;
import javax.ws.rs.sse.SseEventSink;
import org.glassfish.jersey.media.sse.EventOutput;
import org.glassfish.jersey.media.sse.OutboundEvent;
import org.glassfish.jersey.media.sse.SseBroadcaster;
import org.glassfish.jersey.media.sse.SseFeature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.auth.Role;
import org.openhab.core.common.NamedThreadFactory;
import org.openhab.core.events.Event;
import org.openhab.core.io.rest.sse.internal.ItemStatesSseBroadcaster;
import org.openhab.core.io.rest.sse.internal.SseEventOutput;
import org.openhab.core.io.rest.sse.internal.SseStateEventOutput;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.SseBroadcaster;
import org.openhab.core.io.rest.sse.internal.SseItemStatesEventBuilder;
import org.openhab.core.io.rest.sse.internal.SsePublisher;
import org.openhab.core.io.rest.sse.internal.SseSinkItemInfo;
import org.openhab.core.io.rest.sse.internal.SseSinkTopicInfo;
import org.openhab.core.io.rest.sse.internal.dto.EventDTO;
import org.openhab.core.io.rest.sse.internal.util.SseUtil;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.items.events.ItemStateChangedEvent;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
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.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
@@ -63,75 +74,69 @@ import io.swagger.annotations.ApiResponses;
* @author Ivan Iliev - Initial contribution
* @author Yordan Zhelev - Added Swagger annotations
* @author Yannick Schaus - Add endpoints to track item state updates
* @author Markus Rathgeb - Drop Glassfish dependency and use API only
* @author Wouter Born - Rework SSE item state sinks for dropping Glassfish
*/
@Component(immediate = true, service = SseResource.class)
@Component(service = SsePublisher.class)
@JaxrsResource
@JaxrsName(SseResource.PATH_EVENTS)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(SseResource.PATH_EVENTS)
@RolesAllowed({ Role.USER })
@Singleton
@Api(value = SseResource.PATH_EVENTS, hidden = true)
public class SseResource {
@NonNullByDefault
public class SseResource implements SsePublisher {
// The URI path to this resource
public static final String PATH_EVENTS = "events";
private static final String X_ACCEL_BUFFERING_HEADER = "X-Accel-Buffering";
private SseBroadcaster broadcaster;
private ItemStatesSseBroadcaster statesBroadcaster;
private final Logger logger = LoggerFactory.getLogger(SseResource.class);
private @NonNullByDefault({}) Sse sse;
private final SseBroadcaster<SseSinkItemInfo> itemStatesBroadcaster = new SseBroadcaster<>();
private final SseItemStatesEventBuilder itemStatesEventBuilder;
private final SseBroadcaster<SseSinkTopicInfo> topicBroadcaster = new SseBroadcaster<>();
private ExecutorService executorService;
@Reference
private ItemRegistry itemRegistry;
@Context
private UriInfo uriInfo;
@Context
private HttpServletResponse response;
@Context
private HttpServletRequest request;
public void setSse(final Sse sse) {
this.sse = sse;
}
@Activate
public void activate() {
this.executorService = Executors.newSingleThreadExecutor(new NamedThreadFactory("SseResource"));
this.broadcaster = new SseBroadcaster();
this.statesBroadcaster = new ItemStatesSseBroadcaster(itemRegistry);
public SseResource(@Reference SseItemStatesEventBuilder itemStatesEventBuilder) {
this.executorService = Executors.newSingleThreadExecutor();
this.itemStatesEventBuilder = itemStatesEventBuilder;
}
@Deactivate
public void deactivate() {
this.executorService.shutdown();
this.broadcaster = null;
this.statesBroadcaster = null;
itemStatesBroadcaster.close();
topicBroadcaster.close();
executorService.shutdown();
}
/**
* Subscribes the connecting client to the stream of events filtered by the
* given eventFilter.
*
* @param eventFilter
* @return {@link EventOutput} object associated with the incoming
* connection.
* @throws IOException
* @throws InterruptedException
*/
@GET
@Produces(SseFeature.SERVER_SENT_EVENTS)
@ApiOperation(value = "Get all events.", response = EventOutput.class)
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 400, message = "Topic is empty or contains invalid characters") })
public Object getEvents(@QueryParam("topics") @ApiParam(value = "topics") String eventFilter)
throws IOException, InterruptedException {
if (!SseUtil.isValidTopicFilter(eventFilter)) {
return Response.status(Status.BAD_REQUEST).build();
@Override
public void broadcast(Event event) {
if (sse == null) {
logger.trace("broadcast skipped (no one listened since activation)");
return;
}
// construct an EventOutput that will only write out events that match
// the given filter
final EventOutput eventOutput = new SseEventOutput(eventFilter);
broadcaster.add(eventOutput);
executorService.execute(() -> {
handleEventBroadcastTopic(event);
if (event instanceof ItemStateChangedEvent) {
handleEventBroadcastItemState((ItemStateChangedEvent) event);
}
});
}
private void addCommonResponseHeaders(final HttpServletResponse response) {
// Disables proxy buffering when using an nginx http server proxy for this response.
// This allows you to not disable proxy buffering in nginx and still have working sse
response.addHeader(X_ACCEL_BUFFERING_HEADER, "no");
@@ -140,36 +145,57 @@ public class SseResource {
// events at the moment of sending them.
response.addHeader(HttpHeaders.CONTENT_ENCODING, "identity");
return eventOutput;
try {
response.flushBuffer();
} catch (final IOException ex) {
logger.trace("flush buffer failed", ex);
}
}
@GET
@Produces(MediaType.SERVER_SENT_EVENTS)
@ApiOperation(value = "Get all events.")
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK"),
@ApiResponse(code = 400, message = "Topic is empty or contains invalid characters") })
public void listen(@Context final SseEventSink sseEventSink, @Context final HttpServletResponse response,
@QueryParam("topics") @ApiParam(value = "topics") String eventFilter) {
if (!SseUtil.isValidTopicFilter(eventFilter)) {
response.setStatus(Status.BAD_REQUEST.getStatusCode());
return;
}
topicBroadcaster.add(sseEventSink, new SseSinkTopicInfo(eventFilter));
addCommonResponseHeaders(response);
}
private void handleEventBroadcastTopic(Event event) {
final EventDTO eventDTO = SseUtil.buildDTO(event);
final OutboundSseEvent sseEvent = SseUtil.buildEvent(sse.newEventBuilder(), eventDTO);
topicBroadcaster.sendIf(sseEvent, matchesTopic(eventDTO.topic));
}
/**
* Subscribes the connecting client for state updates. It will initially only send a "ready" event with an unique
* connectionId that the client can use to dynamically alter the list of tracked items.
*
* @return {@link EventOutput} object associated with the incoming
* connection.
* @throws IOException
* @throws InterruptedException
* @return {@link EventOutput} object associated with the incoming connection.
*/
@GET
@Path("/states")
@Produces(SseFeature.SERVER_SENT_EVENTS)
@ApiOperation(value = "Initiates a new item state tracker connection", response = EventOutput.class)
@Produces(MediaType.SERVER_SENT_EVENTS)
@ApiOperation(value = "Initiates a new item state tracker connection")
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
public Object getStateEvents() throws IOException, InterruptedException {
final SseStateEventOutput eventOutput = new SseStateEventOutput();
statesBroadcaster.add(eventOutput);
public void getStateEvents(@Context final SseEventSink sseEventSink, @Context final HttpServletResponse response) {
final SseSinkItemInfo sinkItemInfo = new SseSinkItemInfo();
itemStatesBroadcaster.add(sseEventSink, sinkItemInfo);
// Disables proxy buffering when using an nginx http server proxy for this response.
// This allows you to not disable proxy buffering in nginx and still have working sse
response.addHeader(X_ACCEL_BUFFERING_HEADER, "no");
addCommonResponseHeaders(response);
// We want to make sure that the response is not compressed and buffered so that the client receives server sent
// events at the moment of sending them.
response.addHeader(HttpHeaders.CONTENT_ENCODING, "identity");
return eventOutput;
String connectionId = sinkItemInfo.getConnectionId();
OutboundSseEvent readyEvent = sse.newEventBuilder().id("0").name("ready").data(connectionId).build();
itemStatesBroadcaster.sendIf(readyEvent, hasConnectionId(connectionId));
}
/**
@@ -185,26 +211,20 @@ public class SseResource {
@ApiResponse(code = 404, message = "Unknown connectionId") })
public Object updateTrackedItems(@PathParam("connectionId") String connectionId,
@ApiParam("items") Set<String> itemNames) {
try {
statesBroadcaster.updateTrackedItems(connectionId, itemNames);
} catch (IllegalArgumentException e) {
Optional<SseSinkItemInfo> itemStateInfo = itemStatesBroadcaster.getInfoIf(hasConnectionId(connectionId))
.findFirst();
if (!itemStateInfo.isPresent()) {
return Response.status(Status.NOT_FOUND).build();
}
return Response.ok().build();
}
itemStateInfo.get().updateTrackedItems(itemNames);
/**
* Broadcasts an event described by the given parameter to all currently
* listening clients.
*
* @param sseEventType the SSE event type
* @param event the event
*/
public void broadcastEvent(final Event event) {
executorService.execute(() -> {
broadcaster.broadcast(SseUtil.buildEvent(event));
});
OutboundSseEvent itemStateEvent = itemStatesEventBuilder.buildEvent(sse.newEventBuilder(), itemNames);
if (itemStateEvent != null) {
itemStatesBroadcaster.sendIf(itemStateEvent, hasConnectionId(connectionId));
}
return Response.ok().build();
}
/**
@@ -212,12 +232,14 @@ public class SseResource {
*
* @param stateChangeEvent the {@link ItemStateChangedEvent} containing the new state
*/
public void broadcastStateEvent(final ItemStateChangedEvent stateChangeEvent) {
executorService.execute(() -> {
OutboundEvent event = statesBroadcaster.buildStateEvent(Set.of(stateChangeEvent.getItemName()));
public void handleEventBroadcastItemState(final ItemStateChangedEvent stateChangeEvent) {
String itemName = stateChangeEvent.getItemName();
boolean isTracked = itemStatesBroadcaster.getInfoIf(info -> true).anyMatch(tracksItem(itemName));
if (isTracked) {
OutboundSseEvent event = itemStatesEventBuilder.buildEvent(sse.newEventBuilder(), Set.of(itemName));
if (event != null) {
statesBroadcaster.broadcast(event);
itemStatesBroadcaster.sendIf(event, tracksItem(itemName));
}
});
}
}
}
@@ -1,110 +0,0 @@
/**
* Copyright (c) 2010-2020 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.sse.internal;
import javax.ws.rs.ext.RuntimeDelegate;
import org.glassfish.jersey.media.sse.SseFeature;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Bundle activator for openHAB SSE bundle.
*
* @author Ivan Iliev - Initial contribution
*/
public class SseActivator implements BundleActivator {
// See: https://github.com/openhab/openhab-core/issues/597
private static class WorkAroundIssue597 {
public static RuntimeDelegate getRuntimeDelegate(final long millisMax, final long millisSleep)
throws InterruptedException {
final Logger logger = LoggerFactory.getLogger(WorkAroundIssue597.class);
logger.trace("get runtime delegate");
final long begMillis = System.currentTimeMillis();
do {
try {
final RuntimeDelegate runtimeDelegate = RuntimeDelegate.getInstance();
logger.trace("succeeded");
return runtimeDelegate;
} catch (final LinkageError ex) {
logger.trace("linkage error");
if (System.currentTimeMillis() - begMillis <= millisMax) {
Thread.sleep(millisSleep);
}
}
} while (System.currentTimeMillis() - begMillis <= millisMax);
logger.trace("give up");
return null;
}
}
private final Logger logger = LoggerFactory.getLogger(SseActivator.class);
private static BundleContext context;
private ServiceRegistration<?> sseFeatureRegistration;
private ServiceRegistration<?> blockingAsyncFeatureRegistration;
/**
* Called whenever the OSGi framework starts our bundle
*/
@Override
public void start(BundleContext bc) throws Exception {
context = bc;
WorkAroundIssue597.getRuntimeDelegate(5000, 200);
String featureName = SseFeature.class.getName();
if (bc.getServiceReference(featureName) == null) {
sseFeatureRegistration = bc.registerService(featureName, new SseFeature(), null);
logger.debug("SSE API - SseFeature registered.");
}
logger.debug("SSE API has been started.");
}
/**
* Called whenever the OSGi framework stops our bundle
*/
@Override
public void stop(BundleContext bc) throws Exception {
context = null;
if (sseFeatureRegistration != null) {
sseFeatureRegistration.unregister();
logger.debug("SseFeature unregistered.");
}
if (blockingAsyncFeatureRegistration != null) {
blockingAsyncFeatureRegistration.unregister();
logger.debug("BlockingAsyncFeature unregistered.");
}
logger.debug("SSE API has been stopped.");
}
/**
* Returns the bundle context of this bundle
*
* @return the bundle context
*/
public static BundleContext getContext() {
return context;
}
}
@@ -1,51 +0,0 @@
/**
* Copyright (c) 2010-2020 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.sse.internal;
import java.io.IOException;
import java.util.List;
import org.glassfish.jersey.media.sse.EventOutput;
import org.glassfish.jersey.media.sse.OutboundEvent;
import org.openhab.core.io.rest.sse.beans.EventBean;
import org.openhab.core.io.rest.sse.internal.util.SseUtil;
/**
* {@link EventOutput} implementation that takes a filter parameter and only writes out events that match this filter.
* Should only be used when the {@link OutboundEvent}s sent through this {@link EventOutput} contain a data object of
* type {@link EventBean}
*
* @author Ivan Iliev - Initial contribution
*
*/
public class SseEventOutput extends EventOutput {
private List<String> regexFilters;
public SseEventOutput(String topicFilter) {
super();
this.regexFilters = SseUtil.convertToRegex(topicFilter);
}
@Override
public void write(OutboundEvent chunk) throws IOException {
EventBean event = (EventBean) chunk.getData();
for (String filter : regexFilters) {
if (event.topic.matches(filter)) {
super.write(chunk);
return;
}
}
}
}
@@ -12,7 +12,6 @@
*/
package org.openhab.core.io.rest.sse.internal;
import java.io.IOException;
import java.time.DateTimeException;
import java.util.HashMap;
import java.util.Locale;
@@ -21,10 +20,12 @@ import java.util.Set;
import javax.measure.Unit;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.sse.OutboundSseEvent;
import javax.ws.rs.sse.OutboundSseEvent.Builder;
import org.glassfish.jersey.media.sse.OutboundEvent;
import org.glassfish.jersey.media.sse.SseBroadcaster;
import org.glassfish.jersey.server.ChunkedOutput;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.rest.sse.internal.dto.StateDTO;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry;
@@ -37,123 +38,61 @@ import org.openhab.core.types.StateDescription;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;
import org.openhab.core.types.util.UnitUtils;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This {@link SseBroadcaster} keeps track of the {@link SseStateEventOutput} listeners to state changes and tracks them
* by their connectionId.
* The {@link SseItemStatesEventBuilder} builds {@link OutboundSseEvent}s for connections that listen to item state
* changes.
*
* @author Yannick Schaus - initial contribution
* @author Yannick Schaus - Initial contribution
* @author Wouter Born - Rework SSE item state sinks for dropping Glassfish
*/
public class ItemStatesSseBroadcaster extends SseBroadcaster {
@Component(service = SseItemStatesEventBuilder.class)
@NonNullByDefault
public class SseItemStatesEventBuilder {
private final Logger logger = LoggerFactory.getLogger(ItemStatesSseBroadcaster.class);
private final Logger logger = LoggerFactory.getLogger(SseItemStatesEventBuilder.class);
private Map<String, SseStateEventOutput> eventOutputs = new HashMap<>();
private final BundleContext bundleContext;
private final ItemRegistry itemRegistry;
private ItemRegistry itemRegistry;
public ItemStatesSseBroadcaster(ItemRegistry itemRegistry) {
super();
@Activate
public SseItemStatesEventBuilder(final BundleContext bundleContext, final @Reference ItemRegistry itemRegistry) {
this.bundleContext = bundleContext;
this.itemRegistry = itemRegistry;
}
@Override
public <OUT extends ChunkedOutput<OutboundEvent>> boolean add(OUT chunkedOutput) {
if (chunkedOutput instanceof SseStateEventOutput) {
SseStateEventOutput eventOutput = (SseStateEventOutput) chunkedOutput;
OutboundEvent.Builder builder = new OutboundEvent.Builder();
String connectionId = eventOutput.getConnectionId();
try {
eventOutputs.put(connectionId, eventOutput);
eventOutput.writeDirect(builder.id("0").name("ready").data(connectionId).build());
} catch (IOException e) {
logger.error("Cannot write initial ready event to {}, discarding connection", connectionId);
return false;
}
}
return super.add(chunkedOutput);
}
@Override
public <OUT extends ChunkedOutput<OutboundEvent>> boolean remove(OUT chunkedOutput) {
eventOutputs.values().remove(chunkedOutput);
return super.remove(chunkedOutput);
}
@Override
public void onClose(ChunkedOutput<OutboundEvent> chunkedOutput) {
remove(chunkedOutput);
}
@Override
public void onException(ChunkedOutput<OutboundEvent> chunkedOutput, Exception exception) {
remove(chunkedOutput);
}
/**
* Updates the list of tracked items for a connection
*
* @param connectionId the connection id
* @param newTrackedItems the list of items and their current state to send to the client
*/
public void updateTrackedItems(String connectionId, Set<String> newTrackedItems) {
SseStateEventOutput eventOutput = eventOutputs.get(connectionId);
if (eventOutput == null) {
throw new IllegalArgumentException("ConnectionId not found");
}
eventOutput.setTrackedItems(newTrackedItems);
try {
if (!eventOutput.isClosed()) {
OutboundEvent event = buildStateEvent(newTrackedItems);
if (event != null) {
eventOutput.writeDirect(event);
}
}
if (eventOutput.isClosed()) {
onClose(eventOutput);
}
} catch (IOException e) {
onException(eventOutput, e);
}
}
public OutboundEvent buildStateEvent(Set<String> itemNames) {
Map<String, StateDTO> payload = new HashMap<>();
public @Nullable OutboundSseEvent buildEvent(Builder eventBuilder, Set<String> itemNames) {
Map<String, StateDTO> payload = new HashMap<>(itemNames.size());
for (String itemName : itemNames) {
try {
// Check that the item is tracked by at least one connection
if (eventOutputs.values().stream().anyMatch(c -> c.getTrackedItems().contains(itemName))) {
Item item = itemRegistry.getItem(itemName);
StateDTO stateDto = new StateDTO();
stateDto.state = item.getState().toString();
String displayState = getDisplayState(item, Locale.getDefault());
// Only include the display state if it's different than the raw state
if (stateDto.state != null && !stateDto.state.equals(displayState)) {
stateDto.displayState = displayState;
}
payload.put(itemName, stateDto);
Item item = itemRegistry.getItem(itemName);
StateDTO stateDto = new StateDTO();
stateDto.state = item.getState().toString();
String displayState = getDisplayState(item, Locale.getDefault());
// Only include the display state if it's different than the raw state
if (stateDto.state != null && !stateDto.state.equals(displayState)) {
stateDto.displayState = displayState;
}
payload.put(itemName, stateDto);
} catch (ItemNotFoundException e) {
logger.warn("Attempting to send a state update of an item which doesn't exist: {}", itemName);
}
}
if (!payload.isEmpty()) {
OutboundEvent.Builder builder = new OutboundEvent.Builder();
OutboundEvent event = builder.mediaType(MediaType.APPLICATION_JSON_TYPE).data(payload).build();
return event;
return eventBuilder.mediaType(MediaType.APPLICATION_JSON_TYPE).data(payload).build();
}
return null;
}
protected String getDisplayState(Item item, Locale locale) {
private @Nullable String getDisplayState(Item item, Locale locale) {
StateDescription stateDescription = item.getStateDescription(locale);
State state = item.getState();
String displayState = state.toString();
@@ -174,8 +113,7 @@ public class ItemStatesSseBroadcaster extends SseBroadcaster {
if (pattern != null) {
if (TransformationHelper.isTransform(pattern)) {
try {
displayState = TransformationHelper.transform(SseActivator.getContext(), pattern,
state.toString());
displayState = TransformationHelper.transform(bundleContext, pattern, state.toString());
} catch (NoClassDefFoundError ex) {
// TransformationHelper is optional dependency, so ignore if class not found
// return state as it is without transformation
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2020 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.sse.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.events.Event;
/**
* Marker interface for a publisher of events using SSE.
*
* @author Markus Rathgeb - Initial contribution
*/
@NonNullByDefault
public interface SsePublisher {
void broadcast(final Event event);
}
@@ -0,0 +1,59 @@
/**
* Copyright (c) 2010-2020 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.sse.internal;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Predicate;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The specific information we need to hold for a SSE sink which tracks item state updates.
*
* @author Wouter Born - Initial contribution
*/
@NonNullByDefault
public class SseSinkItemInfo {
private final String connectionId = UUID.randomUUID().toString();
private final Set<String> trackedItems = new CopyOnWriteArraySet<>();
/**
* Gets the connection identifier of this {@link SseSinkItemInfo}
*
* @return the connection id
*/
public String getConnectionId() {
return connectionId;
}
/**
* Updates the list of tracked items for a connection
*
* @param itemNames the item names to track
*/
public void updateTrackedItems(Set<String> itemNames) {
trackedItems.clear();
trackedItems.addAll(itemNames);
}
public static Predicate<SseSinkItemInfo> hasConnectionId(String connectionId) {
return info -> info.connectionId.equals(connectionId);
}
public static Predicate<SseSinkItemInfo> tracksItem(String itemName) {
return info -> info.trackedItems.contains(itemName);
}
}
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2020 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.sse.internal;
import java.util.List;
import java.util.function.Predicate;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.rest.sse.internal.util.SseUtil;
/**
* The specific information we need to hold for a SSE sink which subscribes to event topics.
*
* @author Markus Rathgeb - Initial contribution
*/
@NonNullByDefault
public class SseSinkTopicInfo {
private final List<String> regexFilters;
public SseSinkTopicInfo(String topicFilter) {
this.regexFilters = SseUtil.convertToRegex(topicFilter);
}
public static Predicate<SseSinkTopicInfo> matchesTopic(final String topic) {
return info -> info.regexFilters.stream().anyMatch(topic::matches);
}
}
@@ -1,92 +0,0 @@
/**
* Copyright (c) 2010-2020 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.sse.internal;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import org.glassfish.jersey.media.sse.EventOutput;
import org.glassfish.jersey.media.sse.OutboundEvent;
/**
* {@link EventOutput} implementation that takes a list of item names writes out state change events that match this
* list. Should only be used when the {@link OutboundEvent}s sent through this {@link EventOutput} contain a map of item
* names and states.
*
* It also uniquely identifies the connection with a random identifier.
*
* @author Yannick Schaus - Initial contribution
*
*/
public class SseStateEventOutput extends EventOutput {
private String connectionId;
private Collection<String> trackedItems = Collections.emptySet();
public SseStateEventOutput() {
super();
this.connectionId = UUID.randomUUID().toString();
}
/**
* Gets the connection identifier of this {@link SseStateEventOutput}
*
* @return the connection id
*/
public String getConnectionId() {
return connectionId;
}
/**
* Updates the list of tracked items for this connection
*
* @param itemNames the item names to track
*/
public void setTrackedItems(Collection<String> itemNames) {
this.trackedItems = itemNames;
}
/**
* Gets the list of tracked items for this connection
*
* @return a list of tracked item names
*/
protected Collection<String> getTrackedItems() {
return trackedItems;
}
@Override
public void write(OutboundEvent chunk) throws IOException {
if (chunk.getData() instanceof Map<?, ?>) {
Map<?, ?> event = (Map<?, ?>) chunk.getData();
String itemName = event.keySet().iterator().next().toString();
if (trackedItems != null && trackedItems.contains(itemName)) {
super.write(chunk);
}
}
}
/**
* Writes an event without filtering
*
* @param chunk
* @throws IOException
*/
public void writeDirect(OutboundEvent chunk) throws IOException {
super.write(chunk);
}
}
@@ -10,15 +10,18 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.io.rest.sse.beans;
package org.openhab.core.io.rest.sse.internal.dto;
import org.osgi.dto.DTO;
/**
* Event bean for broadcasted events.
*
* @author Ivan Iliev - Initial contribution
* @author Dennis Nobel - Added event type and renamed object to payload
* @author Markus Rathgeb - Follow the Data Transfer Objects Specification
*/
public class EventBean {
public class EventDTO extends DTO {
public String topic;
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.io.rest.sse.internal;
package org.openhab.core.io.rest.sse.internal.dto;
/**
* A DTO class holding the state of an item, and its version suitable for display.
@@ -20,9 +20,7 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.events.Event;
import org.openhab.core.events.EventFilter;
import org.openhab.core.events.EventSubscriber;
import org.openhab.core.io.rest.sse.SseResource;
import org.openhab.core.io.rest.sse.internal.ItemStatesSseBroadcaster;
import org.openhab.core.items.events.ItemStateChangedEvent;
import org.openhab.core.io.rest.sse.internal.SsePublisher;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
@@ -32,7 +30,7 @@ import org.osgi.service.component.annotations.Reference;
* to currently listening SSE clients.
*
* @author Stefan Bußweiler - Initial contribution
* @author Yannick Schaus - Broadcast state events to the specialized {@link ItemStatesSseBroadcaster}
* @author Yannick Schaus - Broadcast state events to the specialized ItemStatesSseBroadcaster
*/
@Component
@NonNullByDefault
@@ -40,11 +38,11 @@ public class SseEventSubscriber implements EventSubscriber {
private final Set<String> subscribedEventTypes = Collections.singleton(EventSubscriber.ALL_EVENT_TYPES);
private final SseResource sseResource;
private final SsePublisher ssePublisher;
@Activate
public SseEventSubscriber(final @Reference SseResource sseResource) {
this.sseResource = sseResource;
public SseEventSubscriber(final @Reference SsePublisher ssePublisher) {
this.ssePublisher = ssePublisher;
}
@Override
@@ -59,9 +57,6 @@ public class SseEventSubscriber implements EventSubscriber {
@Override
public void receive(Event event) {
sseResource.broadcastEvent(event);
if (event instanceof ItemStateChangedEvent) {
sseResource.broadcastStateEvent((ItemStateChangedEvent) event);
}
ssePublisher.broadcast(event);
}
}
@@ -17,39 +17,47 @@ import java.util.List;
import java.util.StringTokenizer;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.sse.OutboundSseEvent;
import org.glassfish.jersey.media.sse.OutboundEvent;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.events.Event;
import org.openhab.core.io.rest.sse.beans.EventBean;
import org.openhab.core.io.rest.sse.internal.dto.EventDTO;
/**
* Utility class containing helper methods for the SSE implementation.
*
* @author Ivan Iliev - Initial contribution
* @author Dennis Nobel - Changed EventBean
* @author Markus Rathgeb - Don't depend on specific application but use APIs if possible
*/
@NonNullByDefault
public class SseUtil {
static final String TOPIC_VALIDATE_PATTERN = "(\\w*\\*?\\/?,?:?-?\\s*)*";
public static EventDTO buildDTO(final Event event) {
EventDTO dto = new EventDTO();
dto.topic = event.getTopic();
dto.type = event.getType();
dto.payload = event.getPayload();
return dto;
}
/**
* Creates a new {@link OutboundEvent} object containing an {@link EventBean} created for the given Eclipse
* Creates a new {@link OutboundSseEvent} object containing an {@link EventDTO} created for the given Eclipse
* SmartHome {@link Event}.
*
* @param event the event
*
* @param eventBuilder the builder that should be used
* @param event the event data transfer object
* @return a new OutboundEvent
*/
public static OutboundEvent buildEvent(Event event) {
EventBean eventBean = new EventBean();
eventBean.topic = event.getTopic();
eventBean.type = event.getType();
eventBean.payload = event.getPayload();
public static OutboundSseEvent buildEvent(OutboundSseEvent.Builder eventBuilder, EventDTO event) {
final OutboundSseEvent sseEvent = eventBuilder.name("message") //
.mediaType(MediaType.APPLICATION_JSON_TYPE) //
.data(event) //
.build();
OutboundEvent.Builder eventBuilder = new OutboundEvent.Builder();
OutboundEvent outboundEvent = eventBuilder.name("message").mediaType(MediaType.APPLICATION_JSON_TYPE)
.data(eventBean).build();
return outboundEvent;
return sseEvent;
}
/**
@@ -59,7 +67,7 @@ public class SseUtil {
* @return true if the given input filter is empty or a valid topic filter string
*
*/
public static boolean isValidTopicFilter(String topicFilter) {
public static boolean isValidTopicFilter(@Nullable String topicFilter) {
return topicFilter == null || topicFilter.isEmpty() || topicFilter.matches(TOPIC_VALIDATE_PATTERN);
}
@@ -70,7 +78,7 @@ public class SseUtil {
* @param topicFilter
* @return
*/
public static List<String> convertToRegex(String topicFilter) {
public static List<String> convertToRegex(@Nullable String topicFilter) {
List<String> filters = new ArrayList<>();
if (topicFilter == null || topicFilter.isEmpty()) {
@@ -6,6 +6,13 @@
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
@@ -16,12 +23,5 @@
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.core.io.rest.optimize</name>
<name>org.openhab.core.io.rest.swagger</name>
<comment></comment>
<projects>
</projects>
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
@@ -10,8 +10,16 @@
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.core.io.rest.optimize</artifactId>
<artifactId>org.openhab.core.io.rest.swagger</artifactId>
<name>openHAB Core :: Bundles :: REST JAX-RS Optimizations</name>
<name>openHAB Core :: Bundles :: REST Interface :: Swagger 1</name>
<dependencies>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.io.rest</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,81 @@
/**
* Copyright (c) 2010-2020 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.swagger.impl;
import java.io.IOException;
import java.util.Map;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.rest.RESTConstants;
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.JaxrsResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.maggu2810.jaxrswb.swagger1.gen.JaxRsWhiteboardSwaggerGenerator;
/**
* An endpoint to generate and provide a Swagger 1 description.
*
* @author Markus Rathgeb - Initial contribution
*/
@Component(service = RESTResource.class)
@JaxrsResource
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path("/swagger.json")
@NonNullByDefault
public class RESTResource {
private final Logger logger = LoggerFactory.getLogger(RESTResource.class);
private final JaxRsWhiteboardSwaggerGenerator generator;
/**
* Creates a new instance.
*
* @param generator the generator
*/
@Activate
public RESTResource(final @Reference JaxRsWhiteboardSwaggerGenerator generator) {
this.generator = generator;
}
/**
* Gets the current JAX-RS Whiteboard provided endpoint information by Swagger 1.
*
* @return a Swagger 1 description of the endpoints
*/
@GET
@Produces({ MediaType.APPLICATION_JSON })
public Object getSwagger() {
final Map<String, Object> map;
try {
map = generator.generateMap();
} catch (final IOException ex) {
logger.warn("Error on Swagger DTO generation.", ex);
return Response.serverError().build();
}
return Response.status(Response.Status.OK).entity(map).build();
}
}
@@ -30,6 +30,7 @@ import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
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.ui.TileDTO;
@@ -42,8 +43,11 @@ 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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -56,11 +60,14 @@ import io.swagger.annotations.ApiResponses;
*
* @author Yannick Schaus - Initial contribution
*/
@Path(UIResource.PATH_UI)
@Api(value = UIResource.PATH_UI)
@Component(service = { RESTResource.class, UIResource.class })
@JaxrsResource
@JaxrsName(UIResource.PATH_UI)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(UIResource.PATH_UI)
@Api(UIResource.PATH_UI)
public class UIResource implements RESTResource {
private final Logger logger = LoggerFactory.getLogger(UIResource.class);
/** The URI path to this resource */
public static final String PATH_UI = "ui";
@@ -36,6 +36,7 @@ import javax.ws.rs.core.UriInfo;
import org.openhab.core.auth.Role;
import org.openhab.core.io.rest.JSONResponse;
import org.openhab.core.io.rest.LocaleService;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
import org.openhab.core.voice.Voice;
import org.openhab.core.voice.VoiceManager;
@@ -45,6 +46,11 @@ 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;
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 io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
@@ -57,14 +63,19 @@ import io.swagger.annotations.ApiResponses;
*
* @author Kai Kreuzer - Initial contribution
* @author Laurent Garnier - add TTS feature to the REST API
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Component
@Path(VoiceResource.PATH_SITEMAPS)
@JaxrsResource
@JaxrsName(VoiceResource.PATH_VOICE)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(VoiceResource.PATH_VOICE)
@RolesAllowed({ Role.USER, Role.ADMIN })
@Api(value = VoiceResource.PATH_SITEMAPS)
@Api(VoiceResource.PATH_VOICE)
public class VoiceResource implements RESTResource {
static final String PATH_SITEMAPS = "voice";
static final String PATH_VOICE = "voice";
@Context
UriInfo uriInfo;
@@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2020 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Marker interface for an input stream that provides a JSON string.
*
* @author Markus Rathgeb - Initial contribution
*/
@NonNullByDefault
public interface JSONInputStream {
}
@@ -23,7 +23,6 @@ import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;
import javax.ws.rs.ext.Provider;
import org.openhab.core.library.types.DateTimeType;
import org.slf4j.Logger;
@@ -162,7 +161,7 @@ public class JSONResponse {
try {
// we will not actively close the PipedInputStream since it is read by the receiving end
// and will be GC'ed once the response is consumed.
PipedInputStream in = new PipedInputStream(out);
PipedJSONInputStream in = new PipedJSONInputStream(out);
rp.entity(in);
} catch (IOException e) {
throw new IllegalStateException(e);
@@ -183,12 +182,23 @@ public class JSONResponse {
return rp.build();
}
/**
* An piped input stream that is marked to produce JSON string.
*
* @author Markus Rathgeb - Initial contribution
*/
private static class PipedJSONInputStream extends PipedInputStream implements JSONInputStream {
public PipedJSONInputStream(PipedOutputStream src) throws IOException {
super(src);
}
}
/**
* trap exceptions
*
* @author Joerg Plewe
*/
@Provider
public static class ExceptionMapper implements javax.ws.rs.ext.ExceptionMapper<Exception> {
private final Logger logger = LoggerFactory.getLogger(ExceptionMapper.class);
@@ -19,6 +19,8 @@ package org.openhab.core.io.rest;
*/
public class RESTConstants {
// URI path used for the REST API
@Deprecated
public static final String REST_URI = "/rest";
public static final String JAX_RS_NAME = "openhab";
}
@@ -0,0 +1,164 @@
/**
* Copyright (c) 2010-2020 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;
import java.io.Closeable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Predicate;
import java.util.stream.Stream;
import javax.ws.rs.sse.OutboundSseEvent;
import javax.ws.rs.sse.SseEventSink;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* We do not use the SseBroadcaster as it seems registered SseEventSinks are not removed if the peer terminates the
* connection.
*
* @author Markus Rathgeb - Initial contribution
*
* @param <I> the type of the SSE event sink specific information
*/
@NonNullByDefault
public class SseBroadcaster<@NonNull I> implements Closeable {
public interface Listener<I> {
void sseEventSinkRemoved(final SseEventSink sink, I info);
}
private final Logger logger = LoggerFactory.getLogger(SseBroadcaster.class);
private final List<Listener<I>> listeners = new CopyOnWriteArrayList<>();
private final Map<SseEventSink, I> sinks = new ConcurrentHashMap<>();
public void addListener(final Listener<I> listener) {
listeners.add(listener);
}
public void removeListener(final Listener<I> listener) {
listeners.remove(listener);
}
public @Nullable I add(final SseEventSink sink, final I info) {
return sinks.put(sink, info);
}
public @Nullable I remove(final SseEventSink sink) {
return sinks.remove(sink);
}
public @Nullable I getInfo(final SseEventSink sink) {
return sinks.get(sink);
}
public Stream<I> getInfoIf(Predicate<I> predicate) {
return sinks.values().stream().filter(predicate);
}
@Override
public void close() {
final Iterator<Entry<SseEventSink, I>> it = sinks.entrySet().iterator();
while (it.hasNext()) {
final Entry<SseEventSink, I> entry = it.next();
it.remove();
close(entry.getKey());
notifyAboutRemoval(entry.getKey(), entry.getValue());
}
}
public void send(final OutboundSseEvent event) {
sendIf(event, info -> true);
}
public void sendIf(final OutboundSseEvent event, Predicate<I> predicate) {
logger.trace("broadcast to potential {} sinks", sinks.size());
sinks.forEach((sink, info) -> {
if (sink.isClosed()) {
// We are using a concurrent collection, so we are allowed to modify the collection asynchronous (we
// don't know if there is currently an iteration in progress or not, but it does not matter).
handleRemoval(sink);
return;
}
// Check if we should send at all.
if (!predicate.test(info)) {
return;
}
sink.send(event).exceptionally(th -> {
close(sink);
// We are using a concurrent collection, so we are allowed to modify the collection asynchronous (we
// don't know if there is currently an iteration in progress or not, but it does not matter).
handleRemoval(sink);
final String thClass = th.getClass().toString();
final String message = th.getMessage();
if (thClass.equals("class org.eclipse.jetty.io.EofException")) {
// The peer terminates the connection.
} else if (th instanceof IllegalStateException && message != null
&& (message.equals("The sink is already closed, unable to queue SSE event for send")
|| message.equals("AsyncContext completed and/or Request lifecycle recycled"))) {
// java.lang.IllegalStateException: The sink is already closed, unable to queue SSE event for
// send
// java.lang.IllegalStateException: AsyncContext completed and/or Request lifecycle recycled
} else {
logger.warn("failure", th);
}
return null;
});
});
}
public void closeAndRemoveIf(Predicate<I> predicate) {
sinks.forEach((sink, info) -> {
if (predicate.test(info)) {
close(sink);
// We are using a concurrent collection, so we are allowed to modify the collection asynchronous (we
// don't know if there is currently an iteration in progress or not, but it does not matter).
handleRemoval(sink);
}
});
}
private void close(final SseEventSink sink) {
try {
sink.close();
} catch (final RuntimeException ex) {
logger.debug("Closing a SSE event sink failed. Nothing we can do here...", ex);
}
}
private void handleRemoval(final SseEventSink sink) {
final I info = sinks.remove(sink);
notifyAboutRemoval(sink, info);
}
private void notifyAboutRemoval(final SseEventSink sink, I info) {
listeners.forEach(listener -> {
listener.sseEventSinkRemoved(sink, info);
});
}
}
@@ -31,7 +31,7 @@ import com.google.gson.GsonBuilder;
*
* @author Henning Treu - Initial contribution
*/
public class Stream2JSONInputStream extends InputStream {
public class Stream2JSONInputStream extends InputStream implements JSONInputStream {
private final Iterator<String> iterator;
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 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.internal;
import javax.ws.rs.core.Application;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.rest.RESTConstants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationBase;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsName;
/**
* The JAX-RS application for the openHAB JAX-RS resources.
*
* @author Markus Rathgeb - Initial contribution
*/
@Component(service = Application.class, property = {
// https://lists.apache.org/thread.html/
// r1379789bd90c6b7e3971d5ffeedb2e0d1e1c9103fd2392cb95458596%40%3Cuser.aries.apache.org%3E
"servlet.init.hide-service-list-page=true" })
@JaxrsName(RESTConstants.JAX_RS_NAME)
@JaxrsApplicationBase("rest")
@NonNullByDefault
public class RESTApplicationImpl extends Application {
}
@@ -23,12 +23,15 @@ import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.Provider;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.internal.Constants;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -41,10 +44,12 @@ import org.slf4j.LoggerFactory;
* This implementation does not allow specific request/response headers nor cookies (allowCredentials).
*
* @author Antoine Besnard - Initial contribution
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Provider
@Component(immediate = true, property = {
@Component(property = {
"service.pid=org.openhab.core.cors" }, configurationPid = "org.openhab.cors", configurationPolicy = ConfigurationPolicy.REQUIRE)
@JaxrsExtension
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
public class CorsFilter implements ContainerResponseFilter {
static final String HTTP_HEAD_METHOD = "HEAD";
@@ -23,9 +23,12 @@ import javax.ws.rs.container.PreMatching;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Provider;
import org.openhab.core.io.rest.RESTConstants;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -34,10 +37,12 @@ import org.slf4j.LoggerFactory;
* context if proxy headers are detected.
*
* @author Ivan Iliev - Initial contribution
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Provider
@Component(configurationPid = "org.openhab.proxyfilter")
@JaxrsExtension
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@PreMatching
@Component(configurationPid = "org.openhab.proxyfilter", immediate = true, service = ProxyFilter.class)
public class ProxyFilter implements ContainerRequestFilter {
static final String PROTO_PROXY_HEADER = "x-forwarded-proto";
@@ -20,10 +20,13 @@ import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Provider;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsApplicationSelect;
import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsExtension;
/**
* A filter that only affects resources implementing the {@link SatisfiableRESTResource} interface.
@@ -32,9 +35,11 @@ import org.osgi.service.component.annotations.Component;
* 503 - Service Unavailable.
*
* @author Ivan Iliev - Initial contribution
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Provider
@Component(immediate = true, service = SatisfiableResourceFilter.class)
@Component
@JaxrsExtension
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
public class SatisfiableResourceFilter implements ContainerRequestFilter {
@Override
@@ -12,35 +12,41 @@
*/
package org.openhab.core.io.rest.internal.resources;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.List;
import java.util.Properties;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
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.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
import org.openhab.core.io.rest.internal.Constants;
import org.openhab.core.io.rest.internal.resources.beans.RootBean;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.openhab.core.io.rest.internal.resources.beans.RootBean.Links;
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;
import org.osgi.service.jaxrs.runtime.JaxrsServiceRuntime;
import org.osgi.service.jaxrs.runtime.dto.ApplicationDTO;
import org.osgi.service.jaxrs.runtime.dto.ResourceDTO;
import org.osgi.service.jaxrs.runtime.dto.RuntimeDTO;
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.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
/**
* <p>
* This class acts as an entry point / root resource for the REST API.
@@ -52,82 +58,61 @@ import org.slf4j.LoggerFactory;
* The result is returned as JSON
*
* @author Kai Kreuzer - Initial contribution
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Path("/")
@Component(service = RootResource.class, configurationPid = "org.openhab.restroot")
@JaxrsResource
@JaxrsName(RootResource.RESOURCE_NAME)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Produces(MediaType.APPLICATION_JSON)
@NonNullByDefault
@Path("/")
@Api(RootResource.RESOURCE_NAME)
public class RootResource {
private final transient Logger logger = LoggerFactory.getLogger(RootResource.class);
public static final String RESOURCE_NAME = "root";
private final List<RESTResource> restResources = new ArrayList<>();
private final Logger logger = LoggerFactory.getLogger(RootResource.class);
private final JaxrsServiceRuntime runtime;
private ConfigurationAdmin configurationAdmin;
@Context
UriInfo uriInfo;
@Activate
public RootResource(final @Reference JaxrsServiceRuntime runtime) {
this.runtime = runtime;
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public Response getRoot(@Context HttpHeaders headers) {
return Response.ok(getRootBean()).build();
}
@ApiOperation(value = "Gets the API version and links to resources.")
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
public Object getRoot(@Context UriInfo uriInfo) {
// key: path, value: name (this way we could ensure that ever path is added only once).
final Map<String, String> collectedLinks = new HashMap<>();
private RootBean getRootBean() {
RootBean bean = new RootBean();
final RuntimeDTO runtimeDTO = runtime.getRuntimeDTO();
final RootBean bean = new RootBean();
for (final ApplicationDTO applicationDTO : runtimeDTO.applicationDTOs) {
for (final ResourceDTO resourceDTO : applicationDTO.resourceDTOs) {
// We are using the JAX-RS name per convention for the link type.
// Let's skip names that begin with a dot (e.g. the generated ones) and empty ones.
final String name = resourceDTO.name;
if (name == null || name.isEmpty() || name.startsWith(".") || RESOURCE_NAME.equals(name)) {
continue;
}
for (RESTResource resource : restResources) {
// we will include all RESTResources that are currently satisfied
if (resource.isSatisfied()) {
String path = resource.getClass().getAnnotation(Path.class).value();
bean.links
.add(new RootBean.Links(path, uriInfo.getBaseUriBuilder().path(path).build().toASCIIString()));
// The path is provided for every resource method by the respective info DTO.
// We don't want to add every REST endpoint but just the "parent" one.
// Per convention the name is similar to the path (without the leading "/") for openHAB REST
// implementations.
final URI uri = uriInfo.getBaseUriBuilder().path("/" + name).build();
if (collectedLinks.put(uri.toASCIIString(), name) != null) {
logger.warn("Duplicate entry: {}", name);
}
}
}
collectedLinks.forEach((path, name) -> {
bean.links.add(new Links(name, path));
});
return bean;
}
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
public void addRESTResource(RESTResource resource) {
restResources.add(resource);
}
public void removeRESTResource(RESTResource resource) {
restResources.remove(resource);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Activate
public void activate() {
Configuration configuration;
try {
configuration = configurationAdmin.getConfiguration(Constants.JAXRS_CONNECTOR_CONFIG, null);
if (configuration != null) {
Dictionary properties = configuration.getProperties();
if (properties == null) {
properties = new Properties();
}
String rootAlias = (String) properties.get(Constants.JAXRS_CONNECTOR_ROOT_PROPERTY);
if (!RESTConstants.REST_URI.equals(rootAlias)) {
properties.put(Constants.JAXRS_CONNECTOR_ROOT_PROPERTY, RESTConstants.REST_URI);
configuration.update(properties);
}
}
} catch (IOException e) {
logger.error("Could not set REST configuration properties!", e);
}
}
@Reference
protected void setConfigurationAdmin(ConfigurationAdmin configurationAdmin) {
this.configurationAdmin = configurationAdmin;
}
protected void unsetConfigurationAdmin(ConfigurationAdmin configurationAdmin) {
this.configurationAdmin = null;
}
}
@@ -23,9 +23,9 @@ import java.util.Collections;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import org.glassfish.jersey.uri.internal.JerseyUriBuilder;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -156,9 +156,9 @@ public class ProxyFilterTest {
private void setupContextURIs(String baseURI, String requestURI) {
try {
when(uriInfo.getBaseUri()).thenReturn(new URI(baseURI));
when(uriInfo.getBaseUriBuilder()).thenReturn(new JerseyUriBuilder().uri(baseURI));
when(uriInfo.getBaseUriBuilder()).thenReturn(UriBuilder.fromUri(baseURI));
when(uriInfo.getRequestUri()).thenReturn(new URI(requestURI));
when(uriInfo.getRequestUriBuilder()).thenReturn(new JerseyUriBuilder().uri(requestURI));
when(uriInfo.getRequestUriBuilder()).thenReturn(UriBuilder.fromUri(requestURI));
} catch (URISyntaxException e) {
throw new IllegalStateException("Error while setting up context mock", e);
}
@@ -26,6 +26,7 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import org.openhab.core.io.rest.LocaleService;
import org.openhab.core.io.rest.RESTConstants;
import org.openhab.core.io.rest.RESTResource;
import org.openhab.core.ui.icon.IconProvider;
import org.openhab.core.ui.icon.IconSet;
@@ -33,16 +34,35 @@ 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;
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 io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
/**
* This is a REST resource that provides information about available icon sets.
*
* @author Kai Kreuzer - Initial contribution
* @author Markus Rathgeb - Migrated to JAX-RS Whiteboard Specification
*/
@Path("iconsets")
@Component
@JaxrsResource
@JaxrsName(IconSetResource.PATH_ICONSETS)
@JaxrsApplicationSelect("(" + JaxrsWhiteboardConstants.JAX_RS_NAME + "=" + RESTConstants.JAX_RS_NAME + ")")
@JSONRequired
@Path(IconSetResource.PATH_ICONSETS)
@Api(IconSetResource.PATH_ICONSETS)
public class IconSetResource implements RESTResource {
/** The URI path to this resource */
public static final String PATH_ICONSETS = "iconsets";
private List<IconProvider> iconProviders = new ArrayList<>(5);
private LocaleService localeService;
@@ -70,6 +90,8 @@ public class IconSetResource implements RESTResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Gets all icon sets.")
@ApiResponses(value = { @ApiResponse(code = 200, message = "OK") })
public Response getAll(@HeaderParam("Accept-Language") String language) {
Locale locale = localeService.getLocale(language);
+1 -1
View File
@@ -64,9 +64,9 @@
<module>org.openhab.core.io.rest.core</module>
<module>org.openhab.core.io.rest.log</module>
<module>org.openhab.core.io.rest.mdns</module>
<module>org.openhab.core.io.rest.optimize</module>
<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.ui</module>
<module>org.openhab.core.io.rest.voice</module>
<module>org.openhab.core.io.transport.dbus</module>
@@ -28,8 +28,8 @@
<requirement>openhab.tp;filter:="(&amp;(feature=xtext)(version&gt;=2.19.0)(!(version&gt;=2.20.0)))"</requirement>
<feature dependency="true">openhab.tp-xtext</feature>
<requirement>openhab.tp;filter:="(feature=jax-rs)"</requirement>
<feature dependency="true">openhab.tp-jax-rs</feature>
<requirement>openhab.tp;filter:="(feature=jax-rs-whiteboard)"</requirement>
<feature dependency="true">openhab.tp-jax-rs-whiteboard</feature>
<requirement>openhab.tp;filter:="(feature=httpclient)"</requirement>
<feature dependency="true">openhab.tp-httpclient</feature>
@@ -56,7 +56,6 @@
<feature dependency="true">pax-http-whiteboard</feature>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.io.http/${project.version}</bundle>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.io.rest/${project.version}</bundle>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.io.rest.optimize/${project.version}</bundle>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.io.rest.core/${project.version}</bundle>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.io.rest.sse/${project.version}</bundle>
</feature>
@@ -171,6 +170,13 @@
<feature dependency="true">openhab-core-model-sitemap</feature>
</feature>
<feature name="openhab-core-io-rest-swagger" version="${project.version}">
<feature>openhab-core-base</feature>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.io.rest.swagger/${project.version}</bundle>
<requirement>openhab.tp;filter:="(feature=jaxrswb-swagger-gen)"</requirement>
<feature dependency="true">openhab.tp-jaxrswb-swagger-gen</feature>
</feature>
<feature name="openhab-core-io-rest-audio" version="${project.version}">
<feature>openhab-core-base</feature>
<bundle>mvn:org.openhab.core.bundles/org.openhab.core.io.rest.audio/${project.version}</bundle>
@@ -78,31 +78,37 @@
</feature>
<feature name="openhab.tp-jackson" description="FasterXML Jackson bundles" version="${project.version}">
<capability>openhab.tp;feature=jackson;version=2.9.10</capability>
<bundle dependency="true">mvn:com.fasterxml.jackson.core/jackson-annotations/2.9.10</bundle>
<bundle dependency="true">mvn:com.fasterxml.jackson.core/jackson-core/2.9.10</bundle>
<bundle dependency="true">mvn:com.fasterxml.jackson.core/jackson-databind/2.9.10</bundle>
<bundle dependency="true">mvn:com.fasterxml.jackson.dataformat/jackson-dataformat-xml/2.9.10</bundle>
<bundle dependency="true">mvn:com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.9.10</bundle>
<bundle dependency="true">mvn:com.fasterxml.jackson.datatype/jackson-datatype-joda/2.9.10</bundle>
<bundle dependency="true">mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-base/2.9.10</bundle>
<bundle dependency="true">mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-json-provider/2.9.10</bundle>
<bundle dependency="true">mvn:com.fasterxml.jackson.module/jackson-module-jaxb-annotations/2.9.10</bundle>
<bundle dependency="true">mvn:org.yaml/snakeyaml/1.23</bundle>
<capability>openhab.tp;feature=jackson;version=2.10.3</capability>
<bundle dependency="true">mvn:com.fasterxml.jackson.core/jackson-annotations/2.10.3</bundle>
<bundle dependency="true">mvn:com.fasterxml.jackson.core/jackson-core/2.10.3</bundle>
<bundle dependency="true">mvn:com.fasterxml.jackson.core/jackson-databind/2.10.3</bundle>
<bundle dependency="true">mvn:com.fasterxml.jackson.dataformat/jackson-dataformat-xml/2.10.3</bundle>
<bundle dependency="true">mvn:com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.10.3</bundle>
<bundle dependency="true">mvn:com.fasterxml.jackson.datatype/jackson-datatype-joda/2.10.3</bundle>
<bundle dependency="true">mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-base/2.10.3</bundle>
<bundle dependency="true">mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-json-provider/2.10.3</bundle>
<bundle dependency="true">mvn:com.fasterxml.jackson.module/jackson-module-jaxb-annotations/2.10.3</bundle>
<bundle dependency="true">mvn:org.yaml/snakeyaml/1.26</bundle>
</feature>
<feature name="openhab.tp-jax-rs" version="${project.version}">
<capability>openhab.tp;feature=jax-rs;version=5.3.1</capability>
<feature>openhab.tp-jax-rs-min</feature>
</feature>
<feature name="openhab.tp-jax-rs-whiteboard" version="${project.version}">
<capability>openhab.tp;feature=jax-rs-whiteboard;version=1.0.8</capability>
<feature name="openhab.tp-jax-rs-min" version="${project.version}">
<capability>openhab.tp;feature=jax-rs-min;version=5.3.1</capability>
<feature dependency="true">http</feature>
<feature dependency="true">openhab.tp-kat.cpy-jersey-min-2.22.2</feature>
<bundle>mvn:com.eclipsesource.jaxrs/publisher/5.3.1</bundle>
<config name="com.eclipsesource.jaxrs.connector">
root=/rest
<!-- BEG: https://issues.apache.org/jira/browse/KARAF-6536 -->
<!-- Add it here, so we do not use to rely on a feature that is using version ranges. -->
<feature>http-whiteboard</feature>
<feature dependency="true">openhab.tp-jaxws</feature>
<bundle dependency="true">mvn:org.apache.aries.spec/org.apache.aries.javax.jax.rs-api/1.0.1</bundle>
<bundle dependency="true">mvn:org.osgi/org.osgi.util.function/1.1.0</bundle>
<bundle dependency="true">mvn:org.osgi/org.osgi.util.promise/1.1.0</bundle>
<bundle dependency="true">mvn:org.osgi/org.osgi.service.jaxrs/1.0.0</bundle>
<bundle dependency="true">mvn:org.apache.servicemix.specs/org.apache.servicemix.specs.annotation-api-1.3/1.3_1</bundle>
<bundle dependency="true">mvn:org.apache.servicemix.specs/org.apache.servicemix.specs.jaxws-api-2.2/2.9.0</bundle>
<bundle>mvn:org.apache.aries.jax.rs/org.apache.aries.jax.rs.whiteboard/1.0.8</bundle>
<!-- END: https://issues.apache.org/jira/browse/KARAF-6536 -->
<config name="org.apache.aries.jax.rs.whiteboard.default">
replace.loopback.address.with.localhost=false
</config>
</feature>
@@ -111,30 +117,6 @@
<bundle dependency="true">mvn:org.openhab.osgiify/com.igormaznitsa.jbbp/1.4.1</bundle>
</feature>
<feature name="openhab.tp-kat.cpy-jersey-min-2.22.2" version="${project.version}">
<feature>http</feature>
<bundle start-level="36">mvn:org.glassfish.jersey.containers/jersey-container-servlet/2.22.2</bundle>
<bundle start-level="36">mvn:org.glassfish.jersey.media/jersey-media-sse/2.22.2</bundle>
<bundle start-level="36">mvn:org.glassfish.jersey.media/jersey-media-multipart/2.22.2</bundle>
<bundle start-level="36" dependency="true">mvn:org.glassfish.jersey.containers/jersey-container-servlet-core/2.22.2</bundle>
<bundle start-level="36" dependency="true">mvn:org.glassfish.jersey.core/jersey-common/2.22.2</bundle>
<bundle start-level="36" dependency="true">mvn:org.glassfish.jersey.bundles.repackaged/jersey-guava/2.22.2</bundle>
<bundle start-level="36" dependency="true">mvn:org.glassfish.jersey.core/jersey-server/2.22.2</bundle>
<bundle start-level="36" dependency="true">mvn:org.glassfish.jersey.core/jersey-client/2.22.2</bundle>
<bundle start-level="36" dependency="true">mvn:org.glassfish.jersey.media/jersey-media-jaxb/2.22.2</bundle>
<bundle start-level="33" dependency="true">mvn:org.glassfish.hk2/hk2-api/2.4.0-b34</bundle>
<bundle start-level="33" dependency="true">mvn:org.glassfish.hk2/hk2-locator/2.4.0-b34</bundle>
<bundle start-level="33" dependency="true">mvn:org.glassfish.hk2/hk2-utils/2.4.0-b34</bundle>
<bundle start-level="33" dependency="true">mvn:org.glassfish.hk2/osgi-resource-locator/1.0.1</bundle>
<bundle start-level="33" dependency="true">mvn:org.glassfish.hk2.external/javax.inject/2.4.0-b34</bundle>
<bundle start-level="33" dependency="true">mvn:org.glassfish.hk2.external/aopalliance-repackaged/2.4.0-b34</bundle>
<bundle dependency="true">mvn:javax.annotation/javax.annotation-api/1.2</bundle>
<bundle dependency="true">mvn:javax.validation/validation-api/1.1.0.Final</bundle>
<bundle dependency="true">mvn:javax.ws.rs/javax.ws.rs-api/2.0.1</bundle>
<bundle dependency="true">mvn:org.javassist/javassist/3.18.1-GA</bundle>
<bundle dependency="true">mvn:org.jvnet.mimepull/mimepull/1.9.6</bundle>
</feature>
<feature name="openhab.tp-netty" description="Netty bundles" version="${project.version}">
<capability>openhab.tp;feature=netty;version=4.1.42.Final</capability>
<bundle dependency="true">mvn:io.netty/netty-buffer/4.1.42.Final</bundle>
@@ -267,37 +249,25 @@
<bundle dependency="true">mvn:org.eclipse.xtext/org.eclipse.xtext.xbase.ide/2.19.0</bundle>
</feature>
<!-- <feature name="openhab.tp-swagger-annotations" description="Swagger Annotations" version="${project.version}"> <bundle dependency="true">mvn:io.swagger/swagger-annotations/1.5.5</bundle> </feature> -->
<feature name="openhab.tp-swagger-jax-rs-provider" description="JAX-RS provider for Swagger integration" version="${project.version}">
<feature name="openhab.tp-jaxrswb-swagger-gen" description="JAX-RS Whiteboard Swagger 1 generator" version="${project.version}">
<details>
This feature is currently only for external usage by products and not used by openHAB Core itself.
</details>
<capability>openhab.tp;feature=jax-rs-provider-swagger;version=1.1.1</capability>
<feature dependency="true">openhab.tp-jax-rs-min</feature>
<capability>openhab.tp;feature=jaxrswb-swagger-gen;version=0.0.4</capability>
<bundle>mvn:de.maggu2810.jaxrswb.bundles/jaxrswb-swagger1-gen/0.0.4</bundle>
<feature dependency="true">openhab.tp-jax-rs-whiteboard</feature>
<feature dependency="true">openhab.tp-jackson</feature>
<!-- BEG: swagger-jersey2-1.5.8 -->
<feature dependency="true">openhab.tp-kat.cpy-jersey-min-2.22.2</feature>
<bundle>mvn:de.maggu2810.thirdparty.modified.io.swagger/swagger-jersey2-jaxrs/1.5.8.v20160511-1038</bundle>
<bundle dependency="true">mvn:io.swagger/swagger-annotations/1.5.8</bundle>
<bundle dependency="true">mvn:io.swagger/swagger-core/1.5.8</bundle>
<bundle dependency="true">mvn:io.swagger/swagger-jaxrs/1.5.8</bundle>
<bundle dependency="true">mvn:io.swagger/swagger-models/1.5.8</bundle>
<bundle dependency="true">mvn:de.maggu2810.thirdparty.modified.org.reflections/reflections/0.9.10.v20160429-1435</bundle>
<bundle dependency="true">mvn:com.google.code.findbugs/annotations/2.0.1</bundle>
<bundle dependency="true">mvn:com.google.guava/guava/18.0</bundle>
<bundle dependency="true">mvn:io.swagger/swagger-jaxrs/1.6.1</bundle>
<bundle dependency="true">mvn:io.swagger/swagger-models/1.6.1</bundle>
<bundle dependency="true">mvn:io.swagger/swagger-annotations/1.6.1</bundle>
<bundle dependency="true">mvn:io.swagger/swagger-core/1.6.1</bundle>
<bundle dependency="true">mvn:com.google.guava/guava/27.1-jre</bundle>
<bundle dependency="true">mvn:com.google.guava/failureaccess/1.0.1</bundle>
<bundle dependency="true">mvn:javax.validation/validation-api/1.1.0.Final</bundle>
<bundle dependency="true">mvn:joda-time/joda-time/2.2</bundle>
<bundle dependency="true">mvn:org.apache.commons/commons-lang3/3.2.1</bundle>
<bundle dependency="true">mvn:org.codehaus.woodstox/stax2-api/3.1.4</bundle>
<bundle dependency="true">mvn:org.javassist/javassist/3.19.0-GA</bundle>
<bundle dependency="true">mvn:org.slf4j/slf4j-api/1.7.7</bundle>
<!-- END: swagger-jersey2-1.5.8 -->
<bundle>mvn:de.maggu2810.thirdparty.modified.com.eclipsesource.jaxrs/provider-swagger/1.1.1.201605111122</bundle>
<config name="com.eclipsesource.jaxrs.swagger.config">
swagger.basePath=/rest
swagger.info.title=openHAB REST API
<bundle dependency="true">mvn:de.maggu2810.thirdparty.modified.org.reflections/reflections/0.9.10.v20160429-1435</bundle>
<config name="de.maggu2810.jaxrswb.swagger1.gen">
info.title=openHAB REST API
</config>
</feature>
@@ -39,7 +39,6 @@ feature.openhab-config: \
org.apache.servicemix.bundles.xstream;version='[1.4.7,1.4.8)',\
org.apache.felix.configadmin;version='[1.9.8,1.9.9)',\
org.apache.felix.log;version='[1.2.0,1.2.1)',\
org.apache.felix.logback;version='[1.0.0,1.0.1)',\
tec.uom.lib.uom-lib-common;version='[1.0.3,1.0.4)',\
tec.uom.se;version='[1.0.10,1.0.11)',\
jollyday;version='[0.5.8,0.5.9)',\
@@ -55,4 +54,5 @@ feature.openhab-config: \
org.openhab.core.config.xml;version='[3.0.0,3.0.1)',\
org.openhab.core.ephemeris;version='[3.0.0,3.0.1)',\
org.openhab.core.ephemeris.tests;version='[3.0.0,3.0.1)',\
org.openhab.core.test;version='[3.0.0,3.0.1)'
org.openhab.core.test;version='[3.0.0,3.0.1)',\
org.apache.felix.logback;version='[1.0.2,1.0.3)'
@@ -4,14 +4,12 @@ Bundle-SymbolicName: ${project.artifactId}
Fragment-Host: org.openhab.core.io.rest.core
-runrequires: \
bnd.identity;id='org.openhab.core.io.rest.core.tests',\
bnd.identity;id='com.eclipsesource.jaxrs.jersey-all'
bnd.identity;id='org.openhab.core.io.rest.core.tests'
#
# done
#
-runbundles: \
com.eclipsesource.jaxrs.jersey-all;version='[2.22.2,2.22.3)',\
com.jayway.jsonpath.json-path;version='[2.4.0,2.4.1)',\
javax.measure.unit-api;version='[1.0.0,1.0.1)',\
net.minidev.accessors-smart;version='[1.2.0,1.2.1)',\
@@ -45,6 +43,22 @@ Fragment-Host: org.openhab.core.io.rest.core
org.eclipse.jetty.server;version='[9.4.20,9.4.21)',\
org.eclipse.jetty.servlet;version='[9.4.20,9.4.21)',\
org.eclipse.jetty.util;version='[9.4.20,9.4.21)',\
org.apache.aries.javax.jax.rs-api;version='[1.0.0,1.0.1)',\
org.apache.aries.jax.rs.whiteboard;version='[1.0.8,1.0.9)',\
org.apache.xbean.bundleutils;version='[4.12.0,4.12.1)',\
org.apache.xbean.finder;version='[4.12.0,4.12.1)',\
org.eclipse.jetty.xml;version='[9.4.20,9.4.21)',\
org.objectweb.asm;version='[7.1.0,7.1.1)',\
org.objectweb.asm.commons;version='[7.1.0,7.1.1)',\
org.objectweb.asm.tree;version='[7.1.0,7.1.1)',\
org.ops4j.pax.swissbox.optional.jcl;version='[1.8.3,1.8.4)',\
org.ops4j.pax.web.pax-web-api;version='[7.2.11,7.2.12)',\
org.ops4j.pax.web.pax-web-jetty;version='[7.2.11,7.2.12)',\
org.ops4j.pax.web.pax-web-runtime;version='[7.2.11,7.2.12)',\
org.ops4j.pax.web.pax-web-spi;version='[7.2.11,7.2.12)',\
org.osgi.service.jaxrs;version='[1.0.0,1.0.1)',\
org.osgi.util.function;version='[1.1.0,1.1.1)',\
org.osgi.util.promise;version='[1.1.0,1.1.1)',\
org.openhab.core;version='[3.0.0,3.0.1)',\
org.openhab.core.config.core;version='[3.0.0,3.0.1)',\
org.openhab.core.config.discovery;version='[3.0.0,3.0.1)',\
@@ -55,4 +69,6 @@ Fragment-Host: org.openhab.core.io.rest.core
org.openhab.core.persistence;version='[3.0.0,3.0.1)',\
org.openhab.core.test;version='[3.0.0,3.0.1)',\
org.openhab.core.thing;version='[3.0.0,3.0.1)',\
org.openhab.core.transform;version='[3.0.0,3.0.1)'
org.openhab.core.transform;version='[3.0.0,3.0.1)',\
javax.xml.soap-api;version='[1.3.5,1.3.6)',\
org.apache.servicemix.specs.jaxws-api-2.2;version='[2.9.0,2.9.1)'
@@ -58,7 +58,7 @@ public class InboxResourceOSGITest extends JavaOSGiTest {
resource.setInbox(inbox);
Response reponse = resource.approve(null, testThing.getUID().toString(), testThingLabel);
assertTrue(reponse.getStatusInfo() == Status.OK);
assertTrue(reponse.getStatusInfo().getStatusCode() == Status.OK.getStatusCode());
}
@Test
@@ -68,6 +68,6 @@ public class InboxResourceOSGITest extends JavaOSGiTest {
resource.setInbox(inbox);
Response reponse = resource.approve(null, testThing.getUID().toString(), testThingLabel);
assertTrue(reponse.getStatusInfo() == Status.NOT_FOUND);
assertTrue(reponse.getStatusInfo().getStatusCode() == Status.NOT_FOUND.getStatusCode());
}
}
@@ -16,18 +16,22 @@ import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.collection.IsCollectionWithSize.hasSize;
import static org.hamcrest.core.IsCollectionContaining.hasItems;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import static org.mockito.MockitoAnnotations.initMocks;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import org.apache.commons.io.IOUtils;
@@ -71,6 +75,9 @@ public class ItemResourceOSGiTest extends JavaOSGiTest {
@Mock
private ItemProvider itemProvider;
private UriInfo uriInfo;
private HttpHeaders httpHeaders;
private ItemResource itemResource;
private ManagedItemProvider managedItemProvider;
@@ -81,8 +88,6 @@ public class ItemResourceOSGiTest extends JavaOSGiTest {
itemResource = getService(RESTResource.class, ItemResource.class);
assertNotNull(itemResource);
itemResource.uriInfo = mock(UriInfo.class);
registerVolatileStorageService();
managedItemProvider = getService(ManagedItemProvider.class);
@@ -93,13 +98,21 @@ public class ItemResourceOSGiTest extends JavaOSGiTest {
when(itemProvider.getAll()).thenReturn(Arrays.asList(item1, item2, item3, item4));
registerService(itemProvider);
UriBuilder uriBuilder = mock(UriBuilder.class);
when(uriBuilder.build(any())).thenReturn(URI.create(""));
uriInfo = mock(UriInfo.class);
when(uriInfo.getAbsolutePathBuilder()).thenReturn(uriBuilder);
when(uriInfo.getPath()).thenReturn("");
httpHeaders = mock(HttpHeaders.class);
when(httpHeaders.getHeaderString(anyString())).thenReturn(null);
}
@Test
public void shouldReturnUnicodeItems() throws IOException {
item4.setLabel(ITEM_LABEL4);
Response response = itemResource.getItems(null, null, null, null, false, null);
Response response = itemResource.getItems(uriInfo, httpHeaders, null, null, null, null, false, null);
assertThat(readItemLabelsFromResponse(response), hasItems(ITEM_LABEL4));
}
@@ -107,7 +120,7 @@ public class ItemResourceOSGiTest extends JavaOSGiTest {
public void shouldReturnUnicodeItem() throws IOException {
item4.setLabel(ITEM_LABEL4);
Response response = itemResource.getItemData(null, null, ITEM_NAME4);
Response response = itemResource.getItemData(uriInfo, httpHeaders, null, null, ITEM_NAME4);
assertThat(readItemLabelsFromResponse(response), hasItems(ITEM_LABEL4));
}
@@ -119,25 +132,25 @@ public class ItemResourceOSGiTest extends JavaOSGiTest {
item3.addTag("Tag2");
item4.addTag("Tag4");
Response response = itemResource.getItems(null, null, "Tag1", null, false, null);
Response response = itemResource.getItems(uriInfo, httpHeaders, null, null, "Tag1", null, false, null);
assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME1, ITEM_NAME2));
response = itemResource.getItems(null, null, "Tag2", null, false, null);
response = itemResource.getItems(uriInfo, httpHeaders, null, null, "Tag2", null, false, null);
assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME2, ITEM_NAME3));
response = itemResource.getItems(null, null, "NotExistingTag", null, false, null);
response = itemResource.getItems(uriInfo, httpHeaders, null, null, "NotExistingTag", null, false, null);
assertThat(readItemNamesFromResponse(response), hasSize(0));
}
@Test
public void shouldFilterItemsByType() throws Exception {
Response response = itemResource.getItems(null, "Switch", null, null, false, null);
Response response = itemResource.getItems(uriInfo, httpHeaders, null, "Switch", null, null, false, null);
assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME1, ITEM_NAME2));
response = itemResource.getItems(null, "Dimmer", null, null, false, null);
response = itemResource.getItems(uriInfo, httpHeaders, null, "Dimmer", null, null, false, null);
assertThat(readItemNamesFromResponse(response), hasItems(ITEM_NAME3));
response = itemResource.getItems(null, "Color", null, null, false, null);
response = itemResource.getItems(uriInfo, httpHeaders, null, "Color", null, null, false, null);
assertThat(readItemNamesFromResponse(response), hasSize(0));
}
@@ -145,15 +158,15 @@ public class ItemResourceOSGiTest extends JavaOSGiTest {
public void shouldAddAndRemoveTags() throws Exception {
managedItemProvider.add(new SwitchItem("Switch"));
Response response = itemResource.getItems(null, null, "MyTag", null, false, null);
Response response = itemResource.getItems(uriInfo, httpHeaders, null, null, "MyTag", null, false, null);
assertThat(readItemNamesFromResponse(response), hasSize(0));
itemResource.addTag("Switch", "MyTag");
response = itemResource.getItems(null, null, "MyTag", null, false, null);
response = itemResource.getItems(uriInfo, httpHeaders, null, null, "MyTag", null, false, null);
assertThat(readItemNamesFromResponse(response), hasSize(1));
itemResource.removeTag("Switch", "MyTag");
response = itemResource.getItems(null, null, "MyTag", null, false, null);
response = itemResource.getItems(uriInfo, httpHeaders, null, null, "MyTag", null, false, null);
assertThat(readItemNamesFromResponse(response), hasSize(0));
}
@@ -162,7 +175,7 @@ public class ItemResourceOSGiTest extends JavaOSGiTest {
JsonParser parser = new JsonParser();
managedItemProvider.add(new SwitchItem("Switch"));
itemResource.addTag("Switch", "MyTag");
Response response = itemResource.getItems(null, null, "MyTag", null, false, "type,name");
Response response = itemResource.getItems(uriInfo, httpHeaders, null, null, "MyTag", null, false, "type,name");
JsonElement result = parser.parse(IOUtils.toString((InputStream) response.getEntity()));
JsonElement expected = parser.parse("[{type: \"Switch\", name: \"Switch\"}]");
@@ -96,4 +96,4 @@ Fragment-Host: org.openhab.core.model.core
org.openhab.core.thing;version='[3.0.0,3.0.1)',\
org.openhab.core.transform;version='[3.0.0,3.0.1)',\
org.openhab.core.voice;version='[3.0.0,3.0.1)',\
org.openhab.core.model.rule.runtime;version='[3.0.0,3.0.1)'
org.openhab.core.model.item.runtime;version='[3.0.0,3.0.1)'
@@ -94,5 +94,4 @@ Fragment-Host: org.openhab.core.model.item
org.openhab.core.test;version='[3.0.0,3.0.1)',\
org.openhab.core.thing;version='[3.0.0,3.0.1)',\
org.openhab.core.transform;version='[3.0.0,3.0.1)',\
org.openhab.core.voice;version='[3.0.0,3.0.1)',\
org.openhab.core.model.rule.runtime;version='[3.0.0,3.0.1)'
org.openhab.core.voice;version='[3.0.0,3.0.1)'
@@ -111,4 +111,4 @@ Fragment-Host: org.openhab.core.model.persistence
org.openhab.core.thing;version='[3.0.0,3.0.1)',\
org.openhab.core.transform;version='[3.0.0,3.0.1)',\
org.openhab.core.voice;version='[3.0.0,3.0.1)',\
org.openhab.core.model.rule.runtime;version='[3.0.0,3.0.1)'
org.openhab.core.model.item.runtime;version='[3.0.0,3.0.1)'
@@ -94,4 +94,5 @@ Fragment-Host: org.openhab.core.model.rule.runtime
org.openhab.core.test;version='[3.0.0,3.0.1)',\
org.openhab.core.thing;version='[3.0.0,3.0.1)',\
org.openhab.core.transform;version='[3.0.0,3.0.1)',\
org.openhab.core.voice;version='[3.0.0,3.0.1)'
org.openhab.core.voice;version='[3.0.0,3.0.1)',\
org.openhab.core.model.item.runtime;version='[3.0.0,3.0.1)'
@@ -94,5 +94,4 @@ Fragment-Host: org.openhab.core.model.script
org.openhab.core.thing;version='[3.0.0,3.0.1)',\
org.openhab.core.transform;version='[3.0.0,3.0.1)',\
org.openhab.core.voice;version='[3.0.0,3.0.1)',\
org.openhab.core.model.rule.runtime;version='[3.0.0,3.0.1)'
org.openhab.core.model.item.runtime;version='[3.0.0,3.0.1)'
@@ -105,6 +105,5 @@ Fragment-Host: org.openhab.core.model.thing
org.openhab.core.thing;version='[3.0.0,3.0.1)',\
org.openhab.core.thing.xml;version='[3.0.0,3.0.1)',\
org.openhab.core.transform;version='[3.0.0,3.0.1)',\
org.openhab.core.voice;version='[3.0.0,3.0.1)',\
org.openhab.core.model.sitemap.runtime;version='[3.0.0,3.0.1)'
org.openhab.core.voice;version='[3.0.0,3.0.1)'