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