2 * Copyright (c) 2010-2021 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.util.ArrayList;
17 import java.util.Comparator;
18 import java.util.HashMap;
19 import java.util.List;
21 import java.util.Objects;
22 import java.util.concurrent.ConcurrentHashMap;
24 import javax.servlet.http.HttpServletRequest;
25 import javax.servlet.http.HttpServletResponse;
27 import org.apache.commons.lang.StringUtils;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.openhab.io.neeo.internal.NeeoConstants;
30 import org.openhab.io.neeo.internal.NeeoUtil;
31 import org.openhab.io.neeo.internal.ServiceContext;
32 import org.openhab.io.neeo.internal.TokenSearch;
33 import org.openhab.io.neeo.internal.models.NeeoDevice;
34 import org.openhab.io.neeo.internal.models.NeeoThingUID;
35 import org.openhab.io.neeo.internal.models.TokenScore;
36 import org.openhab.io.neeo.internal.models.TokenScoreResult;
37 import org.openhab.io.neeo.internal.serialization.NeeoBrainDeviceSerializer;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
41 import com.google.gson.Gson;
42 import com.google.gson.GsonBuilder;
43 import com.google.gson.JsonObject;
46 * The implementation of {@link ServletService} that will handle device search requests from the NEEO Brain
48 * @author Tim Roberts - Initial Contribution
51 public class NeeoBrainSearchService extends DefaultServletService {
54 private final Logger logger = LoggerFactory.getLogger(NeeoBrainSearchService.class);
56 /** The gson used to for json manipulation */
57 private final Gson gson;
60 private final ServiceContext context;
62 /** The last search results */
63 private final ConcurrentHashMap<Integer, NeeoThingUID> lastSearchResults = new ConcurrentHashMap<>();
66 * Constructs the service from the given {@link ServiceContext}.
68 * @param context the non-null {@link ServiceContext}
70 public NeeoBrainSearchService(ServiceContext context) {
71 Objects.requireNonNull(context, "context cannot be null");
73 this.context = context;
75 final GsonBuilder gsonBuilder = NeeoUtil.createGsonBuilder();
76 gsonBuilder.registerTypeAdapter(NeeoDevice.class, new NeeoBrainDeviceSerializer());
78 gson = gsonBuilder.create();
82 * Returns true if the path starts with "db"
84 * @see DefaultServletService#canHandleRoute(String[])
87 public boolean canHandleRoute(String[] paths) {
88 return paths.length >= 1 && StringUtils.equalsIgnoreCase(paths[0], "db");
92 * Handles the get request. If the path is "/db/search", will do a search via
93 * {@link #doSearch(String, HttpServletResponse)}. Otherwise we assume it's a request for device details (via
94 * {@link #doQuery(String, HttpServletResponse)}
96 * As of 52.15 - "/db/adapterdefinition/{id}" get's the latest device details
98 * @see DefaultServletService#handleGet(HttpServletRequest, String[], HttpServletResponse)
101 public void handleGet(HttpServletRequest req, String[] paths, HttpServletResponse resp) throws IOException {
102 Objects.requireNonNull(req, "req cannot be null");
103 Objects.requireNonNull(paths, "paths cannot be null");
104 Objects.requireNonNull(resp, "resp cannot be null");
105 if (paths.length < 2) {
106 throw new IllegalArgumentException("paths must have atleast 2 elements: " + StringUtils.join(paths));
109 final String path = StringUtils.lowerCase(paths[1]);
111 if (StringUtils.equalsIgnoreCase(path, "search")) {
112 String queryString = req.getQueryString();
113 if (queryString != null) {
114 doSearch(queryString, resp);
116 } else if (StringUtils.equalsIgnoreCase(path, "adapterdefinition") && paths.length >= 3) {
117 doAdapterDefinition(paths[2], resp);
124 * Does the search of all things and returns the results
126 * @param queryString the non-null, possibly empty query string
127 * @param resp the non-null response to write to
128 * @throws IOException Signals that an I/O exception has occurred.
130 private void doSearch(String queryString, HttpServletResponse resp) throws IOException {
131 Objects.requireNonNull(queryString, "queryString cannot be null");
132 Objects.requireNonNull(resp, "resp cannot be null");
134 final int idx = StringUtils.indexOf(queryString, '=');
136 if (idx >= 0 && idx + 1 < queryString.length()) {
137 final String search = NeeoUtil.decodeURIComponent(queryString.substring(idx + 1));
139 final List<JsonObject> ja = new ArrayList<>();
140 search(search).stream().sorted(Comparator.comparing(TokenScoreResult<NeeoDevice>::getScore).reversed())
142 final JsonObject jo = (JsonObject) gson.toJsonTree(item);
144 // transfer id from tokenscoreresult to neeodevice (as per NEEO API)
145 final int id = jo.getAsJsonPrimitive("id").getAsInt();
147 jo.getAsJsonObject("item").addProperty("id", id);
151 final String itemStr = gson.toJson(ja);
152 logger.debug("Search '{}', response: {}", search, itemStr);
153 NeeoUtil.write(resp, itemStr);
158 * Does a query for the NEEO device definition
160 * @param id the non-empty (last) search identifier
161 * @param resp the non-null response to write to
162 * @throws IOException Signals that an I/O exception has occurred.
164 private void doAdapterDefinition(String id, HttpServletResponse resp) throws IOException {
165 NeeoThingUID thingUID;
167 thingUID = new NeeoThingUID(id);
168 } catch (IllegalArgumentException e) {
169 logger.debug("Not a valid thingUID: {}", id);
170 NeeoUtil.write(resp, "{}");
174 final NeeoDevice device = context.getDefinitions().getDevice(thingUID);
176 if (device == null) {
177 logger.debug("Called with index position {} but nothing was found", id);
178 NeeoUtil.write(resp, "{}");
180 final String jos = gson.toJson(device);
181 NeeoUtil.write(resp, jos);
183 logger.debug("Query '{}', response: {}", id, jos);
188 * Does a query for the NEEO device definition
190 * @param id the non-empty (last) search identifier
191 * @param resp the non-null response to write to
192 * @throws IOException Signals that an I/O exception has occurred.
194 private void doQuery(String id, HttpServletResponse resp) throws IOException {
195 NeeoUtil.requireNotEmpty(id, "id cannot be empty");
196 Objects.requireNonNull(resp, "resp cannot be null");
198 NeeoDevice device = null;
202 idx = Integer.parseInt(id);
203 } catch (NumberFormatException e) {
204 logger.debug("Device ID was not a number: {}", id);
209 final NeeoThingUID thingUID = lastSearchResults.get(idx);
211 if (thingUID != null) {
212 device = context.getDefinitions().getDevice(thingUID);
216 if (device == null) {
217 logger.debug("Called with index position {} but nothing was found", id);
218 NeeoUtil.write(resp, "{}");
220 final JsonObject jo = (JsonObject) gson.toJsonTree(device);
221 jo.addProperty("id", idx);
223 final String jos = jo.toString();
224 NeeoUtil.write(resp, jos);
226 logger.debug("Query '{}', response: {}", idx, jos);
231 * Performs the actual search of things for the given query
233 * @param queryString the non-null, possibly empty query string
234 * @return the non-null, possibly empty list of {@link TokenScoreResult}
236 private List<TokenScoreResult<NeeoDevice>> search(String queryString) {
237 Objects.requireNonNull(queryString, "queryString cannot be null");
238 final TokenSearch tokenSearch = new TokenSearch(context, NeeoConstants.SEARCH_MATCHFACTOR);
239 final TokenSearch.Result searchResult = tokenSearch.search(queryString);
241 final List<TokenScoreResult<NeeoDevice>> searchItems = new ArrayList<>();
243 for (TokenScore<NeeoDevice> ts : searchResult.getDevices()) {
244 final NeeoDevice device = ts.getItem();
245 final TokenScoreResult<NeeoDevice> result = new TokenScoreResult<>(device, searchItems.size(),
246 ts.getScore(), searchResult.getMaxScore());
248 searchItems.add(result);
251 final Map<Integer, NeeoThingUID> results = new HashMap<>();
252 for (TokenScoreResult<NeeoDevice> tsr : searchItems) {
253 results.put(tsr.getId(), tsr.getItem().getUid());
256 // this isn't really thread safe but close enough for me
257 lastSearchResults.clear();
258 lastSearchResults.putAll(results);