baseType;
31 | private T lastValue;
32 |
33 | /**
34 | * Create a read only value that can only be updated by a Thing's reading.
35 | *
36 | * Example: A sensor is updating its reading, but the reading cannot be set
37 | * externally.
38 | *
39 | * @param initialValue The initial value
40 | */
41 | public Value(final T initialValue) {
42 | this(BaseTypeHelper.derive(initialValue), initialValue, null);
43 | }
44 |
45 | /**
46 | * Create a read only value that can only be updated by a Thing's reading.
47 | * Initial value will be set to null.
48 | *
49 | * Example: A sensor is updating its reading, but the reading cannot be set
50 | * externally.
51 | *
52 | * @param baseType The Class of the values base type
53 | */
54 | public Value(final Class baseType) {
55 | this(baseType, null, null);
56 | }
57 |
58 | /**
59 | * Create a writable value that can be set to a new value.
60 | *
61 | * Example: A light that can be switched off by setting this to false.
62 | *
63 | * @param initialValue The initial value
64 | * @param valueForwarder The method that updates the actual value on the
65 | * thing
66 | */
67 | public Value(final T initialValue, final Consumer valueForwarder) {
68 | this(BaseTypeHelper.derive(initialValue), initialValue, valueForwarder);
69 | }
70 |
71 | /**
72 | * Create a writable value that can be set to a new value.
73 | * Initial value will be set to null.
74 | *
75 | * Example: A light that can be switched off by setting this to false.
76 | *
77 | * @param baseType The Class of the values base type
78 | * @param valueForwarder The method that updates the actual value on the
79 | * thing
80 | */
81 | public Value(final Class baseType, final Consumer valueForwarder) {
82 | this(baseType, null, valueForwarder);
83 | }
84 |
85 | /**
86 | * Create a writable value that can be set to a new value.
87 | *
88 | * Example: A light that can be switched off by setting this to false.
89 | *
90 | * @param baseType The Class of the values base type
91 | * @param initialValue The initial value
92 | * @param valueForwarder The method that updates the actual value on the
93 | * thing
94 | */
95 | public Value(final Class baseType, final T initialValue, final Consumer valueForwarder) {
96 | Objects.requireNonNull(baseType, "The base type of a value must not be null.");
97 | this.baseType = baseType;
98 | this.lastValue = initialValue;
99 | this.valueForwarder = valueForwarder;
100 | }
101 |
102 | /**
103 | * Get the base type of this value.
104 | *
105 | * @return The base type.
106 | */
107 | public Class getBaseType()
108 | {
109 | return this.baseType;
110 | }
111 |
112 | /**
113 | * Set a new Value for this thing.
114 | *
115 | * Example: Switch a light off: set(false)
116 | *
117 | * @param value Value to set
118 | */
119 | public final void set(T value) {
120 | if (valueForwarder != null) {
121 | valueForwarder.accept(value);
122 | }
123 |
124 | this.notifyOfExternalUpdate(value);
125 | }
126 |
127 | /**
128 | * Returns the last known value from the underlying thing.
129 | *
130 | * Example: Returns false, when a light is off.
131 | *
132 | * @return The value.
133 | */
134 | public final T get() {
135 | return this.lastValue;
136 | }
137 |
138 | /**
139 | * Called if the underlying thing reported a new value. This informs
140 | * observers about the update.
141 | *
142 | * Example: A sensor reports a new value.
143 | *
144 | * @param value the newly reported value
145 | */
146 | public final void notifyOfExternalUpdate(T value) {
147 | if (value != null && !value.equals(this.lastValue)) {
148 | this.setChanged();
149 | this.lastValue = value;
150 | notifyObservers(value);
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/main/java/io/webthings/webthing/WebThingServer.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Java Web Thing server implementation.
3 | */
4 | package io.webthings.webthing;
5 |
6 | import org.json.JSONArray;
7 | import org.json.JSONException;
8 | import org.json.JSONObject;
9 |
10 | import java.io.IOException;
11 | import java.net.InetAddress;
12 | import java.security.NoSuchAlgorithmException;
13 | import java.util.ArrayList;
14 | import java.util.HashMap;
15 | import java.util.List;
16 | import java.util.Map;
17 | import java.util.Timer;
18 | import java.util.TimerTask;
19 |
20 | import javax.jmdns.JmDNS;
21 | import javax.jmdns.ServiceInfo;
22 | import javax.net.ssl.SSLServerSocketFactory;
23 |
24 | import fi.iki.elonen.NanoHTTPD;
25 | import fi.iki.elonen.NanoWSD;
26 | import fi.iki.elonen.router.RouterNanoHTTPD;
27 | import io.webthings.webthing.errors.PropertyError;
28 |
29 | /**
30 | * Server to represent a Web Thing over HTTP.
31 | */
32 | public class WebThingServer extends RouterNanoHTTPD {
33 | private static final int SOCKET_READ_TIMEOUT = 30 * 1000;
34 | private static final int WEBSOCKET_PING_INTERVAL = 20 * 1000;
35 | private final int port;
36 | private final ThingsType things;
37 | private final String name;
38 | private String hostname;
39 | private final boolean disableHostValidation;
40 | private final String basePath;
41 | private final List hosts;
42 | private final boolean isTls;
43 | private JmDNS jmdns;
44 |
45 | /**
46 | * Initialize the WebThingServer on port 80.
47 | *
48 | * @param things List of Things managed by this server
49 | * @throws IOException If server fails to bind.
50 | * @throws NullPointerException If something bad happened.
51 | */
52 | public WebThingServer(ThingsType things)
53 | throws IOException, NullPointerException {
54 | this(things, 80, null, null, null, "/", false);
55 | }
56 |
57 | /**
58 | * Initialize the WebThingServer.
59 | *
60 | * @param things List of Things managed by this server
61 | * @param port Port to listen on
62 | * @throws IOException If server fails to bind.
63 | * @throws NullPointerException If something bad happened.
64 | */
65 | public WebThingServer(ThingsType things, int port)
66 | throws IOException, NullPointerException {
67 | this(things, port, null, null, null, "/", false);
68 | }
69 |
70 | /**
71 | * Initialize the WebThingServer.
72 | *
73 | * @param things List of Things managed by this server
74 | * @param port Port to listen on
75 | * @param hostname Host name, i.e. mything.com
76 | * @throws IOException If server fails to bind.
77 | * @throws NullPointerException If something bad happened.
78 | */
79 | public WebThingServer(ThingsType things, int port, String hostname)
80 | throws IOException, NullPointerException {
81 | this(things, port, hostname, null, null, "/", false);
82 | }
83 |
84 | /**
85 | * Initialize the WebThingServer.
86 | *
87 | * @param things List of Things managed by this server
88 | * @param port Port to listen on
89 | * @param hostname Host name, i.e. mything.com
90 | * @param sslOptions SSL options to pass to the NanoHTTPD server
91 | * @throws IOException If server fails to bind.
92 | * @throws NullPointerException If something bad happened.
93 | */
94 | public WebThingServer(ThingsType things,
95 | int port,
96 | String hostname,
97 | SSLOptions sslOptions)
98 | throws IOException, NullPointerException {
99 | this(things, port, hostname, sslOptions, null, "/", false);
100 | }
101 |
102 | /**
103 | * Initialize the WebThingServer.
104 | *
105 | * @param things List of Things managed by this server
106 | * @param port Port to listen on
107 | * @param hostname Host name, i.e. mything.com
108 | * @param sslOptions SSL options to pass to the NanoHTTPD server
109 | * @param additionalRoutes List of additional routes to add to the server
110 | * @throws IOException If server fails to bind.
111 | * @throws NullPointerException If something bad happened.
112 | */
113 | public WebThingServer(ThingsType things,
114 | int port,
115 | String hostname,
116 | SSLOptions sslOptions,
117 | List additionalRoutes)
118 | throws IOException, NullPointerException {
119 | this(things, port, hostname, sslOptions, additionalRoutes, "/", false);
120 | }
121 |
122 | /**
123 | * Initialize the WebThingServer.
124 | *
125 | * @param things List of Things managed by this server
126 | * @param port Port to listen on
127 | * @param hostname Host name, i.e. mything.com
128 | * @param sslOptions SSL options to pass to the NanoHTTPD server
129 | * @param additionalRoutes List of additional routes to add to the server
130 | * @param basePath Base URL path to use, rather than '/'
131 | * @throws IOException If server fails to bind.
132 | * @throws NullPointerException If something bad happened.
133 | */
134 | public WebThingServer(ThingsType things,
135 | int port,
136 | String hostname,
137 | SSLOptions sslOptions,
138 | List additionalRoutes,
139 | String basePath)
140 | throws IOException, NullPointerException {
141 | this(things,
142 | port,
143 | hostname,
144 | sslOptions,
145 | additionalRoutes,
146 | basePath,
147 | false);
148 | }
149 |
150 | /**
151 | * Initialize the WebThingServer.
152 | *
153 | * @param things List of Things managed by this server
154 | * @param port Port to listen on
155 | * @param hostname Host name, i.e. mything.com
156 | * @param sslOptions SSL options to pass to the NanoHTTPD server
157 | * @param additionalRoutes List of additional routes to add to the
158 | * server
159 | * @param basePath Base URL path to use, rather than '/'
160 | * @param disableHostValidation Whether or not to disable host validation --
161 | * note that this can lead to DNS rebinding
162 | * attacks
163 | * @throws IOException If server fails to bind.
164 | * @throws NullPointerException If something bad happened.
165 | */
166 | public WebThingServer(ThingsType things,
167 | int port,
168 | String hostname,
169 | SSLOptions sslOptions,
170 | List additionalRoutes,
171 | String basePath,
172 | boolean disableHostValidation)
173 | throws IOException, NullPointerException {
174 | super(port);
175 | this.port = port;
176 | this.things = things;
177 | this.name = things.getName();
178 | this.isTls = sslOptions != null;
179 | this.hostname = hostname;
180 | this.basePath = basePath.replaceAll("/$", "");
181 | this.disableHostValidation = disableHostValidation;
182 |
183 | this.hosts = new ArrayList<>();
184 | this.hosts.add("localhost");
185 | this.hosts.add(String.format("localhost:%d", this.port));
186 |
187 | for (String address : Utils.getAddresses()) {
188 | this.hosts.add(address);
189 | this.hosts.add(String.format("%s:%d", address, this.port));
190 | }
191 |
192 | if (this.hostname != null) {
193 | this.hostname = this.hostname.toLowerCase();
194 | this.hosts.add(this.hostname);
195 | this.hosts.add(String.format("%s:%d", this.hostname, this.port));
196 | }
197 |
198 | if (this.isTls) {
199 | super.makeSecure(sslOptions.getSocketFactory(),
200 | sslOptions.getProtocols());
201 | }
202 |
203 | this.setRoutePrioritizer(new InsertionOrderRoutePrioritizer());
204 |
205 | if (additionalRoutes != null && additionalRoutes.size() > 0) {
206 | additionalRoutes.forEach(o -> addRoute(this.basePath + o.url,
207 | o.handlerClass,
208 | o.parameters));
209 | }
210 |
211 | if (MultipleThings.class.isInstance(things)) {
212 | List list = things.getThings();
213 | for (int i = 0; i < list.size(); ++i) {
214 | Thing thing = list.get(i);
215 | thing.setHrefPrefix(String.format("%s/%d", this.basePath, i));
216 | }
217 |
218 | // These are matched in the order they are added.
219 | addRoute(this.basePath + "/:thingId/properties/:propertyName",
220 | PropertyHandler.class,
221 | this.things,
222 | this.hosts,
223 | this.isTls,
224 | this.disableHostValidation);
225 | addRoute(this.basePath + "/:thingId/properties",
226 | PropertiesHandler.class,
227 | this.things,
228 | this.hosts,
229 | this.isTls,
230 | this.disableHostValidation);
231 | addRoute(this.basePath + "/:thingId/actions/:actionName/:actionId",
232 | ActionIDHandler.class,
233 | this.things,
234 | this.hosts,
235 | this.isTls,
236 | this.disableHostValidation);
237 | addRoute(this.basePath + "/:thingId/actions/:actionName",
238 | ActionHandler.class,
239 | this.things,
240 | this.hosts,
241 | this.isTls,
242 | this.disableHostValidation);
243 | addRoute(this.basePath + "/:thingId/actions",
244 | ActionsHandler.class,
245 | this.things,
246 | this.hosts,
247 | this.isTls,
248 | this.disableHostValidation);
249 | addRoute(this.basePath + "/:thingId/events/:eventName",
250 | EventHandler.class,
251 | this.things,
252 | this.hosts,
253 | this.isTls,
254 | this.disableHostValidation);
255 | addRoute(this.basePath + "/:thingId/events",
256 | EventsHandler.class,
257 | this.things,
258 | this.hosts,
259 | this.isTls,
260 | this.disableHostValidation);
261 | addRoute(this.basePath + "/:thingId",
262 | ThingHandler.class,
263 | this.things,
264 | this.hosts,
265 | this.isTls,
266 | this.disableHostValidation);
267 | addRoute(this.basePath + "/",
268 | ThingsHandler.class,
269 | this.things,
270 | this.hosts,
271 | this.isTls,
272 | this.disableHostValidation);
273 | } else {
274 | things.getThing(0).setHrefPrefix(this.basePath);
275 |
276 | // These are matched in the order they are added.
277 | addRoute(this.basePath + "/properties/:propertyName",
278 | PropertyHandler.class,
279 | this.things,
280 | this.hosts,
281 | this.isTls,
282 | this.disableHostValidation);
283 | addRoute(this.basePath + "/properties",
284 | PropertiesHandler.class,
285 | this.things,
286 | this.hosts,
287 | this.isTls,
288 | this.disableHostValidation);
289 | addRoute(this.basePath + "/actions/:actionName/:actionId",
290 | ActionIDHandler.class,
291 | this.things,
292 | this.hosts,
293 | this.isTls,
294 | this.disableHostValidation);
295 | addRoute(this.basePath + "/actions/:actionName",
296 | ActionHandler.class,
297 | this.things,
298 | this.hosts,
299 | this.isTls,
300 | this.disableHostValidation);
301 | addRoute(this.basePath + "/actions",
302 | ActionsHandler.class,
303 | this.things,
304 | this.hosts,
305 | this.isTls,
306 | this.disableHostValidation);
307 | addRoute(this.basePath + "/events/:eventName",
308 | EventHandler.class,
309 | this.things,
310 | this.hosts,
311 | this.isTls,
312 | this.disableHostValidation);
313 | addRoute(this.basePath + "/events",
314 | EventsHandler.class,
315 | this.things,
316 | this.hosts,
317 | this.isTls,
318 | this.disableHostValidation);
319 | addRoute(this.basePath + "/",
320 | ThingHandler.class,
321 | this.things,
322 | this.hosts,
323 | this.isTls,
324 | this.disableHostValidation);
325 | }
326 |
327 | setNotFoundHandler(Error404UriHandler.class);
328 | }
329 |
330 | /**
331 | * Start listening for incoming connections.
332 | *
333 | * @param daemon Whether or not to daemonize the server
334 | * @throws IOException on failure to listen on port
335 | */
336 | public void start(boolean daemon) throws IOException {
337 | this.jmdns = JmDNS.create(hostname == null ?
338 | InetAddress.getLocalHost() :
339 | InetAddress.getByName(hostname));
340 |
341 | String systemHostname = this.jmdns.getHostName();
342 | if (systemHostname.endsWith(".")) {
343 | systemHostname =
344 | systemHostname.substring(0, systemHostname.length() - 1);
345 | }
346 | this.hosts.add(systemHostname);
347 | this.hosts.add(String.format("%s:%d", systemHostname, this.port));
348 |
349 | Map txt = new HashMap();
350 | txt.put("path", "/");
351 |
352 | if (this.isTls) {
353 | txt.put("tls", "1");
354 | }
355 |
356 | ServiceInfo serviceInfo = ServiceInfo.create("_webthing._tcp.local",
357 | this.name,
358 | null,
359 | this.port,
360 | 0,
361 | 0,
362 | txt);
363 | this.jmdns.registerService(serviceInfo);
364 |
365 | super.start(WebThingServer.SOCKET_READ_TIMEOUT, daemon);
366 | }
367 |
368 | /**
369 | * Stop listening.
370 | */
371 | public void stop() {
372 | this.jmdns.unregisterAllServices();
373 | super.stop();
374 | }
375 |
376 | interface ThingsType {
377 | /**
378 | * Get the thing at the given index.
379 | *
380 | * @param idx Index of thing.
381 | * @return The thing, or null.
382 | */
383 | Thing getThing(int idx);
384 |
385 | /**
386 | * Get the list of things.
387 | *
388 | * @return The list of things.
389 | */
390 | List getThings();
391 |
392 | /**
393 | * Get the mDNS server name.
394 | *
395 | * @return The server name.
396 | */
397 | String getName();
398 | }
399 |
400 | /**
401 | * Thread to perform an action.
402 | */
403 | private static class ActionRunner extends Thread {
404 | private final Action action;
405 |
406 | /**
407 | * Initialize the object.
408 | *
409 | * @param action The action to perform
410 | */
411 | public ActionRunner(Action action) {
412 | this.action = action;
413 | }
414 |
415 | /**
416 | * Perform the action.
417 | */
418 | public void run() {
419 | this.action.start();
420 | }
421 | }
422 |
423 | /**
424 | * Class to hold options required by SSL server.
425 | */
426 | public static class SSLOptions {
427 | private final String path;
428 | private final String password;
429 | private final String[] protocols;
430 |
431 | /**
432 | * Initialize the object.
433 | *
434 | * @param keystorePath Path to the Java keystore (.jks) file
435 | * @param keystorePassword Password to open the keystore
436 | */
437 | public SSLOptions(String keystorePath, String keystorePassword) {
438 | this(keystorePath, keystorePassword, null);
439 | }
440 |
441 | /**
442 | * Initialize the object.
443 | *
444 | * @param keystorePath Path to the Java keystore (.jks) file
445 | * @param keystorePassword Password to open the keystore
446 | * @param protocols List of protocols to enable. Documentation
447 | * found here: https://docs.oracle.com/javase/8/docs/api/javax/net/ssl/SSLServerSocket.html#setEnabledProtocols-java.lang.String:A-
448 | */
449 | public SSLOptions(String keystorePath,
450 | String keystorePassword,
451 | String[] protocols) {
452 | this.path = keystorePath;
453 | this.password = keystorePassword;
454 | this.protocols = protocols;
455 | }
456 |
457 | /**
458 | * Create an SSLServerSocketFactory as required by NanoHTTPD.
459 | *
460 | * @return The socket factory.
461 | * @throws IOException If server fails to bind.
462 | */
463 | public SSLServerSocketFactory getSocketFactory() throws IOException {
464 | return NanoHTTPD.makeSSLSocketFactory(this.path,
465 | this.password.toCharArray());
466 | }
467 |
468 | /**
469 | * Get the list of enabled protocols.
470 | *
471 | * @return The list of protocols.
472 | */
473 | public String[] getProtocols() {
474 | return this.protocols;
475 | }
476 | }
477 |
478 | /**
479 | * Base handler that responds to every request with a 405 Method Not
480 | * Allowed.
481 | */
482 | public static class BaseHandler implements UriResponder {
483 | /**
484 | * Add necessary CORS headers to response.
485 | *
486 | * @param response Response to add headers to
487 | * @return The Response object.
488 | */
489 | public Response corsResponse(Response response) {
490 | response.addHeader("Access-Control-Allow-Origin", "*");
491 | response.addHeader("Access-Control-Allow-Headers",
492 | "Origin, X-Requested-With, Content-Type, Accept");
493 | response.addHeader("Access-Control-Allow-Methods",
494 | "GET, HEAD, PUT, POST, DELETE");
495 | return response;
496 | }
497 |
498 | /**
499 | * Handle a GET request.
500 | *
501 | * @param uriResource The URI resource that was matched
502 | * @param urlParams Map of URL parameters
503 | * @param session The HTTP session
504 | * @return 405 Method Not Allowed response.
505 | */
506 | public Response get(UriResource uriResource,
507 | Map urlParams,
508 | IHTTPSession session) {
509 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.METHOD_NOT_ALLOWED,
510 | null,
511 | null));
512 | }
513 |
514 | /**
515 | * Handle a PUT request.
516 | *
517 | * @param uriResource The URI resource that was matched
518 | * @param urlParams Map of URL parameters
519 | * @param session The HTTP session
520 | * @return 405 Method Not Allowed response.
521 | */
522 | public Response put(UriResource uriResource,
523 | Map urlParams,
524 | IHTTPSession session) {
525 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.METHOD_NOT_ALLOWED,
526 | null,
527 | null));
528 | }
529 |
530 | /**
531 | * Handle a POST request.
532 | *
533 | * @param uriResource The URI resource that was matched
534 | * @param urlParams Map of URL parameters
535 | * @param session The HTTP session
536 | * @return 405 Method Not Allowed response.
537 | */
538 | public Response post(UriResource uriResource,
539 | Map urlParams,
540 | IHTTPSession session) {
541 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.METHOD_NOT_ALLOWED,
542 | null,
543 | null));
544 | }
545 |
546 | /**
547 | * Handle a DELETE request.
548 | *
549 | * @param uriResource The URI resource that was matched
550 | * @param urlParams Map of URL parameters
551 | * @param session The HTTP session
552 | * @return 405 Method Not Allowed response.
553 | */
554 | public Response delete(UriResource uriResource,
555 | Map urlParams,
556 | IHTTPSession session) {
557 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.METHOD_NOT_ALLOWED,
558 | null,
559 | null));
560 | }
561 |
562 | /**
563 | * Handle any other request.
564 | *
565 | * @param method The HTTP method
566 | * @param uriResource The URI resource that was matched
567 | * @param urlParams Map of URL parameters
568 | * @param session The HTTP session
569 | * @return 405 Method Not Allowed response.
570 | */
571 | public Response other(String method,
572 | UriResource uriResource,
573 | Map urlParams,
574 | IHTTPSession session) {
575 | if (method.equals("OPTIONS")) {
576 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NO_CONTENT,
577 | null,
578 | null));
579 | }
580 |
581 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.METHOD_NOT_ALLOWED,
582 | null,
583 | null));
584 | }
585 |
586 | /**
587 | * Get a parameter from the URI.
588 | *
589 | * @param uri The URI
590 | * @param index Index of the parameter
591 | * @return The URI parameter, or null if index was invalid.
592 | */
593 | public String getUriParam(String uri, int index) {
594 | String[] parts = uri.split("/");
595 | if (parts.length <= index) {
596 | return null;
597 | }
598 |
599 | return parts[index];
600 | }
601 |
602 | /**
603 | * Parse a JSON body.
604 | *
605 | * @param session The HTTP session
606 | * @return The parsed JSON body as a JSONObject, or null on error.
607 | */
608 | public JSONObject parseBody(IHTTPSession session) {
609 | int contentLength = Integer.parseInt(session.getHeaders()
610 | .get("content-length"));
611 | byte[] buffer = new byte[contentLength];
612 | try {
613 | session.getInputStream().read(buffer, 0, contentLength);
614 | return new JSONObject(new String(buffer));
615 | } catch (IOException e) {
616 | return null;
617 | }
618 | }
619 |
620 | /**
621 | * Get the thing this request is for.
622 | *
623 | * @param uriResource The URI resource that was matched
624 | * @param session The HTTP session
625 | * @return The thing, or null if not found.
626 | */
627 | public Thing getThing(UriResource uriResource, IHTTPSession session) {
628 | ThingsType things = uriResource.initParameter(0, ThingsType.class);
629 |
630 | String thingId = this.getUriParam(session.getUri(), 1);
631 | int id;
632 | try {
633 | id = Integer.parseInt(thingId);
634 | } catch (NumberFormatException e) {
635 | id = 0;
636 | }
637 |
638 | return things.getThing(id);
639 | }
640 |
641 | /**
642 | * Validate Host header.
643 | *
644 | * @param uriResource The URI resource that was matched
645 | * @param session The HTTP session
646 | * @return Boolean indicating validation success.
647 | */
648 | public boolean validateHost(UriResource uriResource,
649 | IHTTPSession session) {
650 | boolean disableHostValidation =
651 | uriResource.initParameter(3, Boolean.class);
652 |
653 | if (disableHostValidation) {
654 | return true;
655 | }
656 |
657 | List hosts = uriResource.initParameter(1, List.class);
658 |
659 | String host = session.getHeaders().get("host");
660 | return (host != null && hosts.contains(host.toLowerCase()));
661 | }
662 |
663 | /**
664 | * Determine whether or not this request is HTTPS.
665 | *
666 | * @param uriResource The URI resource that was matched
667 | * @return Boolean indicating whether or not the request is secure.
668 | */
669 | public boolean isSecure(UriResource uriResource) {
670 | return uriResource.initParameter(2, Boolean.class);
671 | }
672 | }
673 |
674 | /**
675 | * Handle a request to / when the server manages multiple things.
676 | */
677 | public static class ThingsHandler extends BaseHandler {
678 | /**
679 | * Handle a GET request, including websocket requests.
680 | *
681 | * @param uriResource The URI resource that was matched
682 | * @param urlParams Map of URL parameters
683 | * @param session The HTTP session
684 | * @return The appropriate response.
685 | */
686 | @Override
687 | public Response get(UriResource uriResource,
688 | Map urlParams,
689 | IHTTPSession session) {
690 | if (!validateHost(uriResource, session)) {
691 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN,
692 | null,
693 | null);
694 | }
695 |
696 | String wsHref = String.format("%s://%s",
697 | this.isSecure(uriResource) ?
698 | "wss" :
699 | "ws",
700 | session.getHeaders().get("host"));
701 |
702 | ThingsType things = uriResource.initParameter(0, ThingsType.class);
703 |
704 | JSONArray list = new JSONArray();
705 | for (Thing thing : things.getThings()) {
706 | JSONObject description = thing.asThingDescription();
707 |
708 | JSONObject link = new JSONObject();
709 | link.put("rel", "alternate");
710 | link.put("href",
711 | String.format("%s%s", wsHref, thing.getHref()));
712 | description.getJSONArray("links").put(link);
713 |
714 | String base = String.format("%s://%s%s",
715 | this.isSecure(uriResource) ?
716 | "https" :
717 | "http",
718 | session.getHeaders().get("host"),
719 | thing.getHref());
720 | description.put("href", thing.getHref());
721 | description.put("base", base);
722 | JSONObject securityDefinitions = new JSONObject();
723 | JSONObject nosecSc = new JSONObject();
724 | nosecSc.put("scheme", "nosec");
725 | securityDefinitions.put("nosec_sc", nosecSc);
726 | description.put("securityDefinitions", securityDefinitions);
727 | description.put("security", "nosec_sc");
728 |
729 | list.put(description);
730 | }
731 |
732 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK,
733 | "application/json",
734 | list.toString()));
735 | }
736 | }
737 |
738 | /**
739 | * Handle a request to /.
740 | */
741 | public static class ThingHandler extends BaseHandler {
742 | /**
743 | * Handle a GET request, including websocket requests.
744 | *
745 | * @param uriResource The URI resource that was matched
746 | * @param urlParams Map of URL parameters
747 | * @param session The HTTP session
748 | * @return The appropriate response.
749 | */
750 | @Override
751 | public Response get(UriResource uriResource,
752 | Map urlParams,
753 | IHTTPSession session) {
754 | if (!validateHost(uriResource, session)) {
755 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN,
756 | null,
757 | null);
758 | }
759 |
760 | Thing thing = this.getThing(uriResource, session);
761 | if (thing == null) {
762 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND,
763 | null,
764 | null));
765 | }
766 |
767 | Map headers = session.getHeaders();
768 | if (isWebSocketRequested(session)) {
769 | if (!NanoWSD.HEADER_WEBSOCKET_VERSION_VALUE.equalsIgnoreCase(
770 | headers.get(NanoWSD.HEADER_WEBSOCKET_VERSION))) {
771 | return corsResponse(newFixedLengthResponse(Response.Status.BAD_REQUEST,
772 | NanoHTTPD.MIME_PLAINTEXT,
773 | "Invalid Websocket-Version " +
774 | headers.get(
775 | NanoWSD.HEADER_WEBSOCKET_VERSION)));
776 | }
777 |
778 | if (!headers.containsKey(NanoWSD.HEADER_WEBSOCKET_KEY)) {
779 | return corsResponse(newFixedLengthResponse(Response.Status.BAD_REQUEST,
780 | NanoHTTPD.MIME_PLAINTEXT,
781 | "Missing Websocket-Key"));
782 | }
783 |
784 | final NanoWSD.WebSocket webSocket =
785 | new ThingWebSocket(thing, session);
786 | Response handshakeResponse = webSocket.getHandshakeResponse();
787 | try {
788 | handshakeResponse.addHeader(NanoWSD.HEADER_WEBSOCKET_ACCEPT,
789 | NanoWSD.makeAcceptKey(headers.get(
790 | NanoWSD.HEADER_WEBSOCKET_KEY)));
791 | } catch (NoSuchAlgorithmException e) {
792 | return corsResponse(newFixedLengthResponse(Response.Status.INTERNAL_ERROR,
793 | NanoHTTPD.MIME_PLAINTEXT,
794 | "The SHA-1 Algorithm required for websockets is not available on the server."));
795 | }
796 |
797 | if (headers.containsKey(NanoWSD.HEADER_WEBSOCKET_PROTOCOL)) {
798 | handshakeResponse.addHeader(NanoWSD.HEADER_WEBSOCKET_PROTOCOL,
799 | headers.get(NanoWSD.HEADER_WEBSOCKET_PROTOCOL)
800 | .split(",")[0]);
801 | }
802 |
803 | final Timer timer = new Timer();
804 | timer.scheduleAtFixedRate(new TimerTask() {
805 | @Override
806 | public void run() {
807 | try {
808 | webSocket.ping(new byte[0]);
809 | } catch (IOException e) {
810 | timer.cancel();
811 | }
812 | }
813 | },
814 | WebThingServer.WEBSOCKET_PING_INTERVAL,
815 | WebThingServer.WEBSOCKET_PING_INTERVAL);
816 |
817 | return handshakeResponse;
818 | }
819 |
820 | String wsHref = String.format("%s://%s%s",
821 | this.isSecure(uriResource) ?
822 | "wss" :
823 | "ws",
824 | session.getHeaders().get("host"),
825 | thing.getHref());
826 | JSONObject description = thing.asThingDescription();
827 | JSONObject link = new JSONObject();
828 | link.put("rel", "alternate");
829 | link.put("href", wsHref);
830 | description.getJSONArray("links").put(link);
831 |
832 | String base = String.format("%s://%s%s",
833 | this.isSecure(uriResource) ?
834 | "https" :
835 | "http",
836 | session.getHeaders().get("host"),
837 | thing.getHref());
838 | description.put("base", base);
839 | JSONObject securityDefinitions = new JSONObject();
840 | JSONObject nosecSc = new JSONObject();
841 | nosecSc.put("scheme", "nosec");
842 | securityDefinitions.put("nosec_sc", nosecSc);
843 | description.put("securityDefinitions", securityDefinitions);
844 | description.put("security", "nosec_sc");
845 |
846 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK,
847 | "application/json",
848 | description.toString()));
849 | }
850 |
851 | /**
852 | * Determine whether or not this is a websocket connection.
853 | *
854 | * @param headers The HTTP request headers
855 | * @return Boolean indicating whether or not this is a websocket
856 | * connection.
857 | */
858 | private boolean isWebSocketConnectionHeader(Map headers) {
859 | String connection = headers.get(NanoWSD.HEADER_CONNECTION);
860 | return connection != null && connection.toLowerCase()
861 | .contains(NanoWSD.HEADER_CONNECTION_VALUE
862 | .toLowerCase());
863 | }
864 |
865 | /**
866 | * Determine whether or not a websocket was requested.
867 | *
868 | * @param session The HTTP session
869 | * @return Boolean indicating whether or not this is a websocket
870 | * request.
871 | */
872 | private boolean isWebSocketRequested(IHTTPSession session) {
873 | Map headers = session.getHeaders();
874 | String upgrade = headers.get(NanoWSD.HEADER_UPGRADE);
875 | boolean isCorrectConnection = isWebSocketConnectionHeader(headers);
876 | boolean isUpgrade =
877 | NanoWSD.HEADER_UPGRADE_VALUE.equalsIgnoreCase(upgrade);
878 | return isUpgrade && isCorrectConnection;
879 | }
880 |
881 | /**
882 | * Class to handle WebSockets to a Thing.
883 | */
884 | public static class ThingWebSocket extends NanoWSD.WebSocket {
885 | private final Thing thing;
886 |
887 | /**
888 | * Initialize the object.
889 | *
890 | * @param thing The Thing managed by the server
891 | * @param handshakeRequest The initial handshake request
892 | */
893 | public ThingWebSocket(Thing thing, IHTTPSession handshakeRequest) {
894 | super(handshakeRequest);
895 | this.thing = thing;
896 | }
897 |
898 | /**
899 | * Handle a new connection.
900 | */
901 | @Override
902 | protected void onOpen() {
903 | this.thing.addSubscriber(this);
904 | }
905 |
906 | /**
907 | * Handle a close event on the socket.
908 | *
909 | * @param code The close code
910 | * @param reason The close reason
911 | * @param initiatedByRemote Whether or not the client closed the
912 | * socket
913 | */
914 | @Override
915 | protected void onClose(NanoWSD.WebSocketFrame.CloseCode code,
916 | String reason,
917 | boolean initiatedByRemote) {
918 | this.thing.removeSubscriber(this);
919 | }
920 |
921 | /**
922 | * Handle an incoming message.
923 | *
924 | * @param message The message to handle
925 | */
926 | @Override
927 | protected void onMessage(NanoWSD.WebSocketFrame message) {
928 | message.setUnmasked();
929 | String data = message.getTextPayload();
930 | JSONObject json = new JSONObject(data);
931 |
932 | if (!json.has("messageType") || !json.has("data")) {
933 | JSONObject error = new JSONObject();
934 | JSONObject inner = new JSONObject();
935 |
936 | inner.put("status", "400 Bad Request");
937 | inner.put("message", "Invalid message");
938 | error.put("messageType", "error");
939 | error.put("data", inner);
940 |
941 | this.sendMessage(error.toString());
942 |
943 | return;
944 | }
945 |
946 | String messageType = json.getString("messageType");
947 | JSONObject messageData = json.getJSONObject("data");
948 | switch (messageType) {
949 | case "setProperty":
950 | JSONArray propertyNames = messageData.names();
951 | if (propertyNames == null) {
952 | break;
953 | }
954 |
955 | for (int i = 0; i < propertyNames.length(); ++i) {
956 | String propertyName = propertyNames.getString(i);
957 | try {
958 | this.thing.setProperty(propertyName,
959 | messageData.get(
960 | propertyName));
961 | } catch (PropertyError e) {
962 | JSONObject error = new JSONObject();
963 | JSONObject inner = new JSONObject();
964 |
965 | inner.put("status", "400 Bad Request");
966 | inner.put("message", e.getMessage());
967 | error.put("messageType", "error");
968 | error.put("data", inner);
969 |
970 | this.sendMessage(e.getMessage());
971 | }
972 | }
973 | break;
974 | case "requestAction":
975 | JSONArray actionNames = messageData.names();
976 | if (actionNames == null) {
977 | break;
978 | }
979 |
980 | for (int i = 0; i < actionNames.length(); ++i) {
981 | String actionName = actionNames.getString(i);
982 | JSONObject params =
983 | messageData.getJSONObject(actionName);
984 | JSONObject input = null;
985 | if (params.has("input")) {
986 | input = params.getJSONObject("input");
987 | }
988 |
989 | Action action =
990 | this.thing.performAction(actionName, input);
991 | if (action != null) {
992 | (new ActionRunner(action)).start();
993 | } else {
994 | JSONObject error = new JSONObject();
995 | JSONObject inner = new JSONObject();
996 |
997 | inner.put("status", "400 Bad Request");
998 | inner.put("message", "Invalid action request");
999 | error.put("messageType", "error");
1000 | error.put("data", inner);
1001 |
1002 | this.sendMessage(error.toString());
1003 | }
1004 | }
1005 | break;
1006 | case "addEventSubscription":
1007 | JSONArray eventNames = messageData.names();
1008 | if (eventNames == null) {
1009 | break;
1010 | }
1011 |
1012 | for (int i = 0; i < eventNames.length(); ++i) {
1013 | String eventName = eventNames.getString(i);
1014 | this.thing.addEventSubscriber(eventName, this);
1015 | }
1016 | break;
1017 | default:
1018 | JSONObject error = new JSONObject();
1019 | JSONObject inner = new JSONObject();
1020 |
1021 | inner.put("status", "400 Bad Request");
1022 | inner.put("message",
1023 | "Unknown messageType: " + messageType);
1024 | error.put("messageType", "error");
1025 | error.put("data", inner);
1026 |
1027 | this.sendMessage(error.toString());
1028 | break;
1029 | }
1030 | }
1031 |
1032 | @Override
1033 | protected void onPong(NanoWSD.WebSocketFrame pong) {
1034 | }
1035 |
1036 | @Override
1037 | protected void onException(IOException exception) {
1038 | }
1039 |
1040 | public void sendMessage(String message) {
1041 | try {
1042 | this.send(message);
1043 | } catch (IOException e) {
1044 | // pass
1045 | }
1046 | }
1047 | }
1048 | }
1049 |
1050 | /**
1051 | * Handle a request to /properties.
1052 | */
1053 | public static class PropertiesHandler extends BaseHandler {
1054 | /**
1055 | * Handle a GET request.
1056 | *
1057 | * @param uriResource The URI resource that was matched
1058 | * @param urlParams Map of URL parameters
1059 | * @param session The HTTP session
1060 | * @return The appropriate response.
1061 | */
1062 | @Override
1063 | public Response get(UriResource uriResource,
1064 | Map urlParams,
1065 | IHTTPSession session) {
1066 | if (!validateHost(uriResource, session)) {
1067 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN,
1068 | null,
1069 | null);
1070 | }
1071 |
1072 | Thing thing = this.getThing(uriResource, session);
1073 | if (thing == null) {
1074 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND,
1075 | null,
1076 | null));
1077 | }
1078 |
1079 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK,
1080 | "application/json",
1081 | thing.getProperties()
1082 | .toString()));
1083 | }
1084 | }
1085 |
1086 | /**
1087 | * Handle a request to /properties/<property>.
1088 | */
1089 | public static class PropertyHandler extends BaseHandler {
1090 | /**
1091 | * Get the property name from the URI.
1092 | *
1093 | * @param uriResource The URI resource that was matched
1094 | * @param session The HTTP session
1095 | * @return The property name.
1096 | */
1097 | public String getPropertyName(UriResource uriResource,
1098 | IHTTPSession session) {
1099 | ThingsType things = uriResource.initParameter(0, ThingsType.class);
1100 |
1101 | if (MultipleThings.class.isInstance(things)) {
1102 | return this.getUriParam(session.getUri(), 3);
1103 | } else {
1104 | return this.getUriParam(session.getUri(), 2);
1105 | }
1106 | }
1107 |
1108 | /**
1109 | * Handle a GET request.
1110 | *
1111 | * @param uriResource The URI resource that was matched
1112 | * @param urlParams Map of URL parameters
1113 | * @param session The HTTP session
1114 | * @return The appropriate response.
1115 | */
1116 | @Override
1117 | public Response get(UriResource uriResource,
1118 | Map urlParams,
1119 | IHTTPSession session) {
1120 | if (!validateHost(uriResource, session)) {
1121 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN,
1122 | null,
1123 | null);
1124 | }
1125 |
1126 | Thing thing = this.getThing(uriResource, session);
1127 | if (thing == null) {
1128 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND,
1129 | null,
1130 | null));
1131 | }
1132 |
1133 | String propertyName = this.getPropertyName(uriResource, session);
1134 | if (!thing.hasProperty(propertyName)) {
1135 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND,
1136 | null,
1137 | null));
1138 | }
1139 |
1140 | JSONObject obj = new JSONObject();
1141 | try {
1142 | Object value = thing.getProperty(propertyName);
1143 | if (value == null) {
1144 | obj.put(propertyName, JSONObject.NULL);
1145 | } else {
1146 | obj.putOpt(propertyName, value);
1147 | }
1148 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK,
1149 | "application/json",
1150 | obj.toString()));
1151 | } catch (JSONException e) {
1152 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.INTERNAL_ERROR,
1153 | null,
1154 | null));
1155 | }
1156 | }
1157 |
1158 | /**
1159 | * Handle a PUT request.
1160 | *
1161 | * @param uriResource The URI resource that was matched
1162 | * @param urlParams Map of URL parameters
1163 | * @param session The HTTP session
1164 | * @return The appropriate response.
1165 | */
1166 | @Override
1167 | public Response put(UriResource uriResource,
1168 | Map urlParams,
1169 | IHTTPSession session) {
1170 | if (!validateHost(uriResource, session)) {
1171 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN,
1172 | null,
1173 | null);
1174 | }
1175 |
1176 | Thing thing = this.getThing(uriResource, session);
1177 | if (thing == null) {
1178 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND,
1179 | null,
1180 | null));
1181 | }
1182 |
1183 | String propertyName = this.getPropertyName(uriResource, session);
1184 | if (!thing.hasProperty(propertyName)) {
1185 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND,
1186 | null,
1187 | null));
1188 | }
1189 |
1190 | JSONObject json = this.parseBody(session);
1191 | if (json == null) {
1192 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.BAD_REQUEST,
1193 | null,
1194 | null));
1195 | }
1196 |
1197 | if (!json.has(propertyName)) {
1198 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.BAD_REQUEST,
1199 | null,
1200 | null));
1201 | }
1202 |
1203 | try {
1204 | thing.setProperty(propertyName, json.get(propertyName));
1205 |
1206 | JSONObject obj = new JSONObject();
1207 | obj.putOpt(propertyName, thing.getProperty(propertyName));
1208 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK,
1209 | "application/json",
1210 | obj.toString()));
1211 | } catch (JSONException e) {
1212 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.INTERNAL_ERROR,
1213 | null,
1214 | null));
1215 | } catch (PropertyError e) {
1216 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.BAD_REQUEST,
1217 | null,
1218 | null));
1219 | }
1220 | }
1221 | }
1222 |
1223 | /**
1224 | * Handle a request to /actions.
1225 | */
1226 | public static class ActionsHandler extends BaseHandler {
1227 | /**
1228 | * Handle a GET request.
1229 | *
1230 | * @param uriResource The URI resource that was matched
1231 | * @param urlParams Map of URL parameters
1232 | * @param session The HTTP session
1233 | * @return The appropriate response.
1234 | */
1235 | @Override
1236 | public Response get(UriResource uriResource,
1237 | Map urlParams,
1238 | IHTTPSession session) {
1239 | if (!validateHost(uriResource, session)) {
1240 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN,
1241 | null,
1242 | null);
1243 | }
1244 |
1245 | Thing thing = this.getThing(uriResource, session);
1246 | if (thing == null) {
1247 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND,
1248 | null,
1249 | null));
1250 | }
1251 |
1252 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK,
1253 | "application/json",
1254 | thing.getActionDescriptions(
1255 | null)
1256 | .toString()));
1257 | }
1258 |
1259 | /**
1260 | * Handle a POST request.
1261 | *
1262 | * @param uriResource The URI resource that was matched
1263 | * @param urlParams Map of URL parameters
1264 | * @param session The HTTP session
1265 | * @return The appropriate response.
1266 | */
1267 | @Override
1268 | public Response post(UriResource uriResource,
1269 | Map urlParams,
1270 | IHTTPSession session) {
1271 | if (!validateHost(uriResource, session)) {
1272 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN,
1273 | null,
1274 | null);
1275 | }
1276 |
1277 | Thing thing = this.getThing(uriResource, session);
1278 | if (thing == null) {
1279 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND,
1280 | null,
1281 | null));
1282 | }
1283 |
1284 | JSONObject json = this.parseBody(session);
1285 | if (json == null) {
1286 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.BAD_REQUEST,
1287 | null,
1288 | null));
1289 | }
1290 |
1291 | try {
1292 | JSONArray actionNames = json.names();
1293 | if (actionNames == null || actionNames.length() != 1) {
1294 | return corsResponse(NanoHTTPD.newFixedLengthResponse(
1295 | Response.Status.BAD_REQUEST,
1296 | null,
1297 | null));
1298 | }
1299 |
1300 | String actionName = actionNames.getString(0);
1301 | JSONObject params = json.getJSONObject(actionName);
1302 | JSONObject input = null;
1303 | if (params.has("input")) {
1304 | input = params.getJSONObject("input");
1305 | }
1306 |
1307 | Action action = thing.performAction(actionName, input);
1308 | if (action != null) {
1309 | JSONObject response = new JSONObject();
1310 | response.put(actionName,
1311 | action.asActionDescription()
1312 | .getJSONObject(actionName));
1313 |
1314 | (new ActionRunner(action)).start();
1315 |
1316 | return corsResponse(NanoHTTPD.newFixedLengthResponse(
1317 | Response.Status.CREATED,
1318 | "application/json",
1319 | response.toString()));
1320 | } else {
1321 | return corsResponse(NanoHTTPD.newFixedLengthResponse(
1322 | Response.Status.BAD_REQUEST,
1323 | null,
1324 | null));
1325 | }
1326 | } catch (JSONException e) {
1327 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.INTERNAL_ERROR,
1328 | null,
1329 | null));
1330 | }
1331 | }
1332 | }
1333 |
1334 | /**
1335 | * Handle a request to /actions/<action_name>.
1336 | */
1337 | public static class ActionHandler extends BaseHandler {
1338 | /**
1339 | * Get the action name from the URI.
1340 | *
1341 | * @param uriResource The URI resource that was matched
1342 | * @param session The HTTP session
1343 | * @return The property name.
1344 | */
1345 | public String getActionName(UriResource uriResource,
1346 | IHTTPSession session) {
1347 | ThingsType things = uriResource.initParameter(0, ThingsType.class);
1348 |
1349 | if (MultipleThings.class.isInstance(things)) {
1350 | return this.getUriParam(session.getUri(), 3);
1351 | } else {
1352 | return this.getUriParam(session.getUri(), 2);
1353 | }
1354 | }
1355 |
1356 | /**
1357 | * Handle a GET request.
1358 | *
1359 | * @param uriResource The URI resource that was matched
1360 | * @param urlParams Map of URL parameters
1361 | * @param session The HTTP session
1362 | * @return The appropriate response.
1363 | */
1364 | @Override
1365 | public Response get(UriResource uriResource,
1366 | Map urlParams,
1367 | IHTTPSession session) {
1368 | if (!validateHost(uriResource, session)) {
1369 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN,
1370 | null,
1371 | null);
1372 | }
1373 |
1374 | Thing thing = this.getThing(uriResource, session);
1375 | if (thing == null) {
1376 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND,
1377 | null,
1378 | null));
1379 | }
1380 |
1381 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK,
1382 | "application/json",
1383 | thing.getActionDescriptions(
1384 | this.getActionName(
1385 | uriResource,
1386 | session))
1387 | .toString()));
1388 | }
1389 |
1390 | /**
1391 | * Handle a POST request.
1392 | *
1393 | * @param uriResource The URI resource that was matched
1394 | * @param urlParams Map of URL parameters
1395 | * @param session The HTTP session
1396 | * @return The appropriate response.
1397 | */
1398 | @Override
1399 | public Response post(UriResource uriResource,
1400 | Map urlParams,
1401 | IHTTPSession session) {
1402 | if (!validateHost(uriResource, session)) {
1403 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN,
1404 | null,
1405 | null);
1406 | }
1407 |
1408 | Thing thing = this.getThing(uriResource, session);
1409 | if (thing == null) {
1410 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND,
1411 | null,
1412 | null));
1413 | }
1414 |
1415 | JSONObject json = this.parseBody(session);
1416 | if (json == null) {
1417 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.BAD_REQUEST,
1418 | null,
1419 | null));
1420 | }
1421 |
1422 | String actionName = this.getActionName(uriResource, session);
1423 |
1424 | try {
1425 | JSONArray actionNames = json.names();
1426 | if (actionNames == null || actionNames.length() != 1) {
1427 | return corsResponse(NanoHTTPD.newFixedLengthResponse(
1428 | Response.Status.BAD_REQUEST,
1429 | null,
1430 | null));
1431 | }
1432 |
1433 | String name = actionNames.getString(0);
1434 | if (!name.equals(actionName)) {
1435 | return corsResponse(NanoHTTPD.newFixedLengthResponse(
1436 | Response.Status.BAD_REQUEST,
1437 | null,
1438 | null));
1439 | }
1440 |
1441 | JSONObject params = json.getJSONObject(name);
1442 | JSONObject input = null;
1443 | if (params.has("input")) {
1444 | input = params.getJSONObject("input");
1445 | }
1446 |
1447 | Action action = thing.performAction(name, input);
1448 | if (action != null) {
1449 | JSONObject response = new JSONObject();
1450 | response.put(name,
1451 | action.asActionDescription()
1452 | .getJSONObject(name));
1453 |
1454 | (new ActionRunner(action)).start();
1455 |
1456 | return corsResponse(NanoHTTPD.newFixedLengthResponse(
1457 | Response.Status.CREATED,
1458 | "application/json",
1459 | response.toString()));
1460 | } else {
1461 | return corsResponse(NanoHTTPD.newFixedLengthResponse(
1462 | Response.Status.BAD_REQUEST,
1463 | null,
1464 | null));
1465 | }
1466 | } catch (JSONException e) {
1467 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.INTERNAL_ERROR,
1468 | null,
1469 | null));
1470 | }
1471 | }
1472 | }
1473 |
1474 | /**
1475 | * Handle a request to /actions/<action_name>/<action_id>.
1476 | */
1477 | public static class ActionIDHandler extends BaseHandler {
1478 | /**
1479 | * Get the action name from the URI.
1480 | *
1481 | * @param uriResource The URI resource that was matched
1482 | * @param session The HTTP session
1483 | * @return The property name.
1484 | */
1485 | public String getActionName(UriResource uriResource,
1486 | IHTTPSession session) {
1487 | ThingsType things = uriResource.initParameter(0, ThingsType.class);
1488 |
1489 | if (MultipleThings.class.isInstance(things)) {
1490 | return this.getUriParam(session.getUri(), 3);
1491 | } else {
1492 | return this.getUriParam(session.getUri(), 2);
1493 | }
1494 | }
1495 |
1496 | /**
1497 | * Get the action ID from the URI.
1498 | *
1499 | * @param uriResource The URI resource that was matched
1500 | * @param session The HTTP session
1501 | * @return The property name.
1502 | */
1503 | public String getActionId(UriResource uriResource,
1504 | IHTTPSession session) {
1505 | ThingsType things = uriResource.initParameter(0, ThingsType.class);
1506 |
1507 | if (MultipleThings.class.isInstance(things)) {
1508 | return this.getUriParam(session.getUri(), 4);
1509 | } else {
1510 | return this.getUriParam(session.getUri(), 3);
1511 | }
1512 | }
1513 |
1514 | /**
1515 | * Handle a GET request.
1516 | *
1517 | * @param uriResource The URI resource that was matched
1518 | * @param urlParams Map of URL parameters
1519 | * @param session The HTTP session
1520 | * @return The appropriate response.
1521 | */
1522 | @Override
1523 | public Response get(UriResource uriResource,
1524 | Map urlParams,
1525 | IHTTPSession session) {
1526 | if (!validateHost(uriResource, session)) {
1527 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN,
1528 | null,
1529 | null);
1530 | }
1531 |
1532 | Thing thing = this.getThing(uriResource, session);
1533 | if (thing == null) {
1534 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND,
1535 | null,
1536 | null));
1537 | }
1538 |
1539 | String actionName = this.getActionName(uriResource, session);
1540 | String actionId = this.getActionId(uriResource, session);
1541 |
1542 | Action action = thing.getAction(actionName, actionId);
1543 | if (action == null) {
1544 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND,
1545 | null,
1546 | null));
1547 | }
1548 |
1549 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK,
1550 | "application/json",
1551 | action.asActionDescription()
1552 | .toString()));
1553 | }
1554 |
1555 | /**
1556 | * Handle a PUT request.
1557 | *
1558 | * @param uriResource The URI resource that was matched
1559 | * @param urlParams Map of URL parameters
1560 | * @param session The HTTP session
1561 | * @return The appropriate response.
1562 | */
1563 | @Override
1564 | public Response put(UriResource uriResource,
1565 | Map urlParams,
1566 | IHTTPSession session) {
1567 | if (!validateHost(uriResource, session)) {
1568 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN,
1569 | null,
1570 | null);
1571 | }
1572 |
1573 | Thing thing = this.getThing(uriResource, session);
1574 | if (thing == null) {
1575 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND,
1576 | null,
1577 | null));
1578 | }
1579 |
1580 | // TODO: this is not yet defined in the spec
1581 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK,
1582 | "application/json",
1583 | ""));
1584 | }
1585 |
1586 | /**
1587 | * Handle a DELETE request.
1588 | *
1589 | * @param uriResource The URI resource that was matched
1590 | * @param urlParams Map of URL parameters
1591 | * @param session The HTTP session
1592 | * @return The appropriate response.
1593 | */
1594 | @Override
1595 | public Response delete(UriResource uriResource,
1596 | Map urlParams,
1597 | IHTTPSession session) {
1598 | if (!validateHost(uriResource, session)) {
1599 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN,
1600 | null,
1601 | null);
1602 | }
1603 |
1604 | Thing thing = this.getThing(uriResource, session);
1605 | if (thing == null) {
1606 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND,
1607 | null,
1608 | null));
1609 | }
1610 |
1611 | String actionName = this.getActionName(uriResource, session);
1612 | String actionId = this.getActionId(uriResource, session);
1613 |
1614 | if (thing.removeAction(actionName, actionId)) {
1615 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NO_CONTENT,
1616 | null,
1617 | null));
1618 | } else {
1619 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND,
1620 | null,
1621 | null));
1622 | }
1623 | }
1624 | }
1625 |
1626 | /**
1627 | * Handle a request to /events.
1628 | */
1629 | public static class EventsHandler extends BaseHandler {
1630 | /**
1631 | * Handle a GET request.
1632 | *
1633 | * @param uriResource The URI resource that was matched
1634 | * @param urlParams Map of URL parameters
1635 | * @param session The HTTP session
1636 | * @return The appropriate response.
1637 | */
1638 | @Override
1639 | public Response get(UriResource uriResource,
1640 | Map urlParams,
1641 | IHTTPSession session) {
1642 | if (!validateHost(uriResource, session)) {
1643 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN,
1644 | null,
1645 | null);
1646 | }
1647 |
1648 | Thing thing = this.getThing(uriResource, session);
1649 | if (thing == null) {
1650 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND,
1651 | null,
1652 | null));
1653 | }
1654 |
1655 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK,
1656 | "application/json",
1657 | thing.getEventDescriptions(
1658 | null)
1659 | .toString()));
1660 | }
1661 | }
1662 |
1663 | /**
1664 | * Handle a request to /events/<event_name>.
1665 | */
1666 | public static class EventHandler extends BaseHandler {
1667 | /**
1668 | * Get the event name from the URI.
1669 | *
1670 | * @param uriResource The URI resource that was matched
1671 | * @param session The HTTP session
1672 | * @return The property name.
1673 | */
1674 | public String getEventName(UriResource uriResource,
1675 | IHTTPSession session) {
1676 | ThingsType things = uriResource.initParameter(0, ThingsType.class);
1677 |
1678 | if (MultipleThings.class.isInstance(things)) {
1679 | return this.getUriParam(session.getUri(), 3);
1680 | } else {
1681 | return this.getUriParam(session.getUri(), 2);
1682 | }
1683 | }
1684 |
1685 | /**
1686 | * Handle a GET request.
1687 | *
1688 | * @param uriResource The URI resource that was matched
1689 | * @param urlParams Map of URL parameters
1690 | * @param session The HTTP session
1691 | * @return The appropriate response.
1692 | */
1693 | @Override
1694 | public Response get(UriResource uriResource,
1695 | Map urlParams,
1696 | IHTTPSession session) {
1697 | if (!validateHost(uriResource, session)) {
1698 | return NanoHTTPD.newFixedLengthResponse(Response.Status.FORBIDDEN,
1699 | null,
1700 | null);
1701 | }
1702 |
1703 | Thing thing = this.getThing(uriResource, session);
1704 | if (thing == null) {
1705 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.NOT_FOUND,
1706 | null,
1707 | null));
1708 | }
1709 |
1710 | return corsResponse(NanoHTTPD.newFixedLengthResponse(Response.Status.OK,
1711 | "application/json",
1712 | thing.getEventDescriptions(
1713 | this.getEventName(
1714 | uriResource,
1715 | session))
1716 | .toString()));
1717 | }
1718 | }
1719 |
1720 | /**
1721 | * A container for a single thing.
1722 | */
1723 | public static class SingleThing implements ThingsType {
1724 | private final Thing thing;
1725 |
1726 | /**
1727 | * Initialize the container.
1728 | *
1729 | * @param thing The thing to store
1730 | */
1731 | public SingleThing(Thing thing) {
1732 | this.thing = thing;
1733 | }
1734 |
1735 | /**
1736 | * Get the thing at the given index.
1737 | *
1738 | * @param idx The index.
1739 | */
1740 | public Thing getThing(int idx) {
1741 | return this.thing;
1742 | }
1743 |
1744 | /**
1745 | * Get the list of things.
1746 | *
1747 | * @return The list of things.
1748 | */
1749 | public List getThings() {
1750 | List things = new ArrayList<>();
1751 | things.add(this.thing);
1752 | return things;
1753 | }
1754 |
1755 | /**
1756 | * Get the mDNS server name.
1757 | *
1758 | * @return The server name.
1759 | */
1760 | public String getName() {
1761 | return this.thing.getTitle();
1762 | }
1763 | }
1764 |
1765 | /**
1766 | * A container for multiple things.
1767 | */
1768 | public static class MultipleThings implements ThingsType {
1769 | private final List things;
1770 | private final String name;
1771 |
1772 | /**
1773 | * Initialize the container.
1774 | *
1775 | * @param things The things to store
1776 | * @param name The mDNS server name
1777 | */
1778 | public MultipleThings(List things, String name) {
1779 | this.things = things;
1780 | this.name = name;
1781 | }
1782 |
1783 | /**
1784 | * Get the thing at the given index.
1785 | *
1786 | * @param idx The index.
1787 | */
1788 | public Thing getThing(int idx) {
1789 | if (idx < 0 || idx >= this.things.size()) {
1790 | return null;
1791 | }
1792 |
1793 | return this.things.get(idx);
1794 | }
1795 |
1796 | /**
1797 | * Get the list of things.
1798 | *
1799 | * @return The list of things.
1800 | */
1801 | public List getThings() {
1802 | return this.things;
1803 | }
1804 |
1805 | /**
1806 | * Get the mDNS server name.
1807 | *
1808 | * @return The server name.
1809 | */
1810 | public String getName() {
1811 | return this.name;
1812 | }
1813 | }
1814 |
1815 | /**
1816 | * Mini-class used to define additional API routes.
1817 | */
1818 | public static class Route {
1819 | public String url;
1820 | public Class> handlerClass;
1821 | public Object[] parameters;
1822 |
1823 | /**
1824 | * Initialize the new route.
1825 | *
1826 | * See: https://github.com/NanoHttpd/nanohttpd/blob/master/nanolets/src/main/java/org/nanohttpd/router/RouterNanoHTTPD.java
1827 | *
1828 | * @param url URL to match.
1829 | * @param handlerClass Class which will handle the request.
1830 | * @param parameters Initialization parameters for class instance.
1831 | */
1832 | public Route(String url, Class> handlerClass, Object... parameters) {
1833 | this.url = url;
1834 | this.handlerClass = handlerClass;
1835 | this.parameters = parameters;
1836 | }
1837 | }
1838 | }
1839 |
--------------------------------------------------------------------------------
/src/main/java/io/webthings/webthing/errors/PropertyError.java:
--------------------------------------------------------------------------------
1 | package io.webthings.webthing.errors;
2 |
3 | public class PropertyError extends Exception {
4 | public PropertyError() {
5 | super("General property error");
6 | }
7 |
8 | public PropertyError(String message) {
9 | super(message);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/io/webthings/webthing/example/MultipleThings.java:
--------------------------------------------------------------------------------
1 | package io.webthings.webthing.example;
2 |
3 | import org.json.JSONArray;
4 | import org.json.JSONObject;
5 |
6 | import java.io.IOException;
7 | import java.util.ArrayList;
8 | import java.util.Arrays;
9 | import java.util.List;
10 | import java.util.UUID;
11 |
12 | import io.webthings.webthing.Action;
13 | import io.webthings.webthing.Event;
14 | import io.webthings.webthing.Property;
15 | import io.webthings.webthing.Thing;
16 | import io.webthings.webthing.Value;
17 | import io.webthings.webthing.WebThingServer;
18 | import io.webthings.webthing.errors.PropertyError;
19 |
20 | public class MultipleThings {
21 | public static void main(String[] args) {
22 | // Create a thing that represents a dimmable light
23 | Thing light = new ExampleDimmableLight();
24 |
25 | // Create a thing that represents a humidity sensor
26 | Thing sensor = new FakeGpioHumiditySensor();
27 |
28 | try {
29 | List things = new ArrayList<>();
30 | things.add(light);
31 | things.add(sensor);
32 |
33 | // If adding more than one thing, use MultipleThings() with a name.
34 | // In the single thing case, the thing's name will be broadcast.
35 | WebThingServer server =
36 | new WebThingServer(new WebThingServer.MultipleThings(things,
37 | "LightAndTempDevice"),
38 | 8888);
39 |
40 | Runtime.getRuntime().addShutdownHook(new Thread(server::stop));
41 |
42 | server.start(false);
43 | } catch (IOException e) {
44 | System.out.println(e.toString());
45 | System.exit(1);
46 | }
47 | }
48 |
49 | /**
50 | * A dimmable light that logs received commands to std::out.
51 | */
52 | public static class ExampleDimmableLight extends Thing {
53 | public ExampleDimmableLight() {
54 | super("urn:dev:ops:my-lamp-1234",
55 | "My Lamp",
56 | new JSONArray(Arrays.asList("OnOffSwitch", "Light")),
57 | "A web connected lamp");
58 |
59 | JSONObject onDescription = new JSONObject();
60 | onDescription.put("@type", "OnOffProperty");
61 | onDescription.put("title", "On/Off");
62 | onDescription.put("type", "boolean");
63 | onDescription.put("description", "Whether the lamp is turned on");
64 |
65 | Value on = new Value<>(true,
66 | // Here, you could send a signal to
67 | // the GPIO that switches the lamp
68 | // off
69 | v -> System.out.printf(
70 | "On-State is now %s\n",
71 | v));
72 |
73 | this.addProperty(new Property(this, "on", on, onDescription));
74 |
75 | JSONObject brightnessDescription = new JSONObject();
76 | brightnessDescription.put("@type", "BrightnessProperty");
77 | brightnessDescription.put("title", "Brightness");
78 | brightnessDescription.put("type", "integer");
79 | brightnessDescription.put("description",
80 | "The level of light from 0-100");
81 | brightnessDescription.put("minimum", 0);
82 | brightnessDescription.put("maximum", 100);
83 | brightnessDescription.put("unit", "percent");
84 |
85 | Value brightness = new Value<>(50,
86 | // Here, you could send a signal
87 | // to the GPIO that controls the
88 | // brightness
89 | l -> System.out.printf(
90 | "Brightness is now %s\n",
91 | l));
92 |
93 | this.addProperty(new Property(this,
94 | "brightness",
95 | brightness,
96 | brightnessDescription));
97 |
98 | JSONObject fadeMetadata = new JSONObject();
99 | JSONObject fadeInput = new JSONObject();
100 | JSONObject fadeProperties = new JSONObject();
101 | JSONObject fadeBrightness = new JSONObject();
102 | JSONObject fadeDuration = new JSONObject();
103 | fadeMetadata.put("title", "Fade");
104 | fadeMetadata.put("description", "Fade the lamp to a given level");
105 | fadeInput.put("type", "object");
106 | fadeInput.put("required",
107 | new JSONArray(Arrays.asList("brightness",
108 | "duration")));
109 | fadeBrightness.put("type", "integer");
110 | fadeBrightness.put("minimum", 0);
111 | fadeBrightness.put("maximum", 100);
112 | fadeBrightness.put("unit", "percent");
113 | fadeDuration.put("type", "integer");
114 | fadeDuration.put("minimum", 1);
115 | fadeDuration.put("unit", "milliseconds");
116 | fadeProperties.put("brightness", fadeBrightness);
117 | fadeProperties.put("duration", fadeDuration);
118 | fadeInput.put("properties", fadeProperties);
119 | fadeMetadata.put("input", fadeInput);
120 | this.addAvailableAction("fade", fadeMetadata, FadeAction.class);
121 |
122 | JSONObject overheatedMetadata = new JSONObject();
123 | overheatedMetadata.put("description",
124 | "The lamp has exceeded its safe operating temperature");
125 | overheatedMetadata.put("type", "number");
126 | overheatedMetadata.put("unit", "degree celsius");
127 | this.addAvailableEvent("overheated", overheatedMetadata);
128 | }
129 |
130 | public static class OverheatedEvent extends Event {
131 | public OverheatedEvent(Thing thing, int data) {
132 | super(thing, "overheated", data);
133 | }
134 | }
135 |
136 | public static class FadeAction extends Action {
137 | public FadeAction(Thing thing, JSONObject input) {
138 | super(UUID.randomUUID().toString(), thing, "fade", input);
139 | }
140 |
141 | @Override
142 | public void performAction() {
143 | Thing thing = this.getThing();
144 | JSONObject input = this.getInput();
145 | try {
146 | Thread.sleep(input.getInt("duration"));
147 | } catch (InterruptedException e) {
148 | // pass
149 | }
150 |
151 | try {
152 | thing.setProperty("brightness", input.getInt("brightness"));
153 | thing.addEvent(new OverheatedEvent(thing, 102));
154 | } catch (PropertyError e) {
155 | // pass
156 | }
157 | }
158 | }
159 | }
160 |
161 | /**
162 | * A humidity sensor which updates its measurement every few seconds.
163 | */
164 | public static class FakeGpioHumiditySensor extends Thing {
165 | private final Value level;
166 |
167 | public FakeGpioHumiditySensor() {
168 | super("urn:dev:ops:my-humidity-sensor-1234",
169 | "My Humidity Sensor",
170 | new JSONArray(Arrays.asList("MultiLevelSensor")),
171 | "A web connected humidity sensor");
172 |
173 | JSONObject levelDescription = new JSONObject();
174 | levelDescription.put("@type", "LevelProperty");
175 | levelDescription.put("title", "Humidity");
176 | levelDescription.put("type", "number");
177 | levelDescription.put("description", "The current humidity in %");
178 | levelDescription.put("minimum", 0);
179 | levelDescription.put("maximum", 100);
180 | levelDescription.put("unit", "percent");
181 | levelDescription.put("readOnly", true);
182 | this.level = new Value<>(0.0);
183 | this.addProperty(new Property(this,
184 | "level",
185 | level,
186 | levelDescription));
187 |
188 | // Start a thread that polls the sensor reading every 3 seconds
189 | new Thread(() -> {
190 | while (true) {
191 | try {
192 | Thread.sleep(3000);
193 | // Update the underlying value, which in turn notifies
194 | // all listeners
195 | double newLevel = this.readFromGPIO();
196 | System.out.printf("setting new humidity level: %f\n",
197 | newLevel);
198 | this.level.notifyOfExternalUpdate(newLevel);
199 | } catch (InterruptedException e) {
200 | throw new IllegalStateException(e);
201 | }
202 | }
203 | }).start();
204 | }
205 |
206 | /**
207 | * Mimic an actual sensor updating its reading every couple seconds.
208 | */
209 | private double readFromGPIO() {
210 | return Math.abs(70.0d * Math.random() * (-0.5 + Math.random()));
211 | }
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/src/main/java/io/webthings/webthing/example/SingleThing.java:
--------------------------------------------------------------------------------
1 | package io.webthings.webthing.example;
2 |
3 | import org.json.JSONArray;
4 | import org.json.JSONObject;
5 |
6 | import java.io.IOException;
7 | import java.util.Arrays;
8 | import java.util.UUID;
9 |
10 | import io.webthings.webthing.Action;
11 | import io.webthings.webthing.Event;
12 | import io.webthings.webthing.Property;
13 | import io.webthings.webthing.Thing;
14 | import io.webthings.webthing.Value;
15 | import io.webthings.webthing.WebThingServer;
16 | import io.webthings.webthing.errors.PropertyError;
17 |
18 | public class SingleThing {
19 | public static Thing makeThing() {
20 | Thing thing = new Thing("urn:dev:ops:my-lamp-1234",
21 | "My Lamp",
22 | new JSONArray(Arrays.asList("OnOffSwitch",
23 | "Light")),
24 | "A web connected lamp");
25 |
26 | JSONObject onDescription = new JSONObject();
27 | onDescription.put("@type", "OnOffProperty");
28 | onDescription.put("title", "On/Off");
29 | onDescription.put("type", "boolean");
30 | onDescription.put("description", "Whether the lamp is turned on");
31 | thing.addProperty(new Property(thing,
32 | "on",
33 | new Value(true),
34 | onDescription));
35 |
36 | JSONObject brightnessDescription = new JSONObject();
37 | brightnessDescription.put("@type", "BrightnessProperty");
38 | brightnessDescription.put("title", "Brightness");
39 | brightnessDescription.put("type", "integer");
40 | brightnessDescription.put("description",
41 | "The level of light from 0-100");
42 | brightnessDescription.put("minimum", 0);
43 | brightnessDescription.put("maximum", 100);
44 | brightnessDescription.put("unit", "percent");
45 | thing.addProperty(new Property(thing,
46 | "brightness",
47 | new Value(50),
48 | brightnessDescription));
49 |
50 | JSONObject fadeMetadata = new JSONObject();
51 | JSONObject fadeInput = new JSONObject();
52 | JSONObject fadeProperties = new JSONObject();
53 | JSONObject fadeBrightness = new JSONObject();
54 | JSONObject fadeDuration = new JSONObject();
55 | fadeMetadata.put("title", "Fade");
56 | fadeMetadata.put("description", "Fade the lamp to a given level");
57 | fadeInput.put("type", "object");
58 | fadeInput.put("required",
59 | new JSONArray(Arrays.asList("brightness", "duration")));
60 | fadeBrightness.put("type", "integer");
61 | fadeBrightness.put("minimum", 0);
62 | fadeBrightness.put("maximum", 100);
63 | fadeBrightness.put("unit", "percent");
64 | fadeDuration.put("type", "integer");
65 | fadeDuration.put("minimum", 1);
66 | fadeDuration.put("unit", "milliseconds");
67 | fadeProperties.put("brightness", fadeBrightness);
68 | fadeProperties.put("duration", fadeDuration);
69 | fadeInput.put("properties", fadeProperties);
70 | fadeMetadata.put("input", fadeInput);
71 | thing.addAvailableAction("fade", fadeMetadata, FadeAction.class);
72 |
73 | JSONObject overheatedMetadata = new JSONObject();
74 | overheatedMetadata.put("description",
75 | "The lamp has exceeded its safe operating temperature");
76 | overheatedMetadata.put("type", "number");
77 | overheatedMetadata.put("unit", "degree celsius");
78 | thing.addAvailableEvent("overheated", overheatedMetadata);
79 |
80 | return thing;
81 | }
82 |
83 | public static void main(String[] args) {
84 | Thing thing = makeThing();
85 | WebThingServer server;
86 |
87 | try {
88 | // If adding more than one thing, use MultipleThings() with a name.
89 | // In the single thing case, the thing's name will be broadcast.
90 | server = new WebThingServer(new WebThingServer.SingleThing(thing),
91 | 8888);
92 |
93 | Runtime.getRuntime().addShutdownHook(new Thread(server::stop));
94 |
95 | server.start(false);
96 | } catch (IOException e) {
97 | System.out.println(e.toString());
98 | System.exit(1);
99 | }
100 | }
101 |
102 | public static class OverheatedEvent extends Event {
103 | public OverheatedEvent(Thing thing, int data) {
104 | super(thing, "overheated", data);
105 | }
106 | }
107 |
108 | public static class FadeAction extends Action {
109 | public FadeAction(Thing thing, JSONObject input) {
110 | super(UUID.randomUUID().toString(), thing, "fade", input);
111 | }
112 |
113 | @Override
114 | public void performAction() {
115 | Thing thing = this.getThing();
116 | JSONObject input = this.getInput();
117 | try {
118 | Thread.sleep(input.getInt("duration"));
119 | } catch (InterruptedException e) {
120 | // pass
121 | }
122 |
123 | try {
124 | thing.setProperty("brightness", input.getInt("brightness"));
125 | thing.addEvent(new OverheatedEvent(thing, 102));
126 | } catch (PropertyError e) {
127 | // pass
128 | }
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/test/java/io/webthings/webthing/ThingTest.java:
--------------------------------------------------------------------------------
1 | package io.webthings.webthing;
2 |
3 | import static org.junit.Assert.assertEquals;
4 | import static org.junit.Assert.assertFalse;
5 | import static org.junit.Assert.assertTrue;
6 |
7 | import java.util.Arrays;
8 | import java.util.Collections;
9 | import java.util.HashMap;
10 | import java.util.Map;
11 |
12 | import org.json.JSONArray;
13 | import org.json.JSONObject;
14 | import org.junit.Test;
15 |
16 | import io.webthings.webthing.errors.PropertyError;
17 |
18 | public class ThingTest {
19 |
20 | Object simulateHttpPutProperty(String key, String jsonBody) {
21 | JSONObject json = new JSONObject(jsonBody);
22 | return json.get(key);
23 | }
24 |
25 | @Test
26 | public void itSupportsIntegerValues() throws PropertyError
27 | {
28 | // given
29 | Thing thing = new Thing("urn:dev:test-123", "My TestThing");
30 |
31 | Value value = new Value<>(42, v -> System.out.println("value: " + v));
32 | thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "integer")));
33 |
34 | // when updating property, then
35 | assertEquals(42, value.get().intValue());
36 |
37 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Integer.MIN_VALUE+"}"));
38 | assertEquals(Integer.MIN_VALUE, value.get().intValue());
39 |
40 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Integer.MAX_VALUE+"}"));
41 | assertEquals(Integer.MAX_VALUE, value.get().intValue());
42 |
43 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4.2}"));
44 | assertEquals(4, value.get().intValue());
45 |
46 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4}"));
47 | assertEquals(4, value.get().intValue());
48 |
49 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":0}"));
50 | assertEquals(0, value.get().intValue());
51 |
52 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":-123}"));
53 | assertEquals(-123, value.get().intValue());
54 | }
55 |
56 | @Test
57 | public void itSupportsLongValues() throws PropertyError
58 | {
59 | // given
60 | Thing thing = new Thing("urn:dev:test-123", "My TestThing");
61 |
62 | Value value = new Value<>(42l, v -> System.out.println("value: " + v));
63 | thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "integer")));
64 |
65 | // when updating property, then
66 | assertEquals(42, value.get().longValue());
67 |
68 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Long.MIN_VALUE+"}"));
69 | assertEquals(Long.MIN_VALUE, value.get().longValue());
70 |
71 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Long.MAX_VALUE+"}"));
72 | assertEquals(Long.MAX_VALUE, value.get().longValue());
73 |
74 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4.2}"));
75 | assertEquals(4, value.get().longValue());
76 |
77 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4}"));
78 | assertEquals(4, value.get().longValue());
79 |
80 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":0}"));
81 | assertEquals(0, value.get().longValue());
82 |
83 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":-123}"));
84 | assertEquals(-123, value.get().longValue());
85 | }
86 |
87 | @Test
88 | public void itSupportsFloatValues() throws PropertyError
89 | {
90 | // given
91 | Thing thing = new Thing("urn:dev:test-123", "My TestThing");
92 |
93 | Value value = new Value<>(42.0123f, v -> System.out.println("value: " + v));
94 | thing.addProperty(new Property<>(thing, "p", value,
95 | new JSONObject().put("type", "number")));
96 |
97 | // when updating property, then
98 | assertEquals(42.0123f, value.get().floatValue(), 0.00001);
99 |
100 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Float.MIN_VALUE+"}"));
101 | assertEquals(Float.MIN_VALUE, value.get().floatValue(), 0.00001);
102 |
103 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Float.MAX_VALUE+"}"));
104 | assertEquals(Float.MAX_VALUE, value.get().floatValue(), 0.00001);
105 |
106 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4.2}"));
107 | assertEquals(4.2f, value.get().floatValue(), 0.00001);
108 |
109 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4}"));
110 | assertEquals(4f, value.get().floatValue(), 0.00001);
111 |
112 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":0}"));
113 | assertEquals(0f, value.get().floatValue(), 0.00001);
114 |
115 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":-123.456}"));
116 | assertEquals(-123.456f, value.get().floatValue(), 0.00001);
117 | }
118 |
119 | @Test
120 | public void itSupportsDoubleValues() throws PropertyError
121 | {
122 | // given
123 | Thing thing = new Thing("urn:dev:test-123", "My TestThing");
124 |
125 | Value value = new Value<>(42.0123, v -> System.out.println("value: " + v));
126 | thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "number")));
127 |
128 | // when updating property, then
129 | assertEquals(42.0123, value.get(), 0.00001);
130 |
131 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Double.MIN_VALUE+"}"));
132 | assertEquals(Double.MIN_VALUE, value.get(), 0.0000000001);
133 |
134 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":"+Double.MAX_VALUE+"}"));
135 | assertEquals(Double.MAX_VALUE, value.get(), 0.0000000001);
136 |
137 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4.2}"));
138 | assertEquals(4.2, value.get(), 0.00001);
139 |
140 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":4}"));
141 | assertEquals(4, value.get(), 0.00001);
142 |
143 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":0}"));
144 | assertEquals(0, value.get(), 0.00001);
145 |
146 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":-123.456}"));
147 | assertEquals(-123.456, value.get(), 0.00001);
148 | }
149 |
150 | @Test
151 | public void itSupportsObjectValues() throws PropertyError
152 | {
153 | // given
154 | Thing thing = new Thing("urn:dev:test-123", "My TestThing");
155 |
156 | Value value = new Value<>(new JSONObject().put("key1", "val1").put("key2", "val2"),
157 | v -> System.out.println("value: " + v));
158 | thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "object")));
159 |
160 | // when updating property, then
161 | Map expectedMap = new HashMap<>();
162 | expectedMap.put("key1", "val1");
163 | expectedMap.put("key2", "val2");
164 | assertEquals(expectedMap, value.get().toMap());
165 |
166 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":{\"key3\":\"val3\"}}"));
167 | assertEquals(Collections.singletonMap("key3", "val3"), value.get().toMap());
168 | }
169 |
170 | @Test
171 | public void itSupportsArrayValues() throws PropertyError
172 | {
173 | // given
174 | Thing thing = new Thing("urn:dev:test-123", "My TestThing");
175 |
176 | Value value = new Value<>(new JSONArray("[1,2,3]"), v -> System.out.println("value: " + v));
177 | thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "array")));
178 |
179 | // when updating property, then
180 | assertEquals(Arrays.asList(1,2,3), value.get().toList());
181 |
182 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":[]}"));
183 | assertEquals(Arrays.asList(), value.get().toList());
184 | }
185 |
186 | @Test
187 | public void itSupportsStringValues() throws PropertyError
188 | {
189 | // given
190 | Thing thing = new Thing("urn:dev:test-123", "My TestThing");
191 |
192 | Value value = new Value<>("the-initial-string", v -> System.out.println("value: " + v));
193 | thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "string")));
194 |
195 | // when updating property, then
196 | assertEquals("the-initial-string", value.get());
197 |
198 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":\"the-updated-string\"}"));
199 | assertEquals("the-updated-string", value.get());
200 | }
201 |
202 | @Test
203 | public void itSupportsBooleanValues() throws PropertyError
204 | {
205 | // given
206 | Thing thing = new Thing("urn:dev:test-123", "My TestThing");
207 |
208 | Value value = new Value<>(false, v -> System.out.println("value: " + v));
209 | thing.addProperty(new Property<>(thing, "p", value, new JSONObject().put("type", "boolean")));
210 |
211 | // when updating property, then
212 | assertFalse(value.get());
213 |
214 | thing.setProperty("p", simulateHttpPutProperty("p", "{\"p\":true}"));
215 | assertTrue(value.get());
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/src/test/java/io/webthings/webthing/ValueTest.java:
--------------------------------------------------------------------------------
1 | package io.webthings.webthing;
2 |
3 | import static org.junit.Assert.assertEquals;
4 | import static org.junit.Assert.assertNull;
5 | import static org.junit.Assert.assertThrows;
6 |
7 | import java.util.Arrays;
8 | import java.util.Collections;
9 |
10 | import org.json.JSONArray;
11 | import org.json.JSONObject;
12 | import org.junit.Test;
13 |
14 | public class ValueTest {
15 |
16 | @Test
17 | public void itKnowsItsBaseTypeAtRuntime()
18 | {
19 | Value doubleNull = new Value<>(Double.class);
20 | assertEquals(Double.class, doubleNull.getBaseType());
21 | assertNull(doubleNull.get());
22 |
23 | Value doubleByValue = new Value<>(42.0123);
24 | assertEquals(Double.class, doubleNull.getBaseType());
25 | assertEquals(42.0123, doubleByValue.get(), 0.00001);
26 |
27 | Value stringByValue = new Value<>("my-string-value");
28 | assertEquals(String.class, stringByValue.getBaseType());
29 | assertEquals("my-string-value", stringByValue.get());
30 |
31 | Value stringNull = new Value<>(String.class, (String str) -> {});
32 | assertEquals(String.class, stringByValue.getBaseType());
33 | assertNull(stringNull.get());
34 |
35 | Value listByValue = new Value<>(new JSONArray("[1,2,3]"), list -> {});
36 | assertEquals(JSONArray.class, listByValue.getBaseType());
37 | assertEquals(Arrays.asList(1, 2, 3), listByValue.get().toList());
38 |
39 | Value objectExplicit;
40 | objectExplicit = new Value<>(JSONObject.class, new JSONObject().put("key", "value"), obj -> {});
41 | assertEquals(JSONObject.class, objectExplicit.getBaseType());
42 | assertEquals(Collections.singletonMap("key", "value"), objectExplicit.get().toMap());
43 | }
44 |
45 | @Test
46 | public void itsBaseTypeIsRequiredAtConstruction()
47 | {
48 | NullPointerException ex;
49 | ex = assertThrows(NullPointerException.class, () -> new Value(null, true, bool -> {}));
50 | assertEquals("The base type of a value must not be null.", ex.getMessage());
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/test/java/io/webthings/webthing/example/SingleThingTest.java:
--------------------------------------------------------------------------------
1 | package io.webthings.webthing.example;
2 |
3 | import org.junit.Test;
4 |
5 | public class SingleThingTest {
6 | @Test
7 | public void testTestServer() {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | # clone the webthing-tester
4 | git clone https://github.com/WebThingsIO/webthing-tester
5 | pip3 install --user -r webthing-tester/requirements.txt
6 |
7 | # build the jar
8 | mvn clean compile assembly:single test
9 | jar=$(find target -type f -name 'webthing-*-jar-with-dependencies.jar')
10 |
11 | # build and test the single-thing example
12 | java -cp "${jar}" io.webthings.webthing.example.SingleThing &
13 | EXAMPLE_PID=$!
14 | sleep 15
15 | ./webthing-tester/test-client.py
16 | kill -15 $EXAMPLE_PID
17 | wait $EXAMPLE_PID || true
18 |
19 | # build and test the multiple-things example
20 | java -cp "${jar}" io.webthings.webthing.example.MultipleThings &
21 | EXAMPLE_PID=$!
22 | sleep 15
23 | ./webthing-tester/test-client.py --path-prefix "/0"
24 | kill -15 $EXAMPLE_PID
25 | wait $EXAMPLE_PID || true
26 |
--------------------------------------------------------------------------------