8aa5823de45c6fa71ef300d6caa05a16ed15fb0a
[iotivity.git] / cloud / interface / src / main / java / org / iotivity / cloud / ciserver / DeviceServerSystem.java
1 /*
2  * //******************************************************************
3  * //
4  * // Copyright 2016 Samsung Electronics All Rights Reserved.
5  * //
6  * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
7  * //
8  * // Licensed under the Apache License, Version 2.0 (the "License");
9  * // you may not use this file except in compliance with the License.
10  * // You may obtain a copy of the License at
11  * //
12  * //      http://www.apache.org/licenses/LICENSE-2.0
13  * //
14  * // Unless required by applicable law or agreed to in writing, software
15  * // distributed under the License is distributed on an "AS IS" BASIS,
16  * // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17  * // See the License for the specific language governing permissions and
18  * // limitations under the License.
19  * //
20  * //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
21  */
22 package org.iotivity.cloud.ciserver;
23
24 import java.util.HashMap;
25 import java.util.Iterator;
26
27 import org.iotivity.cloud.base.OICConstants;
28 import org.iotivity.cloud.base.ServerSystem;
29 import org.iotivity.cloud.base.connector.ConnectorPool;
30 import org.iotivity.cloud.base.device.CoapDevice;
31 import org.iotivity.cloud.base.device.Device;
32 import org.iotivity.cloud.base.device.IRequestChannel;
33 import org.iotivity.cloud.base.exception.ClientException;
34 import org.iotivity.cloud.base.exception.ServerException;
35 import org.iotivity.cloud.base.exception.ServerException.BadRequestException;
36 import org.iotivity.cloud.base.exception.ServerException.UnAuthorizedException;
37 import org.iotivity.cloud.base.protocols.MessageBuilder;
38 import org.iotivity.cloud.base.protocols.coap.CoapRequest;
39 import org.iotivity.cloud.base.protocols.coap.CoapResponse;
40 import org.iotivity.cloud.base.protocols.enums.ContentFormat;
41 import org.iotivity.cloud.base.protocols.enums.RequestMethod;
42 import org.iotivity.cloud.base.protocols.enums.ResponseStatus;
43 import org.iotivity.cloud.base.server.CoapServer;
44 import org.iotivity.cloud.base.server.HttpServer;
45 import org.iotivity.cloud.base.server.Server;
46 import org.iotivity.cloud.util.Bytes;
47 import org.iotivity.cloud.util.Cbor;
48 import org.iotivity.cloud.util.Log;
49
50 import io.netty.channel.ChannelDuplexHandler;
51 import io.netty.channel.ChannelHandler.Sharable;
52 import io.netty.channel.ChannelHandlerContext;
53 import io.netty.channel.ChannelPromise;
54
55 /**
56  *
57  * This class provides a set of APIs to manage all of request
58  *
59  */
60
61 public class DeviceServerSystem extends ServerSystem {
62
63     private Cbor<HashMap<String, Object>> mCbor     = new Cbor<HashMap<String, Object>>();
64
65     IRequestChannel                       mRDServer = null;
66
67     public DeviceServerSystem() {
68         mRDServer = ConnectorPool.getConnection("rd");
69     }
70
71     /**
72      *
73      * This class provides a set of APIs to manage device pool.
74      *
75      */
76     public class CoapDevicePool {
77         HashMap<String, Device> mMapDevice = new HashMap<>();
78
79         /**
80          * API for adding device information into pool.
81          * 
82          * @param device
83          *            device to be added
84          */
85         public void addDevice(Device device) {
86             String deviceId = ((CoapDevice) device).getDeviceId();
87             synchronized (mMapDevice) {
88                 mMapDevice.put(deviceId, device);
89             }
90         }
91
92         /**
93          * API for removing device information into pool.
94          * 
95          * @param device
96          *            device to be removed
97          */
98         public void removeDevice(Device device) throws ClientException {
99             String deviceId = ((CoapDevice) device).getDeviceId();
100             synchronized (mMapDevice) {
101                 if (mMapDevice.get(deviceId) == device) {
102                     mMapDevice.remove(deviceId);
103                 }
104             }
105             removeObserveDevice(device);
106         }
107
108         private void removeObserveDevice(Device device) throws ClientException {
109             Iterator<String> iterator = mMapDevice.keySet().iterator();
110             while (iterator.hasNext()) {
111                 String deviceId = iterator.next();
112                 CoapDevice getDevice = (CoapDevice) mDevicePool
113                         .queryDevice(deviceId);
114                 getDevice.removeObserveChannel(
115                         ((CoapDevice) device).getRequestChannel());
116             }
117         }
118
119         /**
120          * API for getting device information.
121          * 
122          * @param deviceId
123          *            device id to get device
124          */
125         public Device queryDevice(String deviceId) {
126             Device device = null;
127             synchronized (mMapDevice) {
128                 device = mMapDevice.get(deviceId);
129             }
130             return device;
131         }
132     }
133
134     CoapDevicePool mDevicePool = new CoapDevicePool();
135
136     /**
137      *
138      * This class provides a set of APIs to manage life cycle of coap message.
139      *
140      */
141     @Sharable
142     class CoapLifecycleHandler extends ChannelDuplexHandler {
143         @Override
144         public void channelRead(ChannelHandlerContext ctx, Object msg) {
145
146             if (msg instanceof CoapRequest) {
147                 try {
148                     CoapDevice coapDevice = (CoapDevice) ctx.channel()
149                             .attr(keyDevice).get();
150
151                     if (coapDevice.isExpiredTime()) {
152                         throw new UnAuthorizedException("token is expired");
153                     }
154
155                     CoapRequest coapRequest = (CoapRequest) msg;
156                     IRequestChannel targetChannel = null;
157                     if (coapRequest.getUriPathSegments()
158                             .contains(Constants.REQ_DEVICE_ID)) {
159                         CoapDevice targetDevice = (CoapDevice) mDevicePool
160                                 .queryDevice(coapRequest.getUriPathSegments()
161                                         .get(1));
162                         targetChannel = targetDevice.getRequestChannel();
163                     }
164                     switch (coapRequest.getObserve()) {
165                         case SUBSCRIBE:
166                             coapDevice.addObserveRequest(
167                                     Bytes.bytesToLong(coapRequest.getToken()),
168                                     coapRequest);
169                             coapDevice.addObserveChannel(targetChannel);
170                             break;
171                         case UNSUBSCRIBE:
172                             coapDevice.removeObserveChannel(targetChannel);
173                             coapDevice.removeObserveRequest(
174                                     Bytes.bytesToLong(coapRequest.getToken()));
175                             break;
176                         default:
177                             break;
178                     }
179
180                 } catch (Throwable t) {
181                     Log.f(ctx.channel(), t);
182                     ResponseStatus responseStatus = t instanceof ServerException
183                             ? ((ServerException) t).getErrorResponse()
184                             : ResponseStatus.INTERNAL_SERVER_ERROR;
185                     ctx.writeAndFlush(MessageBuilder
186                             .createResponse((CoapRequest) msg, responseStatus));
187                     ctx.close();
188                 }
189             }
190             ctx.fireChannelRead(msg);
191         }
192
193         @Override
194         public void write(ChannelHandlerContext ctx, Object msg,
195                 ChannelPromise promise) throws Exception {
196
197             if (!(msg instanceof CoapResponse)) {
198                 throw new BadRequestException(
199                         "this msg type is not CoapResponse");
200             }
201             // This is CoapResponse
202             // Once the response is valid, add this to deviceList
203             CoapResponse response = (CoapResponse) msg;
204
205             switch (response.getUriPath()) {
206                 case OICConstants.ACCOUNT_SESSION_FULL_URI:
207                     if (response.getStatus() != ResponseStatus.CHANGED) {
208                         throw new UnAuthorizedException();
209                     }
210
211                     if (response.getPayload() != null) {
212                         break;
213                     }
214
215                     ctx.close();
216                     break;
217                 case OICConstants.ACCOUNT_FULL_URI:
218                     if (response.getStatus() != ResponseStatus.DELETED) {
219                         break;
220                     }
221                     ctx.close();
222                     break;
223             }
224             ctx.writeAndFlush(msg);
225         }
226
227         @Override
228         public void channelActive(ChannelHandlerContext ctx) {
229             Device device = ctx.channel().attr(keyDevice).get();
230             // Authenticated device connected
231
232             sendDevicePresence(device.getDeviceId(), "on");
233             mDevicePool.addDevice(device);
234
235             device.onConnected();
236         }
237
238         @Override
239         public void channelInactive(ChannelHandlerContext ctx)
240                 throws ClientException {
241             Device device = ctx.channel().attr(keyDevice).get();
242             // Some cases, this event occurs after new device connected using
243             // same di.
244             // So compare actual value, and remove if same.
245             if (device != null) {
246                 sendDevicePresence(device.getDeviceId(), "off");
247
248                 device.onDisconnected();
249
250                 mDevicePool.removeDevice(device);
251                 ctx.channel().attr(keyDevice).remove();
252
253             }
254         }
255
256         /**
257          * API for sending state to resource directory
258          * 
259          * @param deviceId
260          *            device id to be sent to resource directory
261          * @param state
262          *            device state to be sent to resource directory
263          */
264         public void sendDevicePresence(String deviceId, String state) {
265
266             Cbor<HashMap<String, Object>> cbor = new Cbor<>();
267             HashMap<String, Object> payload = new HashMap<String, Object>();
268             payload.put(Constants.REQ_DEVICE_ID, deviceId);
269             payload.put(Constants.PRESENCE_STATE, state);
270             StringBuffer uriPath = new StringBuffer();
271             uriPath.append("/" + Constants.PREFIX_OIC);
272             uriPath.append("/" + Constants.DEVICE_PRESENCE_URI);
273             mRDServer.sendRequest(MessageBuilder.createRequest(
274                     RequestMethod.POST, uriPath.toString(), null,
275                     ContentFormat.APPLICATION_CBOR,
276                     cbor.encodingPayloadToCbor(payload)), null);
277         }
278     }
279
280     CoapLifecycleHandler mLifeCycleHandler = new CoapLifecycleHandler();
281
282     @Sharable
283     class CoapAuthHandler extends ChannelDuplexHandler {
284
285         @Override
286         public void channelActive(ChannelHandlerContext ctx) {
287             // Actual channel active should decided after authentication.
288         }
289
290         @Override
291         public void write(ChannelHandlerContext ctx, Object msg,
292                 ChannelPromise promise) {
293
294             try {
295
296                 if (!(msg instanceof CoapResponse)) {
297                     throw new BadRequestException(
298                             "this msg type is not CoapResponse");
299                 }
300                 // This is CoapResponse
301                 // Once the response is valid, add this to deviceList
302                 CoapResponse response = (CoapResponse) msg;
303
304                 switch (response.getUriPath()) {
305
306                     case OICConstants.ACCOUNT_SESSION_FULL_URI:
307                         HashMap<String, Object> payloadData = mCbor
308                                 .parsePayloadFromCbor(response.getPayload(),
309                                         HashMap.class);
310
311                         if (response.getStatus() != ResponseStatus.CHANGED) {
312                             throw new UnAuthorizedException();
313                         }
314
315                         if (payloadData == null) {
316                             throw new BadRequestException("payload is empty");
317                         }
318                         int remainTime = (int) payloadData
319                                 .get(Constants.EXPIRES_IN);
320
321                         Device device = ctx.channel().attr(keyDevice).get();
322                         ((CoapDevice) device).setExpiredPolicy(remainTime);
323
324                         // Remove current auth handler and replace to
325                         // LifeCycleHandle
326                         ctx.channel().pipeline().replace(this,
327                                 "LifeCycleHandler", mLifeCycleHandler);
328
329                         // Raise event that we have Authenticated device
330                         ctx.fireChannelActive();
331
332                         break;
333                 }
334
335                 ctx.writeAndFlush(msg);
336
337             } catch (Throwable t) {
338                 Log.f(ctx.channel(), t);
339                 ctx.writeAndFlush(msg);
340                 ctx.close();
341             }
342         }
343
344         @Override
345         public void channelRead(ChannelHandlerContext ctx, Object msg) {
346
347             try {
348                 if (!(msg instanceof CoapRequest)) {
349                     throw new BadRequestException(
350                             "this msg type is not CoapRequest");
351                 }
352
353                 // And check first response is VALID then add or cut
354                 CoapRequest request = (CoapRequest) msg;
355
356                 switch (request.getUriPath()) {
357                     // Check whether request is about account
358                     case OICConstants.ACCOUNT_FULL_URI:
359                     case OICConstants.ACCOUNT_TOKENREFRESH_FULL_URI:
360
361                         if (ctx.channel().attr(keyDevice).get() == null) {
362                             // Create device first and pass to upperlayer
363                             Device device = new CoapDevice(ctx);
364                             ctx.channel().attr(keyDevice).set(device);
365                         }
366
367                         break;
368
369                     case OICConstants.ACCOUNT_SESSION_FULL_URI:
370
371                         HashMap<String, Object> authPayload = mCbor
372                                 .parsePayloadFromCbor(request.getPayload(),
373                                         HashMap.class);
374
375                         Device device = ctx.channel().attr(keyDevice).get();
376
377                         if (device == null) {
378                             device = new CoapDevice(ctx);
379                             ctx.channel().attr(keyDevice).set(device);
380                         }
381
382                         if (authPayload == null) {
383                             throw new BadRequestException("payload is empty");
384                         }
385
386                         ((CoapDevice) device).updateDevice(
387                                 (String) authPayload.get(Constants.DEVICE_ID),
388                                 (String) authPayload.get(Constants.USER_ID),
389                                 (String) authPayload
390                                         .get(Constants.ACCESS_TOKEN));
391
392                         break;
393
394                     case OICConstants.KEEP_ALIVE_FULL_URI:
395                         // TODO: Pass ping request to upper layer
396                         break;
397
398                     default:
399                         throw new UnAuthorizedException(
400                                 "authentication required first");
401                 }
402
403                 ctx.fireChannelRead(msg);
404
405             } catch (Throwable t) {
406                 ResponseStatus responseStatus = t instanceof ServerException
407                         ? ((ServerException) t).getErrorResponse()
408                         : ResponseStatus.UNAUTHORIZED;
409                 ctx.writeAndFlush(MessageBuilder
410                         .createResponse((CoapRequest) msg, responseStatus));
411                 Log.f(ctx.channel(), t);
412             }
413         }
414     }
415
416     @Sharable
417     class HttpAuthHandler extends ChannelDuplexHandler {
418         @Override
419         public void channelActive(ChannelHandlerContext ctx) throws Exception {
420             // After current channel authenticated, raise to upper layer
421         }
422     }
423
424     @Override
425     public void addServer(Server server) {
426         if (server instanceof CoapServer) {
427             server.addHandler(new CoapAuthHandler());
428         }
429
430         if (server instanceof HttpServer) {
431             server.addHandler(new HttpAuthHandler());
432         }
433
434         super.addServer(server);
435     }
436
437     public CoapDevicePool getDevicePool() {
438         return mDevicePool;
439     }
440 }