]> git.basschouten.com Git - openhab-addons.git/blob
ca4e26b336142d88b98e22c127d450cba5c71a67
[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             doSearch(req.getQueryString(), resp);
113         } else if (StringUtils.equalsIgnoreCase(path, "adapterdefinition") && paths.length >= 3) {
114             doAdapterDefinition(paths[2], resp);
115         } else {
116             doQuery(path, resp);
117         }
118     }
119
120     /**
121      * Does the search of all things and returns the results
122      *
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.
126      */
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");
130
131         final int idx = StringUtils.indexOf(queryString, '=');
132
133         if (idx >= 0 && idx + 1 < queryString.length()) {
134             final String search = NeeoUtil.decodeURIComponent(queryString.substring(idx + 1));
135
136             final List<JsonObject> ja = new ArrayList<>();
137             search(search).stream().sorted(Comparator.comparing(TokenScoreResult<NeeoDevice>::getScore).reversed())
138                     .forEach(item -> {
139                         final JsonObject jo = (JsonObject) gson.toJsonTree(item);
140
141                         // transfer id from tokenscoreresult to neeodevice (as per NEEO API)
142                         final int id = jo.getAsJsonPrimitive("id").getAsInt();
143                         jo.remove("id");
144                         jo.getAsJsonObject("item").addProperty("id", id);
145                         ja.add(jo);
146                     });
147
148             final String itemStr = gson.toJson(ja);
149             logger.debug("Search '{}', response: {}", search, itemStr);
150             NeeoUtil.write(resp, itemStr);
151         }
152     }
153
154     /**
155      * Does a query for the NEEO device definition
156      *
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.
160      */
161     private void doAdapterDefinition(String id, HttpServletResponse resp) throws IOException {
162         NeeoThingUID thingUID;
163         try {
164             thingUID = new NeeoThingUID(id);
165         } catch (IllegalArgumentException e) {
166             logger.debug("Not a valid thingUID: {}", id);
167             NeeoUtil.write(resp, "{}");
168             return;
169         }
170
171         final NeeoDevice device = context.getDefinitions().getDevice(thingUID);
172
173         if (device == null) {
174             logger.debug("Called with index position {} but nothing was found", id);
175             NeeoUtil.write(resp, "{}");
176         } else {
177             final String jos = gson.toJson(device);
178             NeeoUtil.write(resp, jos);
179
180             logger.debug("Query '{}', response: {}", id, jos);
181         }
182     }
183
184     /**
185      * Does a query for the NEEO device definition
186      *
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.
190      */
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");
194
195         NeeoDevice device = null;
196
197         int idx = -1;
198         try {
199             idx = Integer.parseInt(id);
200         } catch (NumberFormatException e) {
201             logger.debug("Device ID was not a number: {}", id);
202             idx = -1;
203         }
204
205         if (idx >= 0) {
206             final NeeoThingUID thingUID = lastSearchResults.get(idx);
207
208             if (thingUID != null) {
209                 device = context.getDefinitions().getDevice(thingUID);
210             }
211         }
212
213         if (device == null) {
214             logger.debug("Called with index position {} but nothing was found", id);
215             NeeoUtil.write(resp, "{}");
216         } else {
217             final JsonObject jo = (JsonObject) gson.toJsonTree(device);
218             jo.addProperty("id", idx);
219
220             final String jos = jo.toString();
221             NeeoUtil.write(resp, jos);
222
223             logger.debug("Query '{}', response: {}", idx, jos);
224         }
225     }
226
227     /**
228      * Performs the actual search of things for the given query
229      *
230      * @param queryString the non-null, possibly empty query string
231      * @return the non-null, possibly empty list of {@link TokenScoreResult}
232      */
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);
237
238         final List<TokenScoreResult<NeeoDevice>> searchItems = new ArrayList<>();
239
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());
244
245             searchItems.add(result);
246         }
247
248         final Map<Integer, NeeoThingUID> results = new HashMap<>();
249         for (TokenScoreResult<NeeoDevice> tsr : searchItems) {
250             results.put(tsr.getId(), tsr.getItem().getUid());
251         }
252
253         // this isn't really thread safe but close enough for me
254         lastSearchResults.clear();
255         lastSearchResults.putAll(results);
256
257         return searchItems;
258     }
259 }