]> git.basschouten.com Git - openhab-addons.git/blob
a31bd36631aef2d7a37a0dbd93dc13ceb806e77d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.io.neeo.internal.servletservices;
14
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;
20 import java.util.Map;
21 import java.util.Objects;
22 import java.util.concurrent.ConcurrentHashMap;
23
24 import javax.servlet.http.HttpServletRequest;
25 import javax.servlet.http.HttpServletResponse;
26
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;
39
40 import com.google.gson.Gson;
41 import com.google.gson.GsonBuilder;
42 import com.google.gson.JsonObject;
43
44 /**
45  * The implementation of {@link ServletService} that will handle device search requests from the NEEO Brain
46  *
47  * @author Tim Roberts - Initial Contribution
48  */
49 @NonNullByDefault
50 public class NeeoBrainSearchService extends DefaultServletService {
51
52     /** The logger */
53     private final Logger logger = LoggerFactory.getLogger(NeeoBrainSearchService.class);
54
55     /** The gson used to for json manipulation */
56     private final Gson gson;
57
58     /** The context. */
59     private final ServiceContext context;
60
61     /** The last search results */
62     private final Map<Integer, NeeoThingUID> lastSearchResults = new ConcurrentHashMap<>();
63
64     /**
65      * Constructs the service from the given {@link ServiceContext}.
66      *
67      * @param context the non-null {@link ServiceContext}
68      */
69     public NeeoBrainSearchService(ServiceContext context) {
70         Objects.requireNonNull(context, "context cannot be null");
71
72         this.context = context;
73
74         final GsonBuilder gsonBuilder = NeeoUtil.createGsonBuilder();
75         gsonBuilder.registerTypeAdapter(NeeoDevice.class, new NeeoBrainDeviceSerializer());
76
77         gson = gsonBuilder.create();
78     }
79
80     /**
81      * Returns true if the path starts with "db"
82      *
83      * @see DefaultServletService#canHandleRoute(String[])
84      */
85     @Override
86     public boolean canHandleRoute(String[] paths) {
87         return paths.length >= 1 && "db".equalsIgnoreCase(paths[0]);
88     }
89
90     /**
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)}
94      *
95      * As of 52.15 - "/db/adapterdefinition/{id}" get's the latest device details
96      *
97      * @see DefaultServletService#handleGet(HttpServletRequest, String[], HttpServletResponse)
98      */
99     @Override
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));
106         }
107
108         final String path = paths[1].toLowerCase();
109
110         if ("search".equalsIgnoreCase(path)) {
111             String queryString = req.getQueryString();
112             if (queryString != null) {
113                 doSearch(queryString, resp);
114             }
115         } else if ("adapterdefinition".equalsIgnoreCase(path) && paths.length >= 3) {
116             doAdapterDefinition(paths[2], resp);
117         } else {
118             doQuery(path, resp);
119         }
120     }
121
122     /**
123      * Does the search of all things and returns the results
124      *
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.
128      */
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");
132
133         final int idx = queryString.indexOf("=");
134
135         if (idx >= 0 && idx + 1 < queryString.length()) {
136             final String search = NeeoUtil.decodeURIComponent(queryString.substring(idx + 1));
137
138             final List<JsonObject> ja = new ArrayList<>();
139             search(search).stream().sorted(Comparator.comparing(TokenScoreResult<NeeoDevice>::getScore).reversed())
140                     .forEach(item -> {
141                         final JsonObject jo = (JsonObject) gson.toJsonTree(item);
142
143                         // transfer id from tokenscoreresult to neeodevice (as per NEEO API)
144                         final int id = jo.getAsJsonPrimitive("id").getAsInt();
145                         jo.remove("id");
146                         jo.getAsJsonObject("item").addProperty("id", id);
147                         ja.add(jo);
148                     });
149
150             final String itemStr = gson.toJson(ja);
151             logger.debug("Search '{}', response: {}", search, itemStr);
152             NeeoUtil.write(resp, itemStr);
153         }
154     }
155
156     /**
157      * Does a query for the NEEO device definition
158      *
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.
162      */
163     private void doAdapterDefinition(String id, HttpServletResponse resp) throws IOException {
164         NeeoThingUID thingUID;
165         try {
166             thingUID = new NeeoThingUID(id);
167         } catch (IllegalArgumentException e) {
168             logger.debug("Not a valid thingUID: {}", id);
169             NeeoUtil.write(resp, "{}");
170             return;
171         }
172
173         final NeeoDevice device = context.getDefinitions().getDevice(thingUID);
174
175         if (device == null) {
176             logger.debug("Called with index position {} but nothing was found", id);
177             NeeoUtil.write(resp, "{}");
178         } else {
179             final String jos = gson.toJson(device);
180             NeeoUtil.write(resp, jos);
181
182             logger.debug("Query '{}', response: {}", id, jos);
183         }
184     }
185
186     /**
187      * Does a query for the NEEO device definition
188      *
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.
192      */
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");
196
197         NeeoDevice device = null;
198
199         int idx = -1;
200         try {
201             idx = Integer.parseInt(id);
202         } catch (NumberFormatException e) {
203             logger.debug("Device ID was not a number: {}", id);
204             idx = -1;
205         }
206
207         if (idx >= 0) {
208             final NeeoThingUID thingUID = lastSearchResults.get(idx);
209
210             if (thingUID != null) {
211                 device = context.getDefinitions().getDevice(thingUID);
212             }
213         }
214
215         if (device == null) {
216             logger.debug("Called with index position {} but nothing was found", id);
217             NeeoUtil.write(resp, "{}");
218         } else {
219             final JsonObject jo = (JsonObject) gson.toJsonTree(device);
220             jo.addProperty("id", idx);
221
222             final String jos = jo.toString();
223             NeeoUtil.write(resp, jos);
224
225             logger.debug("Query '{}', response: {}", idx, jos);
226         }
227     }
228
229     /**
230      * Performs the actual search of things for the given query
231      *
232      * @param queryString the non-null, possibly empty query string
233      * @return the non-null, possibly empty list of {@link TokenScoreResult}
234      */
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);
239
240         final List<TokenScoreResult<NeeoDevice>> searchItems = new ArrayList<>();
241
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());
246
247             searchItems.add(result);
248         }
249
250         final Map<Integer, NeeoThingUID> results = new HashMap<>();
251         for (TokenScoreResult<NeeoDevice> tsr : searchItems) {
252             results.put(tsr.getId(), tsr.getItem().getUid());
253         }
254
255         // this isn't really thread safe but close enough for me
256         lastSearchResults.clear();
257         lastSearchResults.putAll(results);
258
259         return searchItems;
260     }
261 }