2 * Copyright (c) 2010-2023 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.nest.internal.wwn.test;
15 import static org.openhab.binding.nest.internal.wwn.WWNBindingConstants.*;
16 import static org.openhab.binding.nest.internal.wwn.rest.WWNStreamingRestClient.*;
18 import java.io.IOException;
19 import java.io.InputStreamReader;
20 import java.io.PrintWriter;
21 import java.util.HashMap;
23 import java.util.Queue;
25 import java.util.concurrent.ArrayBlockingQueue;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.concurrent.TimeUnit;
29 import javax.servlet.ServletException;
30 import javax.servlet.http.HttpServlet;
31 import javax.servlet.http.HttpServletRequest;
32 import javax.servlet.http.HttpServletResponse;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
37 import com.google.gson.Gson;
38 import com.google.gson.GsonBuilder;
39 import com.google.gson.reflect.TypeToken;
42 * The {@link WWNTestApiServlet} mocks the Nest API during tests.
44 * @author Wouter Born - Initial contribution
46 public class WWNTestApiServlet extends HttpServlet {
48 private static final long serialVersionUID = -5414910055159062745L;
50 private static final String NEW_LINE = "\n";
52 private static final String UPDATE_PATHS[] = { NEST_CAMERA_UPDATE_PATH, NEST_SMOKE_ALARM_UPDATE_PATH,
53 NEST_STRUCTURE_UPDATE_PATH, NEST_THERMOSTAT_UPDATE_PATH };
55 private final Logger logger = LoggerFactory.getLogger(WWNTestApiServlet.class);
57 private class SseEvent {
61 public SseEvent(String name) {
65 public SseEvent(String name, String data) {
70 public String getData() {
74 public String getName() {
78 public boolean hasData() {
79 return data != null && !data.isEmpty();
83 private final Map<String, Map<String, String>> nestIdPropertiesMap = new ConcurrentHashMap<>();
85 private final Map<Thread, Queue<SseEvent>> listenerQueues = new ConcurrentHashMap<>();
87 private final ThreadLocal<PrintWriter> threadLocalWriter = new ThreadLocal<>();
89 private final Gson gson = new GsonBuilder().create();
91 public void closeConnections() {
92 Set<Thread> threads = listenerQueues.keySet();
93 listenerQueues.clear();
94 threads.forEach(thread -> thread.interrupt());
98 nestIdPropertiesMap.clear();
101 public void queueEvent(String eventName) {
102 SseEvent event = new SseEvent(eventName);
103 listenerQueues.forEach((thread, queue) -> queue.add(event));
106 public void queueEvent(String eventName, String data) {
107 SseEvent event = new SseEvent(eventName, data);
108 listenerQueues.forEach((thread, queue) -> queue.add(event));
111 @SuppressWarnings("resource")
112 private void writeEvent(SseEvent event) {
113 logger.debug("Writing {} event", event.getName());
115 PrintWriter writer = threadLocalWriter.get();
117 writer.write("event: ");
118 writer.write(event.getName());
119 writer.write(NEW_LINE);
121 if (event.hasData()) {
122 for (String dataLine : event.getData().split(NEW_LINE)) {
123 writer.write("data: ");
124 writer.write(dataLine);
125 writer.write(NEW_LINE);
129 writer.write(NEW_LINE);
133 private void writeEvent(String eventName) {
134 writeEvent(new SseEvent(eventName));
138 public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
139 ArrayBlockingQueue<SseEvent> queue = new ArrayBlockingQueue<>(10);
140 listenerQueues.put(Thread.currentThread(), queue);
142 response.setContentType("text/event-stream");
143 response.setCharacterEncoding("UTF-8");
144 response.flushBuffer();
146 logger.debug("Opened event stream to {}:{}", request.getRemoteHost(), request.getRemotePort());
148 PrintWriter writer = response.getWriter();
149 threadLocalWriter.set(writer);
152 while (listenerQueues.containsKey(Thread.currentThread()) && !writer.checkError()) {
154 SseEvent event = queue.poll(KEEP_ALIVE_MILLIS, TimeUnit.MILLISECONDS);
158 writeEvent(KEEP_ALIVE);
160 } catch (InterruptedException e) {
161 logger.debug("Evaluating loop conditions after interrupt");
165 listenerQueues.remove(Thread.currentThread());
166 threadLocalWriter.remove();
169 logger.debug("Closed event stream to {}:{}", request.getRemoteHost(), request.getRemotePort());
173 protected void doPut(HttpServletRequest request, HttpServletResponse response)
174 throws ServletException, IOException {
175 logger.debug("Received put request: {}", request);
177 String uri = request.getRequestURI();
178 String nestId = getNestIdFromURI(uri);
180 if (nestId == null) {
181 logger.error("Unsupported URI: {}", uri);
182 response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
186 InputStreamReader reader = new InputStreamReader(request.getInputStream());
187 Map<String, String> propertiesUpdate = gson.fromJson(reader, new TypeToken<Map<String, String>>() {
190 Map<String, String> properties = getOrCreateProperties(nestId);
191 properties.putAll(propertiesUpdate);
193 gson.toJson(propertiesUpdate, response.getWriter());
195 response.setStatus(HttpServletResponse.SC_OK);
198 private String getNestIdFromURI(String uri) {
199 for (String updatePath : UPDATE_PATHS) {
200 if (uri.startsWith(updatePath)) {
201 return uri.replaceAll(updatePath, "");
207 private Map<String, String> getOrCreateProperties(String nestId) {
208 Map<String, String> properties = nestIdPropertiesMap.get(nestId);
209 if (properties == null) {
210 properties = new HashMap<>();
211 nestIdPropertiesMap.put(nestId, properties);
216 public String getNestIdPropertyState(String nestId, String propertyName) {
217 Map<String, String> properties = nestIdPropertiesMap.get(nestId);
218 return properties == null ? null : properties.get(propertyName);