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.Objects;
21 import javax.servlet.http.HttpServletRequest;
22 import javax.servlet.http.HttpServletResponse;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.openhab.core.thing.ChannelUID;
26 import org.openhab.io.neeo.NeeoService;
27 import org.openhab.io.neeo.internal.NeeoBrainServlet;
28 import org.openhab.io.neeo.internal.NeeoConstants;
29 import org.openhab.io.neeo.internal.NeeoUtil;
30 import org.openhab.io.neeo.internal.ServiceContext;
31 import org.openhab.io.neeo.internal.models.NeeoDevice;
32 import org.openhab.io.neeo.internal.models.NeeoDeviceChannel;
33 import org.openhab.io.neeo.internal.models.NeeoDeviceChannelKind;
34 import org.openhab.io.neeo.internal.models.NeeoDeviceType;
35 import org.openhab.io.neeo.internal.models.NeeoThingUID;
36 import org.openhab.io.neeo.internal.servletservices.models.ReturnStatus;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
40 import com.google.gson.Gson;
41 import com.google.gson.JsonParseException;
44 * A subclass of {@link DefaultServletService} that handles thing status/definitions for the web pages
46 * @author Tim Roberts - Initial Contribution
49 public class ThingDashboardService extends DefaultServletService {
52 private final Logger logger = LoggerFactory.getLogger(ThingDashboardService.class);
54 /** The gson used for json manipulation */
55 private final Gson gson;
57 /** The service context */
58 private final ServiceContext context;
61 private final NeeoService service;
64 * Constructs the servlet from the {@link NeeoService} and {@link ServiceContext}
66 * @param service the non-null {@link NeeoService}
67 * @param context the non-null {@link ServiceContext}
69 public ThingDashboardService(NeeoService service, ServiceContext context) {
70 Objects.requireNonNull(service, "service cannot be null");
71 Objects.requireNonNull(context, "context cannot be null");
73 this.context = context;
74 this.service = service;
75 gson = NeeoUtil.createNeeoDeviceGsonBuilder(service, context).create();
79 * Returns true if the path starts with "thingstatus", "getchannel", "getvirtualdevice", "restoredevice",
80 * "refreshdevice", "deletedevice", "exportrules", "updatedevice"
82 * @see DefaultServletService#canHandleRoute(String[])
85 public boolean canHandleRoute(String[] paths) {
86 return paths.length >= 1 && (paths[0].equalsIgnoreCase("thingstatus") //
87 || paths[0].equalsIgnoreCase("getchannel") //
88 || paths[0].equalsIgnoreCase("getvirtualdevice") //
89 || paths[0].equalsIgnoreCase("restoredevice") //
90 || paths[0].equalsIgnoreCase("refreshdevice") //
91 || paths[0].equalsIgnoreCase("deletedevice") //
92 || paths[0].equalsIgnoreCase("exportrules") //
93 || paths[0].equalsIgnoreCase("updatedevice"));
97 * Handles the get for the 'thingstatus' and 'getchannel' URL (all other URLs do posts)
99 * @see DefaultServletService#handleGet(HttpServletRequest, String[], HttpServletResponse)
102 public void handleGet(HttpServletRequest req, String[] paths, HttpServletResponse resp) throws IOException {
103 Objects.requireNonNull(req, "req cannot be null");
104 Objects.requireNonNull(paths, "paths cannot be null");
105 Objects.requireNonNull(resp, "resp cannot be null");
108 if (paths[0].equalsIgnoreCase("thingstatus")) {
109 final List<NeeoDevice> devices = context.getDefinitions().getAllDevices();
110 NeeoUtil.write(resp, gson.toJson(devices));
111 } else if (paths[0].equalsIgnoreCase("getchannel")) {
112 final String itemName = NeeoUtil.decodeURIComponent(req.getParameter("itemname"));
113 final List<NeeoDeviceChannel> channels = context.getDefinitions().getNeeoDeviceChannel(itemName);
114 if (channels == null) {
115 NeeoUtil.write(resp, gson.toJson(new ReturnStatus("Channel no longer exists")));
117 NeeoUtil.write(resp, gson.toJson(new ReturnStatus(channels)));
119 } else if (paths[0].equalsIgnoreCase("getvirtualdevice")) {
120 final NeeoThingUID uid = context.generate(NeeoConstants.VIRTUAL_THING_TYPE);
121 final NeeoDevice device = new NeeoDevice(uid, 0, NeeoDeviceType.EXCLUDE, "NEEO Integration",
122 "New Virtual Thing", new ArrayList<>(), null, null, null, null);
123 NeeoUtil.write(resp, gson.toJson(new ReturnStatus(device)));
125 logger.debug("Unknown get path: {}", String.join(",", paths));
127 } catch (JsonParseException | IllegalArgumentException | NullPointerException e) {
128 logger.debug("Exception handling get: {}", e.getMessage(), e);
129 NeeoUtil.write(resp, gson.toJson(new ReturnStatus(e.getMessage())));
134 * Handles the post for the 'updatedevice', 'restoredevice' or 'refreshdevice'.
136 * @see DefaultServletService#handlePost(HttpServletRequest, String[], HttpServletResponse)
139 public void handlePost(HttpServletRequest req, String[] paths, HttpServletResponse resp) throws IOException {
140 Objects.requireNonNull(req, "req cannot be null");
141 Objects.requireNonNull(paths, "paths cannot be null");
142 Objects.requireNonNull(resp, "resp cannot be null");
143 if (paths.length == 0) {
144 throw new IllegalArgumentException("paths cannot be empty");
148 if (paths[0].equalsIgnoreCase("updatedevice")) {
149 final NeeoDevice device = gson.fromJson(req.getReader(), NeeoDevice.class);
150 context.getDefinitions().put(device);
152 for (NeeoBrainServlet servlet : service.getServlets()) {
153 servlet.getBrainApi().restart(); // restart so brain will query changes
156 NeeoUtil.write(resp, gson.toJson(ReturnStatus.SUCCESS));
157 } else if (paths[0].equalsIgnoreCase("restoredevice")) {
158 final NeeoThingUID uid = new NeeoThingUID(
159 new String(req.getInputStream().readAllBytes(), StandardCharsets.UTF_8));
160 context.getDefinitions().remove(uid);
161 final NeeoDevice device = context.getDefinitions().getDevice(uid);
162 if (device == null) {
163 NeeoUtil.write(resp, gson.toJson(new ReturnStatus("Device no longer exists in openHAB!")));
165 NeeoUtil.write(resp, gson.toJson(new ReturnStatus(device)));
167 } else if (paths[0].equalsIgnoreCase("refreshdevice")) {
168 final NeeoThingUID uid = new NeeoThingUID(
169 new String(req.getInputStream().readAllBytes(), StandardCharsets.UTF_8));
170 final NeeoDevice device = context.getDefinitions().getDevice(uid);
171 if (device == null) {
172 NeeoUtil.write(resp, gson.toJson(new ReturnStatus("Device no longer exists in openHAB!")));
174 NeeoUtil.write(resp, gson.toJson(new ReturnStatus(device)));
176 } else if (paths[0].equalsIgnoreCase("deletedevice")) {
177 final NeeoThingUID uid = new NeeoThingUID(
178 new String(req.getInputStream().readAllBytes(), StandardCharsets.UTF_8));
179 final boolean deleted = context.getDefinitions().remove(uid);
180 NeeoUtil.write(resp, gson.toJson(new ReturnStatus(
181 deleted ? null : "Device " + uid + " was not found (possibly already deleted?)")));
182 } else if (paths[0].equalsIgnoreCase("exportrules")) {
183 final NeeoThingUID uid = new NeeoThingUID(
184 new String(req.getInputStream().readAllBytes(), StandardCharsets.UTF_8));
185 final NeeoDevice device = context.getDefinitions().getDevice(uid);
186 if (device == null) {
187 NeeoUtil.write(resp, gson.toJson(new ReturnStatus("Device " + uid + " was not found")));
189 writeExampleRules(resp, device);
192 logger.debug("Unknown post path: {}", String.join(",", paths));
194 } catch (JsonParseException | IllegalArgumentException | NullPointerException e) {
195 logger.debug("Exception handling post: {}", e.getMessage(), e);
196 NeeoUtil.write(resp, gson.toJson(new ReturnStatus(e.getMessage())));
201 * Helper method to produce an examples rules file and write it to the {@link HttpServletResponse}
203 * @param resp the non-null {@link HttpServletResponse}
204 * @param device the non-null {@link NeeoDevice}
205 * @throws IOException if an IOException occurs while writing the file
207 private void writeExampleRules(HttpServletResponse resp, NeeoDevice device) throws IOException {
208 Objects.requireNonNull(resp, "resp cannot be null");
209 Objects.requireNonNull(device, "device cannot be null");
211 final StringBuilder sb = new StringBuilder(5000);
212 appendLine(sb, "//////////////////////////////////////////////////////////////////////");
213 sb.append("// Example Rules for ");
214 appendLine(sb, device.getName());
215 appendLine(sb, "//////////////////////////////////////////////////////////////////////");
216 sb.append(System.lineSeparator());
218 device.getChannels().stream().filter(c -> c.getKind() == NeeoDeviceChannelKind.TRIGGER).forEach(channel -> {
219 sb.append("rule \"");
220 sb.append(channel.getItemName());
221 appendLine(sb, "\"");
223 appendLine(sb, "when");
224 sb.append(" Channel '");
225 sb.append(new ChannelUID(device.getUid(), channel.getItemName()));
226 appendLine(sb, "' triggered");
227 appendLine(sb, "then");
228 appendLine(sb, " var data = receivedEvent.getEvent()");
229 appendLine(sb, " # do something here with your data");
230 appendLine(sb, "end");
233 resp.setContentType("text/plain");
234 resp.setHeader("Content-disposition", "attachment; filename=\"" + device.getName() + ".rules\"");
235 resp.getOutputStream().write(sb.toString().getBytes(StandardCharsets.UTF_8));
239 * Helper method to append a line of text ot the string builder with a line separator
241 * @param sb a non-null string builder
242 * @param text the non-null, possibly empty text
244 private void appendLine(StringBuilder sb, String text) {
245 Objects.requireNonNull(sb, "sb cannot be null");
246 Objects.requireNonNull(text, "text cannot be null");
249 sb.append(System.lineSeparator());