]> git.basschouten.com Git - openhab-addons.git/blob
6ffbed4e38dfddb30666c3916dffa6f259063923
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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 import java.util.Set;
18
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;
26
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;
57
58 /**
59  * Provides a Hue compatible HTTP REST API on /api.
60  * <p>
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}.
64  * <p>
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}.
67  * <p>
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.
70  *
71  * @author David Graeff - Initial Contribution
72  */
73 @NonNullByDefault
74 @Component(immediate = true, service = HueEmulationService.class)
75 public class HueEmulationService implements EventHandler {
76
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";
80
81     @PreMatching
82     public class RequestInterceptor implements ContainerRequestFilter {
83         @NonNullByDefault({})
84         @Override
85         public void filter(ContainerRequestContext requestContext) {
86             /**
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.
89              */
90             if (HttpMethod.GET.equals(requestContext.getMethod())
91                     && requestContext.getHeaders().containsKey(HttpHeader.CONTENT_TYPE.asString())) {
92                 requestContext.getHeaders().remove(HttpHeader.CONTENT_TYPE.asString());
93             }
94         }
95     }
96
97     public class LogAccessInterceptor implements ContainerResponseFilter {
98         @NonNullByDefault({})
99         @Override
100         public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
101             if (!logger.isDebugEnabled()) {
102                 return;
103             }
104
105             logger.debug("REST request {} {}", requestContext.getMethod(), requestContext.getUriInfo().getPath());
106             logger.debug("REST response: {}", responseContext.getEntity());
107         }
108     }
109
110     private final ContainerRequestFilter requestCleaner = new RequestInterceptor();
111
112     /**
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
115      * start up.
116      */
117     @JaxrsName(REST_APP_NAME)
118     private class RESTapplication extends Application {
119         private String root;
120
121         RESTapplication(String root) {
122             this.root = root;
123         }
124
125         @NonNullByDefault({})
126         @Override
127         public Set<Object> getSingletons() {
128             return Set.of(userManagement, configurationAccess, lightItems, sensors, scenes, schedules, rules,
129                     statusResource, accessInterceptor, requestCleaner);
130         }
131
132         Dictionary<String, String> serviceProperties() {
133             Dictionary<String, String> dict = new Hashtable<>();
134             dict.put(JaxrsWhiteboardConstants.JAX_RS_APPLICATION_BASE, root);
135             return dict;
136         }
137     }
138
139     private final Logger logger = LoggerFactory.getLogger(HueEmulationService.class);
140     private final LogAccessInterceptor accessInterceptor = new LogAccessInterceptor();
141
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;
147     @Reference
148     protected @NonNullByDefault({}) ConfigStore cs;
149     @Reference
150     protected @NonNullByDefault({}) UserManagement userManagement;
151     @Reference
152     protected @NonNullByDefault({}) ConfigurationAccess configurationAccess;
153     @Reference
154     protected @NonNullByDefault({}) LightsAndGroups lightItems;
155     @Reference
156     protected @NonNullByDefault({}) Sensors sensors;
157     @Reference
158     protected @NonNullByDefault({}) Scenes scenes;
159     @Reference
160     protected @NonNullByDefault({}) Schedules schedules;
161     @Reference
162     protected @NonNullByDefault({}) Rules rules;
163     @Reference
164     protected @NonNullByDefault({}) StatusResource statusResource;
165
166     private @Nullable ServiceRegistration<?> eventHandler;
167     private @Nullable ServiceRegistration<Application> restService;
168
169     @Activate
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);
174         if (cs.isReady()) {
175             handleEvent(null);
176         }
177     }
178
179     // Don't restart the service on config change
180     @Modified
181     protected void modified() {
182     }
183
184     @Deactivate
185     protected void deactivate() {
186         unregisterEventHandler();
187
188         ServiceRegistration<Application> localRestService = restService;
189         if (localRestService != null) {
190             localRestService.unregister();
191         }
192     }
193
194     /**
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.
199      */
200     @Override
201     public void handleEvent(@Nullable Event event) {
202         unregisterEventHandler();
203
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);
210         }
211     }
212
213     private void unregisterEventHandler() {
214         ServiceRegistration<?> localEventHandler = eventHandler;
215         if (localEventHandler != null) {
216             try {
217                 localEventHandler.unregister();
218                 eventHandler = null;
219             } catch (IllegalStateException e) {
220                 logger.debug("EventHandler already unregistered", e);
221             }
222         }
223     }
224 }