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.io.neeo.internal.servletservices;
15 import java.io.IOException;
16 import java.nio.charset.StandardCharsets;
17 import java.util.ArrayList;
18 import java.util.List;
19 import java.util.Locale;
20 import java.util.Objects;
23 import javax.servlet.http.HttpServletRequest;
24 import javax.servlet.http.HttpServletResponse;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.openhab.core.thing.ChannelUID;
28 import org.openhab.io.neeo.NeeoService;
29 import org.openhab.io.neeo.internal.NeeoBrainServlet;
30 import org.openhab.io.neeo.internal.NeeoConstants;
31 import org.openhab.io.neeo.internal.NeeoUtil;
32 import org.openhab.io.neeo.internal.ServiceContext;
33 import org.openhab.io.neeo.internal.models.NeeoDevice;
34 import org.openhab.io.neeo.internal.models.NeeoDeviceChannel;
35 import org.openhab.io.neeo.internal.models.NeeoDeviceChannelKind;
36 import org.openhab.io.neeo.internal.models.NeeoDeviceType;
37 import org.openhab.io.neeo.internal.models.NeeoThingUID;
38 import org.openhab.io.neeo.internal.servletservices.models.ReturnStatus;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
42 import com.google.gson.Gson;
43 import com.google.gson.JsonParseException;
46 * A subclass of {@link DefaultServletService} that handles thing status/definitions for the web pages
48 * @author Tim Roberts - Initial Contribution
51 public class ThingDashboardService extends DefaultServletService {
54 private final Logger logger = LoggerFactory.getLogger(ThingDashboardService.class);
56 private static final Set<String> STARTERS = Set.of("thingstatus", "getchannel", "getvirtualdevice", "restoredevice",
57 "refreshdevice", "deletedevice", "exportrules", "updatedevice");
59 /** The gson used for json manipulation */
60 private final Gson gson;
62 /** The service context */
63 private final ServiceContext context;
66 private final NeeoService service;
69 * Constructs the servlet from the {@link NeeoService} and {@link ServiceContext}
71 * @param service the non-null {@link NeeoService}
72 * @param context the non-null {@link ServiceContext}
74 public ThingDashboardService(NeeoService service, ServiceContext context) {
75 Objects.requireNonNull(service, "service cannot be null");
76 Objects.requireNonNull(context, "context cannot be null");
78 this.context = context;
79 this.service = service;
80 gson = NeeoUtil.createNeeoDeviceGsonBuilder(service, context).create();
84 * Returns true if the path starts with "thingstatus", "getchannel", "getvirtualdevice", "restoredevice",
85 * "refreshdevice", "deletedevice", "exportrules", "updatedevice"
87 * @see DefaultServletService#canHandleRoute(String[])
90 public boolean canHandleRoute(String[] paths) {
91 return paths.length >= 1 && STARTERS.contains(paths[0].toLowerCase(Locale.ROOT));
95 * Handles the get for the 'thingstatus' and 'getchannel' URL (all other URLs do posts)
97 * @see DefaultServletService#handleGet(HttpServletRequest, String[], HttpServletResponse)
100 public void handleGet(HttpServletRequest req, String[] paths, HttpServletResponse resp) throws IOException {
101 Objects.requireNonNull(req, "req cannot be null");
102 Objects.requireNonNull(paths, "paths cannot be null");
103 Objects.requireNonNull(resp, "resp cannot be null");
106 if ("thingstatus".equalsIgnoreCase(paths[0])) {
107 final List<NeeoDevice> devices = context.getDefinitions().getAllDevices();
108 NeeoUtil.write(resp, gson.toJson(devices));
109 } else if ("getchannel".equalsIgnoreCase(paths[0])) {
110 final String itemName = NeeoUtil.decodeURIComponent(req.getParameter("itemname"));
111 final List<NeeoDeviceChannel> channels = context.getDefinitions().getNeeoDeviceChannel(itemName);
112 if (channels == null) {
113 NeeoUtil.write(resp, gson.toJson(new ReturnStatus("Channel no longer exists")));
115 NeeoUtil.write(resp, gson.toJson(new ReturnStatus(channels)));
117 } else if ("getvirtualdevice".equalsIgnoreCase(paths[0])) {
118 final NeeoThingUID uid = context.generate(NeeoConstants.VIRTUAL_THING_TYPE);
119 final NeeoDevice device = new NeeoDevice(uid, 0, NeeoDeviceType.EXCLUDE, "NEEO Integration",
120 "New Virtual Thing", new ArrayList<>(), null, null, null, null);
121 NeeoUtil.write(resp, gson.toJson(new ReturnStatus(device)));
123 logger.debug("Unknown get path: {}", String.join(",", paths));
125 } catch (JsonParseException | IllegalArgumentException | NullPointerException e) {
126 logger.debug("Exception handling get: {}", e.getMessage(), e);
127 NeeoUtil.write(resp, gson.toJson(new ReturnStatus(e.getMessage())));
132 * Handles the post for the 'updatedevice', 'restoredevice' or 'refreshdevice'.
134 * @see DefaultServletService#handlePost(HttpServletRequest, String[], HttpServletResponse)
137 public void handlePost(HttpServletRequest req, String[] paths, HttpServletResponse resp) throws IOException {
138 Objects.requireNonNull(req, "req cannot be null");
139 Objects.requireNonNull(paths, "paths cannot be null");
140 Objects.requireNonNull(resp, "resp cannot be null");
141 if (paths.length == 0) {
142 throw new IllegalArgumentException("paths cannot be empty");
146 if ("updatedevice".equalsIgnoreCase(paths[0])) {
147 final NeeoDevice device = gson.fromJson(req.getReader(), NeeoDevice.class);
148 context.getDefinitions().put(device);
150 for (NeeoBrainServlet servlet : service.getServlets()) {
151 servlet.getBrainApi().restart(); // restart so brain will query changes
154 NeeoUtil.write(resp, gson.toJson(ReturnStatus.SUCCESS));
155 } else if ("restoredevice".equalsIgnoreCase(paths[0])) {
156 final NeeoThingUID uid = new NeeoThingUID(
157 new String(req.getInputStream().readAllBytes(), StandardCharsets.UTF_8));
158 context.getDefinitions().remove(uid);
159 final NeeoDevice device = context.getDefinitions().getDevice(uid);
160 if (device == null) {
161 NeeoUtil.write(resp, gson.toJson(new ReturnStatus("Device no longer exists in openHAB!")));
163 NeeoUtil.write(resp, gson.toJson(new ReturnStatus(device)));
165 } else if ("refreshdevice".equalsIgnoreCase(paths[0])) {
166 final NeeoThingUID uid = new NeeoThingUID(
167 new String(req.getInputStream().readAllBytes(), StandardCharsets.UTF_8));
168 final NeeoDevice device = context.getDefinitions().getDevice(uid);
169 if (device == null) {
170 NeeoUtil.write(resp, gson.toJson(new ReturnStatus("Device no longer exists in openHAB!")));
172 NeeoUtil.write(resp, gson.toJson(new ReturnStatus(device)));
174 } else if ("deletedevice".equalsIgnoreCase(paths[0])) {
175 final NeeoThingUID uid = new NeeoThingUID(
176 new String(req.getInputStream().readAllBytes(), StandardCharsets.UTF_8));
177 final boolean deleted = context.getDefinitions().remove(uid);
178 NeeoUtil.write(resp, gson.toJson(new ReturnStatus(
179 deleted ? null : "Device " + uid + " was not found (possibly already deleted?)")));
180 } else if ("exportrules".equalsIgnoreCase(paths[0])) {
181 final NeeoThingUID uid = new NeeoThingUID(
182 new String(req.getInputStream().readAllBytes(), StandardCharsets.UTF_8));
183 final NeeoDevice device = context.getDefinitions().getDevice(uid);
184 if (device == null) {
185 NeeoUtil.write(resp, gson.toJson(new ReturnStatus("Device " + uid + " was not found")));
187 writeExampleRules(resp, device);
190 logger.debug("Unknown post path: {}", String.join(",", paths));
192 } catch (JsonParseException | IllegalArgumentException | NullPointerException e) {
193 logger.debug("Exception handling post: {}", e.getMessage(), e);
194 NeeoUtil.write(resp, gson.toJson(new ReturnStatus(e.getMessage())));
199 * Helper method to produce an examples rules file and write it to the {@link HttpServletResponse}
201 * @param resp the non-null {@link HttpServletResponse}
202 * @param device the non-null {@link NeeoDevice}
203 * @throws IOException if an IOException occurs while writing the file
205 private void writeExampleRules(HttpServletResponse resp, NeeoDevice device) throws IOException {
206 Objects.requireNonNull(resp, "resp cannot be null");
207 Objects.requireNonNull(device, "device cannot be null");
209 final StringBuilder sb = new StringBuilder(5000);
210 appendLine(sb, "//////////////////////////////////////////////////////////////////////");
211 sb.append("// Example Rules for ");
212 appendLine(sb, device.getName());
213 appendLine(sb, "//////////////////////////////////////////////////////////////////////");
214 sb.append(System.lineSeparator());
216 device.getChannels().stream().filter(c -> c.getKind() == NeeoDeviceChannelKind.TRIGGER).forEach(channel -> {
217 sb.append("rule \"");
218 sb.append(channel.getItemName());
219 appendLine(sb, "\"");
221 appendLine(sb, "when");
222 sb.append(" Channel '");
223 sb.append(new ChannelUID(device.getUid(), channel.getItemName()));
224 appendLine(sb, "' triggered");
225 appendLine(sb, "then");
226 appendLine(sb, " var data = receivedEvent.getEvent()");
227 appendLine(sb, " # do something here with your data");
228 appendLine(sb, "end");
231 resp.setContentType("text/plain");
232 resp.setHeader("Content-disposition", "attachment; filename=\"" + device.getName() + ".rules\"");
233 resp.getOutputStream().write(sb.toString().getBytes(StandardCharsets.UTF_8));
237 * Helper method to append a line of text ot the string builder with a line separator
239 * @param sb a non-null string builder
240 * @param text the non-null, possibly empty text
242 private void appendLine(StringBuilder sb, String text) {
243 Objects.requireNonNull(sb, "sb cannot be null");
244 Objects.requireNonNull(text, "text cannot be null");
247 sb.append(System.lineSeparator());