2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.io.hueemulation.internal;
15 import java.util.Dictionary;
16 import java.util.Hashtable;
18 import javax.servlet.ServletException;
19 import javax.ws.rs.ApplicationPath;
20 import javax.ws.rs.HttpMethod;
21 import javax.ws.rs.container.ContainerRequestContext;
22 import javax.ws.rs.container.ContainerRequestFilter;
23 import javax.ws.rs.container.ContainerResponseContext;
24 import javax.ws.rs.container.ContainerResponseFilter;
25 import javax.ws.rs.container.PreMatching;
26 import javax.ws.rs.core.Application;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.http.HttpHeader;
31 import org.glassfish.jersey.server.ResourceConfig;
32 import org.glassfish.jersey.server.ServerProperties;
33 import org.glassfish.jersey.servlet.ServletContainer;
34 import org.glassfish.jersey.servlet.ServletProperties;
35 import org.openhab.io.hueemulation.internal.rest.ConfigurationAccess;
36 import org.openhab.io.hueemulation.internal.rest.LightsAndGroups;
37 import org.openhab.io.hueemulation.internal.rest.Rules;
38 import org.openhab.io.hueemulation.internal.rest.Scenes;
39 import org.openhab.io.hueemulation.internal.rest.Schedules;
40 import org.openhab.io.hueemulation.internal.rest.Sensors;
41 import org.openhab.io.hueemulation.internal.rest.StatusResource;
42 import org.openhab.io.hueemulation.internal.rest.UserManagement;
43 import org.openhab.io.hueemulation.internal.upnp.UpnpServer;
44 import org.osgi.framework.BundleContext;
45 import org.osgi.framework.ServiceRegistration;
46 import org.osgi.service.component.annotations.Activate;
47 import org.osgi.service.component.annotations.Component;
48 import org.osgi.service.component.annotations.Deactivate;
49 import org.osgi.service.component.annotations.Modified;
50 import org.osgi.service.component.annotations.Reference;
51 import org.osgi.service.component.annotations.ReferenceCardinality;
52 import org.osgi.service.component.annotations.ReferencePolicyOption;
53 import org.osgi.service.event.Event;
54 import org.osgi.service.event.EventAdmin;
55 import org.osgi.service.event.EventConstants;
56 import org.osgi.service.event.EventHandler;
57 import org.osgi.service.http.HttpService;
58 import org.osgi.service.http.NamespaceException;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
63 * Provides a Hue compatible HTTP REST API on /api.
65 * References all different rest endpoints implemented as JAX-RS annotated classes in the sub-package "rest".
66 * Those are very modular and have (almost) no inter-dependencies. The UPnP related part is encapsulated in
67 * the also referenced {@link UpnpServer}.
69 * openHAB items via the {@link org.openhab.core.items.ItemRegistry} are for example mapped to
70 * /api/{username}/lights and /api/{username}/groups in {@link LightsAndGroups}.
72 * The user management is realized in the {@link UserManagement} component, that is referenced by almost all
73 * other components and is the only inter-component dependency.
75 * @author David Graeff - Initial Contribution
78 @Component(immediate = true, service = { HueEmulationService.class }, property = {
79 "com.eclipsesource.jaxrs.publish=false" })
80 public class HueEmulationService implements EventHandler {
82 public static final String CONFIG_PID = "org.openhab.hueemulation";
83 public static final String RESTAPI_PATH = "/api";
85 @ApplicationPath(RESTAPI_PATH)
86 public static class JerseyApplication extends Application {
91 public class RequestInterceptor implements ContainerRequestFilter {
94 public void filter(ContainerRequestContext requestContext) {
96 * Jetty returns 415 on any GET request if a client sends the Content-Type header.
97 * This is a workaround - stripping it away in the preMatching stage.
99 if (requestContext.getMethod() == HttpMethod.GET
100 && requestContext.getHeaders().containsKey(HttpHeader.CONTENT_TYPE.asString())) {
101 requestContext.getHeaders().remove(HttpHeader.CONTENT_TYPE.asString());
106 public class LogAccessInterceptor implements ContainerResponseFilter {
107 @NonNullByDefault({})
109 public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
110 if (!logger.isDebugEnabled()) {
114 logger.debug("REST request {} {}", requestContext.getMethod(), requestContext.getUriInfo().getPath());
115 logger.debug("REST response: {}", responseContext.getEntity());
119 private final ContainerRequestFilter requestCleaner = new RequestInterceptor();
120 private final Logger logger = LoggerFactory.getLogger(HueEmulationService.class);
121 private final LogAccessInterceptor accessInterceptor = new LogAccessInterceptor();
123 //// Required services ////
124 // Don't fail the service if the upnp server does not come up
125 // That part is required for discovery only but does not affect already configured hue applications
126 @Reference(cardinality = ReferenceCardinality.OPTIONAL, policyOption = ReferencePolicyOption.GREEDY)
127 protected @Nullable UpnpServer discovery;
129 protected @NonNullByDefault({}) ConfigStore cs;
131 protected @NonNullByDefault({}) UserManagement userManagement;
133 protected @NonNullByDefault({}) ConfigurationAccess configurationAccess;
135 protected @NonNullByDefault({}) LightsAndGroups lightItems;
137 protected @NonNullByDefault({}) Sensors sensors;
139 protected @NonNullByDefault({}) Scenes scenes;
141 protected @NonNullByDefault({}) Schedules schedules;
143 protected @NonNullByDefault({}) Rules rules;
145 protected @NonNullByDefault({}) StatusResource statusResource;
148 protected @NonNullByDefault({}) HttpService httpService;
149 private @NonNullByDefault({}) ServiceRegistration<?> eventHandler;
152 protected void activate(BundleContext bc) {
153 Dictionary<String, Object> properties = new Hashtable<>();
154 properties.put(EventConstants.EVENT_TOPIC, ConfigStore.EVENT_ADDRESS_CHANGED);
155 eventHandler = bc.registerService(EventHandler.class, this, properties);
161 // Don't restart the service on config change
163 protected void modified() {
167 protected void deactivate() {
169 if (eventHandler != null) {
170 eventHandler.unregister();
172 } catch (IllegalStateException ignore) {
175 httpService.unregister(RESTAPI_PATH);
176 } catch (IllegalArgumentException ignore) {
181 * We have a hard dependency on the {@link ConfigStore} and that it has initialized the Hue DataStore config
182 * completely. That initialization happens asynchronously and therefore we cannot rely on OSGi activate/modified
183 * state changes. Instead the {@link EventAdmin} is used and we listen for the
184 * {@link ConfigStore#EVENT_ADDRESS_CHANGED} event that is fired as soon as the config is ready.
187 public void handleEvent(@Nullable Event event) {
188 try { // Only receive this event once
189 eventHandler.unregister();
191 } catch (IllegalStateException ignore) {
194 ResourceConfig resourceConfig = ResourceConfig.forApplicationClass(JerseyApplication.class);
195 resourceConfig.property(ServerProperties.APPLICATION_NAME, "HueEmulation");
196 // don't look for implementations described by META-INF/services/*
197 resourceConfig.property(ServerProperties.METAINF_SERVICES_LOOKUP_DISABLE, true);
198 // disable auto discovery on server, as it's handled via OSGI
199 resourceConfig.property(ServerProperties.FEATURE_AUTO_DISCOVERY_DISABLE, true);
201 resourceConfig.property(ServerProperties.PROCESSING_RESPONSE_ERRORS_ENABLED, true);
203 resourceConfig.registerInstances(userManagement, configurationAccess, lightItems, sensors, scenes, schedules,
204 rules, statusResource, accessInterceptor, requestCleaner);
207 Hashtable<String, String> initParams = new Hashtable<>();
208 initParams.put("com.sun.jersey.api.json.POJOMappingFeature", "false");
209 initParams.put(ServletProperties.PROVIDER_WEB_APP, "false");
210 httpService.registerServlet(RESTAPI_PATH, new ServletContainer(resourceConfig), initParams, null);
211 UpnpServer localDiscovery = discovery;
212 if (localDiscovery == null) {
213 logger.warn("The UPnP Server service has not been started!");
214 } else if (!localDiscovery.upnpAnnouncementThreadRunning()) {
215 localDiscovery.handleEvent(null);
217 statusResource.startUpnpSelfTest();
218 logger.info("Hue Emulation service available under {}", RESTAPI_PATH);
219 } catch (ServletException | NamespaceException e) {
220 logger.warn("Could not start Hue Emulation service: {}", e.getMessage(), e);