2 * Copyright (c) 2010-2023 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;
19 import javax.ws.rs.HttpMethod;
20 import javax.ws.rs.container.ContainerRequestContext;
21 import javax.ws.rs.container.ContainerRequestFilter;
22 import javax.ws.rs.container.ContainerResponseContext;
23 import javax.ws.rs.container.ContainerResponseFilter;
24 import javax.ws.rs.container.PreMatching;
25 import javax.ws.rs.core.Application;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.eclipse.jetty.http.HttpHeader;
30 import org.openhab.io.hueemulation.internal.rest.ConfigurationAccess;
31 import org.openhab.io.hueemulation.internal.rest.LightsAndGroups;
32 import org.openhab.io.hueemulation.internal.rest.Rules;
33 import org.openhab.io.hueemulation.internal.rest.Scenes;
34 import org.openhab.io.hueemulation.internal.rest.Schedules;
35 import org.openhab.io.hueemulation.internal.rest.Sensors;
36 import org.openhab.io.hueemulation.internal.rest.StatusResource;
37 import org.openhab.io.hueemulation.internal.rest.UserManagement;
38 import org.openhab.io.hueemulation.internal.upnp.UpnpServer;
39 import org.osgi.framework.BundleContext;
40 import org.osgi.framework.FrameworkUtil;
41 import org.osgi.framework.ServiceRegistration;
42 import org.osgi.service.component.annotations.Activate;
43 import org.osgi.service.component.annotations.Component;
44 import org.osgi.service.component.annotations.Deactivate;
45 import org.osgi.service.component.annotations.Modified;
46 import org.osgi.service.component.annotations.Reference;
47 import org.osgi.service.component.annotations.ReferenceCardinality;
48 import org.osgi.service.component.annotations.ReferencePolicyOption;
49 import org.osgi.service.event.Event;
50 import org.osgi.service.event.EventAdmin;
51 import org.osgi.service.event.EventConstants;
52 import org.osgi.service.event.EventHandler;
53 import org.osgi.service.jaxrs.whiteboard.JaxrsWhiteboardConstants;
54 import org.osgi.service.jaxrs.whiteboard.propertytypes.JaxrsName;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
59 * Provides a Hue compatible HTTP REST API on /api.
61 * References all different rest endpoints implemented as JAX-RS annotated classes in the sub-package "rest".
62 * Those are very modular and have (almost) no inter-dependencies. The UPnP related part is encapsulated in
63 * the also referenced {@link UpnpServer}.
65 * openHAB items via the {@link org.openhab.core.items.ItemRegistry} are for example mapped to
66 * /api/{username}/lights and /api/{username}/groups in {@link LightsAndGroups}.
68 * The user management is realized in the {@link UserManagement} component, that is referenced by almost all
69 * other components and is the only inter-component dependency.
71 * @author David Graeff - Initial Contribution
74 @Component(immediate = true, service = HueEmulationService.class)
75 public class HueEmulationService implements EventHandler {
77 public static final String CONFIG_PID = "org.openhab.hueemulation";
78 public static final String RESTAPI_PATH = "/api";
79 public static final String REST_APP_NAME = "HueEmulation";
82 public class RequestInterceptor implements ContainerRequestFilter {
85 public void filter(ContainerRequestContext requestContext) {
87 * Jetty returns 415 on any GET request if a client sends the Content-Type header.
88 * This is a workaround - stripping it away in the preMatching stage.
90 if (HttpMethod.GET.equals(requestContext.getMethod())
91 && requestContext.getHeaders().containsKey(HttpHeader.CONTENT_TYPE.asString())) {
92 requestContext.getHeaders().remove(HttpHeader.CONTENT_TYPE.asString());
97 public class LogAccessInterceptor implements ContainerResponseFilter {
100 public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
101 if (!logger.isDebugEnabled()) {
105 logger.debug("REST request {} {}", requestContext.getMethod(), requestContext.getUriInfo().getPath());
106 logger.debug("REST response: {}", responseContext.getEntity());
110 private final ContainerRequestFilter requestCleaner = new RequestInterceptor();
113 * The Jax-RS application that starts up all REST activities.
114 * It registers itself as a Jax-RS Whiteboard service and all Jax-RS resources that are targeting REST_APP_NAME will
117 @JaxrsName(REST_APP_NAME)
118 private class RESTapplication extends Application {
121 RESTapplication(String root) {
125 @NonNullByDefault({})
127 public Set<Object> getSingletons() {
128 return Set.of(userManagement, configurationAccess, lightItems, sensors, scenes, schedules, rules,
129 statusResource, accessInterceptor, requestCleaner);
132 Dictionary<String, String> serviceProperties() {
133 Dictionary<String, String> dict = new Hashtable<>();
134 dict.put(JaxrsWhiteboardConstants.JAX_RS_APPLICATION_BASE, root);
139 private final Logger logger = LoggerFactory.getLogger(HueEmulationService.class);
140 private final LogAccessInterceptor accessInterceptor = new LogAccessInterceptor();
142 //// Required services ////
143 // Don't fail the service if the upnp server does not come up
144 // That part is required for discovery only but does not affect already configured hue applications
145 @Reference(cardinality = ReferenceCardinality.OPTIONAL, policyOption = ReferencePolicyOption.GREEDY)
146 protected @Nullable UpnpServer discovery;
148 protected @NonNullByDefault({}) ConfigStore cs;
150 protected @NonNullByDefault({}) UserManagement userManagement;
152 protected @NonNullByDefault({}) ConfigurationAccess configurationAccess;
154 protected @NonNullByDefault({}) LightsAndGroups lightItems;
156 protected @NonNullByDefault({}) Sensors sensors;
158 protected @NonNullByDefault({}) Scenes scenes;
160 protected @NonNullByDefault({}) Schedules schedules;
162 protected @NonNullByDefault({}) Rules rules;
164 protected @NonNullByDefault({}) StatusResource statusResource;
166 private @Nullable ServiceRegistration<?> eventHandler;
167 private @Nullable ServiceRegistration<Application> restService;
170 protected void activate(BundleContext bc) {
171 Dictionary<String, Object> properties = new Hashtable<>();
172 properties.put(EventConstants.EVENT_TOPIC, ConfigStore.EVENT_ADDRESS_CHANGED);
173 eventHandler = bc.registerService(EventHandler.class, this, properties);
179 // Don't restart the service on config change
181 protected void modified() {
185 protected void deactivate() {
186 unregisterEventHandler();
188 ServiceRegistration<Application> localRestService = restService;
189 if (localRestService != null) {
190 localRestService.unregister();
195 * We have a hard dependency on the {@link ConfigStore} and that it has initialized the Hue DataStore config
196 * completely. That initialization happens asynchronously and therefore we cannot rely on OSGi activate/modified
197 * state changes. Instead the {@link EventAdmin} is used and we listen for the
198 * {@link ConfigStore#EVENT_ADDRESS_CHANGED} event that is fired as soon as the config is ready.
201 public void handleEvent(@Nullable Event event) {
202 unregisterEventHandler();
204 ServiceRegistration<Application> localRestService = restService;
205 if (localRestService == null) {
206 RESTapplication app = new RESTapplication(RESTAPI_PATH);
207 BundleContext context = FrameworkUtil.getBundle(getClass()).getBundleContext();
208 restService = context.registerService(Application.class, app, app.serviceProperties());
209 logger.info("Hue Emulation service available under {}", RESTAPI_PATH);
213 private void unregisterEventHandler() {
214 ServiceRegistration<?> localEventHandler = eventHandler;
215 if (localEventHandler != null) {
217 localEventHandler.unregister();
219 } catch (IllegalStateException e) {
220 logger.debug("EventHandler already unregistered", e);