]> git.basschouten.com Git - openhab-addons.git/blob
8a660977b4c435b51e0035a948b8240fd15d62fb
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.binding.bticinosmarther.internal.account;
14
15 import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.*;
16
17 import java.io.FileNotFoundException;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.net.URL;
21 import java.util.Hashtable;
22 import java.util.Map;
23 import java.util.Optional;
24 import java.util.Set;
25 import java.util.concurrent.ConcurrentHashMap;
26
27 import javax.servlet.ServletException;
28 import javax.servlet.http.HttpServlet;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.bticinosmarther.internal.api.dto.Notification;
33 import org.openhab.binding.bticinosmarther.internal.api.dto.Sender;
34 import org.openhab.binding.bticinosmarther.internal.api.exception.SmartherGatewayException;
35 import org.openhab.binding.bticinosmarther.internal.util.StringUtil;
36 import org.osgi.framework.BundleContext;
37 import org.osgi.service.component.ComponentContext;
38 import org.osgi.service.component.annotations.Activate;
39 import org.osgi.service.component.annotations.Component;
40 import org.osgi.service.component.annotations.Deactivate;
41 import org.osgi.service.component.annotations.Reference;
42 import org.osgi.service.http.HttpService;
43 import org.osgi.service.http.NamespaceException;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 /**
48  * The {@code SmartherAccountService} class manages the servlets and bind authorization servlet to Bridges.
49  *
50  * @author Fabio Possieri - Initial contribution
51  */
52 @Component(service = SmartherAccountService.class, configurationPid = "binding.bticinosmarther.accountService")
53 @NonNullByDefault
54 public class SmartherAccountService {
55
56     private static final String TEMPLATE_PATH = "templates/";
57     private static final String IMAGE_PATH = "web";
58     private static final String TEMPLATE_APPLICATION = TEMPLATE_PATH + "application.html";
59     private static final String TEMPLATE_INDEX = TEMPLATE_PATH + "index.html";
60     private static final String ERROR_UKNOWN_BRIDGE = "Returned 'state' doesn't match any Bridges. Has the bridge been removed?";
61
62     private final Logger logger = LoggerFactory.getLogger(SmartherAccountService.class);
63
64     private final Set<SmartherAccountHandler> handlers = ConcurrentHashMap.newKeySet();
65
66     private @Nullable HttpService httpService;
67     private @Nullable BundleContext bundleContext;
68
69     @Activate
70     protected void activate(ComponentContext componentContext, Map<String, Object> properties) {
71         try {
72             this.bundleContext = componentContext.getBundleContext();
73
74             final HttpService localHttpService = this.httpService;
75             if (localHttpService != null) {
76                 // Register the authorization servlet
77                 localHttpService.registerServlet(AUTH_SERVLET_ALIAS, createAuthorizationServlet(), new Hashtable<>(),
78                         localHttpService.createDefaultHttpContext());
79                 localHttpService.registerResources(AUTH_SERVLET_ALIAS + IMG_SERVLET_ALIAS, IMAGE_PATH, null);
80
81                 // Register the notification servlet
82                 localHttpService.registerServlet(NOTIFY_SERVLET_ALIAS, createNotificationServlet(), new Hashtable<>(),
83                         localHttpService.createDefaultHttpContext());
84             }
85         } catch (NamespaceException | ServletException | IOException e) {
86             logger.warn("Error during Smarther servlet startup", e);
87         }
88     }
89
90     @Deactivate
91     protected void deactivate(ComponentContext componentContext) {
92         final HttpService localHttpService = this.httpService;
93         if (localHttpService != null) {
94             // Unregister the authorization servlet
95             localHttpService.unregister(AUTH_SERVLET_ALIAS);
96             localHttpService.unregister(AUTH_SERVLET_ALIAS + IMG_SERVLET_ALIAS);
97
98             // Unregister the notification servlet
99             localHttpService.unregister(NOTIFY_SERVLET_ALIAS);
100         }
101     }
102
103     /**
104      * Constructs a {@code SmartherAuthorizationServlet}.
105      *
106      * @return the newly created servlet
107      *
108      * @throws {@link IOException}
109      *             in case of issues reading one of the internal html templates
110      */
111     private HttpServlet createAuthorizationServlet() throws IOException {
112         return new SmartherAuthorizationServlet(this, readTemplate(TEMPLATE_INDEX), readTemplate(TEMPLATE_APPLICATION));
113     }
114
115     /**
116      * Constructs a {@code SmartherNotificationServlet}.
117      *
118      * @return the newly created servlet
119      */
120     private HttpServlet createNotificationServlet() {
121         return new SmartherNotificationServlet(this);
122     }
123
124     /**
125      * Reads a template from file and returns its content as string.
126      *
127      * @param templateName
128      *            the name of the template file to read
129      *
130      * @return a string representing the content of the template file
131      *
132      * @throws {@link IOException}
133      *             in case of issues reading the template from file
134      */
135     private String readTemplate(String templateName) throws IOException {
136         final BundleContext localBundleContext = this.bundleContext;
137         if (localBundleContext != null) {
138             final URL index = localBundleContext.getBundle().getEntry(templateName);
139
140             if (index == null) {
141                 throw new FileNotFoundException(String
142                         .format("Cannot find template '%s' - failed to initialize Smarther servlet", templateName));
143             } else {
144                 try (InputStream input = index.openStream()) {
145                     return StringUtil.streamToString(input);
146                 }
147             }
148         } else {
149             throw new IOException("Cannot get template, bundle context is null");
150         }
151     }
152
153     /**
154      * Dispatches the received Smarther API authorization response to the proper Smarther account handler.
155      * Part of the Legrand/Bticino OAuth2 authorization process.
156      *
157      * @param servletBaseURL
158      *            the authorization servlet url needed to derive the notification endpoint url
159      * @param state
160      *            the authorization state needed to match the correct Smarther account handler to authorize
161      * @param code The BTicino/Legrand API returned code value
162      *            the authorization code to authorize with the account handler
163      *
164      * @return a string containing the name of the authorized BTicino/Legrand portal user
165      *
166      * @throws {@link SmartherGatewayException}
167      *             in case of communication issues with the Smarther API or no account handler found
168      */
169     public String dispatchAuthorization(String servletBaseURL, String state, String code)
170             throws SmartherGatewayException {
171         // Searches the SmartherAccountHandler instance that matches the given state
172         final SmartherAccountHandler accountHandler = getAccountHandlerByUID(state);
173         if (accountHandler != null) {
174             // Generates the notification URL from servletBaseURL
175             final String notificationUrl = servletBaseURL.replace(AUTH_SERVLET_ALIAS, NOTIFY_SERVLET_ALIAS);
176
177             logger.debug("API authorization: calling authorize on {}", accountHandler.getUID());
178
179             // Passes the authorization to the handler
180             return accountHandler.authorize(servletBaseURL, code, notificationUrl);
181         } else {
182             logger.trace("API authorization: request redirected with state '{}'", state);
183             logger.warn("API authorization: no matching bridge was found. Possible bridge has been removed.");
184             throw new SmartherGatewayException(ERROR_UKNOWN_BRIDGE);
185         }
186     }
187
188     /**
189      * Dispatches the received C2C Webhook notification to the proper Smarther notification handler.
190      *
191      * @param notification
192      *            the received notification to handle
193      *
194      * @throws {@link SmartherGatewayException}
195      *             in case of communication issues with the Smarther API or no notification handler found
196      */
197     public void dispatchNotification(Notification notification) throws SmartherGatewayException {
198         final Sender sender = notification.getSender();
199         if (sender != null) {
200             // Searches the SmartherAccountHandler instance that matches the given location
201             final SmartherAccountHandler accountHandler = getAccountHandlerByLocation(sender.getPlant().getId());
202             if (accountHandler == null) {
203                 logger.warn("C2C notification [{}]: no matching bridge was found. Possible bridge has been removed.",
204                         notification.getId());
205                 throw new SmartherGatewayException(ERROR_UKNOWN_BRIDGE);
206             } else if (accountHandler.isOnline()) {
207                 final SmartherNotificationHandler notificationHandler = (SmartherNotificationHandler) accountHandler;
208
209                 if (notificationHandler.useNotifications()) {
210                     // Passes the notification to the handler
211                     notificationHandler.handleNotification(notification);
212                 } else {
213                     logger.debug(
214                             "C2C notification [{}]: notification discarded as bridge does not handle notifications.",
215                             notification.getId());
216                 }
217             } else {
218                 logger.debug("C2C notification [{}]: notification discarded as bridge is offline.",
219                         notification.getId());
220             }
221         } else {
222             logger.debug("C2C notification [{}]: notification discarded as payload is invalid.", notification.getId());
223         }
224     }
225
226     /**
227      * Adds a {@link SmartherAccountHandler} handler to the set of account service handlers.
228      *
229      * @param handler
230      *            the handler to add to the handlers set
231      */
232     public void addSmartherAccountHandler(SmartherAccountHandler handler) {
233         handlers.add(handler);
234     }
235
236     /**
237      * Removes a {@link SmartherAccountHandler} handler from the set of account service handlers.
238      *
239      * @param handler
240      *            the handler to remove from the handlers set
241      */
242     public void removeSmartherAccountHandler(SmartherAccountHandler handler) {
243         handlers.remove(handler);
244     }
245
246     /**
247      * Returns all the {@link SmartherAccountHandler} account service handlers.
248      *
249      * @return a set containing all the account service handlers
250      */
251     public Set<SmartherAccountHandler> getSmartherAccountHandlers() {
252         return handlers;
253     }
254
255     /**
256      * Searches the {@link SmartherAccountHandler} handler that matches the given Thing UID.
257      *
258      * @param thingUID
259      *            the UID of the Thing to match the handler with
260      *
261      * @return the handler matching the given Thing UID, or {@code null} if none matches
262      */
263     private @Nullable SmartherAccountHandler getAccountHandlerByUID(String thingUID) {
264         final Optional<SmartherAccountHandler> maybeHandler = handlers.stream().filter(l -> l.equalsThingUID(thingUID))
265                 .findFirst();
266         return (maybeHandler.isPresent()) ? maybeHandler.get() : null;
267     }
268
269     /**
270      * Searches the {@link SmartherAccountHandler} handler that matches the given location plant.
271      *
272      * @param plantId
273      *            the identifier of the plant to match the handler with
274      *
275      * @return the handler matching the given location plant, or {@code null} if none matches
276      */
277     private @Nullable SmartherAccountHandler getAccountHandlerByLocation(String plantId) {
278         final Optional<SmartherAccountHandler> maybeHandler = handlers.stream().filter(l -> l.hasLocation(plantId))
279                 .findFirst();
280         return (maybeHandler.isPresent()) ? maybeHandler.get() : null;
281     }
282
283     @Reference
284     protected void setHttpService(HttpService httpService) {
285         this.httpService = httpService;
286     }
287
288     protected void unsetHttpService(HttpService httpService) {
289         this.httpService = null;
290     }
291 }