2 * Copyright (c) 2010-2022 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.binding.bticinosmarther.internal.account;
15 import static org.openhab.binding.bticinosmarther.internal.SmartherBindingConstants.*;
17 import java.io.FileNotFoundException;
18 import java.io.IOException;
19 import java.io.InputStream;
21 import java.util.Hashtable;
23 import java.util.Optional;
25 import java.util.concurrent.ConcurrentHashMap;
27 import javax.servlet.ServletException;
28 import javax.servlet.http.HttpServlet;
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;
48 * The {@code SmartherAccountService} class manages the servlets and bind authorization servlet to Bridges.
50 * @author Fabio Possieri - Initial contribution
52 @Component(service = SmartherAccountService.class, configurationPid = "binding.bticinosmarther.accountService")
54 public class SmartherAccountService {
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?";
62 private final Logger logger = LoggerFactory.getLogger(SmartherAccountService.class);
64 private final Set<SmartherAccountHandler> handlers = ConcurrentHashMap.newKeySet();
66 private @Nullable HttpService httpService;
67 private @Nullable BundleContext bundleContext;
70 protected void activate(ComponentContext componentContext, Map<String, Object> properties) {
72 this.bundleContext = componentContext.getBundleContext();
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);
81 // Register the notification servlet
82 localHttpService.registerServlet(NOTIFY_SERVLET_ALIAS, createNotificationServlet(), new Hashtable<>(),
83 localHttpService.createDefaultHttpContext());
85 } catch (NamespaceException | ServletException | IOException e) {
86 logger.warn("Error during Smarther servlet startup", e);
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);
98 // Unregister the notification servlet
99 localHttpService.unregister(NOTIFY_SERVLET_ALIAS);
104 * Constructs a {@code SmartherAuthorizationServlet}.
106 * @return the newly created servlet
108 * @throws {@link IOException}
109 * in case of issues reading one of the internal html templates
111 private HttpServlet createAuthorizationServlet() throws IOException {
112 return new SmartherAuthorizationServlet(this, readTemplate(TEMPLATE_INDEX), readTemplate(TEMPLATE_APPLICATION));
116 * Constructs a {@code SmartherNotificationServlet}.
118 * @return the newly created servlet
120 private HttpServlet createNotificationServlet() {
121 return new SmartherNotificationServlet(this);
125 * Reads a template from file and returns its content as string.
127 * @param templateName
128 * the name of the template file to read
130 * @return a string representing the content of the template file
132 * @throws {@link IOException}
133 * in case of issues reading the template from file
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);
141 throw new FileNotFoundException(String
142 .format("Cannot find template '%s' - failed to initialize Smarther servlet", templateName));
144 try (InputStream input = index.openStream()) {
145 return StringUtil.streamToString(input);
149 throw new IOException("Cannot get template, bundle context is null");
154 * Dispatches the received Smarther API authorization response to the proper Smarther account handler.
155 * Part of the Legrand/Bticino OAuth2 authorization process.
157 * @param servletBaseURL
158 * the authorization servlet url needed to derive the notification endpoint url
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
164 * @return a string containing the name of the authorized BTicino/Legrand portal user
166 * @throws {@link SmartherGatewayException}
167 * in case of communication issues with the Smarther API or no account handler found
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);
177 logger.debug("API authorization: calling authorize on {}", accountHandler.getUID());
179 // Passes the authorization to the handler
180 return accountHandler.authorize(servletBaseURL, code, notificationUrl);
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);
189 * Dispatches the received C2C Webhook notification to the proper Smarther notification handler.
191 * @param notification
192 * the received notification to handle
194 * @throws {@link SmartherGatewayException}
195 * in case of communication issues with the Smarther API or no notification handler found
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;
209 if (notificationHandler.useNotifications()) {
210 // Passes the notification to the handler
211 notificationHandler.handleNotification(notification);
214 "C2C notification [{}]: notification discarded as bridge does not handle notifications.",
215 notification.getId());
218 logger.debug("C2C notification [{}]: notification discarded as bridge is offline.",
219 notification.getId());
222 logger.debug("C2C notification [{}]: notification discarded as payload is invalid.", notification.getId());
227 * Adds a {@link SmartherAccountHandler} handler to the set of account service handlers.
230 * the handler to add to the handlers set
232 public void addSmartherAccountHandler(SmartherAccountHandler handler) {
233 handlers.add(handler);
237 * Removes a {@link SmartherAccountHandler} handler from the set of account service handlers.
240 * the handler to remove from the handlers set
242 public void removeSmartherAccountHandler(SmartherAccountHandler handler) {
243 handlers.remove(handler);
247 * Returns all the {@link SmartherAccountHandler} account service handlers.
249 * @return a set containing all the account service handlers
251 public Set<SmartherAccountHandler> getSmartherAccountHandlers() {
256 * Searches the {@link SmartherAccountHandler} handler that matches the given Thing UID.
259 * the UID of the Thing to match the handler with
261 * @return the handler matching the given Thing UID, or {@code null} if none matches
263 private @Nullable SmartherAccountHandler getAccountHandlerByUID(String thingUID) {
264 final Optional<SmartherAccountHandler> maybeHandler = handlers.stream().filter(l -> l.equalsThingUID(thingUID))
266 return (maybeHandler.isPresent()) ? maybeHandler.get() : null;
270 * Searches the {@link SmartherAccountHandler} handler that matches the given location plant.
273 * the identifier of the plant to match the handler with
275 * @return the handler matching the given location plant, or {@code null} if none matches
277 private @Nullable SmartherAccountHandler getAccountHandlerByLocation(String plantId) {
278 final Optional<SmartherAccountHandler> maybeHandler = handlers.stream().filter(l -> l.hasLocation(plantId))
280 return (maybeHandler.isPresent()) ? maybeHandler.get() : null;
284 protected void setHttpService(HttpService httpService) {
285 this.httpService = httpService;
288 protected void unsetHttpService(HttpService httpService) {
289 this.httpService = null;