]> git.basschouten.com Git - openhab-addons.git/blob
368428deac64084add9870acc6b7fb8cc6b3f199
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.io.hueemulation.internal;
14
15 import java.util.Dictionary;
16 import java.util.Hashtable;
17
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;
27
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;
61
62 /**
63  * Provides a Hue compatible HTTP REST API on /api.
64  * <p>
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}.
68  * <p>
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}.
71  * <p>
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.
74  *
75  * @author David Graeff - Initial Contribution
76  */
77 @NonNullByDefault
78 @Component(immediate = true, service = { HueEmulationService.class }, property = {
79         "com.eclipsesource.jaxrs.publish=false" })
80 public class HueEmulationService implements EventHandler {
81
82     public static final String CONFIG_PID = "org.openhab.hueemulation";
83     public static final String RESTAPI_PATH = "/api";
84
85     @ApplicationPath(RESTAPI_PATH)
86     public static class JerseyApplication extends Application {
87
88     }
89
90     @PreMatching
91     public class RequestInterceptor implements ContainerRequestFilter {
92         @NonNullByDefault({})
93         @Override
94         public void filter(ContainerRequestContext requestContext) {
95             /**
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.
98              */
99             if (requestContext.getMethod() == HttpMethod.GET
100                     && requestContext.getHeaders().containsKey(HttpHeader.CONTENT_TYPE.asString())) {
101                 requestContext.getHeaders().remove(HttpHeader.CONTENT_TYPE.asString());
102             }
103         }
104     }
105
106     public class LogAccessInterceptor implements ContainerResponseFilter {
107         @NonNullByDefault({})
108         @Override
109         public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
110             if (!logger.isDebugEnabled()) {
111                 return;
112             }
113
114             logger.debug("REST request {} {}", requestContext.getMethod(), requestContext.getUriInfo().getPath());
115             logger.debug("REST response: {}", responseContext.getEntity());
116         }
117     }
118
119     private final ContainerRequestFilter requestCleaner = new RequestInterceptor();
120     private final Logger logger = LoggerFactory.getLogger(HueEmulationService.class);
121     private final LogAccessInterceptor accessInterceptor = new LogAccessInterceptor();
122
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;
128     @Reference
129     protected @NonNullByDefault({}) ConfigStore cs;
130     @Reference
131     protected @NonNullByDefault({}) UserManagement userManagement;
132     @Reference
133     protected @NonNullByDefault({}) ConfigurationAccess configurationAccess;
134     @Reference
135     protected @NonNullByDefault({}) LightsAndGroups lightItems;
136     @Reference
137     protected @NonNullByDefault({}) Sensors sensors;
138     @Reference
139     protected @NonNullByDefault({}) Scenes scenes;
140     @Reference
141     protected @NonNullByDefault({}) Schedules schedules;
142     @Reference
143     protected @NonNullByDefault({}) Rules rules;
144     @Reference
145     protected @NonNullByDefault({}) StatusResource statusResource;
146
147     @Reference
148     protected @NonNullByDefault({}) HttpService httpService;
149     private @NonNullByDefault({}) ServiceRegistration<?> eventHandler;
150
151     @Activate
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);
156         if (cs.isReady()) {
157             handleEvent(null);
158         }
159     }
160
161     // Don't restart the service on config change
162     @Modified
163     protected void modified() {
164     }
165
166     @Deactivate
167     protected void deactivate() {
168         try {
169             if (eventHandler != null) {
170                 eventHandler.unregister();
171             }
172         } catch (IllegalStateException ignore) {
173         }
174         try {
175             httpService.unregister(RESTAPI_PATH);
176         } catch (IllegalArgumentException ignore) {
177         }
178     }
179
180     /**
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.
185      */
186     @Override
187     public void handleEvent(@Nullable Event event) {
188         try { // Only receive this event once
189             eventHandler.unregister();
190             eventHandler = null;
191         } catch (IllegalStateException ignore) {
192         }
193
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);
200
201         resourceConfig.property(ServerProperties.PROCESSING_RESPONSE_ERRORS_ENABLED, true);
202
203         resourceConfig.registerInstances(userManagement, configurationAccess, lightItems, sensors, scenes, schedules,
204                 rules, statusResource, accessInterceptor, requestCleaner);
205
206         try {
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);
216             }
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);
221         }
222     }
223 }