2 * Copyright (c) 2010-2020 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 doSearch(req.getQueryString(), resp);
113 } else if (StringUtils.equalsIgnoreCase(path, "adapterdefinition") && paths.length >= 3) {
114 doAdapterDefinition(paths[2], resp);
121 * Does the search of all things and returns the results
123 * @param queryString the non-null, possibly empty query string
124 * @param resp the non-null response to write to
125 * @throws IOException Signals that an I/O exception has occurred.
127 private void doSearch(String queryString, HttpServletResponse resp) throws IOException {
128 Objects.requireNonNull(queryString, "queryString cannot be null");
129 Objects.requireNonNull(resp, "resp cannot be null");
131 final int idx = StringUtils.indexOf(queryString, '=');
133 if (idx >= 0 && idx + 1 < queryString.length()) {
134 final String search = NeeoUtil.decodeURIComponent(queryString.substring(idx + 1));
136 final List<JsonObject> ja = new ArrayList<>();
137 search(search).stream().sorted(Comparator.comparing(TokenScoreResult<NeeoDevice>::getScore).reversed())
139 final JsonObject jo = (JsonObject) gson.toJsonTree(item);
141 // transfer id from tokenscoreresult to neeodevice (as per NEEO API)
142 final int id = jo.getAsJsonPrimitive("id").getAsInt();
144 jo.getAsJsonObject("item").addProperty("id", id);
148 final String itemStr = gson.toJson(ja);
149 logger.debug("Search '{}', response: {}", search, itemStr);
150 NeeoUtil.write(resp, itemStr);
155 * Does a query for the NEEO device definition
157 * @param id the non-empty (last) search identifier
158 * @param resp the non-null response to write to
159 * @throws IOException Signals that an I/O exception has occurred.
161 private void doAdapterDefinition(String id, HttpServletResponse resp) throws IOException {
162 NeeoThingUID thingUID;
164 thingUID = new NeeoThingUID(id);
165 } catch (IllegalArgumentException e) {
166 logger.debug("Not a valid thingUID: {}", id);
167 NeeoUtil.write(resp, "{}");
171 final NeeoDevice device = context.getDefinitions().getDevice(thingUID);
173 if (device == null) {
174 logger.debug("Called with index position {} but nothing was found", id);
175 NeeoUtil.write(resp, "{}");
177 final String jos = gson.toJson(device);
178 NeeoUtil.write(resp, jos);
180 logger.debug("Query '{}', response: {}", id, jos);
185 * Does a query for the NEEO device definition
187 * @param id the non-empty (last) search identifier
188 * @param resp the non-null response to write to
189 * @throws IOException Signals that an I/O exception has occurred.
191 private void doQuery(String id, HttpServletResponse resp) throws IOException {
192 NeeoUtil.requireNotEmpty(id, "id cannot be empty");
193 Objects.requireNonNull(resp, "resp cannot be null");
195 NeeoDevice device = null;
199 idx = Integer.parseInt(id);
200 } catch (NumberFormatException e) {
201 logger.debug("Device ID was not a number: {}", id);
206 final NeeoThingUID thingUID = lastSearchResults.get(idx);
208 if (thingUID != null) {
209 device = context.getDefinitions().getDevice(thingUID);
213 if (device == null) {
214 logger.debug("Called with index position {} but nothing was found", id);
215 NeeoUtil.write(resp, "{}");
217 final JsonObject jo = (JsonObject) gson.toJsonTree(device);
218 jo.addProperty("id", idx);
220 final String jos = jo.toString();
221 NeeoUtil.write(resp, jos);
223 logger.debug("Query '{}', response: {}", idx, jos);
228 * Performs the actual search of things for the given query
230 * @param queryString the non-null, possibly empty query string
231 * @return the non-null, possibly empty list of {@link TokenScoreResult}
233 private List<TokenScoreResult<NeeoDevice>> search(String queryString) {
234 Objects.requireNonNull(queryString, "queryString cannot be null");
235 final TokenSearch tokenSearch = new TokenSearch(context, NeeoConstants.SEARCH_MATCHFACTOR);
236 final TokenSearch.Result searchResult = tokenSearch.search(queryString);
238 final List<TokenScoreResult<NeeoDevice>> searchItems = new ArrayList<>();
240 for (TokenScore<NeeoDevice> ts : searchResult.getDevices()) {
241 final NeeoDevice device = ts.getItem();
242 final TokenScoreResult<NeeoDevice> result = new TokenScoreResult<>(device, searchItems.size(),
243 ts.getScore(), searchResult.getMaxScore());
245 searchItems.add(result);
248 final Map<Integer, NeeoThingUID> results = new HashMap<>();
249 for (TokenScoreResult<NeeoDevice> tsr : searchItems) {
250 results.put(tsr.getId(), tsr.getItem().getUid());
253 // this isn't really thread safe but close enough for me
254 lastSearchResults.clear();
255 lastSearchResults.putAll(results);