]> git.basschouten.com Git - openhab-addons.git/blob
f3748f90b4e1207f9615ee56b62494e7e8161bf3
[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.spotify.internal;
14
15 import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.*;
16
17 import java.io.FileNotFoundException;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.net.URL;
21 import java.nio.charset.StandardCharsets;
22 import java.util.ArrayList;
23 import java.util.Hashtable;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Optional;
27
28 import javax.servlet.ServletException;
29 import javax.servlet.http.HttpServlet;
30
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.spotify.internal.api.exception.SpotifyException;
34 import org.osgi.framework.BundleContext;
35 import org.osgi.service.component.ComponentContext;
36 import org.osgi.service.component.annotations.Activate;
37 import org.osgi.service.component.annotations.Component;
38 import org.osgi.service.component.annotations.Deactivate;
39 import org.osgi.service.component.annotations.Reference;
40 import org.osgi.service.http.HttpService;
41 import org.osgi.service.http.NamespaceException;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 /**
46  * The {@link SpotifyAuthService} class to manage the servlets and bind authorization servlet to bridges.
47  *
48  * @author Andreas Stenlund - Initial contribution
49  * @author Hilbrand Bouwkamp - Made this the service class instead of only interface. Added templates
50  */
51 @Component(service = SpotifyAuthService.class, configurationPid = "binding.spotify.authService")
52 @NonNullByDefault
53 public class SpotifyAuthService {
54
55     private static final String TEMPLATE_PATH = "templates/";
56     private static final String TEMPLATE_PLAYER = TEMPLATE_PATH + "player.html";
57     private static final String TEMPLATE_INDEX = TEMPLATE_PATH + "index.html";
58     private static final String ERROR_UKNOWN_BRIDGE = "Returned 'state' by doesn't match any Bridges. Has the bridge been removed?";
59
60     private final Logger logger = LoggerFactory.getLogger(SpotifyAuthService.class);
61
62     private final List<SpotifyAccountHandler> handlers = new ArrayList<>();
63
64     private @NonNullByDefault({}) HttpService httpService;
65     private @NonNullByDefault({}) BundleContext bundleContext;
66
67     @Activate
68     protected void activate(ComponentContext componentContext, Map<String, Object> properties) {
69         try {
70             bundleContext = componentContext.getBundleContext();
71             httpService.registerServlet(SPOTIFY_ALIAS, createServlet(), new Hashtable<>(),
72                     httpService.createDefaultHttpContext());
73             httpService.registerResources(SPOTIFY_ALIAS + SPOTIFY_IMG_ALIAS, "web", null);
74         } catch (NamespaceException | ServletException | IOException e) {
75             logger.warn("Error during spotify servlet startup", e);
76         }
77     }
78
79     @Deactivate
80     protected void deactivate(ComponentContext componentContext) {
81         httpService.unregister(SPOTIFY_ALIAS);
82         httpService.unregister(SPOTIFY_ALIAS + SPOTIFY_IMG_ALIAS);
83     }
84
85     /**
86      * Creates a new {@link SpotifyAuthServlet}.
87      *
88      * @return the newly created servlet
89      * @throws IOException thrown when an HTML template could not be read
90      */
91     private HttpServlet createServlet() throws IOException {
92         return new SpotifyAuthServlet(this, readTemplate(TEMPLATE_INDEX), readTemplate(TEMPLATE_PLAYER));
93     }
94
95     /**
96      * Reads a template from file and returns the content as String.
97      *
98      * @param templateName name of the template file to read
99      * @return The content of the template file
100      * @throws IOException thrown when an HTML template could not be read
101      */
102     private String readTemplate(String templateName) throws IOException {
103         final URL index = bundleContext.getBundle().getEntry(templateName);
104
105         if (index == null) {
106             throw new FileNotFoundException(
107                     String.format("Cannot find '{}' - failed to initialize Spotify servlet", templateName));
108         } else {
109             try (InputStream inputStream = index.openStream()) {
110                 return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
111             }
112         }
113     }
114
115     /**
116      * Call with Spotify redirect uri returned State and Code values to get the refresh and access tokens and persist
117      * these values
118      *
119      * @param servletBaseURL the servlet base, which will be the Spotify redirect url
120      * @param state The Spotify returned state value
121      * @param code The Spotify returned code value
122      * @return returns the name of the Spotify user that is authorized
123      */
124     public String authorize(String servletBaseURL, String state, String code) {
125         final SpotifyAccountHandler listener = getSpotifyAuthListener(state);
126
127         if (listener == null) {
128             logger.debug(
129                     "Spotify redirected with state '{}' but no matching bridge was found. Possible bridge has been removed.",
130                     state);
131             throw new SpotifyException(ERROR_UKNOWN_BRIDGE);
132         } else {
133             return listener.authorize(servletBaseURL, code);
134         }
135     }
136
137     /**
138      * @param listener Adds the given handler
139      */
140     public void addSpotifyAccountHandler(SpotifyAccountHandler listener) {
141         if (!handlers.contains(listener)) {
142             handlers.add(listener);
143         }
144     }
145
146     /**
147      * @param handler Removes the given handler
148      */
149     public void removeSpotifyAccountHandler(SpotifyAccountHandler handler) {
150         handlers.remove(handler);
151     }
152
153     /**
154      * @return Returns all {@link SpotifyAccountHandler}s.
155      */
156     public List<SpotifyAccountHandler> getSpotifyAccountHandlers() {
157         return handlers;
158     }
159
160     /**
161      * Get the {@link SpotifyAccountHandler} that matches the given thing UID.
162      *
163      * @param thingUID UID of the thing to match the handler with
164      * @return the {@link SpotifyAccountHandler} matching the thing UID or null
165      */
166     private @Nullable SpotifyAccountHandler getSpotifyAuthListener(String thingUID) {
167         final Optional<SpotifyAccountHandler> maybeListener = handlers.stream().filter(l -> l.equalsThingUID(thingUID))
168                 .findFirst();
169         return maybeListener.isPresent() ? maybeListener.get() : null;
170     }
171
172     @Reference
173     protected void setHttpService(HttpService httpService) {
174         this.httpService = httpService;
175     }
176
177     protected void unsetHttpService(HttpService httpService) {
178         this.httpService = null;
179     }
180 }