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.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.eclipse.jdt.annotation.NonNullByDefault;
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.TokenSearch;
32 import org.openhab.io.neeo.internal.models.NeeoDevice;
33 import org.openhab.io.neeo.internal.models.NeeoThingUID;
34 import org.openhab.io.neeo.internal.models.TokenScore;
35 import org.openhab.io.neeo.internal.models.TokenScoreResult;
36 import org.openhab.io.neeo.internal.serialization.NeeoBrainDeviceSerializer;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
40 import com.google.gson.Gson;
41 import com.google.gson.GsonBuilder;
42 import com.google.gson.JsonObject;
45 * The implementation of {@link ServletService} that will handle device search requests from the NEEO Brain
47 * @author Tim Roberts - Initial Contribution
50 public class NeeoBrainSearchService extends DefaultServletService {
53 private final Logger logger = LoggerFactory.getLogger(NeeoBrainSearchService.class);
55 /** The gson used to for json manipulation */
56 private final Gson gson;
59 private final ServiceContext context;
61 /** The last search results */
62 private final Map<Integer, NeeoThingUID> lastSearchResults = new ConcurrentHashMap<>();
65 * Constructs the service from the given {@link ServiceContext}.
67 * @param context the non-null {@link ServiceContext}
69 public NeeoBrainSearchService(ServiceContext context) {
70 Objects.requireNonNull(context, "context cannot be null");
72 this.context = context;
74 final GsonBuilder gsonBuilder = NeeoUtil.createGsonBuilder();
75 gsonBuilder.registerTypeAdapter(NeeoDevice.class, new NeeoBrainDeviceSerializer());
77 gson = gsonBuilder.create();
81 * Returns true if the path starts with "db"
83 * @see DefaultServletService#canHandleRoute(String[])
86 public boolean canHandleRoute(String[] paths) {
87 return paths.length >= 1 && paths[0].equalsIgnoreCase("db");
91 * Handles the get request. If the path is "/db/search", will do a search via
92 * {@link #doSearch(String, HttpServletResponse)}. Otherwise we assume it's a request for device details (via
93 * {@link #doQuery(String, HttpServletResponse)}
95 * As of 52.15 - "/db/adapterdefinition/{id}" get's the latest device details
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");
104 if (paths.length < 2) {
105 throw new IllegalArgumentException("paths must have atleast 2 elements: " + String.join("", paths));
108 final String path = paths[1].toLowerCase();
110 if (path.equalsIgnoreCase("search")) {
111 String queryString = req.getQueryString();
112 if (queryString != null) {
113 doSearch(queryString, resp);
115 } else if (path.equalsIgnoreCase("adapterdefinition") && paths.length >= 3) {
116 doAdapterDefinition(paths[2], resp);
123 * Does the search of all things and returns the results
125 * @param queryString the non-null, possibly empty query string
126 * @param resp the non-null response to write to
127 * @throws IOException Signals that an I/O exception has occurred.
129 private void doSearch(String queryString, HttpServletResponse resp) throws IOException {
130 Objects.requireNonNull(queryString, "queryString cannot be null");
131 Objects.requireNonNull(resp, "resp cannot be null");
133 final int idx = queryString.indexOf("=");
135 if (idx >= 0 && idx + 1 < queryString.length()) {
136 final String search = NeeoUtil.decodeURIComponent(queryString.substring(idx + 1));
138 final List<JsonObject> ja = new ArrayList<>();
139 search(search).stream().sorted(Comparator.comparing(TokenScoreResult<NeeoDevice>::getScore).reversed())
141 final JsonObject jo = (JsonObject) gson.toJsonTree(item);
143 // transfer id from tokenscoreresult to neeodevice (as per NEEO API)
144 final int id = jo.getAsJsonPrimitive("id").getAsInt();
146 jo.getAsJsonObject("item").addProperty("id", id);
150 final String itemStr = gson.toJson(ja);
151 logger.debug("Search '{}', response: {}", search, itemStr);
152 NeeoUtil.write(resp, itemStr);
157 * Does a query for the NEEO device definition
159 * @param id the non-empty (last) search identifier
160 * @param resp the non-null response to write to
161 * @throws IOException Signals that an I/O exception has occurred.
163 private void doAdapterDefinition(String id, HttpServletResponse resp) throws IOException {
164 NeeoThingUID thingUID;
166 thingUID = new NeeoThingUID(id);
167 } catch (IllegalArgumentException e) {
168 logger.debug("Not a valid thingUID: {}", id);
169 NeeoUtil.write(resp, "{}");
173 final NeeoDevice device = context.getDefinitions().getDevice(thingUID);
175 if (device == null) {
176 logger.debug("Called with index position {} but nothing was found", id);
177 NeeoUtil.write(resp, "{}");
179 final String jos = gson.toJson(device);
180 NeeoUtil.write(resp, jos);
182 logger.debug("Query '{}', response: {}", id, jos);
187 * Does a query for the NEEO device definition
189 * @param id the non-empty (last) search identifier
190 * @param resp the non-null response to write to
191 * @throws IOException Signals that an I/O exception has occurred.
193 private void doQuery(String id, HttpServletResponse resp) throws IOException {
194 NeeoUtil.requireNotEmpty(id, "id cannot be empty");
195 Objects.requireNonNull(resp, "resp cannot be null");
197 NeeoDevice device = null;
201 idx = Integer.parseInt(id);
202 } catch (NumberFormatException e) {
203 logger.debug("Device ID was not a number: {}", id);
208 final NeeoThingUID thingUID = lastSearchResults.get(idx);
210 if (thingUID != null) {
211 device = context.getDefinitions().getDevice(thingUID);
215 if (device == null) {
216 logger.debug("Called with index position {} but nothing was found", id);
217 NeeoUtil.write(resp, "{}");
219 final JsonObject jo = (JsonObject) gson.toJsonTree(device);
220 jo.addProperty("id", idx);
222 final String jos = jo.toString();
223 NeeoUtil.write(resp, jos);
225 logger.debug("Query '{}', response: {}", idx, jos);
230 * Performs the actual search of things for the given query
232 * @param queryString the non-null, possibly empty query string
233 * @return the non-null, possibly empty list of {@link TokenScoreResult}
235 private List<TokenScoreResult<NeeoDevice>> search(String queryString) {
236 Objects.requireNonNull(queryString, "queryString cannot be null");
237 final TokenSearch tokenSearch = new TokenSearch(context, NeeoConstants.SEARCH_MATCHFACTOR);
238 final TokenSearch.Result searchResult = tokenSearch.search(queryString);
240 final List<TokenScoreResult<NeeoDevice>> searchItems = new ArrayList<>();
242 for (TokenScore<NeeoDevice> ts : searchResult.getDevices()) {
243 final NeeoDevice device = ts.getItem();
244 final TokenScoreResult<NeeoDevice> result = new TokenScoreResult<>(device, searchItems.size(),
245 ts.getScore(), searchResult.getMaxScore());
247 searchItems.add(result);
250 final Map<Integer, NeeoThingUID> results = new HashMap<>();
251 for (TokenScoreResult<NeeoDevice> tsr : searchItems) {
252 results.put(tsr.getId(), tsr.getItem().getUid());
255 // this isn't really thread safe but close enough for me
256 lastSearchResults.clear();
257 lastSearchResults.putAll(results);