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.spotify.internal;
15 import static org.openhab.binding.spotify.internal.SpotifyBindingConstants.*;
17 import java.io.FileNotFoundException;
18 import java.io.IOException;
19 import java.io.InputStream;
21 import java.nio.charset.StandardCharsets;
22 import java.util.ArrayList;
23 import java.util.Hashtable;
24 import java.util.List;
26 import java.util.Optional;
28 import javax.servlet.ServletException;
29 import javax.servlet.http.HttpServlet;
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;
46 * The {@link SpotifyAuthService} class to manage the servlets and bind authorization servlet to bridges.
48 * @author Andreas Stenlund - Initial contribution
49 * @author Hilbrand Bouwkamp - Made this the service class instead of only interface. Added templates
51 @Component(service = SpotifyAuthService.class, configurationPid = "binding.spotify.authService")
53 public class SpotifyAuthService {
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?";
60 private final Logger logger = LoggerFactory.getLogger(SpotifyAuthService.class);
62 private final List<SpotifyAccountHandler> handlers = new ArrayList<>();
64 private @NonNullByDefault({}) HttpService httpService;
65 private @NonNullByDefault({}) BundleContext bundleContext;
68 protected void activate(ComponentContext componentContext, Map<String, Object> properties) {
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);
80 protected void deactivate(ComponentContext componentContext) {
81 httpService.unregister(SPOTIFY_ALIAS);
82 httpService.unregister(SPOTIFY_ALIAS + SPOTIFY_IMG_ALIAS);
86 * Creates a new {@link SpotifyAuthServlet}.
88 * @return the newly created servlet
89 * @throws IOException thrown when an HTML template could not be read
91 private HttpServlet createServlet() throws IOException {
92 return new SpotifyAuthServlet(this, readTemplate(TEMPLATE_INDEX), readTemplate(TEMPLATE_PLAYER));
96 * Reads a template from file and returns the content as String.
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
102 private String readTemplate(String templateName) throws IOException {
103 final URL index = bundleContext.getBundle().getEntry(templateName);
106 throw new FileNotFoundException(
107 String.format("Cannot find '{}' - failed to initialize Spotify servlet", templateName));
109 try (InputStream inputStream = index.openStream()) {
110 return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
116 * Call with Spotify redirect uri returned State and Code values to get the refresh and access tokens and persist
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
124 public String authorize(String servletBaseURL, String state, String code) {
125 final SpotifyAccountHandler listener = getSpotifyAuthListener(state);
127 if (listener == null) {
129 "Spotify redirected with state '{}' but no matching bridge was found. Possible bridge has been removed.",
131 throw new SpotifyException(ERROR_UKNOWN_BRIDGE);
133 return listener.authorize(servletBaseURL, code);
138 * @param listener Adds the given handler
140 public void addSpotifyAccountHandler(SpotifyAccountHandler listener) {
141 if (!handlers.contains(listener)) {
142 handlers.add(listener);
147 * @param handler Removes the given handler
149 public void removeSpotifyAccountHandler(SpotifyAccountHandler handler) {
150 handlers.remove(handler);
154 * @return Returns all {@link SpotifyAccountHandler}s.
156 public List<SpotifyAccountHandler> getSpotifyAccountHandlers() {
161 * Get the {@link SpotifyAccountHandler} that matches the given thing UID.
163 * @param thingUID UID of the thing to match the handler with
164 * @return the {@link SpotifyAccountHandler} matching the thing UID or null
166 private @Nullable SpotifyAccountHandler getSpotifyAuthListener(String thingUID) {
167 final Optional<SpotifyAccountHandler> maybeListener = handlers.stream().filter(l -> l.equalsThingUID(thingUID))
169 return maybeListener.isPresent() ? maybeListener.get() : null;
173 protected void setHttpService(HttpService httpService) {
174 this.httpService = httpService;
177 protected void unsetHttpService(HttpService httpService) {
178 this.httpService = null;