From 6709360249c3b80e8818d2087b2679fc376bdac4 Mon Sep 17 00:00:00 2001 From: ayyoob Date: Thu, 14 Apr 2016 19:56:33 +0530 Subject: [PATCH 1/6] adding das receivers and publisher with OAUTH --- .../pom.xml | 5 + .../AndroidSenseControllerServiceImpl.java | 86 ++-- .../transport/AndroidSenseMQTTConnector.java | 6 +- .../service/impl/util/APIUtil.java | 94 ++++ .../service/impl/util/SensorData.java | 43 +- .../service/impl/util/SensorRecord.java | 68 +++ .../pom.xml | 5 + .../impl/ArduinoControllerServiceImpl.java | 55 +-- .../impl/ArduinoManagerServiceImpl.java | 7 +- .../arduino/service/impl/dto/SensorData.java | 44 -- .../service/impl/dto/SensorRecord.java | 68 +++ .../arduino/service/impl/util/APIUtil.java | 119 ++++- .../impl/util/ArduinoServiceUtils.java | 6 +- .../pom.xml | 165 +++++++ .../input/adapter/extensions/ContentInfo.java | 55 +++ .../adapter/extensions/ContentValidator.java | 33 ++ .../extensions/http/HTTPEventAdapter.java | 213 ++++++++ .../http/HTTPEventAdapterFactory.java | 160 ++++++ .../extensions/http/HTTPMessageServlet.java | 339 +++++++++++++ .../oauth/OAuthTokenValidaterStubFactory.java | 180 +++++++ .../OAuthTokenValidationException.java | 56 +++ .../http/util/AuthenticationInfo.java | 52 ++ .../http/util/HTTPContentValidator.java | 45 ++ .../http/util/HTTPEventAdapterConstants.java | 72 +++ .../EventAdapterServiceComponent.java | 77 +++ .../EventAdapterServiceDataHolder.java | 50 ++ .../adapter/extensions/mqtt/Constants.java | 38 ++ .../extensions/mqtt/MQTTEventAdapter.java | 147 ++++++ .../mqtt/MQTTEventAdapterFactory.java | 153 ++++++ ...ntentValidatorInitializationException.java | 53 ++ .../mqtt/util/MQTTAdapterListener.java | 280 +++++++++++ .../MQTTBrokerConnectionConfiguration.java | 119 +++++ .../mqtt/util/MQTTContentValidator.java | 53 ++ .../mqtt/util/MQTTEventAdapterConstants.java | 46 ++ .../extensions/mqtt/util/MQTTUtil.java | 99 ++++ .../mqtt/util/RegistrationProfile.java | 73 +++ .../extensions/http/i18n/Resources.properties | 38 ++ .../extensions/mqtt/i18n/Resources.properties | 38 ++ .../pom.xml | 82 ++++ .../src/main/java/SubscriptionEndpoint.java | 74 +++ .../java/SuperTenantSubscriptionEndpoint.java | 129 +++++ .../main/java/TenantSubscriptionEndpoint.java | 127 +++++ .../main/java/oauth/OAuthTokenValdiator.java | 174 +++++++ .../oauth/OAuthTokenValidaterStubFactory.java | 177 +++++++ .../OAuthTokenValidationException.java | 56 +++ .../wso2/carbon/servlet/ServiceHolder.java | 28 ++ .../SuperTenantEventRetrievalEndpoint.java | 85 ++++ .../servlet/TenantEventRetrievalEndpoint.java | 89 ++++ .../main/java/util/AuthenticationInfo.java | 43 ++ .../src/main/java/util/UIConstants.java | 38 ++ .../src/main/webapp/WEB-INF/web.xml | 41 ++ .../pom.xml | 137 ++++++ .../adapter/extensions/ui/UIEventAdapter.java | 459 ++++++++++++++++++ .../extensions/ui/UIEventAdapterFactory.java | 89 ++++ .../ui/UIOutputCallbackControllerService.java | 61 +++ ...UIOutputCallbackControllerServiceImpl.java | 210 ++++++++ ...tAdaptorEventStreamServiceValueHolder.java | 23 + ...ventAdaptorServiceInternalValueHolder.java | 56 +++ .../ui/internal/ds/UILocalEventAdapterDS.java | 80 +++ .../ui/internal/util/CEPWebSocketSession.java | 62 +++ .../util/UIEventAdapterConstants.java | 54 +++ .../extensions/ui/i18n/Resources.properties | 22 + components/iot-plugins/das-extensions/pom.xml | 40 ++ .../DigitalDisplayManagerServiceImpl.java | 10 +- .../service/impl/util/APIUtil.java | 14 + .../service/impl/DroneManagerServiceImpl.java | 7 +- .../service/impl/util/APIUtil.java | 13 + components/iot-plugins/pom.xml | 1 + .../pom.xml | 5 + .../RaspberryPiControllerServiceImpl.java | 52 +- .../impl/RaspberryPiManagerServiceImpl.java | 7 +- .../service/impl/dto/SensorRecord.java | 68 +++ .../service/impl/util/APIUtil.java | 121 ++++- .../impl/util/RaspberrypiServiceUtils.java | 6 +- .../pom.xml | 5 + ...VirtualFireAlarmControllerServiceImpl.java | 63 +-- .../VirtualFireAlarmManagerServiceImpl.java | 7 +- .../service/impl/dto/SensorData.java | 44 -- .../service/impl/dto/SensorRecord.java | 68 +++ .../service/impl/util/APIUtil.java | 119 ++++- .../util/VirtualFireAlarmServiceUtils.java | 6 +- .../pom.xml | 120 +++++ .../src/main/resources/build.properties | 20 + .../src/main/resources/p2.inf | 4 + .../resources/websocket-validation.properties | 5 + .../das-extensions-feature/pom.xml | 40 ++ features/iot-plugins-feature/pom.xml | 1 + pom.xml | 89 +++- 88 files changed, 6222 insertions(+), 349 deletions(-) create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/util/SensorRecord.java delete mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/dto/SensorData.java create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/dto/SensorRecord.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/pom.xml create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/ContentInfo.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/ContentValidator.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/HTTPEventAdapter.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/HTTPEventAdapterFactory.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/HTTPMessageServlet.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/oauth/OAuthTokenValidaterStubFactory.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/oauth/exception/OAuthTokenValidationException.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/AuthenticationInfo.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/HTTPContentValidator.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/HTTPEventAdapterConstants.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/internal/EventAdapterServiceComponent.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/internal/EventAdapterServiceDataHolder.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/Constants.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/MQTTEventAdapter.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/MQTTEventAdapterFactory.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/exception/MQTTContentValidatorInitializationException.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTAdapterListener.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTBrokerConnectionConfiguration.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTContentValidator.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTEventAdapterConstants.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTUtil.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/RegistrationProfile.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/resources/org/wso2/carbon/event/input/adapter/extensions/http/i18n/Resources.properties create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/resources/org/wso2/carbon/event/input/adapter/extensions/mqtt/i18n/Resources.properties create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/pom.xml create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/SubscriptionEndpoint.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/SuperTenantSubscriptionEndpoint.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/TenantSubscriptionEndpoint.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/oauth/OAuthTokenValdiator.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/oauth/OAuthTokenValidaterStubFactory.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/oauth/exception/OAuthTokenValidationException.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/org/wso2/carbon/servlet/ServiceHolder.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/org/wso2/carbon/servlet/SuperTenantEventRetrievalEndpoint.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/org/wso2/carbon/servlet/TenantEventRetrievalEndpoint.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/util/AuthenticationInfo.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/util/UIConstants.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/webapp/WEB-INF/web.xml create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/pom.xml create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/UIEventAdapter.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/UIEventAdapterFactory.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/UIOutputCallbackControllerService.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/UIOutputCallbackControllerServiceImpl.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/ds/OutputAdaptorEventStreamServiceValueHolder.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/ds/UIEventAdaptorServiceInternalValueHolder.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/ds/UILocalEventAdapterDS.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/util/CEPWebSocketSession.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/util/UIEventAdapterConstants.java create mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/resources/org/wso2/carbon/event/output/adapter/extensions/ui/i18n/Resources.properties create mode 100644 components/iot-plugins/das-extensions/pom.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/dto/SensorRecord.java delete mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/dto/SensorData.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/dto/SensorRecord.java create mode 100644 features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/pom.xml create mode 100644 features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/src/main/resources/build.properties create mode 100644 features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/src/main/resources/p2.inf create mode 100644 features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/src/main/resources/websocket-validation.properties create mode 100644 features/iot-plugins-feature/das-extensions-feature/pom.xml diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/pom.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/pom.xml index d9cff3eee..52489251b 100644 --- a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/pom.xml +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/pom.xml @@ -179,6 +179,11 @@ org.wso2.carbon.apimgt.webapp.publisher provided + + org.wso2.carbon.analytics + org.wso2.carbon.analytics.api + provided + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/AndroidSenseControllerServiceImpl.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/AndroidSenseControllerServiceImpl.java index 3d0dfa7f3..312a9d9aa 100644 --- a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/AndroidSenseControllerServiceImpl.java +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/AndroidSenseControllerServiceImpl.java @@ -20,26 +20,26 @@ package org.wso2.carbon.device.mgt.iot.androidsense.service.impl; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.analytics.dataservice.commons.SORT; +import org.wso2.carbon.analytics.dataservice.commons.SortByField; +import org.wso2.carbon.analytics.datasource.commons.exception.AnalyticsException; import org.wso2.carbon.context.PrivilegedCarbonContext; -import org.wso2.carbon.device.mgt.analytics.data.publisher.AnalyticsDataRecord; import org.wso2.carbon.device.mgt.analytics.data.publisher.exception.DataPublisherConfigurationException; -import org.wso2.carbon.device.mgt.analytics.data.publisher.exception.DeviceManagementAnalyticsException; -import org.wso2.carbon.device.mgt.analytics.data.publisher.service.DeviceAnalyticsService; +import org.wso2.carbon.device.mgt.analytics.data.publisher.service.EventsPublisherService; import org.wso2.carbon.device.mgt.iot.androidsense.service.impl.transport.AndroidSenseMQTTConnector; +import org.wso2.carbon.device.mgt.iot.androidsense.service.impl.util.APIUtil; import org.wso2.carbon.device.mgt.iot.androidsense.service.impl.util.DeviceData; import org.wso2.carbon.device.mgt.iot.androidsense.service.impl.util.SensorData; +import org.wso2.carbon.device.mgt.iot.androidsense.service.impl.util.SensorRecord; import org.wso2.carbon.device.mgt.iot.androidsense.plugin.constants.AndroidSenseConstants; import org.wso2.carbon.device.mgt.iot.controlqueue.mqtt.MqttConfig; import org.wso2.carbon.device.mgt.iot.exception.DeviceControllerException; import org.wso2.carbon.device.mgt.iot.sensormgt.SensorDataManager; -import org.wso2.carbon.device.mgt.iot.sensormgt.SensorRecord; import org.wso2.carbon.device.mgt.iot.service.IoTServerStartupListener; import org.wso2.carbon.device.mgt.iot.transport.TransportHandlerException; import javax.ws.rs.core.Response; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.List; /** @@ -100,8 +100,8 @@ public class AndroidSenseControllerServiceImpl implements AndroidSenseController public Response addSensorData(DeviceData dataMsg) { PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); - DeviceAnalyticsService deviceAnalyticsService = (DeviceAnalyticsService) ctx - .getOSGiService(DeviceAnalyticsService.class, null); + EventsPublisherService deviceAnalyticsService = (EventsPublisherService) ctx + .getOSGiService(EventsPublisherService.class, null); SensorData[] sensorData = dataMsg.values; String streamDef = null; Object payloadData[] = null; @@ -202,7 +202,7 @@ public class AndroidSenseControllerServiceImpl implements AndroidSenseController public Response getLightData(String deviceId) { try { - SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, AndroidSenseConstants + org.wso2.carbon.device.mgt.iot.sensormgt.SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, AndroidSenseConstants .SENSOR_LIGHT); return Response.ok().entity(sensorRecord).build(); } catch (DeviceControllerException e) { @@ -212,7 +212,7 @@ public class AndroidSenseControllerServiceImpl implements AndroidSenseController public Response getBattery(String deviceId) { try { - SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, AndroidSenseConstants + org.wso2.carbon.device.mgt.iot.sensormgt.SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, AndroidSenseConstants .SENSOR_BATTERY); return Response.ok().entity(sensorRecord).build(); } catch (DeviceControllerException e) { @@ -222,7 +222,7 @@ public class AndroidSenseControllerServiceImpl implements AndroidSenseController public Response getGPS(String deviceId) { try { - SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, AndroidSenseConstants + org.wso2.carbon.device.mgt.iot.sensormgt.SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, AndroidSenseConstants .SENSOR_GPS); return Response.ok().entity(sensorRecord).build(); } catch (DeviceControllerException e) { @@ -232,7 +232,7 @@ public class AndroidSenseControllerServiceImpl implements AndroidSenseController public Response readMagnetic(String deviceId) { try { - SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, AndroidSenseConstants + org.wso2.carbon.device.mgt.iot.sensormgt.SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, AndroidSenseConstants .SENSOR_MAGNETIC); return Response.ok().entity(sensorRecord).build(); } catch (DeviceControllerException e) { @@ -242,7 +242,7 @@ public class AndroidSenseControllerServiceImpl implements AndroidSenseController public Response readAccelerometer(String deviceId) { try { - SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, + org.wso2.carbon.device.mgt.iot.sensormgt.SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, AndroidSenseConstants.SENSOR_ACCELEROMETER); return Response.ok().entity(sensorRecord).build(); } catch (DeviceControllerException e) { @@ -252,7 +252,7 @@ public class AndroidSenseControllerServiceImpl implements AndroidSenseController public Response readRotation(String deviceId) { try { - SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, + org.wso2.carbon.device.mgt.iot.sensormgt.SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, AndroidSenseConstants.SENSOR_ROTATION); return Response.ok().entity(sensorRecord).build(); } catch (DeviceControllerException e) { @@ -262,7 +262,7 @@ public class AndroidSenseControllerServiceImpl implements AndroidSenseController public Response readProximity(String deviceId) { try { - SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, + org.wso2.carbon.device.mgt.iot.sensormgt.SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, AndroidSenseConstants.SENSOR_PROXIMITY); return Response.ok().entity(sensorRecord).build(); } catch (DeviceControllerException e) { @@ -272,7 +272,7 @@ public class AndroidSenseControllerServiceImpl implements AndroidSenseController public Response readGyroscope(String deviceId) { try { - SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, + org.wso2.carbon.device.mgt.iot.sensormgt.SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, AndroidSenseConstants.SENSOR_GYROSCOPE); return Response.ok().entity(sensorRecord).build(); } catch (DeviceControllerException e) { @@ -282,7 +282,7 @@ public class AndroidSenseControllerServiceImpl implements AndroidSenseController public Response readPressure(String deviceId) { try { - SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, + org.wso2.carbon.device.mgt.iot.sensormgt.SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, AndroidSenseConstants.SENSOR_PRESSURE); return Response.ok().entity(sensorRecord).build(); } catch (DeviceControllerException e) { @@ -292,7 +292,7 @@ public class AndroidSenseControllerServiceImpl implements AndroidSenseController public Response readGravity(String deviceId) { try { - SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, + org.wso2.carbon.device.mgt.iot.sensormgt.SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, AndroidSenseConstants.SENSOR_GRAVITY); return Response.ok().entity(sensorRecord).build(); } catch (DeviceControllerException e) { @@ -302,8 +302,8 @@ public class AndroidSenseControllerServiceImpl implements AndroidSenseController public Response getWords(String deviceId, String sessionId) { try { - SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, - AndroidSenseConstants.SENSOR_WORDCOUNT); + org.wso2.carbon.device.mgt.iot.sensormgt.SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, + AndroidSenseConstants.SENSOR_WORDCOUNT); return Response.ok().entity(sensorRecord).build(); } catch (DeviceControllerException e) { return Response.status(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()).build(); @@ -344,55 +344,27 @@ public class AndroidSenseControllerServiceImpl implements AndroidSenseController String fromDate = String.valueOf(from); String toDate = String.valueOf(to); String user = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUsername(); - List sensorDatas = new ArrayList<>(); - PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); - DeviceAnalyticsService deviceAnalyticsService = (DeviceAnalyticsService) ctx - .getOSGiService(DeviceAnalyticsService.class, null); String query = "owner:" + user + " AND deviceId:" + deviceId + " AND deviceType:" + AndroidSenseConstants.DEVICE_TYPE + " AND time : [" + fromDate + " TO " + toDate + "]"; if (sensor.equals(AndroidSenseConstants.SENSOR_WORDCOUNT)) { query = "owner:" + user + " AND deviceId:" + deviceId; } String sensorTableName = getSensorEventTableName(sensor); + List sensorDatas; try { - List records = deviceAnalyticsService.getAllEventsForDevice(sensorTableName, query); if (sensor.equals(AndroidSenseConstants.SENSOR_WORDCOUNT)) { - for (AnalyticsDataRecord record : records) { - SensorData sensorData = new SensorData(); - sensorData.setKey((String) record.getValue("word")); - sensorData.setTime((long) record.getValue("occurence")); - sensorData.setValue((String) record.getValue("sessionId")); - sensorDatas.add(sensorData); - } + List sortByFields = new ArrayList<>(); + SortByField sortByField = new SortByField("time", SORT.ASC, false); + sortByFields.add(sortByField); + sensorDatas = APIUtil.getAllEventsForDevice(sensorTableName, query, sortByFields); } else { - Collections.sort(records, new Comparator() { - @Override - public int compare(AnalyticsDataRecord o1, AnalyticsDataRecord o2) { - long t1 = (Long) o1.getValue("time"); - long t2 = (Long) o2.getValue("time"); - if (t1 < t2) { - return -1; - } else if (t1 > t2) { - return 1; - } else { - return 0; - } - } - }); - for (AnalyticsDataRecord record : records) { - SensorData sensorData = new SensorData(); - sensorData.setTime((long) record.getValue("time")); - sensorData.setValue("" + (float) record.getValue(sensor)); - sensorDatas.add(sensorData); - } + sensorDatas = APIUtil.getAllEventsForDevice(sensorTableName, query, null); } - SensorData[] sensorDetails = sensorDatas.toArray(new SensorData[sensorDatas.size()]); - return Response.ok().entity(sensorDetails).build(); - } catch (DeviceManagementAnalyticsException e) { + return Response.ok().entity(sensorDatas).build(); + } catch (AnalyticsException e) { String errorMsg = "Error on retrieving stats on table " + sensorTableName + " with query " + query; log.error(errorMsg); - SensorData[] senserDetails = sensorDatas.toArray(new SensorData[sensorDatas.size()]); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()).entity(senserDetails).build(); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()).entity(errorMsg).build(); } } diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/transport/AndroidSenseMQTTConnector.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/transport/AndroidSenseMQTTConnector.java index 0b8f30e22..bd86bca26 100644 --- a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/transport/AndroidSenseMQTTConnector.java +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/transport/AndroidSenseMQTTConnector.java @@ -26,7 +26,7 @@ import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.device.mgt.analytics.data.publisher.exception.DataPublisherConfigurationException; -import org.wso2.carbon.device.mgt.analytics.data.publisher.service.DeviceAnalyticsService; +import org.wso2.carbon.device.mgt.analytics.data.publisher.service.EventsPublisherService; import org.wso2.carbon.device.mgt.common.Device; import org.wso2.carbon.device.mgt.common.DeviceIdentifier; import org.wso2.carbon.device.mgt.common.DeviceManagementException; @@ -289,8 +289,8 @@ public class AndroidSenseMQTTConnector extends MQTTTransportHandler { if (device != null) { String owner = device.getEnrolmentInfo().getOwner(); ctx.setTenantDomain(MultitenantUtils.getTenantDomain(owner), true); - DeviceAnalyticsService deviceAnalyticsService = (DeviceAnalyticsService) ctx - .getOSGiService(DeviceAnalyticsService.class, null); + EventsPublisherService deviceAnalyticsService = (EventsPublisherService) ctx + .getOSGiService(EventsPublisherService.class, null); if (deviceAnalyticsService != null) { Object metaData[] = {owner, AndroidSenseConstants.DEVICE_TYPE, deviceId, time}; if (streamDefinition != null && payloadData != null && payloadData.length > 0) { diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/util/APIUtil.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/util/APIUtil.java index 73287696e..c8d90a1c1 100644 --- a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/util/APIUtil.java +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/util/APIUtil.java @@ -2,9 +2,23 @@ package org.wso2.carbon.device.mgt.iot.androidsense.service.impl.util; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.analytics.api.AnalyticsDataAPI; +import org.wso2.carbon.analytics.dataservice.commons.AnalyticsDataResponse; +import org.wso2.carbon.analytics.dataservice.commons.AnalyticsDrillDownRequest; +import org.wso2.carbon.analytics.dataservice.commons.SearchResultEntry; +import org.wso2.carbon.analytics.dataservice.commons.SortByField; +import org.wso2.carbon.analytics.dataservice.core.AnalyticsDataServiceUtils; +import org.wso2.carbon.analytics.datasource.commons.Record; +import org.wso2.carbon.analytics.datasource.commons.exception.AnalyticsException; +import org.wso2.carbon.context.CarbonContext; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + /** * This class provides utility functions used by REST-API. */ @@ -33,4 +47,84 @@ public class APIUtil { } return deviceManagementProviderService; } + + public static AnalyticsDataAPI getAnalyticsDataAPI() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + AnalyticsDataAPI analyticsDataAPI = + (AnalyticsDataAPI) ctx.getOSGiService(AnalyticsDataAPI.class, null); + if (analyticsDataAPI == null) { + String msg = "Analytics api service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return analyticsDataAPI; + } + + public static List getAllEventsForDevice(String tableName, String query, List sortByFields) throws AnalyticsException { + int tenantId = CarbonContext.getThreadLocalCarbonContext().getTenantId(); + AnalyticsDataAPI analyticsDataAPI = getAnalyticsDataAPI(); + int eventCount = analyticsDataAPI.searchCount(tenantId, tableName, query); + if (eventCount == 0) { + return null; + } + AnalyticsDrillDownRequest drillDownRequest = new AnalyticsDrillDownRequest(); + drillDownRequest.setQuery(query); + drillDownRequest.setTableName(tableName); + drillDownRequest.setRecordCount(eventCount); + if (sortByFields != null) { + drillDownRequest.setSortByFields(sortByFields); + } + List resultEntries = analyticsDataAPI.drillDownSearch(tenantId, drillDownRequest); + List recordIds = getRecordIds(resultEntries); + AnalyticsDataResponse response = analyticsDataAPI.get(tenantId, tableName, 1, null, recordIds); + Map sensorDatas = createSensorData(AnalyticsDataServiceUtils.listRecords( + analyticsDataAPI, response)); + List sortedSensorData = getSortedSensorData(sensorDatas, resultEntries); + return sortedSensorData; + } + + private static List getRecordIds(List searchResults) { + List ids = new ArrayList<>(); + for (SearchResultEntry searchResult : searchResults) { + ids.add(searchResult.getId()); + } + return ids; + } + + public static List getSortedSensorData(Map sensorDatas, + List searchResults) { + List sortedRecords = new ArrayList<>(); + for (SearchResultEntry searchResultEntry : searchResults) { + sortedRecords.add(sensorDatas.get(searchResultEntry.getId())); + } + return sortedRecords; + } + + /** + * Creates the SensorDatas from records. + * + * @param records the records + * @return the Map of SensorRecord + */ + public static Map createSensorData(List records) { + Map sensorDatas = new HashMap<>(); + for (Record record : records) { + SensorRecord sensorData = createSensorData(record); + sensorDatas.put(sensorData.getId(), sensorData); + } + return sensorDatas; + } + + /** + * Create a SensorRecord object out of a Record object + * + * @param record the record object + * @return SensorRecord object + */ + public static SensorRecord createSensorData(Record record) { + SensorRecord recordBean = new SensorRecord(); + recordBean.setId(record.getId()); + recordBean.setValues(record.getValues()); + return recordBean; + } } diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/util/SensorData.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/util/SensorData.java index becd17085..67890f2f0 100644 --- a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/util/SensorData.java +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/util/SensorData.java @@ -12,33 +12,32 @@ import javax.xml.bind.annotation.XmlRootElement; @JsonIgnoreProperties(ignoreUnknown = true) public class SensorData { - @XmlElement public Long time; - @XmlElement public String key; + @XmlElement public Long time; + @XmlElement public String key; + @XmlElement public String value; - public String getValue() { - return value; - } + public String getValue() { + return value; + } - public void setValue(String value) { - this.value = value; - } + public void setValue(String value) { + this.value = value; + } - public String getKey() { - return key; - } + public String getKey() { + return key; + } - public void setKey(String key) { - this.key = key; - } + public void setKey(String key) { + this.key = key; + } - public Long getTime() { - return time; - } + public Long getTime() { + return time; + } - public void setTime(Long time) { - this.time = time; - } - - @XmlElement public String value; + public void setTime(Long time) { + this.time = time; + } } diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/util/SensorRecord.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/util/SensorRecord.java new file mode 100644 index 000000000..2159a0830 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/util/SensorRecord.java @@ -0,0 +1,68 @@ +package org.wso2.carbon.device.mgt.iot.androidsense.service.impl.util; + +import org.codehaus.jackson.annotate.JsonIgnoreProperties; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@XmlRootElement +/** + * This stores sensor event data for android sense. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SensorRecord { + + @XmlElementWrapper(required = true, name = "values") + private Map values; + + /** The id. */ + @XmlElement(required = false, name = "id") + private String id; + + /** + * Gets the values. + * @return the values + */ + public Map getValues() { + return values; + } + + /** + * Sets the values. + * @param values the values + */ + public void setValues(Map values) { + this.values = values; + } + + /** + * Sets the id. + * @param id the new id + */ + public void setId(String id) { + this.id = id; + } + + /** + * Gets the id. + * @return the id + */ + public String getId() { + return id; + } + + @Override + public String toString(){ + List valueList = new ArrayList(); + for (Map.Entry entry : values.entrySet()) { + valueList.add(entry.getKey() + ":" + entry.getValue()); + } + return valueList.toString(); + + } + +} diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/pom.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/pom.xml index 7d698874a..77142701b 100644 --- a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/pom.xml +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/pom.xml @@ -160,6 +160,11 @@ org.wso2.carbon.apimgt.webapp.publisher provided + + org.wso2.carbon.analytics + org.wso2.carbon.analytics.api + provided + diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/ArduinoControllerServiceImpl.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/ArduinoControllerServiceImpl.java index 0b7de4e64..98ae9de0e 100644 --- a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/ArduinoControllerServiceImpl.java +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/ArduinoControllerServiceImpl.java @@ -20,23 +20,21 @@ package org.wso2.carbon.device.mgt.iot.arduino.service.impl; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.analytics.dataservice.commons.SORT; +import org.wso2.carbon.analytics.dataservice.commons.SortByField; +import org.wso2.carbon.analytics.datasource.commons.exception.AnalyticsException; import org.wso2.carbon.context.PrivilegedCarbonContext; -import org.wso2.carbon.device.mgt.analytics.data.publisher.AnalyticsDataRecord; -import org.wso2.carbon.device.mgt.analytics.data.publisher.exception.DeviceManagementAnalyticsException; -import org.wso2.carbon.device.mgt.analytics.data.publisher.service.DeviceAnalyticsService; import org.wso2.carbon.device.mgt.iot.arduino.service.impl.dto.DeviceData; -import org.wso2.carbon.device.mgt.iot.arduino.service.impl.dto.SensorData; +import org.wso2.carbon.device.mgt.iot.arduino.service.impl.dto.SensorRecord; +import org.wso2.carbon.device.mgt.iot.arduino.service.impl.util.APIUtil; import org.wso2.carbon.device.mgt.iot.arduino.service.impl.util.ArduinoServiceUtils; import org.wso2.carbon.device.mgt.iot.arduino.plugin.constants.ArduinoConstants; import org.wso2.carbon.device.mgt.iot.exception.DeviceControllerException; import org.wso2.carbon.device.mgt.iot.sensormgt.SensorDataManager; -import org.wso2.carbon.device.mgt.iot.sensormgt.SensorRecord; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.Response; import java.util.ArrayList; import java.util.Calendar; -import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -83,10 +81,9 @@ public class ArduinoControllerServiceImpl implements ArduinoControllerService { @Override public Response requestTemperature(String deviceId, String protocol) { - try { - SensorRecord sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, - ArduinoConstants.SENSOR_TEMPERATURE); + org.wso2.carbon.device.mgt.iot.sensormgt.SensorRecord sensorRecord = + SensorDataManager.getInstance().getSensorRecord(deviceId, ArduinoConstants.SENSOR_TEMPERATURE); return Response.status(Response.Status.OK.getStatusCode()).entity(sensorRecord).build(); } catch (DeviceControllerException e) { return Response.status(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()).build(); @@ -157,43 +154,19 @@ public class ArduinoControllerServiceImpl implements ArduinoControllerService { public Response getArduinoTemperatureStats(String deviceId, long from, long to) { String fromDate = String.valueOf(from); String toDate = String.valueOf(to); - List sensorDatas = new ArrayList<>(); - PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); - DeviceAnalyticsService deviceAnalyticsService = (DeviceAnalyticsService) ctx - .getOSGiService(DeviceAnalyticsService.class, null); String query = "deviceId:" + deviceId + " AND deviceType:" + ArduinoConstants.DEVICE_TYPE + " AND time : [" + fromDate + " TO " + toDate + "]"; String sensorTableName = ArduinoConstants.TEMPERATURE_EVENT_TABLE; - SensorData[] sensorDetails; try { - List records = deviceAnalyticsService.getAllEventsForDevice(sensorTableName, query); - Collections.sort(records, new Comparator() { - @Override - public int compare(AnalyticsDataRecord o1, AnalyticsDataRecord o2) { - long t1 = (Long) o1.getValue("time"); - long t2 = (Long) o2.getValue("time"); - if (t1 < t2) { - return -1; - } else if (t1 > t2) { - return 1; - } else { - return 0; - } - } - }); - for (AnalyticsDataRecord record : records) { - SensorData sensorData = new SensorData(); - sensorData.setTime((long) record.getValue("time")); - sensorData.setValue("" + (float) record.getValue(ArduinoConstants.SENSOR_TEMPERATURE)); - sensorDatas.add(sensorData); - } - sensorDetails = sensorDatas.toArray(new SensorData[sensorDatas.size()]); - return Response.status(Response.Status.OK.getStatusCode()).entity(sensorDetails).build(); - } catch (DeviceManagementAnalyticsException e) { + List sortByFields = new ArrayList<>(); + SortByField sortByField = new SortByField("time", SORT.ASC, false); + sortByFields.add(sortByField); + List sensorRecords = APIUtil.getAllEventsForDevice(sensorTableName, query, sortByFields); + return Response.status(Response.Status.OK.getStatusCode()).entity(sensorRecords).build(); + } catch (AnalyticsException e) { String errorMsg = "Error on retrieving stats on table " + sensorTableName + " with query " + query; log.error(errorMsg); - sensorDetails = sensorDatas.toArray(new SensorData[sensorDatas.size()]); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()).entity(sensorDetails).build(); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()).entity(errorMsg).build(); } } diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/ArduinoManagerServiceImpl.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/ArduinoManagerServiceImpl.java index 3ef897128..fa7566b22 100644 --- a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/ArduinoManagerServiceImpl.java +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/ArduinoManagerServiceImpl.java @@ -34,7 +34,6 @@ import org.wso2.carbon.device.mgt.iot.exception.DeviceControllerException; import org.wso2.carbon.device.mgt.iot.util.ZipArchive; import org.wso2.carbon.device.mgt.iot.util.ZipUtil; import org.wso2.carbon.identity.jwt.client.extension.JWTClient; -import org.wso2.carbon.identity.jwt.client.extension.JWTClientManager; import org.wso2.carbon.identity.jwt.client.extension.dto.AccessTokenInfo; import org.wso2.carbon.identity.jwt.client.extension.exception.JWTClientException; import org.wso2.carbon.user.api.UserStoreException; @@ -184,14 +183,14 @@ public class ArduinoManagerServiceImpl implements ArduinoManagerService { apiApplicationKey = apiManagementProviderService.generateAndRetrieveApplicationKeys( ArduinoConstants.DEVICE_TYPE, tags, KEY_TYPE, applicationUsername, true); } - JWTClient jwtClient = JWTClientManager.getInstance().getJWTClient(); + JWTClient jwtClient = APIUtil.getJWTClientManagerService().getJWTClient(); String scopes = "device_type_" + ArduinoConstants.DEVICE_TYPE + " device_" + deviceId; AccessTokenInfo accessTokenInfo = jwtClient.getAccessToken(apiApplicationKey.getConsumerKey(), apiApplicationKey.getConsumerSecret(), owner, scopes); //create token - String accessToken = accessTokenInfo.getAccess_token(); - String refreshToken = accessTokenInfo.getRefresh_token(); + String accessToken = accessTokenInfo.getAccessToken(); + String refreshToken = accessTokenInfo.getRefreshToken(); //Register the device with CDMF boolean status = register(deviceId, deviceName); if (!status) { diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/dto/SensorData.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/dto/SensorData.java deleted file mode 100644 index 9e46a7cad..000000000 --- a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/dto/SensorData.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.wso2.carbon.device.mgt.iot.arduino.service.impl.dto; - -import org.codehaus.jackson.annotate.JsonIgnoreProperties; - -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -@XmlRootElement -/** - * This stores sensor event data for the device type. - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class SensorData { - - @XmlElement public Long time; - @XmlElement public String key; - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } - - public Long getTime() { - return time; - } - - public void setTime(Long time) { - this.time = time; - } - - @XmlElement public String value; - -} diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/dto/SensorRecord.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/dto/SensorRecord.java new file mode 100644 index 000000000..2593ed83b --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/dto/SensorRecord.java @@ -0,0 +1,68 @@ +package org.wso2.carbon.device.mgt.iot.arduino.service.impl.dto; + +import org.codehaus.jackson.annotate.JsonIgnoreProperties; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@XmlRootElement +/** + * This stores sensor event data for android sense. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SensorRecord { + + @XmlElementWrapper(required = true, name = "values") + private Map values; + + /** The id. */ + @XmlElement(required = false, name = "id") + private String id; + + /** + * Gets the values. + * @return the values + */ + public Map getValues() { + return values; + } + + /** + * Sets the values. + * @param values the values + */ + public void setValues(Map values) { + this.values = values; + } + + /** + * Sets the id. + * @param id the new id + */ + public void setId(String id) { + this.id = id; + } + + /** + * Gets the id. + * @return the id + */ + public String getId() { + return id; + } + + @Override + public String toString(){ + List valueList = new ArrayList(); + for (Map.Entry entry : values.entrySet()) { + valueList.add(entry.getKey() + ":" + entry.getValue()); + } + return valueList.toString(); + + } + +} diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/util/APIUtil.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/util/APIUtil.java index fb759f25a..44ce396e5 100644 --- a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/util/APIUtil.java +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/util/APIUtil.java @@ -2,9 +2,25 @@ package org.wso2.carbon.device.mgt.iot.arduino.service.impl.util; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.analytics.api.AnalyticsDataAPI; +import org.wso2.carbon.analytics.dataservice.commons.AnalyticsDataResponse; +import org.wso2.carbon.analytics.dataservice.commons.AnalyticsDrillDownRequest; +import org.wso2.carbon.analytics.dataservice.commons.SearchResultEntry; +import org.wso2.carbon.analytics.dataservice.commons.SortByField; +import org.wso2.carbon.analytics.dataservice.core.AnalyticsDataServiceUtils; +import org.wso2.carbon.analytics.datasource.commons.Record; +import org.wso2.carbon.analytics.datasource.commons.exception.AnalyticsException; import org.wso2.carbon.apimgt.application.extension.APIManagementProviderService; +import org.wso2.carbon.context.CarbonContext; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; +import org.wso2.carbon.device.mgt.iot.arduino.service.impl.dto.SensorRecord; +import org.wso2.carbon.identity.jwt.client.extension.service.JWTClientManagerService; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * This class provides utility functions used by REST-API. @@ -23,12 +39,6 @@ public class APIUtil { return username; } - public static String getTenantDomainOftheUser() { - PrivilegedCarbonContext threadLocalCarbonContext = PrivilegedCarbonContext.getThreadLocalCarbonContext(); - String tenantDomain = threadLocalCarbonContext.getTenantDomain(); - return tenantDomain; - } - public static DeviceManagementProviderService getDeviceManagementService() { PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); DeviceManagementProviderService deviceManagementProviderService = @@ -41,6 +51,86 @@ public class APIUtil { return deviceManagementProviderService; } + public static AnalyticsDataAPI getAnalyticsDataAPI() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + AnalyticsDataAPI analyticsDataAPI = + (AnalyticsDataAPI) ctx.getOSGiService(AnalyticsDataAPI.class, null); + if (analyticsDataAPI == null) { + String msg = "Analytics api service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return analyticsDataAPI; + } + + public static List getAllEventsForDevice(String tableName, String query, List sortByFields) throws AnalyticsException { + int tenantId = CarbonContext.getThreadLocalCarbonContext().getTenantId(); + AnalyticsDataAPI analyticsDataAPI = getAnalyticsDataAPI(); + int eventCount = analyticsDataAPI.searchCount(tenantId, tableName, query); + if (eventCount == 0) { + return null; + } + AnalyticsDrillDownRequest drillDownRequest = new AnalyticsDrillDownRequest(); + drillDownRequest.setQuery(query); + drillDownRequest.setTableName(tableName); + drillDownRequest.setRecordCount(eventCount); + if (sortByFields != null) { + drillDownRequest.setSortByFields(sortByFields); + } + List resultEntries = analyticsDataAPI.drillDownSearch(tenantId, drillDownRequest); + List recordIds = getRecordIds(resultEntries); + AnalyticsDataResponse response = analyticsDataAPI.get(tenantId, tableName, 1, null, recordIds); + Map sensorDatas = createSensorData(AnalyticsDataServiceUtils.listRecords( + analyticsDataAPI, response)); + List sortedSensorData = getSortedSensorData(sensorDatas, resultEntries); + return sortedSensorData; + } + + private static List getRecordIds(List searchResults) { + List ids = new ArrayList<>(); + for (SearchResultEntry searchResult : searchResults) { + ids.add(searchResult.getId()); + } + return ids; + } + + public static List getSortedSensorData(Map sensorDatas, + List searchResults) { + List sortedRecords = new ArrayList<>(); + for (SearchResultEntry searchResultEntry : searchResults) { + sortedRecords.add(sensorDatas.get(searchResultEntry.getId())); + } + return sortedRecords; + } + + /** + * Creates the SensorDatas from records. + * + * @param records the records + * @return the Map of SensorRecord + */ + public static Map createSensorData(List records) { + Map sensorDatas = new HashMap<>(); + for (Record record : records) { + SensorRecord sensorData = createSensorData(record); + sensorDatas.put(sensorData.getId(), sensorData); + } + return sensorDatas; + } + + /** + * Create a SensorRecord object out of a Record object + * + * @param record the record object + * @return SensorRecord object + */ + public static SensorRecord createSensorData(Record record) { + SensorRecord recordBean = new SensorRecord(); + recordBean.setId(record.getId()); + recordBean.setValues(record.getValues()); + return recordBean; + } + public static APIManagementProviderService getAPIManagementProviderService() { PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); APIManagementProviderService apiManagementProviderService = @@ -52,4 +142,21 @@ public class APIUtil { } return apiManagementProviderService; } + + public static JWTClientManagerService getJWTClientManagerService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + JWTClientManagerService jwtClientManagerService = + (JWTClientManagerService) ctx.getOSGiService(JWTClientManagerService.class, null); + if (jwtClientManagerService == null) { + String msg = "JWT Client manager service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return jwtClientManagerService; + } + + public static String getTenantDomainOftheUser() { + PrivilegedCarbonContext threadLocalCarbonContext = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + return threadLocalCarbonContext.getTenantDomain(); + } } diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/util/ArduinoServiceUtils.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/util/ArduinoServiceUtils.java index 421541c41..fcf49a80e 100644 --- a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/util/ArduinoServiceUtils.java +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.api/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/service/impl/util/ArduinoServiceUtils.java @@ -27,7 +27,7 @@ import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; import org.apache.http.impl.nio.client.HttpAsyncClients; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.device.mgt.analytics.data.publisher.exception.DataPublisherConfigurationException; -import org.wso2.carbon.device.mgt.analytics.data.publisher.service.DeviceAnalyticsService; +import org.wso2.carbon.device.mgt.analytics.data.publisher.service.EventsPublisherService; import org.wso2.carbon.device.mgt.common.DeviceManagementException; import org.wso2.carbon.device.mgt.iot.arduino.plugin.constants.ArduinoConstants; import javax.ws.rs.HttpMethod; @@ -193,8 +193,8 @@ public class ArduinoServiceUtils { String owner = ctx.getUsername(); Object metdaData[] = {owner, ArduinoConstants.DEVICE_TYPE, deviceId, System.currentTimeMillis()}; Object payloadData[] = {temperature}; - DeviceAnalyticsService deviceAnalyticsService = (DeviceAnalyticsService) ctx - .getOSGiService(DeviceAnalyticsService.class, null); + EventsPublisherService deviceAnalyticsService = (EventsPublisherService) ctx + .getOSGiService(EventsPublisherService.class, null); if (deviceAnalyticsService != null) { try { deviceAnalyticsService.publishEvent(TEMPERATURE_STREAM_DEFINITION, "1.0.0", metdaData, diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/pom.xml b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/pom.xml new file mode 100644 index 000000000..1c27e77a1 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/pom.xml @@ -0,0 +1,165 @@ + + + + + das-extensions + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + 4.0.0 + + org.wso2.carbon.event.input.adapter.extensions + bundle + WSO2 Carbon - Event Input MQTT Adapter Module + This provides the capability of connecting to existing broker that supports OAUTH + http://wso2.org + + + + org.wso2.carbon.analytics-common + org.wso2.carbon.event.input.adapter.core + + + org.wso2.carbon + org.wso2.carbon.logging + + + org.wso2.carbon + org.wso2.carbon.core + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + + + org.apache.httpcomponents.wso2 + httpcore + + + org.wso2.orbit.org.apache.httpcomponents + httpclient + + + com.googlecode.json-simple.wso2 + json-simple + + + org.wso2.carbon.devicemgt + org.wso2.carbon.identity.jwt.client.extension + + + com.jayway.jsonpath.wso2 + json-path + + + org.wso2.carbon.identity + org.wso2.carbon.identity.oauth.stub + + + + + + + org.apache.felix + maven-scr-plugin + + + generate-scr-descriptor + + scr + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.apache.felix + maven-bundle-plugin + true + + + ${project.artifactId} + ${project.artifactId} + + org.wso2.carbon.event.input.adapter.extensions.internal, + org.wso2.carbon.event.input.adapter.extensions.internal.* + + + !org.wso2.carbon.event.input.adapter.extensions.internal, + !org.wso2.carbon.event.input.adapter.extensions.internal.*, + org.wso2.carbon.event.input.adapter.extensions.* + + + org.wso2.carbon.event.input.adapter.core, + org.wso2.carbon.event.input.adapter.core.*, + javax.xml.namespace; version=0.0.0, + org.eclipse.paho.client.mqttv3.*, + org.apache.http;version="${httpclient.version.range}", + org.apache.http.message;version="${httpclient.version.range}", + org.apache.http.client;version="${httpclient.version.range}", + org.apache.http.impl;version="${httpclient.version.range}", + org.apache.http.conn.*;version="${httpclient.version.range}", + org.apache.http.util;version="${httpclient.version.range}", + org.apache.http.client.entity;version="${httpclient.version.range}", + org.apache.http.client.methods;version="${httpclient.version.range}", + org.apache.http.impl.client;version="${httpclient.version.range}", + org.json.simple.*, + org.wso2.carbon.identity.jwt.client.extension.*, + com.jayway.jsonpath.*, + javax.net.ssl, + org.apache.commons.logging, + org.apache.http.entity, + org.osgi.framework, + org.osgi.service.component, + org.wso2.carbon.context, + org.wso2.carbon.core, + javax.servlet, + javax.servlet.http, + org.apache.axiom.om.util, + org.osgi.service.http, + org.wso2.carbon.user.api, + org.wso2.carbon.user.core.service, + org.wso2.carbon.user.core.tenant, + org.wso2.carbon.utils, + org.wso2.carbon.utils.multitenancy, + org.wso2.carbon.identity.oauth2.stub;version="${carbon.identity.version.range}", + org.wso2.carbon.identity.oauth2.stub.dto;version="${carbon.identity.version.range}", + org.apache.axis2, + org.apache.axis2.client, + org.apache.axis2.context, + org.apache.axis2.transport.http, + org.apache.commons.httpclient, + org.apache.commons.httpclient.contrib.ssl, + org.apache.commons.httpclient.params, + org.apache.commons.httpclient.protocol, + org.apache.commons.pool, + org.apache.commons.pool.impl, + org.apache.log4j + + + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/ContentInfo.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/ContentInfo.java new file mode 100644 index 000000000..0cd60b993 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/ContentInfo.java @@ -0,0 +1,55 @@ +/* +* Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +* +* WSO2 Inc. licenses this file to you under the Apache License, +* Version 2.0 (the "License"); you may not use this file except +* in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +package org.wso2.carbon.event.input.adapter.extensions; + +/** + * This is the return type of the ContentValidator. + */ +public class ContentInfo { + /** + * true if the content is valid if not when false then content will not be published. + */ + private boolean isValidContent; + /** + * msgText to be returned. eg: if the content is encrypted then we can decrypt the content and then validate and + * return it. + */ + private String msgText; + + public ContentInfo(boolean isValidContent, String msgText) { + this.isValidContent = isValidContent; + this.msgText = msgText; + } + + public boolean isValidContent() { + return isValidContent; + } + + public void setIsValidContent(boolean isValidContent) { + this.isValidContent = isValidContent; + } + + public String getMsgText() { + return msgText; + } + + public void setMsgText(String msgText) { + this.msgText = msgText; + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/ContentValidator.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/ContentValidator.java new file mode 100644 index 000000000..8a0ca50ad --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/ContentValidator.java @@ -0,0 +1,33 @@ +/* +* Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +* +* WSO2 Inc. licenses this file to you under the Apache License, +* Version 2.0 (the "License"); you may not use this file except +* in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +package org.wso2.carbon.event.input.adapter.extensions; + +import java.util.Map; + +/** + * This interface will be triggered to validate the stream content before publishing. + */ +public interface ContentValidator { + /** + * + * @param params that related to input adapter to identify the client and the content + * @return + */ + ContentInfo validate(Map params); +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/HTTPEventAdapter.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/HTTPEventAdapter.java new file mode 100644 index 000000000..1c8c7f331 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/HTTPEventAdapter.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.wso2.carbon.event.input.adapter.extensions.http; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.osgi.service.http.HttpService; +import org.osgi.service.http.NamespaceException; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.event.input.adapter.core.InputEventAdapter; +import org.wso2.carbon.event.input.adapter.core.InputEventAdapterConfiguration; +import org.wso2.carbon.event.input.adapter.core.InputEventAdapterListener; +import org.wso2.carbon.event.input.adapter.core.exception.InputEventAdapterException; +import org.wso2.carbon.event.input.adapter.core.exception.InputEventAdapterRuntimeException; +import org.wso2.carbon.event.input.adapter.core.exception.TestConnectionNotSupportedException; +import org.wso2.carbon.event.input.adapter.extensions.http.util.HTTPEventAdapterConstants; +import org.wso2.carbon.event.input.adapter.extensions.internal.EventAdapterServiceDataHolder; +import org.wso2.carbon.utils.multitenancy.MultitenantConstants; + +import javax.servlet.ServletException; +import java.util.Hashtable; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public final class HTTPEventAdapter implements InputEventAdapter { + + private final InputEventAdapterConfiguration eventAdapterConfiguration; + private final Map globalProperties; + private InputEventAdapterListener eventAdaptorListener; + private final String id = UUID.randomUUID().toString(); + public static ExecutorService executorService; + private static final Log log = LogFactory.getLog(HTTPEventAdapter.class); + private boolean isConnected = false; + + public HTTPEventAdapter(InputEventAdapterConfiguration eventAdapterConfiguration, + Map globalProperties) { + this.eventAdapterConfiguration = eventAdapterConfiguration; + this.globalProperties = globalProperties; + } + + @Override + public void init(InputEventAdapterListener eventAdaptorListener) throws InputEventAdapterException { + this.eventAdaptorListener = eventAdaptorListener; + + //ThreadPoolExecutor will be assigned if it is null + if (executorService == null) { + int minThread; + int maxThread; + long defaultKeepAliveTime; + int jobQueueSize; + + //If global properties are available those will be assigned else constant values will be assigned + if (globalProperties.get(HTTPEventAdapterConstants.ADAPTER_MIN_THREAD_POOL_SIZE_NAME) != null) { + minThread = Integer + .parseInt(globalProperties.get(HTTPEventAdapterConstants.ADAPTER_MIN_THREAD_POOL_SIZE_NAME)); + } else { + minThread = HTTPEventAdapterConstants.ADAPTER_MIN_THREAD_POOL_SIZE; + } + + if (globalProperties.get(HTTPEventAdapterConstants.ADAPTER_MAX_THREAD_POOL_SIZE_NAME) != null) { + maxThread = Integer + .parseInt(globalProperties.get(HTTPEventAdapterConstants.ADAPTER_MAX_THREAD_POOL_SIZE_NAME)); + } else { + maxThread = HTTPEventAdapterConstants.ADAPTER_MAX_THREAD_POOL_SIZE; + } + + if (globalProperties.get(HTTPEventAdapterConstants.ADAPTER_KEEP_ALIVE_TIME_NAME) != null) { + defaultKeepAliveTime = Integer + .parseInt(globalProperties.get(HTTPEventAdapterConstants.ADAPTER_KEEP_ALIVE_TIME_NAME)); + } else { + defaultKeepAliveTime = HTTPEventAdapterConstants.DEFAULT_KEEP_ALIVE_TIME_IN_MILLS; + } + + if (globalProperties.get(HTTPEventAdapterConstants.ADAPTER_EXECUTOR_JOB_QUEUE_SIZE_NAME) != null) { + jobQueueSize = Integer + .parseInt(globalProperties.get(HTTPEventAdapterConstants.ADAPTER_EXECUTOR_JOB_QUEUE_SIZE_NAME)); + } else { + jobQueueSize = HTTPEventAdapterConstants.ADAPTER_EXECUTOR_JOB_QUEUE_SIZE; + } + + RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + try { + executor.getQueue().put(r); + } catch (InterruptedException e) { + log.error("Exception while adding event to executor queue : " + e.getMessage(), e); + } + } + + }; + + executorService = new ThreadPoolExecutor(minThread, maxThread, defaultKeepAliveTime, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(jobQueueSize), rejectedExecutionHandler); + + } + } + + @Override + public void testConnect() throws TestConnectionNotSupportedException { + throw new TestConnectionNotSupportedException("not-supported"); + } + + @Override + public void connect() { + registerDynamicEndpoint(eventAdapterConfiguration.getName()); + isConnected = true; + } + + @Override + public void disconnect() { + if (isConnected){ + isConnected = false; + unregisterDynamicEndpoint(eventAdapterConfiguration.getName()); + } + } + + @Override + public void destroy() { + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof HTTPEventAdapter)) + return false; + + HTTPEventAdapter that = (HTTPEventAdapter) o; + + return id.equals(that.id); + + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + @Override + public boolean isEventDuplicatedInCluster() { + return false; + } + + @Override + public boolean isPolling() { + return false; + } + + private void registerDynamicEndpoint(String adapterName) { + + String tenantDomain = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantDomain(); + int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId(); + + String endpoint; + if (MultitenantConstants.SUPER_TENANT_DOMAIN_NAME.equals(tenantDomain)) { + endpoint = HTTPEventAdapterConstants.ENDPOINT_PREFIX + adapterName; + } else { + endpoint = HTTPEventAdapterConstants.ENDPOINT_PREFIX + HTTPEventAdapterConstants.ENDPOINT_TENANT_KEY + + HTTPEventAdapterConstants.ENDPOINT_URL_SEPARATOR + tenantDomain + + HTTPEventAdapterConstants.ENDPOINT_URL_SEPARATOR + adapterName; + } + + try { + HttpService httpService = EventAdapterServiceDataHolder.getHTTPService(); + if (httpService == null) { + throw new InputEventAdapterRuntimeException( + "HttpService not available, Error in registering endpoint " + endpoint); + } + httpService.registerServlet(endpoint, new HTTPMessageServlet(eventAdaptorListener, tenantId, + eventAdapterConfiguration), + new Hashtable(), httpService.createDefaultHttpContext()); + } catch (ServletException | NamespaceException e) { + throw new InputEventAdapterRuntimeException("Error in registering endpoint " + endpoint, e); + } + + } + + private void unregisterDynamicEndpoint(String adapterName) { + HttpService httpService = EventAdapterServiceDataHolder.getHTTPService(); + String tenantDomain = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantDomain(); + String endpoint; + if (MultitenantConstants.SUPER_TENANT_DOMAIN_NAME.equals(tenantDomain)) { + endpoint = HTTPEventAdapterConstants.ENDPOINT_PREFIX + adapterName; + } else { + endpoint = HTTPEventAdapterConstants.ENDPOINT_PREFIX + HTTPEventAdapterConstants.ENDPOINT_TENANT_KEY + + HTTPEventAdapterConstants.ENDPOINT_URL_SEPARATOR + tenantDomain + + HTTPEventAdapterConstants.ENDPOINT_URL_SEPARATOR + adapterName; + } + if (httpService != null) { + httpService.unregister(endpoint); + } + + } +} \ No newline at end of file diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/HTTPEventAdapterFactory.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/HTTPEventAdapterFactory.java new file mode 100644 index 000000000..4a810d7d6 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/HTTPEventAdapterFactory.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2005 - 2014, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.wso2.carbon.event.input.adapter.extensions.http; + + +import org.wso2.carbon.event.input.adapter.core.InputEventAdapter; +import org.wso2.carbon.event.input.adapter.core.InputEventAdapterConfiguration; +import org.wso2.carbon.event.input.adapter.core.InputEventAdapterFactory; +import org.wso2.carbon.event.input.adapter.core.MessageType; +import org.wso2.carbon.event.input.adapter.core.Property; +import org.wso2.carbon.event.input.adapter.extensions.http.util.HTTPEventAdapterConstants; +import org.wso2.carbon.utils.CarbonUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.ResourceBundle; + +/** + * The http event adapter factory class to create a http input adapter + */ +public class HTTPEventAdapterFactory extends InputEventAdapterFactory { + + private ResourceBundle resourceBundle = + ResourceBundle.getBundle("org.wso2.carbon.event.input.adapter.extensions.http.i18n.Resources", Locale.getDefault()); + private int httpPort; + private int httpsPort; + private int portOffset; + + public HTTPEventAdapterFactory() { + portOffset = getPortOffset(); + httpPort = HTTPEventAdapterConstants.DEFAULT_HTTP_PORT + portOffset; + httpsPort = HTTPEventAdapterConstants.DEFAULT_HTTPS_PORT + portOffset; + } + + @Override + public String getType() { + return HTTPEventAdapterConstants.ADAPTER_TYPE_HTTP; + } + + @Override + public List getSupportedMessageFormats() { + List supportInputMessageTypes = new ArrayList(); + supportInputMessageTypes.add(MessageType.XML); + supportInputMessageTypes.add(MessageType.JSON); + supportInputMessageTypes.add(MessageType.TEXT); + return supportInputMessageTypes; + } + + @Override + public List getPropertyList() { + + List propertyList = new ArrayList(); + + // Transport Exposed + Property exposedTransportsProperty = new Property(HTTPEventAdapterConstants.EXPOSED_TRANSPORTS); + exposedTransportsProperty.setRequired(true); + exposedTransportsProperty.setDisplayName( + resourceBundle.getString(HTTPEventAdapterConstants.EXPOSED_TRANSPORTS)); + exposedTransportsProperty.setOptions( + new String[]{HTTPEventAdapterConstants.HTTPS, HTTPEventAdapterConstants.HTTP, + HTTPEventAdapterConstants.LOCAL, HTTPEventAdapterConstants.ALL}); + exposedTransportsProperty.setDefaultValue(HTTPEventAdapterConstants.ALL); + propertyList.add(exposedTransportsProperty); + + // OAUTH validation endpoint admin service username + Property username = new Property(HTTPEventAdapterConstants.USERNAME); + username.setRequired(true); + username.setDisplayName(resourceBundle.getString(HTTPEventAdapterConstants.USERNAME)); + username.setHint(resourceBundle.getString(HTTPEventAdapterConstants.USERNAME_HINT)); + propertyList.add(username); + + // OAUTH validation endpoint admin service password + Property password = new Property(HTTPEventAdapterConstants.PASSWORD); + password.setRequired(true); + password.setDisplayName(resourceBundle.getString(HTTPEventAdapterConstants.PASSWORD)); + password.setHint(resourceBundle.getString(HTTPEventAdapterConstants.PASSWORD_HINT)); + propertyList.add(password); + + // OAUTH validation endpoint + Property tokenValidationEndpoint = new Property(HTTPEventAdapterConstants.TOKEN_VALIDATION_ENDPOINT_URL); + tokenValidationEndpoint.setRequired(true); + tokenValidationEndpoint.setDisplayName(resourceBundle.getString(HTTPEventAdapterConstants.TOKEN_VALIDATION_ENDPOINT_URL)); + tokenValidationEndpoint.setHint(resourceBundle.getString(HTTPEventAdapterConstants.TOKEN_VALIDATION_ENDPOINT_URL_HINT)); + propertyList.add(tokenValidationEndpoint); + + Property maximumHttpConnectionPerHost = new Property(HTTPEventAdapterConstants.MAXIMUM_HTTP_CONNECTION_PER_HOST); + maximumHttpConnectionPerHost.setRequired(true); + maximumHttpConnectionPerHost.setDisplayName(resourceBundle.getString( + HTTPEventAdapterConstants.MAXIMUM_HTTP_CONNECTION_PER_HOST)); + maximumHttpConnectionPerHost.setHint(resourceBundle.getString( + HTTPEventAdapterConstants.MAXIMUM_HTTP_CONNECTION_PER_HOST_HINT)); + maximumHttpConnectionPerHost.setDefaultValue(HTTPEventAdapterConstants.MAX_HTTP_CONNECTION); + propertyList.add(maximumHttpConnectionPerHost); + + Property maxTotalHttpConnection = new Property(HTTPEventAdapterConstants.MAXIMUM_TOTAL_HTTP_CONNECTION); + maxTotalHttpConnection.setRequired(true); + maxTotalHttpConnection.setDisplayName(resourceBundle.getString( + HTTPEventAdapterConstants.MAXIMUM_TOTAL_HTTP_CONNECTION)); + maxTotalHttpConnection.setHint(resourceBundle.getString( + HTTPEventAdapterConstants.MAXIMUM_TOTAL_HTTP_CONNECTION_HINT)); + maxTotalHttpConnection.setDefaultValue(HTTPEventAdapterConstants.MAX_TOTAL_HTTP_CONNECTION); + propertyList.add(maxTotalHttpConnection); + + //Content Validator details + Property contentValidator = new Property(HTTPEventAdapterConstants.ADAPTER_CONF_CONTENT_VALIDATOR_CLASSNAME); + contentValidator.setDisplayName( + resourceBundle.getString(HTTPEventAdapterConstants.ADAPTER_CONF_CONTENT_VALIDATOR_CLASSNAME)); + contentValidator.setRequired(false); + contentValidator.setHint( + resourceBundle.getString(HTTPEventAdapterConstants.ADAPTER_CONF_CONTENT_VALIDATOR_CLASSNAME_HINT)); + propertyList.add(contentValidator); + contentValidator.setDefaultValue(HTTPEventAdapterConstants.DEFAULT); + propertyList.add(contentValidator); + + //Content Validator Params details + Property contentValidatorParams = new Property(HTTPEventAdapterConstants.ADAPTER_CONF_CONTENT_VALIDATOR_PARAMS); + contentValidatorParams.setDisplayName( + resourceBundle.getString(HTTPEventAdapterConstants.ADAPTER_CONF_CONTENT_VALIDATOR_PARAMS)); + contentValidatorParams.setRequired(false); + contentValidatorParams.setHint( + resourceBundle.getString(HTTPEventAdapterConstants.ADAPTER_CONF_CONTENT_VALIDATOR_PARAMS_HINT)); + propertyList.add(contentValidatorParams); + contentValidatorParams.setDefaultValue(HTTPEventAdapterConstants.MQTT_CONTENT_VALIDATION_DEFAULT_PARAMETERS); + propertyList.add(contentValidatorParams); + return propertyList; + } + + @Override + public String getUsageTips() { + return resourceBundle.getString(HTTPEventAdapterConstants.ADAPTER_USAGE_TIPS_PREFIX) + httpPort + + resourceBundle.getString(HTTPEventAdapterConstants.ADAPTER_USAGE_TIPS_MID1) + httpsPort + + resourceBundle.getString(HTTPEventAdapterConstants.ADAPTER_USAGE_TIPS_MID2) + httpPort + + resourceBundle.getString(HTTPEventAdapterConstants.ADAPTER_USAGE_TIPS_MID3) + httpsPort + + resourceBundle.getString(HTTPEventAdapterConstants.ADAPTER_USAGE_TIPS_POSTFIX); + } + + @Override + public InputEventAdapter createEventAdapter(InputEventAdapterConfiguration eventAdapterConfiguration, + Map globalProperties) { + return new HTTPEventAdapter(eventAdapterConfiguration, globalProperties); + } + + private int getPortOffset() { + return CarbonUtils.getPortFromServerConfig(HTTPEventAdapterConstants.CARBON_CONFIG_PORT_OFFSET_NODE) + 1; + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/HTTPMessageServlet.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/HTTPMessageServlet.java new file mode 100644 index 000000000..4220ec773 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/HTTPMessageServlet.java @@ -0,0 +1,339 @@ +/* + * Copyright (c) 2005 - 2014, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.wso2.carbon.event.input.adapter.extensions.http; + +import org.apache.axis2.context.ServiceContext; +import org.apache.axis2.transport.http.HTTPConstants; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.commons.pool.impl.GenericObjectPool; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.event.input.adapter.core.InputEventAdapterConfiguration; +import org.wso2.carbon.event.input.adapter.core.InputEventAdapterListener; +import org.wso2.carbon.event.input.adapter.extensions.ContentInfo; +import org.wso2.carbon.event.input.adapter.extensions.ContentValidator; +import org.wso2.carbon.event.input.adapter.extensions.http.oauth.OAuthTokenValidaterStubFactory; +import org.wso2.carbon.event.input.adapter.extensions.http.util.AuthenticationInfo; +import org.wso2.carbon.event.input.adapter.extensions.http.util.HTTPContentValidator; +import org.wso2.carbon.event.input.adapter.extensions.http.util.HTTPEventAdapterConstants; +import org.wso2.carbon.event.input.adapter.extensions.internal.EventAdapterServiceDataHolder; +import org.wso2.carbon.identity.oauth2.stub.OAuth2TokenValidationServiceStub; +import org.wso2.carbon.identity.oauth2.stub.dto.OAuth2TokenValidationRequestDTO; +import org.wso2.carbon.identity.oauth2.stub.dto.OAuth2TokenValidationRequestDTO_OAuth2AccessToken; +import org.wso2.carbon.identity.oauth2.stub.dto.OAuth2TokenValidationResponseDTO; +import org.wso2.carbon.user.api.UserStoreException; +import org.wso2.carbon.user.core.service.RealmService; +import org.wso2.carbon.utils.multitenancy.MultitenantUtils; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.rmi.RemoteException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class HTTPMessageServlet extends HttpServlet { + + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String AUTH_MESSAGE_STORE_AUTHENTICATION_INFO = "AUTH_MESSAGE_STORE_AUTHENTICATION_INFO"; + private static final String AUTH_FAILURE_RESPONSE = "_AUTH_FAILURE_"; + private static final Pattern PATTERN = Pattern.compile("[B|b]earer\\s"); + private static final String TOKEN_TYPE = "bearer"; + private static String cookie; + private static Log log = LogFactory.getLog(HTTPMessageServlet.class); + private GenericObjectPool stubs; + + private InputEventAdapterListener eventAdaptorListener; + private int tenantId; + private String exposedTransports; + + public HTTPMessageServlet(InputEventAdapterListener eventAdaptorListener, int tenantId, + InputEventAdapterConfiguration eventAdapterConfiguration) { + this.eventAdaptorListener = eventAdaptorListener; + this.tenantId = tenantId; + this.exposedTransports = eventAdapterConfiguration.getProperties().get( + HTTPEventAdapterConstants.EXPOSED_TRANSPORTS); + this.stubs = new GenericObjectPool(new OAuthTokenValidaterStubFactory(eventAdapterConfiguration)); + } + + private String getBearerToken(HttpServletRequest request) { + String authorizationHeader = request.getHeader(AUTHORIZATION_HEADER); + if (authorizationHeader != null) { + Matcher matcher = PATTERN.matcher(authorizationHeader); + if (matcher.find()) { + authorizationHeader = authorizationHeader.substring(matcher.end()); + } + } + return authorizationHeader; + } + + private AuthenticationInfo checkAuthentication(HttpServletRequest req) { + AuthenticationInfo authenticationInfo = (AuthenticationInfo) req.getSession().getAttribute( + AUTH_MESSAGE_STORE_AUTHENTICATION_INFO); + if (authenticationInfo != null) { + return authenticationInfo; + } + String bearerToken = getBearerToken(req); + if (bearerToken == null) { + return authenticationInfo; + } + + RealmService realmService = EventAdapterServiceDataHolder.getRealmService(); + try { + authenticationInfo = validateToken(bearerToken); + boolean success = authenticationInfo.isAuthenticated(); + if (success) { + req.getSession().setAttribute(AUTH_MESSAGE_STORE_AUTHENTICATION_INFO, authenticationInfo); + } + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug("checkAuthentication() fail: " + e.getMessage(), e); + } + } + return authenticationInfo; + } + + /** + * This method gets a string accessToken and validates it + * + * @param token which need to be validated. + * @return AuthenticationInfo with the validated results. + */ + private AuthenticationInfo validateToken(String token) { + OAuth2TokenValidationServiceStub tokenValidationServiceStub = null; + try { + Object stub = this.stubs.borrowObject(); + if (stub != null) { + tokenValidationServiceStub = (OAuth2TokenValidationServiceStub) stub; + if (cookie != null) { + tokenValidationServiceStub._getServiceClient().getOptions().setProperty( + HTTPConstants.COOKIE_STRING, cookie); + } + return getAuthenticationInfo(token, tokenValidationServiceStub); + } else { + log.warn("Stub initialization failed."); + } + } catch (RemoteException e) { + log.error("Error on connecting with the validation endpoint.", e); + } catch (Exception e) { + log.error("Error occurred in borrowing an validation stub from the pool.", e); + + } finally { + try { + if (tokenValidationServiceStub != null) { + this.stubs.returnObject(tokenValidationServiceStub); + } + } catch (Exception e) { + log.warn("Error occurred while returning the object back to the oauth token validation service " + + "stub pool.", e); + } + } + AuthenticationInfo authenticationInfo = new AuthenticationInfo(); + authenticationInfo.setAuthenticated(false); + authenticationInfo.setTenantId(-1); + return authenticationInfo; + } + + /** + * This creates an AuthenticationInfo object that is used for authorization. This method will validate the token + * and + * sets the required parameters to the object. + * + * @param token that needs to be validated. + * @param tokenValidationServiceStub stub that is used to call the external service. + * @return AuthenticationInfo This contains the information related to authenticated client. + * @throws RemoteException that triggers when failing to call the external service.. + */ + private AuthenticationInfo getAuthenticationInfo(String token, + OAuth2TokenValidationServiceStub tokenValidationServiceStub) + throws RemoteException, UserStoreException { + AuthenticationInfo authenticationInfo = new AuthenticationInfo(); + OAuth2TokenValidationRequestDTO validationRequest = new OAuth2TokenValidationRequestDTO(); + OAuth2TokenValidationRequestDTO_OAuth2AccessToken accessToken = + new OAuth2TokenValidationRequestDTO_OAuth2AccessToken(); + accessToken.setTokenType(TOKEN_TYPE); + accessToken.setIdentifier(token); + validationRequest.setAccessToken(accessToken); + boolean authenticated; + OAuth2TokenValidationResponseDTO tokenValidationResponse; + tokenValidationResponse = tokenValidationServiceStub.validate(validationRequest); + if (tokenValidationResponse == null) { + authenticationInfo.setAuthenticated(false); + return authenticationInfo; + } + authenticated = tokenValidationResponse.getValid(); + if (authenticated) { + String authorizedUser = tokenValidationResponse.getAuthorizedUser(); + String username = MultitenantUtils.getTenantAwareUsername(authorizedUser); + String tenantDomain = MultitenantUtils.getTenantDomain(authorizedUser); + authenticationInfo.setUsername(username); + authenticationInfo.setTenantDomain(tenantDomain); + RealmService realmService = EventAdapterServiceDataHolder.getRealmService(); + int tenantId = realmService.getTenantManager().getTenantId(authenticationInfo.getTenantDomain()); + authenticationInfo.setTenantId(tenantId); + } else { + if (log.isDebugEnabled()) { + log.debug("Token validation failed for token: " + token); + } + } + ServiceContext serviceContext = tokenValidationServiceStub._getServiceClient() + .getLastOperationContext().getServiceContext(); + cookie = (String) serviceContext.getProperty(HTTPConstants.COOKIE_STRING); + authenticationInfo.setAuthenticated(authenticated); + return authenticationInfo; + } + + + private String inputStreamToString(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buff = new byte[1024]; + int i; + while ((i = in.read(buff)) > 0) { + out.write(buff, 0, i); + } + out.close(); + return out.toString(); + } + + @Override + protected void doPost(HttpServletRequest req, + HttpServletResponse res) throws IOException { + + String data = this.inputStreamToString(req.getInputStream()); + if (data == null) { + log.warn("Event Object is empty/null"); + return; + } + AuthenticationInfo authenticationInfo = null; + if (exposedTransports.equalsIgnoreCase(HTTPEventAdapterConstants.HTTPS)) { + if (!req.isSecure()) { + res.setStatus(403); + log.error("Only Secured endpoint is enabled for requests"); + return; + } else { + authenticationInfo = this.checkAuthentication(req); + int tenantId = authenticationInfo != null ? authenticationInfo.getTenantId() : -1; + if (tenantId == -1) { + res.getOutputStream().write(AUTH_FAILURE_RESPONSE.getBytes()); + res.setStatus(401); + log.error("Authentication failed for the request"); + return; + } else if (tenantId != this.tenantId) { + res.getOutputStream().write(AUTH_FAILURE_RESPONSE.getBytes()); + res.setStatus(401); + log.error("Authentication failed for the request"); + return; + } + } + } else if (exposedTransports.equalsIgnoreCase(HTTPEventAdapterConstants.HTTP)) { + if (req.isSecure()) { + res.setStatus(403); + log.error("Only unsecured endpoint is enabled for requests"); + return; + } + } else { + if (req.isSecure()) { + authenticationInfo = this.checkAuthentication(req); + int tenantId = authenticationInfo != null ? authenticationInfo.getTenantId() : -1; + if (tenantId == -1) { + res.getOutputStream().write(AUTH_FAILURE_RESPONSE.getBytes()); + res.setStatus(401); + log.error("Authentication failed for the request"); + return; + } else if (tenantId != this.tenantId) { + res.getOutputStream().write(AUTH_FAILURE_RESPONSE.getBytes()); + res.setStatus(401); + log.error("Authentication failed for the request"); + return; + } + } + + } + + if (log.isDebugEnabled()) { + log.debug("Message : " + data); + } + + if (authenticationInfo != null) { + Map paramMap = new HashMap<>(); + Enumeration reqParameterNames = req.getParameterNames(); + while (reqParameterNames.hasMoreElements()) { + paramMap.put(reqParameterNames.nextElement(), req.getParameter(reqParameterNames.nextElement())); + } + paramMap.put(HTTPEventAdapterConstants.USERNAME_TAG, authenticationInfo.getUsername()); + paramMap.put(HTTPEventAdapterConstants.TENANT_DOMAIN_TAG, authenticationInfo.getTenantDomain()); + paramMap.put(HTTPEventAdapterConstants.PAYLOAD_TAG, data); + ContentValidator contentValidator = new HTTPContentValidator(); + ContentInfo contentInfo = contentValidator.validate(paramMap); + if (contentInfo != null && contentInfo.isValidContent()) { + HTTPEventAdapter.executorService.submit(new HTTPRequestProcessor(eventAdaptorListener, + contentInfo.getMsgText(), tenantId)); + + } + + } + + } + + @Override + protected void doGet(HttpServletRequest req, + HttpServletResponse res) throws IOException { + doPost(req, res); + } + + public class HTTPRequestProcessor implements Runnable { + + private InputEventAdapterListener inputEventAdapterListener; + private String payload; + private int tenantId; + + public HTTPRequestProcessor(InputEventAdapterListener inputEventAdapterListener, + String payload, int tenantId) { + this.inputEventAdapterListener = inputEventAdapterListener; + this.payload = payload; + this.tenantId = tenantId; + } + + public void run() { + try { + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantId(tenantId); + + if (log.isDebugEnabled()) { + log.debug("Event received in HTTP Event Adapter - " + payload); + } + + if (payload.trim() != null) { + inputEventAdapterListener.onEvent(payload); + } else { + log.warn("Dropping the empty/null event received through http adapter"); + } + } catch (Exception e) { + log.error("Error while parsing http request for processing: " + e.getMessage(), e); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + } + +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/oauth/OAuthTokenValidaterStubFactory.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/oauth/OAuthTokenValidaterStubFactory.java new file mode 100644 index 000000000..57962f007 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/oauth/OAuthTokenValidaterStubFactory.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.wso2.carbon.event.input.adapter.extensions.http.oauth; + +import org.apache.axis2.AxisFault; +import org.apache.axis2.Constants; +import org.apache.axis2.client.Options; +import org.apache.axis2.client.ServiceClient; +import org.apache.axis2.transport.http.HTTPConstants; +import org.apache.axis2.transport.http.HttpTransportProperties; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpConnectionManager; +import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; +import org.apache.commons.httpclient.contrib.ssl.EasySSLProtocolSocketFactory; +import org.apache.commons.httpclient.params.HttpConnectionManagerParams; +import org.apache.commons.httpclient.protocol.Protocol; +import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; +import org.apache.commons.pool.BasePoolableObjectFactory; +import org.apache.log4j.Logger; +import org.wso2.carbon.event.input.adapter.core.InputEventAdapterConfiguration; +import org.wso2.carbon.event.input.adapter.extensions.http.oauth.exception.OAuthTokenValidationException; +import org.wso2.carbon.event.input.adapter.extensions.http.util.HTTPEventAdapterConstants; +import org.wso2.carbon.identity.oauth2.stub.OAuth2TokenValidationServiceStub; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.GeneralSecurityException; + +/** + * This follows object pool pattern to manage the stub for oauth validation service. + */ +public class OAuthTokenValidaterStubFactory extends BasePoolableObjectFactory { + private static final Logger log = Logger.getLogger(OAuthTokenValidaterStubFactory.class); + private HttpClient httpClient; + InputEventAdapterConfiguration eventAdapterConfiguration; + + + public OAuthTokenValidaterStubFactory(InputEventAdapterConfiguration eventAdapterConfiguration) { + this.eventAdapterConfiguration = eventAdapterConfiguration; + this.httpClient = createHttpClient(); + } + + /** + * This creates a OAuth2TokenValidationServiceStub object to the pool. + * + * @return an OAuthValidationStub object + * @throws Exception thrown when creating the object. + */ + @Override + public Object makeObject() throws Exception { + return this.generateStub(); + } + + /** + * This is used to clean up the OAuth validation stub and releases to the object pool. + * + * @param o object that needs to be released. + * @throws Exception throws when failed to release to the pool + */ + @Override + public void passivateObject(Object o) throws Exception { + if (o instanceof OAuth2TokenValidationServiceStub) { + OAuth2TokenValidationServiceStub stub = (OAuth2TokenValidationServiceStub) o; + stub._getServiceClient().cleanupTransport(); + } + } + + /** + * This is used to create a stub which will be triggered through object pool factory, which will create an + * instance of it. + * + * @return OAuth2TokenValidationServiceStub stub that is used to call an external service. + * @throws OAuthTokenValidationException will be thrown when initialization failed. + */ + private OAuth2TokenValidationServiceStub generateStub() throws OAuthTokenValidationException { + OAuth2TokenValidationServiceStub stub; + try { + URL hostURL = new URL(eventAdapterConfiguration.getProperties().get( + HTTPEventAdapterConstants.TOKEN_VALIDATION_ENDPOINT_URL)); + if (hostURL != null) { + stub = new OAuth2TokenValidationServiceStub(hostURL.toString()); + if (stub != null) { + ServiceClient client = stub._getServiceClient(); + client.getServiceContext().getConfigurationContext().setProperty( + HTTPConstants.CACHED_HTTP_CLIENT, httpClient); + + HttpTransportProperties.Authenticator auth = + new HttpTransportProperties.Authenticator(); + auth.setPreemptiveAuthentication(true); + String username = eventAdapterConfiguration.getProperties().get(HTTPEventAdapterConstants + .USERNAME); + String password = eventAdapterConfiguration.getProperties().get(HTTPEventAdapterConstants + .PASSWORD); + auth.setPassword(username); + auth.setUsername(password); + Options options = client.getOptions(); + options.setProperty(HTTPConstants.AUTHENTICATE, auth); + options.setProperty(HTTPConstants.REUSE_HTTP_CLIENT, Constants.VALUE_TRUE); + client.setOptions(options); + if (hostURL.getProtocol().equals("https")) { + // set up ssl factory since axis2 https transport is used. + EasySSLProtocolSocketFactory sslProtocolSocketFactory = + createProtocolSocketFactory(); + Protocol authhttps = new Protocol(hostURL.getProtocol(), + (ProtocolSocketFactory) sslProtocolSocketFactory, + hostURL.getPort()); + Protocol.registerProtocol(hostURL.getProtocol(), authhttps); + options.setProperty(HTTPConstants.CUSTOM_PROTOCOL_HANDLER, authhttps); + } + } else { + String errorMsg = "OAuth Validation instanization failed."; + throw new OAuthTokenValidationException(errorMsg); + } + } else { + String errorMsg = "host url is invalid"; + throw new OAuthTokenValidationException(errorMsg); + } + } catch (AxisFault axisFault) { + throw new OAuthTokenValidationException( + "Error occurred while creating the OAuth2TokenValidationServiceStub.", axisFault); + } catch (MalformedURLException e) { + throw new OAuthTokenValidationException( + "Error occurred while parsing token endpoint URL", e); + } + + return stub; + } + + /** + * This is required to create a trusted connection with the external entity. + * Have to manually configure it since we use CommonHTTPTransport(axis2 transport) in axis2. + * + * @return an EasySSLProtocolSocketFactory for SSL communication. + */ + private EasySSLProtocolSocketFactory createProtocolSocketFactory() throws OAuthTokenValidationException { + try { + EasySSLProtocolSocketFactory easySSLPSFactory = new EasySSLProtocolSocketFactory(); + return easySSLPSFactory; + } catch (IOException e) { + String errorMsg = "Failed to initiate EasySSLProtocolSocketFactory."; + throw new OAuthTokenValidationException(errorMsg, e); + } catch (GeneralSecurityException e) { + String errorMsg = "Failed to set the key material in easy ssl factory."; + throw new OAuthTokenValidationException(errorMsg, e); + } + } + + /** + * This created httpclient pool that can be used to connect to external entity. This connection can be configured + * via broker.xml by setting up the required http connection parameters. + * + * @return an instance of HttpClient that is configured with MultiThreadedHttpConnectionManager + */ + private HttpClient createHttpClient() { + HttpConnectionManagerParams params = new HttpConnectionManagerParams(); + params.setDefaultMaxConnectionsPerHost(Integer.parseInt(eventAdapterConfiguration.getProperties().get( + HTTPEventAdapterConstants.MAXIMUM_HTTP_CONNECTION_PER_HOST))); + params.setMaxTotalConnections(Integer.parseInt(eventAdapterConfiguration.getProperties().get( + HTTPEventAdapterConstants.MAXIMUM_TOTAL_HTTP_CONNECTION))); + HttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager(); + connectionManager.setParams(params); + return new HttpClient(connectionManager); + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/oauth/exception/OAuthTokenValidationException.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/oauth/exception/OAuthTokenValidationException.java new file mode 100644 index 000000000..d7b7b5797 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/oauth/exception/OAuthTokenValidationException.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.wso2.carbon.event.input.adapter.extensions.http.oauth.exception; + +/** + * This Exception will be thrown, when there any interference with token validation flow. + */ +public class OAuthTokenValidationException extends Exception { + private String errMessage; + + public OAuthTokenValidationException(String msg, Exception nestedEx) { + super(msg, nestedEx); + setErrorMessage(msg); + } + + public OAuthTokenValidationException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public OAuthTokenValidationException(String msg) { + super(msg); + setErrorMessage(msg); + } + + public OAuthTokenValidationException() { + super(); + } + + public OAuthTokenValidationException(Throwable cause) { + super(cause); + } + + public String getErrorMessage() { + return errMessage; + } + + public void setErrorMessage(String errMessage) { + this.errMessage = errMessage; + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/AuthenticationInfo.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/AuthenticationInfo.java new file mode 100644 index 000000000..7e64cd36e --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/AuthenticationInfo.java @@ -0,0 +1,52 @@ +package org.wso2.carbon.event.input.adapter.extensions.http.util; + +public class AuthenticationInfo { + + /** + * this variable is used to check whether the client is authenticated. + */ + private boolean authenticated; + private String username; + private String tenantDomain; + private int tenantId; + /** + * returns whether the client is authenticated + */ + public boolean isAuthenticated() { + return authenticated; + } + + public void setAuthenticated(boolean authenticated) { + this.authenticated = authenticated; + } + + /** + * returns the authenticated client username + */ + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + /** + * return the authenticated client tenant domain + */ + public String getTenantDomain() { + return tenantDomain; + } + + public void setTenantDomain(String tenantDomain) { + this.tenantDomain = tenantDomain; + } + + public int getTenantId() { + return tenantId; + } + + public void setTenantId(int tenantId) { + this.tenantId = tenantId; + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/HTTPContentValidator.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/HTTPContentValidator.java new file mode 100644 index 000000000..d6a163e87 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/HTTPContentValidator.java @@ -0,0 +1,45 @@ +/* +* Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +* +* WSO2 Inc. licenses this file to you under the Apache License, +* Version 2.0 (the "License"); you may not use this file except +* in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +package org.wso2.carbon.event.input.adapter.extensions.http.util; + +import com.jayway.jsonpath.JsonPath; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.event.input.adapter.extensions.ContentInfo; +import org.wso2.carbon.event.input.adapter.extensions.ContentValidator; + +import java.util.Map; + +public class HTTPContentValidator implements ContentValidator { + private static final Log log = LogFactory.getLog(HTTPContentValidator.class); + + @Override + public ContentInfo validate(Map paramMap) { + String deviceId = paramMap.get("deviceId"); + + String msg = paramMap.get(HTTPEventAdapterConstants.PAYLOAD_TAG); + String deviceIdJsonPath = paramMap.get(HTTPEventAdapterConstants.DEVICE_ID_JSON_PATH); + Object res = JsonPath.read(msg, deviceIdJsonPath); + String deviceIdFromContent = (res != null) ? res.toString() : ""; + if (deviceIdFromContent.equals(deviceId)) { + return new ContentInfo(true, msg); + } + return new ContentInfo(false, msg); + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/HTTPEventAdapterConstants.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/HTTPEventAdapterConstants.java new file mode 100644 index 000000000..a03112c77 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/HTTPEventAdapterConstants.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.wso2.carbon.event.input.adapter.extensions.http.util; + + +public final class HTTPEventAdapterConstants { + + private HTTPEventAdapterConstants() { + } + + public static final String ADAPTER_TYPE_HTTP = "oauth-http"; + public static final String ADAPTER_USAGE_TIPS_PREFIX = "http.usage.tips_prefix"; + public static final String ADAPTER_USAGE_TIPS_MID1 = "http.usage.tips_mid1"; + public static final String ADAPTER_USAGE_TIPS_MID2 = "http.usage.tips_mid2"; + public static final String ADAPTER_USAGE_TIPS_MID3 = "http.usage.tips_mid3"; + public static final String ADAPTER_USAGE_TIPS_POSTFIX = "http.usage.tips_postfix"; + public static final int ADAPTER_MIN_THREAD_POOL_SIZE = 8; + public static final int ADAPTER_MAX_THREAD_POOL_SIZE = 100; + public static final int ADAPTER_EXECUTOR_JOB_QUEUE_SIZE = 10000; + public static final long DEFAULT_KEEP_ALIVE_TIME_IN_MILLS = 20000; + public static final String ENDPOINT_PREFIX = "/endpoints/"; + public static final String ENDPOINT_URL_SEPARATOR = "/"; + public static final String ENDPOINT_TENANT_KEY = "t"; + public static final String ADAPTER_MIN_THREAD_POOL_SIZE_NAME = "minThread"; + public static final String ADAPTER_MAX_THREAD_POOL_SIZE_NAME = "maxThread"; + public static final String ADAPTER_KEEP_ALIVE_TIME_NAME = "keepAliveTimeInMillis"; + public static final String ADAPTER_EXECUTOR_JOB_QUEUE_SIZE_NAME = "jobQueueSize"; + public static final String EXPOSED_TRANSPORTS = "transports"; + public static final String HTTPS = "https"; + public static final String HTTP = "http"; + public static final String LOCAL = "local"; + public static final String ALL = "all"; + public static final String CARBON_CONFIG_PORT_OFFSET_NODE = "Ports.Offset"; + public static final int DEFAULT_HTTP_PORT = 9763; + public static final int DEFAULT_HTTPS_PORT = 9443; + public static final String MAXIMUM_TOTAL_HTTP_CONNECTION = "maximumTotalHttpConnection"; + public static final String MAXIMUM_TOTAL_HTTP_CONNECTION_HINT = "maximumTotalHttpConnection.hint"; + public static final String MAXIMUM_HTTP_CONNECTION_PER_HOST = "maximumHttpConnectionPerHost"; + public static final String MAXIMUM_HTTP_CONNECTION_PER_HOST_HINT = "maximumHttpConnectionPerHost.hint"; + public static final String TOKEN_VALIDATION_ENDPOINT_URL = "tokenValidationEndpointUrl"; + public static final String TOKEN_VALIDATION_ENDPOINT_URL_HINT = "tokenValidationEndpointUrl.hint"; + public static final String USERNAME = "username"; + public static final String USERNAME_HINT = "username.hint"; + public static final String PASSWORD = "password"; + public static final String PASSWORD_HINT = "password.hint"; + public static final String DEFAULT_STRING = "default"; + public static final String MAX_HTTP_CONNECTION = "2"; + public static final String MAX_TOTAL_HTTP_CONNECTION = "100"; + public static final String TENANT_DOMAIN_TAG = "tenantDomain"; + public static final String USERNAME_TAG = "username"; + public static final String PAYLOAD_TAG = "payload"; + public static final String DEVICE_ID_JSON_PATH = "device_id_json_path"; + public static final String ADAPTER_CONF_CONTENT_VALIDATOR_CLASSNAME = "contentValidation"; + public static final String ADAPTER_CONF_CONTENT_VALIDATOR_CLASSNAME_HINT = "contentValidation.hint"; + public static final String ADAPTER_CONF_CONTENT_VALIDATOR_PARAMS = "contentValidationParams"; + public static final String ADAPTER_CONF_CONTENT_VALIDATOR_PARAMS_HINT = "contentValidationParams.hint"; + public static final String DEFAULT = "default"; + public static final String MQTT_CONTENT_VALIDATION_DEFAULT_PARAMETERS = + "device_id_json_path:meta_deviceId"; +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/internal/EventAdapterServiceComponent.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/internal/EventAdapterServiceComponent.java new file mode 100644 index 000000000..397487ab5 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/internal/EventAdapterServiceComponent.java @@ -0,0 +1,77 @@ +/* +* Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +* +* WSO2 Inc. licenses this file to you under the Apache License, +* Version 2.0 (the "License"); you may not use this file except +* in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.wso2.carbon.event.input.adapter.extensions.internal; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.http.HttpService; +import org.wso2.carbon.event.input.adapter.core.InputEventAdapterFactory; +import org.wso2.carbon.event.input.adapter.extensions.http.HTTPEventAdapterFactory; +import org.wso2.carbon.event.input.adapter.extensions.mqtt.MQTTEventAdapterFactory; +import org.wso2.carbon.user.core.service.RealmService; + +/** + * @scr.component component.name="input.iot.Mqtt.AdapterService.component" immediate="true" + */ + +/** + * @scr.component name="org.wso2.carbon.event.input.adapter.extension.EventAdapterServiceComponent" immediate="true" + * @scr.reference name="user.realmservice.default" + * interface="org.wso2.carbon.user.core.service.RealmService" cardinality="1..1" + * policy="dynamic" bind="setRealmService" unbind="unsetRealmService" + * @scr.reference name="http.service" interface="org.osgi.service.http.HttpService" + * cardinality="1..1" policy="dynamic" bind="setHttpService" unbind="unsetHttpService" + */ +public class EventAdapterServiceComponent { + + private static final Log log = LogFactory.getLog(EventAdapterServiceComponent.class); + + protected void activate(ComponentContext context) { + try { + InputEventAdapterFactory mqttEventAdapterFactory = new MQTTEventAdapterFactory(); + context.getBundleContext().registerService(InputEventAdapterFactory.class.getName(), + mqttEventAdapterFactory, null); + InputEventAdapterFactory httpEventEventAdapterFactory = new HTTPEventAdapterFactory(); + context.getBundleContext().registerService(InputEventAdapterFactory.class.getName(), + httpEventEventAdapterFactory, null); + if (log.isDebugEnabled()) { + log.debug("Successfully deployed the input IoT-MQTT adapter service"); + } + } catch (RuntimeException e) { + log.error("Can not create the input IoT-MQTT adapter service ", e); + } + } + + protected void setRealmService(RealmService realmService) { + EventAdapterServiceDataHolder.registerRealmService(realmService); + } + + protected void unsetRealmService(RealmService realmService) { + EventAdapterServiceDataHolder.registerRealmService(null); + } + + protected void setHttpService(HttpService httpService) { + EventAdapterServiceDataHolder.registerHTTPService(httpService); + } + + protected void unsetHttpService(HttpService httpService) { + EventAdapterServiceDataHolder.registerHTTPService(null); + } + +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/internal/EventAdapterServiceDataHolder.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/internal/EventAdapterServiceDataHolder.java new file mode 100644 index 000000000..324196bf9 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/internal/EventAdapterServiceDataHolder.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.wso2.carbon.event.input.adapter.extensions.internal; + +import org.osgi.service.http.HttpService; +import org.wso2.carbon.user.core.service.RealmService; + +/** + * common place to hold some OSGI service references. + */ +public final class EventAdapterServiceDataHolder { + + private static RealmService realmService; + private static HttpService httpService; + + private EventAdapterServiceDataHolder() { + } + + public static void registerRealmService( + RealmService realmService) { + EventAdapterServiceDataHolder.realmService = realmService; + } + + public static RealmService getRealmService() { + return realmService; + } + + public static void registerHTTPService( + HttpService httpService) { + EventAdapterServiceDataHolder.httpService = httpService; + } + + public static HttpService getHTTPService() { + return httpService; + } + + +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/Constants.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/Constants.java new file mode 100644 index 000000000..284397cb1 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/Constants.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.wso2.carbon.event.input.adapter.extensions.mqtt; + +/** + * This holds the constants related to MQTT input adapter. + */ +public class Constants { + public static final String EMPTY_STRING = ""; + public static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer refresh_token"; + public static final String TOKEN_SCOPE = "production"; + public static final String APPLICATION_TYPE = "device"; + public static final String CLIENT_ID = "client_id"; + public static final String CLIENT_SECRET = "client_secret"; + public static final String CLIENT_NAME = "client_name"; + public static final String DEFAULT = "default"; + public static final String MQTT_CONTENT_VALIDATION_DEFAULT_PARAMETERS = + "device_id_json_path:meta_deviceId,device_id_topic_hierarchy_index:2"; + public static final String TOPIC = "topic"; + public static final String PAYLOAD = "payload"; + public static final String DEVICE_ID_JSON_PATH = "device_id_json_path"; + public static final String DEVICE_ID_TOPIC_HIERARCHY_INDEX = "device_id_topic_hierarchy_index"; +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/MQTTEventAdapter.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/MQTTEventAdapter.java new file mode 100644 index 000000000..c273e5af0 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/MQTTEventAdapter.java @@ -0,0 +1,147 @@ +/* +* Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +* +* WSO2 Inc. licenses this file to you under the Apache License, +* Version 2.0 (the "License"); you may not use this file except +* in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.wso2.carbon.event.input.adapter.extensions.mqtt; + +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.event.input.adapter.core.InputEventAdapter; +import org.wso2.carbon.event.input.adapter.core.InputEventAdapterConfiguration; +import org.wso2.carbon.event.input.adapter.core.InputEventAdapterListener; +import org.wso2.carbon.event.input.adapter.core.exception.InputEventAdapterException; +import org.wso2.carbon.event.input.adapter.core.exception.TestConnectionNotSupportedException; +import org.wso2.carbon.event.input.adapter.extensions.mqtt.util.MQTTAdapterListener; +import org.wso2.carbon.event.input.adapter.extensions.mqtt.util.MQTTBrokerConnectionConfiguration; +import org.wso2.carbon.event.input.adapter.extensions.mqtt.util.MQTTEventAdapterConstants; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Input MQTTEventAdapter will be used to receive events with MQTT protocol using specified broker and topic. + */ +public class MQTTEventAdapter implements InputEventAdapter { + + private final InputEventAdapterConfiguration eventAdapterConfiguration; + private final Map globalProperties; + private InputEventAdapterListener eventAdapterListener; + private final String id = UUID.randomUUID().toString(); + private MQTTAdapterListener mqttAdapterListener; + private MQTTBrokerConnectionConfiguration mqttBrokerConnectionConfiguration; + + + public MQTTEventAdapter(InputEventAdapterConfiguration eventAdapterConfiguration, + Map globalProperties) { + this.eventAdapterConfiguration = eventAdapterConfiguration; + this.globalProperties = globalProperties; + } + + @Override + public void init(InputEventAdapterListener eventAdapterListener) throws InputEventAdapterException { + this.eventAdapterListener = eventAdapterListener; + try { + int keepAlive; + + //If global properties are available those will be assigned else constant values will be assigned + if (globalProperties.get(MQTTEventAdapterConstants.ADAPTER_CONF_KEEP_ALIVE) != null) { + keepAlive = Integer.parseInt((globalProperties.get(MQTTEventAdapterConstants.ADAPTER_CONF_KEEP_ALIVE))); + } else { + keepAlive = MQTTEventAdapterConstants.ADAPTER_CONF_DEFAULT_KEEP_ALIVE; + } + String contentValidationParams = eventAdapterConfiguration.getProperties().get(MQTTEventAdapterConstants.ADAPTER_CONF_CONTENT_VALIDATOR_PARAMS); + String params[] = contentValidationParams.split(","); + Map paramsMap = new HashMap<>(); + for (String param: params) { + String paramsKeyAndValue[] = param.split("/_(.+)?/"); + if (paramsKeyAndValue.length != 2) { + throw new InputEventAdapterException("Invalid parameters for content validation - " + param); + } + paramsMap.put(paramsKeyAndValue[0], paramsKeyAndValue[1]); + } + + mqttBrokerConnectionConfiguration = new MQTTBrokerConnectionConfiguration( + eventAdapterConfiguration.getProperties().get(MQTTEventAdapterConstants.ADAPTER_CONF_URL), + eventAdapterConfiguration.getProperties().get(MQTTEventAdapterConstants.ADAPTER_CONF_USERNAME), + eventAdapterConfiguration.getProperties().get(MQTTEventAdapterConstants.ADAPTER_CONF_SCOPES), + eventAdapterConfiguration.getProperties().get(MQTTEventAdapterConstants.ADAPTER_CONF_DCR_URL), + eventAdapterConfiguration.getProperties().get(MQTTEventAdapterConstants.ADAPTER_CONF_CLEAN_SESSION), + keepAlive, + eventAdapterConfiguration.getProperties().get(MQTTEventAdapterConstants.ADAPTER_CONF_CONTENT_VALIDATOR_CLASSNAME), + paramsMap + ); + + + mqttAdapterListener = new MQTTAdapterListener(mqttBrokerConnectionConfiguration, + eventAdapterConfiguration.getProperties().get(MQTTEventAdapterConstants.ADAPTER_MESSAGE_TOPIC), + eventAdapterConfiguration.getProperties().get(MQTTEventAdapterConstants.ADAPTER_CONF_CLIENTID), + eventAdapterListener, PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId()); + + } catch (Throwable t) { + throw new InputEventAdapterException(t.getMessage(), t); + } + } + + @Override + public void testConnect() throws TestConnectionNotSupportedException { + throw new TestConnectionNotSupportedException("not-supported"); + } + + @Override + public void connect() { + mqttAdapterListener.createConnection(); + } + + @Override + public void disconnect() { + if (mqttAdapterListener != null) { + mqttAdapterListener.stopListener(eventAdapterConfiguration.getName()); + } + } + + @Override + public void destroy() { + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MQTTEventAdapter)) return false; + + MQTTEventAdapter that = (MQTTEventAdapter) o; + + if (!id.equals(that.id)) return false; + + return true; + } + + @Override + public int hashCode() { + return id.hashCode(); + } + + + @Override + public boolean isEventDuplicatedInCluster() { + return true; + } + + @Override + public boolean isPolling() { + return true; + } + +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/MQTTEventAdapterFactory.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/MQTTEventAdapterFactory.java new file mode 100644 index 000000000..be1111bce --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/MQTTEventAdapterFactory.java @@ -0,0 +1,153 @@ +/* +* Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +* +* WSO2 Inc. licenses this file to you under the Apache License, +* Version 2.0 (the "License"); you may not use this file except +* in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.wso2.carbon.event.input.adapter.extensions.mqtt; + +import org.wso2.carbon.event.input.adapter.core.*; +import org.wso2.carbon.event.input.adapter.extensions.mqtt.util.MQTTEventAdapterConstants; + +import java.util.*; + +/** + * The mqtt event adapter factory class to create a mqtt input adapter + */ +public class MQTTEventAdapterFactory extends InputEventAdapterFactory { + + private ResourceBundle resourceBundle = ResourceBundle.getBundle + ("org.wso2.carbon.event.input.adapter.extensions.mqtt.i18n.Resources", Locale.getDefault()); + + @Override + public String getType() { + return MQTTEventAdapterConstants.ADAPTER_TYPE_MQTT; + } + + @Override + public List getSupportedMessageFormats() { + List supportInputMessageTypes = new ArrayList(); + + supportInputMessageTypes.add(MessageType.TEXT); + supportInputMessageTypes.add(MessageType.JSON); + supportInputMessageTypes.add(MessageType.XML); + + return supportInputMessageTypes; + } + + @Override + public List getPropertyList() { + List propertyList = new ArrayList(); + + // set topic + Property topicProperty = new Property(MQTTEventAdapterConstants.ADAPTER_MESSAGE_TOPIC); + topicProperty.setDisplayName( + resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_MESSAGE_TOPIC)); + topicProperty.setRequired(true); + topicProperty.setHint( + resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_MESSAGE_TOPIC_HINT)); + propertyList.add(topicProperty); + + //Broker Url + Property brokerUrl = new Property(MQTTEventAdapterConstants.ADAPTER_CONF_URL); + brokerUrl.setDisplayName( + resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_URL)); + brokerUrl.setRequired(true); + brokerUrl.setHint(resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_URL_HINT)); + propertyList.add(brokerUrl); + + //DCR endpoint details + Property dcrUrl = new Property(MQTTEventAdapterConstants.ADAPTER_CONF_DCR_URL); + dcrUrl.setDisplayName( + resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_DCR_URL)); + dcrUrl.setRequired(false); + dcrUrl.setHint( + resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_DCR_URL_HINT)); + propertyList.add(dcrUrl); + + //Content Validator details + Property contentValidator = new Property(MQTTEventAdapterConstants.ADAPTER_CONF_CONTENT_VALIDATOR_CLASSNAME); + contentValidator.setDisplayName( + resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_CONTENT_VALIDATOR_CLASSNAME)); + contentValidator.setRequired(false); + contentValidator.setHint( + resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_CONTENT_VALIDATOR_CLASSNAME_HINT)); + propertyList.add(contentValidator); + contentValidator.setDefaultValue(Constants.DEFAULT); + propertyList.add(contentValidator); + + //Content Validator Params details + Property contentValidatorParams = new Property(MQTTEventAdapterConstants.ADAPTER_CONF_CONTENT_VALIDATOR_PARAMS); + contentValidatorParams.setDisplayName( + resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_CONTENT_VALIDATOR_PARAMS)); + contentValidatorParams.setRequired(false); + contentValidatorParams.setHint( + resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_CONTENT_VALIDATOR_PARAMS_HINT)); + propertyList.add(contentValidatorParams); + contentValidatorParams.setDefaultValue(Constants.MQTT_CONTENT_VALIDATION_DEFAULT_PARAMETERS); + propertyList.add(contentValidatorParams); + + //Broker Username + Property userName = new Property(MQTTEventAdapterConstants.ADAPTER_CONF_USERNAME); + userName.setDisplayName( + resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_USERNAME)); + userName.setRequired(false); + userName.setHint( + resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_USERNAME_HINT)); + propertyList.add(userName); + propertyList.add(userName); + + //Broker Required Scopes. + Property scopes = new Property(MQTTEventAdapterConstants.ADAPTER_CONF_SCOPES); + scopes.setDisplayName( + resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_SCOPES)); + scopes.setRequired(false); + scopes.setHint( + resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_SCOPES_HINT)); + propertyList.add(scopes); + + //Broker clear session + Property clearSession = new Property(MQTTEventAdapterConstants.ADAPTER_CONF_CLEAN_SESSION); + clearSession.setDisplayName( + resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_CLEAN_SESSION)); + clearSession.setRequired(false); + clearSession.setOptions(new String[]{"true", "false"}); + clearSession.setDefaultValue("true"); + clearSession.setHint( + resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_CLEAN_SESSION_HINT)); + propertyList.add(clearSession); + + // set clientId + Property clientId = new Property(MQTTEventAdapterConstants.ADAPTER_CONF_CLIENTID); + clientId.setDisplayName( + resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_CLIENTID)); + clientId.setRequired(false); + clientId.setHint( + resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_CLIENTID_HINT)); + propertyList.add(clientId); + + return propertyList; + } + + @Override + public String getUsageTips() { + return null; + } + + @Override + public InputEventAdapter createEventAdapter(InputEventAdapterConfiguration eventAdapterConfiguration, + Map globalProperties) { + return new MQTTEventAdapter(eventAdapterConfiguration, globalProperties); + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/exception/MQTTContentValidatorInitializationException.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/exception/MQTTContentValidatorInitializationException.java new file mode 100644 index 000000000..a03cde2f9 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/exception/MQTTContentValidatorInitializationException.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.wso2.carbon.event.input.adapter.extensions.mqtt.exception; + +public class MQTTContentValidatorInitializationException extends RuntimeException { + private String errMessage; + + public MQTTContentValidatorInitializationException(String msg, Exception nestedEx) { + super(msg, nestedEx); + setErrorMessage(msg); + } + + public MQTTContentValidatorInitializationException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public MQTTContentValidatorInitializationException(String msg) { + super(msg); + setErrorMessage(msg); + } + + public MQTTContentValidatorInitializationException() { + super(); + } + + public MQTTContentValidatorInitializationException(Throwable cause) { + super(cause); + } + + public String getErrorMessage() { + return errMessage; + } + + public void setErrorMessage(String errMessage) { + this.errMessage = errMessage; + } +} \ No newline at end of file diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTAdapterListener.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTAdapterListener.java new file mode 100644 index 000000000..0da5ee21d --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTAdapterListener.java @@ -0,0 +1,280 @@ +/* +* Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +* +* WSO2 Inc. licenses this file to you under the Apache License, +* Version 2.0 (the "License"); you may not use this file except +* in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.wso2.carbon.event.input.adapter.extensions.mqtt.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.eclipse.paho.client.mqttv3.*; +import org.eclipse.paho.client.mqttv3.persist.MqttDefaultFilePersistence; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.core.ServerStatus; +import org.wso2.carbon.event.input.adapter.core.InputEventAdapterListener; +import org.wso2.carbon.event.input.adapter.core.exception.InputEventAdapterRuntimeException; +import org.wso2.carbon.event.input.adapter.extensions.ContentInfo; +import org.wso2.carbon.event.input.adapter.extensions.ContentValidator; +import org.wso2.carbon.event.input.adapter.extensions.mqtt.Constants; +import org.wso2.carbon.event.input.adapter.extensions.mqtt.exception.MQTTContentValidatorInitializationException; +import org.wso2.carbon.identity.jwt.client.extension.dto.AccessTokenInfo; +import org.wso2.carbon.identity.jwt.client.extension.exception.JWTClientException; +import org.wso2.carbon.identity.jwt.client.extension.service.JWTClientManagerService; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.util.Map; + +public class MQTTAdapterListener implements MqttCallback, Runnable { + private static final Log log = LogFactory.getLog(MQTTAdapterListener.class); + + private MqttClient mqttClient; + private MqttConnectOptions connectionOptions; + private boolean cleanSession; + private int keepAlive; + + private MQTTBrokerConnectionConfiguration mqttBrokerConnectionConfiguration; + private String mqttClientId; + private String topic; + private int tenantId; + private boolean connectionSucceeded = false; + ContentValidator contentValidator; + Map contentValidationParams; + + private InputEventAdapterListener eventAdapterListener = null; + + + public MQTTAdapterListener(MQTTBrokerConnectionConfiguration mqttBrokerConnectionConfiguration, + String topic, String mqttClientId, + InputEventAdapterListener inputEventAdapterListener, int tenantId) { + + if(mqttClientId == null || mqttClientId.trim().isEmpty()){ + mqttClientId = MqttClient.generateClientId(); + } + + this.mqttClientId = mqttClientId; + this.mqttBrokerConnectionConfiguration = mqttBrokerConnectionConfiguration; + this.cleanSession = mqttBrokerConnectionConfiguration.isCleanSession(); + this.keepAlive = mqttBrokerConnectionConfiguration.getKeepAlive(); + this.topic = topic; + this.eventAdapterListener = inputEventAdapterListener; + this.tenantId = tenantId; + + //SORTING messages until the server fetches them + String temp_directory = System.getProperty("java.io.tmpdir"); + MqttDefaultFilePersistence dataStore = new MqttDefaultFilePersistence(temp_directory); + + + try { + // Construct the connection options object that contains connection parameters + // such as cleanSession and LWT + connectionOptions = new MqttConnectOptions(); + connectionOptions.setCleanSession(cleanSession); + connectionOptions.setKeepAliveInterval(keepAlive); + + // Construct an MQTT blocking mode client + mqttClient = new MqttClient(this.mqttBrokerConnectionConfiguration.getBrokerUrl(), this.mqttClientId, + dataStore); + + // Set this wrapper as the callback handler + mqttClient.setCallback(this); + String contentValidatorClassName = this.mqttBrokerConnectionConfiguration.getContentValidatorClassName(); + + if (contentValidatorClassName != null && contentValidatorClassName.equals(Constants.DEFAULT)) { + contentValidator = new MQTTContentValidator(); + } else if (contentValidatorClassName != null && !contentValidatorClassName.isEmpty()) { + try { + Class contentValidatorClass = Class.forName(contentValidatorClassName) + .asSubclass(ContentValidator.class); + contentValidator = contentValidatorClass.newInstance(); + } catch (ClassNotFoundException e) { + throw new MQTTContentValidatorInitializationException( + "Unable to find the class authorizer: " + contentValidatorClassName, e); + } catch (InstantiationException e) { + throw new MQTTContentValidatorInitializationException( + "Unable to create an instance of :" + contentValidatorClassName, e); + } catch (IllegalAccessException e) { + throw new MQTTContentValidatorInitializationException("Access of the instance in not allowed.", e); + } + } + + contentValidationParams = mqttBrokerConnectionConfiguration.getContentValidatorParams(); + + } catch (MqttException e) { + log.error("Exception occurred while subscribing to MQTT broker at " + + mqttBrokerConnectionConfiguration.getBrokerUrl()); + throw new InputEventAdapterRuntimeException(e); + } catch (Throwable e) { + log.error("Exception occurred while subscribing to MQTT broker at " + + mqttBrokerConnectionConfiguration.getBrokerUrl()); + throw new InputEventAdapterRuntimeException(e); + } + + } + + public void startListener() throws MqttException { + if (this.mqttBrokerConnectionConfiguration.getBrokerUsername() != null && this.mqttBrokerConnectionConfiguration.getDcrUrl() != null) { + String username = this.mqttBrokerConnectionConfiguration.getBrokerUsername(); + String dcrUrlString = this.mqttBrokerConnectionConfiguration.getDcrUrl(); + String scopes = this.mqttBrokerConnectionConfiguration.getBrokerScopes(); + //getJWT Client Parameters. + if (dcrUrlString != null && !dcrUrlString.isEmpty()) { + try { + URL dcrUrl = new URL(dcrUrlString); + HttpClient httpClient = MQTTUtil.getHttpClient(dcrUrl.getProtocol()); + HttpPost postMethod = new HttpPost(dcrUrlString); + RegistrationProfile registrationProfile = new RegistrationProfile(); + registrationProfile.setCallbackUrl(Constants.EMPTY_STRING); + registrationProfile.setGrantType(Constants.GRANT_TYPE); + registrationProfile.setOwner(username); + registrationProfile.setTokenScope(Constants.TOKEN_SCOPE); + registrationProfile.setApplicationType(Constants.APPLICATION_TYPE); + int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId(true); + registrationProfile.setClientName(username + "_" + tenantId); + String jsonString = registrationProfile.toJSON(); + StringEntity requestEntity = new StringEntity(jsonString, ContentType.APPLICATION_JSON); + postMethod.setEntity(requestEntity); + HttpResponse httpResponse = httpClient.execute(postMethod); + String response = MQTTUtil.getResponseString(httpResponse); + try { + JSONParser jsonParser = new JSONParser(); + JSONObject jsonPayload = (JSONObject) jsonParser.parse(response); + String clientId = (String) jsonPayload.get(Constants.CLIENT_ID); + String clientSecret = (String) jsonPayload.get(Constants.CLIENT_SECRET); + JWTClientManagerService jwtClientManagerService = MQTTUtil.getJWTClientManagerService(); + AccessTokenInfo accessTokenInfo = jwtClientManagerService.getJWTClient().getAccessToken( + clientId, clientSecret, username, scopes); + connectionOptions.setUserName(accessTokenInfo.getAccessToken()); + } catch (ParseException e) { + String msg = "error occurred while parsing client credential payload"; + log.error(msg, e); + } catch (JWTClientException e) { + String msg = "error occurred while parsing the response from JWT Client"; + log.error(msg, e); + } + } catch (MalformedURLException e) { + log.error("Invalid dcrUrl : " + dcrUrlString); + } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | IOException e) { + log.error("Failed to create an https connection.", e); + } + } + } + // Connect to the MQTT server + mqttClient.connect(connectionOptions); + + // Subscribe to the requested topic + // The QoS specified is the maximum level that messages will be sent to the client at. + // For instance if QoS 1 is specified, any messages originally published at QoS 2 will + // be downgraded to 1 when delivering to the client but messages published at 1 and 0 + // will be received at the same level they were published at. + mqttClient.subscribe(topic); + } + + public void stopListener(String adapterName) { + if (connectionSucceeded) { + try { + // Un-subscribe accordingly and disconnect from the MQTT server. + if (!ServerStatus.getCurrentStatus().equals(ServerStatus.STATUS_SHUTTING_DOWN) || cleanSession) { + mqttClient.unsubscribe(topic); + } + mqttClient.disconnect(3000); + } catch (MqttException e) { + log.error("Can not unsubscribe from the destination " + topic + + " with the event adapter " + adapterName, e); + } + } + //This is to stop all running reconnection threads + connectionSucceeded = true; + } + + @Override + public void connectionLost(Throwable throwable) { + log.warn("MQTT connection not reachable " + throwable); + connectionSucceeded = false; + new Thread(this).start(); + } + + @Override + public void messageArrived(String topic, MqttMessage mqttMessage) throws Exception { + try { + String msgText = mqttMessage.toString(); + if (log.isDebugEnabled()) { + log.debug(msgText); + } + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantId(tenantId); + + if (log.isDebugEnabled()) { + log.debug("Event received in MQTT Event Adapter - " + msgText); + } + ContentInfo contentInfo; + synchronized (contentValidationParams) { + contentValidationParams.put(Constants.TOPIC, topic); + contentValidationParams.put(Constants.PAYLOAD, msgText); + contentInfo = contentValidator.validate(contentValidationParams); + contentValidationParams.remove(Constants.TOPIC); + contentValidationParams.remove(Constants.PAYLOAD); + } + if (contentValidator != null) { + if (contentInfo != null && contentInfo.isValidContent()) { + eventAdapterListener.onEvent(contentInfo.getMsgText()); + } + } else { + eventAdapterListener.onEvent(msgText); + } + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + + } + + @Override + public void run() { + while (!connectionSucceeded) { + try { + MQTTEventAdapterConstants.initialReconnectDuration = MQTTEventAdapterConstants.initialReconnectDuration + * MQTTEventAdapterConstants.reconnectionProgressionFactor; + Thread.sleep(MQTTEventAdapterConstants.initialReconnectDuration); + startListener(); + connectionSucceeded = true; + log.info("MQTT Connection successful"); + } catch (InterruptedException e) { + log.error("Interruption occurred while waiting for reconnection", e); + } catch (MqttException e) { + log.error("MQTT Exception occurred when starting listener", e); + } + } + } + + public void createConnection() { + new Thread(this).start(); + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTBrokerConnectionConfiguration.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTBrokerConnectionConfiguration.java new file mode 100644 index 000000000..504d46ff6 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTBrokerConnectionConfiguration.java @@ -0,0 +1,119 @@ +/* +* Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +* +* WSO2 Inc. licenses this file to you under the Apache License, +* Version 2.0 (the "License"); you may not use this file except +* in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.wso2.carbon.event.input.adapter.extensions.mqtt.util; + +import org.wso2.carbon.event.input.adapter.extensions.mqtt.Constants; + +import java.util.Map; + +public class MQTTBrokerConnectionConfiguration { + + private String brokerUsername = null; + private String brokerScopes = null; + private boolean cleanSession = true; + private int keepAlive; + private String brokerUrl; + private String dcrUrl; + private String contentValidatorClassName; + private Map contentValidatorParams; + + public String getBrokerScopes() { + return brokerScopes; + } + + public void setBrokerScopes(String brokerScopes) { + this.brokerScopes = brokerScopes; + } + + public String getBrokerUsername() { + return brokerUsername; + } + + public void setBrokerUsername(String brokerUsername) { + this.brokerUsername = brokerUsername; + } + + public void setCleanSession(boolean cleanSession) { + this.cleanSession = cleanSession; + } + + + public boolean isCleanSession() { + return cleanSession; + } + + public String getBrokerUrl() { + return brokerUrl; + } + + public void setBrokerUrl(String brokerUrl) { + this.brokerUrl = brokerUrl; + } + + public String getDcrUrl() { + return dcrUrl; + } + + public void setDcrUrl(String dcrUrl) { + this.dcrUrl = dcrUrl; + } + + public int getKeepAlive() { + return keepAlive; + } + + public void setKeepAlive(int keepAlive) { + this.keepAlive = keepAlive; + } + + public String getContentValidatorClassName() { + return contentValidatorClassName; + } + + public void setContentValidatorClassName(String contentValidatorClassName) { + this.contentValidatorClassName = contentValidatorClassName; + } + + public Map getContentValidatorParams() { + return contentValidatorParams; + } + + public void setContentValidatorParams(Map contentValidatorParams) { + this.contentValidatorParams = contentValidatorParams; + } + + public MQTTBrokerConnectionConfiguration(String brokerUrl, String brokerUsername, String brokerScopes, + String dcrUrl, String cleanSession, int keepAlive, + String contentValidatorClassName, Map contentValidatorParams) { + this.brokerUsername = brokerUsername; + this.brokerScopes = brokerScopes; + if (brokerScopes == null) { + this.brokerScopes = Constants.EMPTY_STRING; + } + this.brokerUrl = brokerUrl; + this.dcrUrl = dcrUrl; + this.contentValidatorClassName = contentValidatorClassName; + if (cleanSession != null) { + this.cleanSession = Boolean.parseBoolean(cleanSession); + } + this.keepAlive = keepAlive; + if (contentValidatorParams != null) { + this.contentValidatorParams = contentValidatorParams; + } + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTContentValidator.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTContentValidator.java new file mode 100644 index 000000000..96c1eee16 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTContentValidator.java @@ -0,0 +1,53 @@ +/* +* Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +* +* WSO2 Inc. licenses this file to you under the Apache License, +* Version 2.0 (the "License"); you may not use this file except +* in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +package org.wso2.carbon.event.input.adapter.extensions.mqtt.util; + +import com.jayway.jsonpath.JsonPath; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.event.input.adapter.extensions.ContentInfo; +import org.wso2.carbon.event.input.adapter.extensions.ContentValidator; +import org.wso2.carbon.event.input.adapter.extensions.mqtt.Constants; + +import java.util.Map; + +public class MQTTContentValidator implements ContentValidator { + private static final Log log = LogFactory.getLog(MQTTContentValidator.class); + + @Override + public ContentInfo validate(Map params) { + String topic = params.get(Constants.TOPIC); + String topics[] = topic.split("/"); + + String msg = params.get(Constants.PAYLOAD); + String deviceIdJsonPath = params.get(Constants.DEVICE_ID_JSON_PATH); + String deviceIdInTopicHierarchyLevel = params.get(Constants.DEVICE_ID_TOPIC_HIERARCHY_INDEX); + int deviceIdInTopicHierarchyLevelIndex = 0; + if (deviceIdInTopicHierarchyLevel != null && !deviceIdInTopicHierarchyLevel.isEmpty()) { + deviceIdInTopicHierarchyLevelIndex = Integer.parseInt(deviceIdInTopicHierarchyLevel); + } + String deviceIdFromTopic = topics[deviceIdInTopicHierarchyLevelIndex]; + Object res = JsonPath.read(msg, deviceIdJsonPath); + String deviceIdFromContent = (res != null) ? res.toString() : ""; + if (deviceIdFromContent.equals(deviceIdFromTopic)) { + return new ContentInfo(true, msg); + } + return new ContentInfo(false, msg); + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTEventAdapterConstants.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTEventAdapterConstants.java new file mode 100644 index 000000000..d9a564d96 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTEventAdapterConstants.java @@ -0,0 +1,46 @@ +/* +* Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +* +* WSO2 Inc. licenses this file to you under the Apache License, +* Version 2.0 (the "License"); you may not use this file except +* in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ +package org.wso2.carbon.event.input.adapter.extensions.mqtt.util; + +public class MQTTEventAdapterConstants { + + public static final String ADAPTER_TYPE_MQTT = "oauth-mqtt"; + public static final String ADAPTER_CONF_URL = "url"; + public static final String ADAPTER_CONF_USERNAME = "username"; + public static final String ADAPTER_CONF_USERNAME_HINT = "username.hint"; + public static final String ADAPTER_CONF_SCOPES = "scopes"; + public static final String ADAPTER_CONF_SCOPES_HINT = "scopes.hint"; + public static final String ADAPTER_CONF_URL_HINT = "url.hint"; + public static final String ADAPTER_CONF_DCR_URL = "dcrUrl"; + public static final String ADAPTER_CONF_DCR_URL_HINT = "dcrUrl.hint"; + public static final String ADAPTER_CONF_CONTENT_VALIDATOR_CLASSNAME = "contentValidation"; + public static final String ADAPTER_CONF_CONTENT_VALIDATOR_CLASSNAME_HINT = "contentValidation.hint"; + public static final String ADAPTER_CONF_CONTENT_VALIDATOR_PARAMS = "contentValidationParams"; + public static final String ADAPTER_CONF_CONTENT_VALIDATOR_PARAMS_HINT = "contentValidationParams.hint"; + public static final String ADAPTER_MESSAGE_TOPIC = "topic"; + public static final String ADAPTER_MESSAGE_TOPIC_HINT = "topic.hint"; + public static final String ADAPTER_CONF_CLIENTID = "clientId"; + public static final String ADAPTER_CONF_CLIENTID_HINT = "clientId.hint"; + public static final String ADAPTER_CONF_CLEAN_SESSION = "cleanSession"; + public static final String ADAPTER_CONF_CLEAN_SESSION_HINT = "cleanSession.hint"; + public static final String ADAPTER_CONF_KEEP_ALIVE = "keepAlive"; + public static final int ADAPTER_CONF_DEFAULT_KEEP_ALIVE = 60000; + + public static int initialReconnectDuration = 10000; + public static final int reconnectionProgressionFactor = 2; +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTUtil.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTUtil.java new file mode 100644 index 000000000..29bae3583 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTUtil.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.event.input.adapter.extensions.mqtt.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLContextBuilder; +import org.apache.http.conn.ssl.TrustSelfSignedStrategy; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.identity.jwt.client.extension.service.JWTClientManagerService; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; + +/** + * This is the utility class that is used for MQTT input adapater. + */ +public class MQTTUtil { + private static final String HTTPS_PROTOCOL = "https"; + private static final Log log = LogFactory.getLog(MQTTUtil.class); + /** + * Return a http client instance + * + * @param protocol- service endpoint protocol http/https + * @return + */ + public static HttpClient getHttpClient(String protocol) + throws IOException, KeyStoreException, NoSuchAlgorithmException, KeyManagementException { + HttpClient httpclient; + if (HTTPS_PROTOCOL.equals(protocol)) { + SSLContextBuilder builder = new SSLContextBuilder(); + builder.loadTrustMaterial(null, new TrustSelfSignedStrategy()); + SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(builder.build()); + httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build(); + } else { + httpclient = HttpClients.createDefault(); + } + return httpclient; + } + + public static String getResponseString(HttpResponse httpResponse) throws IOException { + BufferedReader br = null; + try { + br = new BufferedReader(new InputStreamReader(httpResponse.getEntity().getContent())); + String readLine; + String response = ""; + while (((readLine = br.readLine()) != null)) { + response += readLine; + } + return response; + } finally { + EntityUtils.consumeQuietly(httpResponse.getEntity()); + if (br != null) { + try { + br.close(); + } catch (IOException e) { + log.warn("Error while closing the connection! " + e.getMessage()); + } + } + } + } + + public static JWTClientManagerService getJWTClientManagerService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + JWTClientManagerService jwtClientManagerService = + (JWTClientManagerService) ctx.getOSGiService(JWTClientManagerService.class, null); + if (jwtClientManagerService == null) { + String msg = "JWT management service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return jwtClientManagerService; + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/RegistrationProfile.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/RegistrationProfile.java new file mode 100644 index 000000000..fec69d61b --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/RegistrationProfile.java @@ -0,0 +1,73 @@ +package org.wso2.carbon.event.input.adapter.extensions.mqtt.util; + +/** + * This class represents the data that are required to register + * the oauth application. + */ +public class RegistrationProfile { + + private String callbackUrl; + private String clientName; + private String tokenScope; + private String owner; + private String grantType; + private String applicationType; + + private static final String TAG = RegistrationProfile.class.getSimpleName(); + + public String getCallbackUrl() { + return callbackUrl; + } + + public void setCallbackUrl(String callBackUrl) { + this.callbackUrl = callBackUrl; + } + + public String getClientName() { + return clientName; + } + + public void setClientName(String clientName) { + this.clientName = clientName; + } + + public String getTokenScope() { + return tokenScope; + } + + public void setTokenScope(String tokenScope) { + this.tokenScope = tokenScope; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getGrantType() { + return grantType; + } + + public void setGrantType(String grantType) { + this.grantType = grantType; + } + + public String getApplicationType() { + return applicationType; + } + + public void setApplicationType(String applicationType) { + this.applicationType = applicationType; + } + + public String toJSON() { + String jsonString = + "{\"callbackUrl\": \"" + callbackUrl + "\",\"clientName\": \"" + clientName + "\", \"tokenScope\": " + + "\"" + tokenScope + "\", \"owner\": \"" + owner + "\"," + "\"grantType\": \"" + grantType + + "\", \"saasApp\" :false }\n"; + return jsonString; + } +} \ No newline at end of file diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/resources/org/wso2/carbon/event/input/adapter/extensions/http/i18n/Resources.properties b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/resources/org/wso2/carbon/event/input/adapter/extensions/http/i18n/Resources.properties new file mode 100644 index 000000000..de84fc012 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/resources/org/wso2/carbon/event/input/adapter/extensions/http/i18n/Resources.properties @@ -0,0 +1,38 @@ +# +# Copyright (c) 2005-2014, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +# +# WSO2 Inc. licenses this file to you under the Apache License, +# Version 2.0 (the "License"); you may not use this file except +# in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +transports=Transport(s) +http.usage.tips_prefix=Following url formats are used to receive events
For super tenants:
  http://localhost: +http.usage.tips_mid1=/endpoints/<event_receiver_name>
  https://localhost: +http.usage.tips_mid2=/endpoints/<event_receiver_name>

For other tenants:
  http://localhost: +http.usage.tips_mid3=/endpoints/t/<tenant_domain>/<event_receiver_name>
  https://localhost: +http.usage.tips_postfix=/endpoints/t/<tenant_domain>/<event_receiver_name> +tokenValidationEndpointUrl=tokenEndpointUrl +tokenValidationEndpointUrl.hint=OAUTH Token Validation Endpoint +username=username +username.hint=username of the user to connect to the admin services +password=password +password.hint=password of the user to connect to the admin services. +maximumTotalHttpConnection=maximumTotalHttpConnection +maximumTotalHttpConnection.hint=Maximum Total connection to be made with the endpoint +maximumHttpConnectionPerHost=maximumHttpConnectionPerHost +maximumHttpConnectionPerHost.hint=Maximum Http connection per host. +contentValidation=contentValidation +contentValidation.hint=Class Name of the content Validation or 'default' to set default class, required to implement (if required) +contentValidationParams=contentValidationParams +contentValidationParams.hint=ContentValidationParams, comma seperated. (if required) \ No newline at end of file diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/resources/org/wso2/carbon/event/input/adapter/extensions/mqtt/i18n/Resources.properties b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/resources/org/wso2/carbon/event/input/adapter/extensions/mqtt/i18n/Resources.properties new file mode 100644 index 000000000..70d4019e4 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/resources/org/wso2/carbon/event/input/adapter/extensions/mqtt/i18n/Resources.properties @@ -0,0 +1,38 @@ +# +# Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +# +# WSO2 Inc. licenses this file to you under the Apache License, +# Version 2.0 (the "License"); you may not use this file except +# in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +topic=Topic +topic.hint=Topic subscribed +clientId=Client Id +clientId.hint=client identifier is used by the server to identify a client when it reconnects, It used for durable subscriptions or reliable delivery of messages is required. +url=Broker Url +username=Username +username.hint=Username of the broker (if required) +scopes=Scopes +scopes.hint=Scopes required to connect to broker (if required) +dcrUrl=dcrUrl +dcrUrl.hint=dynamic client registration endpoint URL to create application (if required) eg: https://localhost:9443/dynamic-client-web/register +contentValidation=contentValidation +contentValidation.hint=Class Name of the content Validation or 'default' to set default class, required to implement (if required) +contentValidationParams=contentValidationParams +contentValidationParams.hint=ContentValidationParams, comma seperated. (if required) +url.hint=MQTT broker url tcp://localhost:1883 +cleanSession=Clean Session +cleanSession.hint=Persist topic subscriptions and ack positions across client sessions +keepAlive=Keep Alive (In seconds) +events.duplicated.in.cluster=Is events duplicated in cluster \ No newline at end of file diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/pom.xml b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/pom.xml new file mode 100644 index 000000000..466120b71 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/pom.xml @@ -0,0 +1,82 @@ + + + + + + + das-extensions + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.event.output.adapter.extensions.ui.endpoint + war + WSO2 Carbon UI Webapp - Webapp for UI Output Event Adapter + http://wso2.org + + + + junit + junit + test + + + org.apache.tomcat + tomcat-websocket-api + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.event.output.adapter.extensions.ui + provided + + + org.apache.httpcomponents.wso2 + httpcore + + + javax.ws.rs + javax.ws.rs-api + + + org.apache.cxf + cxf-rt-frontend-jaxrs + + + org.apache.httpcomponents.wso2 + httpcore + provided + + + org.wso2.orbit.org.apache.httpcomponents + httpclient + provided + + + org.wso2.carbon.identity + org.wso2.carbon.identity.oauth.stub + provided + + + + + secured-outputui + + diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/SubscriptionEndpoint.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/SubscriptionEndpoint.java new file mode 100644 index 000000000..d6e0e4909 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/SubscriptionEndpoint.java @@ -0,0 +1,74 @@ +/* + * + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + +import oauth.OAuthTokenValdiator; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.servlet.ServiceHolder; + +import javax.websocket.CloseReason; +import javax.websocket.Session; + +/** + * Interface for subscription and un-subscription for web socket + */ + +public class SubscriptionEndpoint { + + private static final Log log = LogFactory.getLog(SubscriptionEndpoint.class); + + public SubscriptionEndpoint() { + + } + + /** + * Web socket onClose - Remove the registered sessions + * + * @param session - Users registered session. + * @param reason - Status code for web-socket close. + * @param streamName - StreamName extracted from the ws url. + * @param version - Version extracted from the ws url. + */ + public void onClose(Session session, CloseReason reason, String streamName, String version) { + if (log.isDebugEnabled()) { + log.debug("Closing a WebSocket due to " + reason.getReasonPhrase() + ", for session ID:" + session.getId + () + + ", for request URI - " + session.getRequestURI()); + } + ServiceHolder.getInstance().getUiOutputCallbackControllerService().unsubscribeWebsocket(streamName, version, + session); + } + + /** + * Web socket onError - Remove the registered sessions + * + * @param session - Users registered session. + * @param throwable - Status code for web-socket close. + * @param streamName - StreamName extracted from the ws url. + * @param version - Version extracted from the ws url. + */ + public void onError(Session session, Throwable throwable, String streamName, String version) { + log.error( + "Error occurred in session ID: " + session.getId() + ", for request URI - " + session.getRequestURI() + + ", " + throwable.getMessage(), throwable); + ServiceHolder.getInstance().getUiOutputCallbackControllerService().unsubscribeWebsocket(streamName, version, + session); + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/SuperTenantSubscriptionEndpoint.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/SuperTenantSubscriptionEndpoint.java new file mode 100644 index 000000000..674b32447 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/SuperTenantSubscriptionEndpoint.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import oauth.OAuthTokenValdiator; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.servlet.ServiceHolder; +import org.wso2.carbon.utils.multitenancy.MultitenantConstants; +import util.AuthenticationInfo; + +import javax.websocket.CloseReason; +import javax.websocket.OnClose; +import javax.websocket.OnError; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.server.PathParam; +import javax.websocket.server.ServerEndpoint; + +/** + * Connect to web socket with Super tenant + */ + +@ServerEndpoint(value = "/{streamname}/{version}") +public class SuperTenantSubscriptionEndpoint extends SubscriptionEndpoint { + + private static final Log log = LogFactory.getLog(SuperTenantSubscriptionEndpoint.class); + + /** + * Web socket onOpen - When client sends a message + * + * @param session - Users registered session. + * @param streamName - StreamName extracted from the ws url. + * @param version - Version extracted from the ws url. + */ + @OnOpen + public void onOpen(Session session, @PathParam("streamname") String streamName, + @PathParam("version") String version) { + if (log.isDebugEnabled()) { + log.debug("WebSocket opened, for Session id: " + session.getId() + ", for the Stream:" + streamName); + } + AuthenticationInfo authenticationInfo = OAuthTokenValdiator.getInstance().validateToken(session); + //TODO Authorization + if (authenticationInfo != null && authenticationInfo.isAuthenticated()) { + try { + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantId(MultitenantConstants.SUPER_TENANT_ID); + + ServiceHolder.getInstance().getUiOutputCallbackControllerService().subscribeWebsocket(streamName, + version, + session); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + } + + /** + * Web socket onMessage - When client sens a message + * + * @param session - Users registered session. + * @param message - Status code for web-socket close. + * @param streamName - StreamName extracted from the ws url. + */ + @OnMessage + public void onMessage(Session session, String message, @PathParam("streamname") String streamName) { + if (log.isDebugEnabled()) { + log.debug("Received and dropped message from client. Message: " + message + ", " + + "for Session id: " + session.getId() + ", for the Stream:" + streamName); + } + } + + /** + * Web socket onClose - Remove the registered sessions + * + * @param session - Users registered session. + * @param reason - Status code for web-socket close. + * @param streamName - StreamName extracted from the ws url. + * @param version - Version extracted from the ws url. + */ + @OnClose + public void onClose(Session session, CloseReason reason, @PathParam("streamname") String streamName, + @PathParam("version") String version) { + try { + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantId(MultitenantConstants.SUPER_TENANT_ID); + super.onClose(session, reason, streamName, version); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + /** + * Web socket onError - Remove the registered sessions + * + * @param session - Users registered session. + * @param throwable - Status code for web-socket close. + * @param streamName - StreamName extracted from the ws url. + * @param version - Version extracted from the ws url. + */ + @OnError + public void onError(Session session, Throwable throwable, @PathParam("streamname") String streamName, + @PathParam("version") String version) { + try { + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantId(MultitenantConstants.SUPER_TENANT_ID); + super.onError(session, throwable, streamName, version); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/TenantSubscriptionEndpoint.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/TenantSubscriptionEndpoint.java new file mode 100644 index 000000000..640fb0de1 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/TenantSubscriptionEndpoint.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import oauth.OAuthTokenValdiator; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.servlet.ServiceHolder; +import util.AuthenticationInfo; + +import javax.websocket.CloseReason; +import javax.websocket.OnClose; +import javax.websocket.OnError; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.server.PathParam; +import javax.websocket.server.ServerEndpoint; + +/** + * Connect to web socket with a tenant + */ + +@ServerEndpoint(value = "/t/{tdomain}/{streamname}/{version}") +public class TenantSubscriptionEndpoint extends SubscriptionEndpoint { + + private static final Log log = LogFactory.getLog(TenantSubscriptionEndpoint.class); + + /** + * Web socket onOpen - When client sends a message + * + * @param session - Users registered session. + * @param streamName - StreamName extracted from the ws url. + * @param version - Version extracted from the ws url. + * @param tdomain - Tenant domain extracted from ws url. + */ + @OnOpen + public void onOpen (Session session, @PathParam("streamname") String streamName , + @PathParam("version") String version, @PathParam("tdomain") String tdomain) { + if (log.isDebugEnabled()) { + log.debug("WebSocket opened, for Session id: "+session.getId()+", for the Stream:"+streamName); + } + AuthenticationInfo authenticationInfo = OAuthTokenValdiator.getInstance().validateToken(session); + //TODO Authorization + if (authenticationInfo != null && authenticationInfo.isAuthenticated()) { + try { + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantDomain(tdomain, true); + ServiceHolder.getInstance().getUiOutputCallbackControllerService().subscribeWebsocket(streamName, + version, session); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + } + + /** + * Web socket onMessage - When client sens a message + * + * @param session - Users registered session. + * @param message - Status code for web-socket close. + * @param streamName - StreamName extracted from the ws url. + */ + @OnMessage + public void onMessage (Session session, String message, @PathParam("streamname") String streamName, @PathParam("tdomain") String tdomain) { + if (log.isDebugEnabled()) { + log.debug("Received and dropped message from client. Message: " + message+", for Session id: "+session.getId()+", for tenant domain"+tdomain+", for the Adaptor:"+streamName); + } + } + + /** + * Web socket onClose - Remove the registered sessions + * + * @param session - Users registered session. + * @param reason - Status code for web-socket close. + * @param streamName - StreamName extracted from the ws url. + * @param version - Version extracted from the ws url. + */ + @OnClose + public void onClose (Session session, CloseReason reason, @PathParam("streamname") String streamName, + @PathParam("version") String version, @PathParam("tdomain") String tdomain) { + + try { + PrivilegedCarbonContext.getThreadLocalCarbonContext().startTenantFlow(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantDomain(tdomain,true); + super.onClose(session, reason, streamName, version); + } finally { + PrivilegedCarbonContext.getThreadLocalCarbonContext().endTenantFlow(); + } + } + + /** + * Web socket onError - Remove the registered sessions + * + * @param session - Users registered session. + * @param throwable - Status code for web-socket close. + * @param streamName - StreamName extracted from the ws url. + * @param version - Version extracted from the ws url. + */ + @OnError + public void onError (Session session, Throwable throwable, @PathParam("streamname") String streamName, + @PathParam("version") String version, @PathParam("tdomain") String tdomain) { + + try { + PrivilegedCarbonContext.getThreadLocalCarbonContext().startTenantFlow(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantDomain(tdomain, true); + super.onError(session, throwable, streamName, version); + } finally { + PrivilegedCarbonContext.getThreadLocalCarbonContext().endTenantFlow(); + } + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/oauth/OAuthTokenValdiator.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/oauth/OAuthTokenValdiator.java new file mode 100644 index 000000000..1abe5f179 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/oauth/OAuthTokenValdiator.java @@ -0,0 +1,174 @@ +package oauth; + +import org.apache.axis2.context.ServiceContext; +import org.apache.axis2.transport.http.HTTPConstants; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.commons.pool.impl.GenericObjectPool; +import org.wso2.carbon.identity.oauth2.stub.OAuth2TokenValidationServiceStub; +import org.wso2.carbon.identity.oauth2.stub.dto.OAuth2TokenValidationRequestDTO; +import org.wso2.carbon.identity.oauth2.stub.dto.OAuth2TokenValidationRequestDTO_OAuth2AccessToken; +import org.wso2.carbon.identity.oauth2.stub.dto.OAuth2TokenValidationResponseDTO; +import org.wso2.carbon.user.api.UserStoreException; +import org.wso2.carbon.utils.CarbonUtils; +import org.wso2.carbon.utils.multitenancy.MultitenantUtils; +import util.AuthenticationInfo; + +import javax.websocket.Session; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.rmi.RemoteException; +import java.util.Properties; + +public class OAuthTokenValdiator { + + private static String cookie; + private GenericObjectPool stubs; + private static Log log = LogFactory.getLog(OAuthTokenValdiator.class); + private static final String WEBSOCKET_CONFIG_LOCATION = + CarbonUtils.getEtcCarbonConfigDirPath() + File.separator + "websocket-validation.properties"; + private static final String QUERY_STRING_SEPERATOR = "&"; + private static final String QUERY_KEY_VALUE_SEPERATOR = "="; + private static final String TOKEN_TYPE = "bearer"; + private static final String TOKEN_IDENTIFIER = "token"; + private static OAuthTokenValdiator oAuthTokenValdiator = new OAuthTokenValdiator(); + + public static OAuthTokenValdiator getInstance() { + return oAuthTokenValdiator; + } + + private OAuthTokenValdiator() { + Properties properties = null; + try { + properties = getWebSocketConfig(); + this.stubs = new GenericObjectPool(new OAuthTokenValidaterStubFactory(properties)); + } catch (IOException e) { + log.error("Failed to parse the web socket config file " + WEBSOCKET_CONFIG_LOCATION); + } + } + + /** + * This method gets a string accessToken and validates it + * + * @param session which need to be validated. + * @return AuthenticationInfo with the validated results. + */ + public AuthenticationInfo validateToken(Session session) { + String token = getTokenFromSession(session); + if (token == null) { + AuthenticationInfo authenticationInfo = new AuthenticationInfo(); + authenticationInfo.setAuthenticated(false); + return authenticationInfo; + } + OAuth2TokenValidationServiceStub tokenValidationServiceStub = null; + try { + Object stub = this.stubs.borrowObject(); + if (stub != null) { + tokenValidationServiceStub = (OAuth2TokenValidationServiceStub) stub; + if (cookie != null) { + tokenValidationServiceStub._getServiceClient().getOptions().setProperty( + HTTPConstants.COOKIE_STRING, cookie); + } + return getAuthenticationInfo(token, tokenValidationServiceStub); + } else { + log.warn("Stub initialization failed."); + } + } catch (RemoteException e) { + log.error("Error on connecting with the validation endpoint.", e); + } catch (Exception e) { + log.error("Error occurred in borrowing an validation stub from the pool.", e); + + } finally { + try { + if (tokenValidationServiceStub != null) { + this.stubs.returnObject(tokenValidationServiceStub); + } + } catch (Exception e) { + log.warn("Error occurred while returning the object back to the oauth token validation service " + + "stub pool.", e); + } + } + AuthenticationInfo authenticationInfo = new AuthenticationInfo(); + authenticationInfo.setAuthenticated(false); + return authenticationInfo; + } + + /** + * This creates an AuthenticationInfo object that is used for authorization. This method will validate the token + * and + * sets the required parameters to the object. + * + * @param token that needs to be validated. + * @param tokenValidationServiceStub stub that is used to call the external service. + * @return AuthenticationInfo This contains the information related to authenticated client. + * @throws RemoteException that triggers when failing to call the external service.. + */ + private AuthenticationInfo getAuthenticationInfo(String token, + OAuth2TokenValidationServiceStub tokenValidationServiceStub) + throws RemoteException, UserStoreException { + AuthenticationInfo authenticationInfo = new AuthenticationInfo(); + OAuth2TokenValidationRequestDTO validationRequest = new OAuth2TokenValidationRequestDTO(); + OAuth2TokenValidationRequestDTO_OAuth2AccessToken accessToken = + new OAuth2TokenValidationRequestDTO_OAuth2AccessToken(); + accessToken.setTokenType(TOKEN_TYPE); + accessToken.setIdentifier(token); + validationRequest.setAccessToken(accessToken); + boolean authenticated; + OAuth2TokenValidationResponseDTO tokenValidationResponse; + tokenValidationResponse = tokenValidationServiceStub.validate(validationRequest); + if (tokenValidationResponse == null) { + authenticationInfo.setAuthenticated(false); + return authenticationInfo; + } + authenticated = tokenValidationResponse.getValid(); + if (authenticated) { + String authorizedUser = tokenValidationResponse.getAuthorizedUser(); + String username = MultitenantUtils.getTenantAwareUsername(authorizedUser); + String tenantDomain = MultitenantUtils.getTenantDomain(authorizedUser); + authenticationInfo.setUsername(username); + authenticationInfo.setTenantDomain(tenantDomain); + } else { + if (log.isDebugEnabled()) { + log.debug("Token validation failed for token: " + token); + } + } + ServiceContext serviceContext = tokenValidationServiceStub._getServiceClient() + .getLastOperationContext().getServiceContext(); + cookie = (String) serviceContext.getProperty(HTTPConstants.COOKIE_STRING); + authenticationInfo.setAuthenticated(authenticated); + return authenticationInfo; + } + + /** + * Retrieve JWT configs from registry. + */ + private Properties getWebSocketConfig() throws IOException { + Properties properties = new Properties(); + InputStream inputStream = getClass().getClassLoader().getResourceAsStream(WEBSOCKET_CONFIG_LOCATION); + if (inputStream != null) { + properties.load(inputStream); + } + return properties; + } + + private String getTokenFromSession(Session session) { + String queryString = session.getQueryString(); + if (queryString != null) { + String[] allQueryParamPairs = queryString.split(QUERY_STRING_SEPERATOR); + + for (String keyValuePair : allQueryParamPairs) { + String[] queryParamPair = keyValuePair.split(QUERY_KEY_VALUE_SEPERATOR); + + if (queryParamPair.length != 2) { + log.warn("Invalid query string [" + queryString + "] passed in."); + break; + } + if (queryParamPair[0].equals(TOKEN_IDENTIFIER)) { + return queryParamPair[1]; + } + } + } + return null; + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/oauth/OAuthTokenValidaterStubFactory.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/oauth/OAuthTokenValidaterStubFactory.java new file mode 100644 index 000000000..a43f87472 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/oauth/OAuthTokenValidaterStubFactory.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package oauth; + +import oauth.exception.OAuthTokenValidationException; +import org.apache.axis2.AxisFault; +import org.apache.axis2.Constants; +import org.apache.axis2.client.Options; +import org.apache.axis2.client.ServiceClient; +import org.apache.axis2.transport.http.HTTPConstants; +import org.apache.axis2.transport.http.HttpTransportProperties; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpConnectionManager; +import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; +import org.apache.commons.httpclient.contrib.ssl.EasySSLProtocolSocketFactory; +import org.apache.commons.httpclient.params.HttpConnectionManagerParams; +import org.apache.commons.httpclient.protocol.Protocol; +import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; +import org.apache.commons.pool.BasePoolableObjectFactory; +import org.apache.log4j.Logger; +import org.wso2.carbon.identity.oauth2.stub.OAuth2TokenValidationServiceStub; +import util.UIConstants; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.util.Properties; + +/** + * This follows object pool pattern to manage the stub for oauth validation service. + */ +public class OAuthTokenValidaterStubFactory extends BasePoolableObjectFactory { + private static final Logger log = Logger.getLogger(OAuthTokenValidaterStubFactory.class); + private HttpClient httpClient; + Properties tokenValidationProperties; + + + public OAuthTokenValidaterStubFactory(Properties tokenValidationProperties) { + this.tokenValidationProperties = tokenValidationProperties; + this.httpClient = createHttpClient(); + } + + /** + * This creates a OAuth2TokenValidationServiceStub object to the pool. + * + * @return an OAuthValidationStub object + * @throws Exception thrown when creating the object. + */ + @Override + public Object makeObject() throws Exception { + return this.generateStub(); + } + + /** + * This is used to clean up the OAuth validation stub and releases to the object pool. + * + * @param o object that needs to be released. + * @throws Exception throws when failed to release to the pool + */ + @Override + public void passivateObject(Object o) throws Exception { + if (o instanceof OAuth2TokenValidationServiceStub) { + OAuth2TokenValidationServiceStub stub = (OAuth2TokenValidationServiceStub) o; + stub._getServiceClient().cleanupTransport(); + } + } + + /** + * This is used to create a stub which will be triggered through object pool factory, which will create an + * instance of it. + * + * @return OAuth2TokenValidationServiceStub stub that is used to call an external service. + * @throws OAuthTokenValidationException will be thrown when initialization failed. + */ + private OAuth2TokenValidationServiceStub generateStub() throws OAuthTokenValidationException { + OAuth2TokenValidationServiceStub stub; + try { + URL hostURL = new URL(tokenValidationProperties.getProperty((UIConstants.TOKEN_VALIDATION_ENDPOINT_URL))); + if (hostURL != null) { + stub = new OAuth2TokenValidationServiceStub(hostURL.toString()); + if (stub != null) { + ServiceClient client = stub._getServiceClient(); + client.getServiceContext().getConfigurationContext().setProperty( + HTTPConstants.CACHED_HTTP_CLIENT, httpClient); + + HttpTransportProperties.Authenticator auth = + new HttpTransportProperties.Authenticator(); + auth.setPreemptiveAuthentication(true); + String username = tokenValidationProperties.getProperty(UIConstants.USERNAME); + String password = tokenValidationProperties.getProperty(UIConstants.PASSWORD); + auth.setPassword(username); + auth.setUsername(password); + Options options = client.getOptions(); + options.setProperty(HTTPConstants.AUTHENTICATE, auth); + options.setProperty(HTTPConstants.REUSE_HTTP_CLIENT, Constants.VALUE_TRUE); + client.setOptions(options); + if (hostURL.getProtocol().equals("https")) { + // set up ssl factory since axis2 https transport is used. + EasySSLProtocolSocketFactory sslProtocolSocketFactory = + createProtocolSocketFactory(); + Protocol authhttps = new Protocol(hostURL.getProtocol(), + (ProtocolSocketFactory) sslProtocolSocketFactory, + hostURL.getPort()); + Protocol.registerProtocol(hostURL.getProtocol(), authhttps); + options.setProperty(HTTPConstants.CUSTOM_PROTOCOL_HANDLER, authhttps); + } + } else { + String errorMsg = "OAuth Validation instanization failed."; + throw new OAuthTokenValidationException(errorMsg); + } + } else { + String errorMsg = "host url is invalid"; + throw new OAuthTokenValidationException(errorMsg); + } + } catch (AxisFault axisFault) { + throw new OAuthTokenValidationException( + "Error occurred while creating the OAuth2TokenValidationServiceStub.", axisFault); + } catch (MalformedURLException e) { + throw new OAuthTokenValidationException( + "Error occurred while parsing token endpoint URL", e); + } + + return stub; + } + + /** + * This is required to create a trusted connection with the external entity. + * Have to manually configure it since we use CommonHTTPTransport(axis2 transport) in axis2. + * + * @return an EasySSLProtocolSocketFactory for SSL communication. + */ + private EasySSLProtocolSocketFactory createProtocolSocketFactory() throws OAuthTokenValidationException { + try { + EasySSLProtocolSocketFactory easySSLPSFactory = new EasySSLProtocolSocketFactory(); + return easySSLPSFactory; + } catch (IOException e) { + String errorMsg = "Failed to initiate EasySSLProtocolSocketFactory."; + throw new OAuthTokenValidationException(errorMsg, e); + } catch (GeneralSecurityException e) { + String errorMsg = "Failed to set the key material in easy ssl factory."; + throw new OAuthTokenValidationException(errorMsg, e); + } + } + + /** + * This created httpclient pool that can be used to connect to external entity. This connection can be configured + * via broker.xml by setting up the required http connection parameters. + * + * @return an instance of HttpClient that is configured with MultiThreadedHttpConnectionManager + */ + private HttpClient createHttpClient() { + HttpConnectionManagerParams params = new HttpConnectionManagerParams(); + params.setDefaultMaxConnectionsPerHost(Integer.parseInt(tokenValidationProperties.getProperty( + UIConstants.MAXIMUM_HTTP_CONNECTION_PER_HOST))); + params.setMaxTotalConnections(Integer.parseInt(tokenValidationProperties.getProperty( + UIConstants.MAXIMUM_TOTAL_HTTP_CONNECTION))); + HttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager(); + connectionManager.setParams(params); + return new HttpClient(connectionManager); + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/oauth/exception/OAuthTokenValidationException.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/oauth/exception/OAuthTokenValidationException.java new file mode 100644 index 000000000..3f54c5244 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/oauth/exception/OAuthTokenValidationException.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package oauth.exception; + +/** + * This Exception will be thrown, when there any interference with token validation flow. + */ +public class OAuthTokenValidationException extends Exception { + private String errMessage; + + public OAuthTokenValidationException(String msg, Exception nestedEx) { + super(msg, nestedEx); + setErrorMessage(msg); + } + + public OAuthTokenValidationException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public OAuthTokenValidationException(String msg) { + super(msg); + setErrorMessage(msg); + } + + public OAuthTokenValidationException() { + super(); + } + + public OAuthTokenValidationException(Throwable cause) { + super(cause); + } + + public String getErrorMessage() { + return errMessage; + } + + public void setErrorMessage(String errMessage) { + this.errMessage = errMessage; + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/org/wso2/carbon/servlet/ServiceHolder.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/org/wso2/carbon/servlet/ServiceHolder.java new file mode 100644 index 000000000..08d07a44c --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/org/wso2/carbon/servlet/ServiceHolder.java @@ -0,0 +1,28 @@ +package org.wso2.carbon.servlet; + + +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.event.output.adapter.extensions.ui.UIOutputCallbackControllerService; + +public class ServiceHolder { + + private static ServiceHolder instance; + private UIOutputCallbackControllerService uiOutputCallbackControllerService; + + private ServiceHolder(){ + uiOutputCallbackControllerService = (UIOutputCallbackControllerService) PrivilegedCarbonContext + .getThreadLocalCarbonContext() + .getOSGiService(UIOutputCallbackControllerService.class,null); + } + + public synchronized static ServiceHolder getInstance(){ + if (instance==null){ + instance= new ServiceHolder(); + } + return instance; + } + + public UIOutputCallbackControllerService getUiOutputCallbackControllerService() { + return uiOutputCallbackControllerService; + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/org/wso2/carbon/servlet/SuperTenantEventRetrievalEndpoint.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/org/wso2/carbon/servlet/SuperTenantEventRetrievalEndpoint.java new file mode 100644 index 000000000..41a0d8b33 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/org/wso2/carbon/servlet/SuperTenantEventRetrievalEndpoint.java @@ -0,0 +1,85 @@ +package org.wso2.carbon.servlet;/* + * + * Copyright (c) 2014-2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.utils.multitenancy.MultitenantConstants; +import util.UIConstants; + +import javax.ws.rs.GET; +import javax.ws.rs.QueryParam; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +/** + * Getting events for HTTP request for super tenant + */ + +@Path("/") +public class SuperTenantEventRetrievalEndpoint{ + + public SuperTenantEventRetrievalEndpoint() { + } + + /** + * Retrieve events when polling + * + * @param streamName - StreamName extracted from the http url. + * @param version - Version extracted from the http url. + * @param lastUpdatedTime - Last event's dispatched name. + * @return respnse + */ + @GET + @Path("/{streamname}/{version}") + public Response retrieveEvents(@PathParam("streamname") String streamName, @PathParam("version") String version, + @QueryParam("lastUpdatedTime") String lastUpdatedTime) { + String streamId; + JsonObject eventDetails; + String jsonString; + + try { + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantId(MultitenantConstants.SUPER_TENANT_ID); + streamId = streamName + UIConstants.ADAPTER_UI_COLON + version; + + eventDetails = ServiceHolder.getInstance().getUiOutputCallbackControllerService().retrieveEvents(streamName, version, + lastUpdatedTime); + + } finally { + PrivilegedCarbonContext.getThreadLocalCarbonContext().endTenantFlow(); + } + + if(eventDetails == null){ + JsonObject errorData = new JsonObject(); + errorData.addProperty("error","StreamId: " + streamId + " is not registered to receive events."); + jsonString = new Gson().toJson(errorData); + return Response.status(Response.Status.NOT_FOUND).entity(jsonString).header + ("Access-Control-Allow-Origin","*").build(); + } else{ + jsonString = new Gson().toJson(eventDetails); + return Response.ok(jsonString, MediaType.APPLICATION_JSON).header("Access-Control-Allow-Origin", + "*").build(); + } + } + +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/org/wso2/carbon/servlet/TenantEventRetrievalEndpoint.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/org/wso2/carbon/servlet/TenantEventRetrievalEndpoint.java new file mode 100644 index 000000000..ec14e5722 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/org/wso2/carbon/servlet/TenantEventRetrievalEndpoint.java @@ -0,0 +1,89 @@ +package org.wso2.carbon.servlet; +/* + * + * Copyright (c) 2014-2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import util.UIConstants; + +import javax.ws.rs.GET; +import javax.ws.rs.QueryParam; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +/** + * Getting events for HTTP request from a tenant + */ + +@Path("/t/{tdomain}/") +public class TenantEventRetrievalEndpoint { + + private static final Log log = LogFactory.getLog(SuperTenantEventRetrievalEndpoint.class); + + public TenantEventRetrievalEndpoint() { + + } + + /** + * Retrieve events when polling + * + * @param streamName - StreamName extracted from the http url. + * @param version - Version extracted from the http url. + * @param lastUpdatedTime - Last event's dispatched name. + * @param tdomain - Tenant domain extracted from http url + * @return response + */ + @GET + @Path("/{streamname}/{version}") + public Response retrieveEvents(@PathParam("streamname") String streamName, @PathParam("version") String version, + @QueryParam("lastUpdatedTime") String lastUpdatedTime, + @PathParam("tdomain") String tdomain) { + + String streamId; + JsonObject eventDetails; + try { + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantDomain(tdomain, true); + streamId = streamName + UIConstants.ADAPTER_UI_COLON + version; + eventDetails = ServiceHolder.getInstance().getUiOutputCallbackControllerService().retrieveEvents + (streamName, version, lastUpdatedTime); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + + String jsonString; + if (eventDetails == null) { + JsonObject errorData = new JsonObject(); + errorData.addProperty("error", "StreamId: " + streamId + " is not registered to receive events."); + jsonString = new Gson().toJson(errorData); + return Response.status(Response.Status.NOT_FOUND).entity(jsonString).header + ("Access-Control-Allow-Origin", "*").build(); + } else { + jsonString = new Gson().toJson(eventDetails); + return Response.ok(jsonString, MediaType.APPLICATION_JSON).header("Access-Control-Allow-Origin", + "*").build(); + } + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/util/AuthenticationInfo.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/util/AuthenticationInfo.java new file mode 100644 index 000000000..2bac34e68 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/util/AuthenticationInfo.java @@ -0,0 +1,43 @@ +package util; + +public class AuthenticationInfo { + + /** + * this variable is used to check whether the client is authenticated. + */ + private boolean authenticated; + private String username; + private String tenantDomain; + /** + * returns whether the client is authenticated + */ + public boolean isAuthenticated() { + return authenticated; + } + + public void setAuthenticated(boolean authenticated) { + this.authenticated = authenticated; + } + + /** + * returns the authenticated client username + */ + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + /** + * return the authenticated client tenant domain + */ + public String getTenantDomain() { + return tenantDomain; + } + + public void setTenantDomain(String tenantDomain) { + this.tenantDomain = tenantDomain; + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/util/UIConstants.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/util/UIConstants.java new file mode 100644 index 000000000..52f61f020 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/util/UIConstants.java @@ -0,0 +1,38 @@ +/* + * + * * + * * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * * + * * WSO2 Inc. licenses this file to you under the Apache License, + * * Version 2.0 (the "License"); you may not use this file except + * * in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, + * * software distributed under the License is distributed on an + * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * * KIND, either express or implied. See the License for the + * * specific language governing permissions and limitations + * * under the License. + * * + * + */ + +package util; + +/** + * This class contains the constants related to ui client. + */ +public class UIConstants { + + private UIConstants() { + } + public static final String ADAPTER_UI_COLON = ":"; + public static final String MAXIMUM_TOTAL_HTTP_CONNECTION = "maximumTotalHttpConnection"; + public static final String MAXIMUM_HTTP_CONNECTION_PER_HOST = "maximumHttpConnectionPerHost"; + public static final String TOKEN_VALIDATION_ENDPOINT_URL = "tokenValidationEndpointUrl"; + public static final String USERNAME = "username"; + public static final String PASSWORD = "password"; +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/webapp/WEB-INF/web.xml b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..4c01cc52c --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,41 @@ + + + + + Output WebSocket + + JAXServlet + + org.apache.cxf.jaxrs.servlet.CXFNonSpringJaxrsServlet + + + jaxrs.serviceClasses + org.wso2.carbon.servlet.TenantEventRetrievalEndpoint org.wso2.carbon.servlet.SuperTenantEventRetrievalEndpoint + + 1 + + + + + JAXServlet + /* + + diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/pom.xml b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/pom.xml new file mode 100644 index 000000000..0dee07435 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/pom.xml @@ -0,0 +1,137 @@ + + + + + + das-extensions + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.event.output.adapter.extensions.ui + bundle + WSO2 Carbon - Event Output UI Adapter Module + org.wso2.carbon.event.output.adapter.ui provides the back-end functionality of + ui event adapter + + http://wso2.org + + + + org.wso2.carbon.analytics-common + org.wso2.carbon.event.output.adapter.core + + + org.wso2.carbon + org.wso2.carbon.logging + + + org.wso2.carbon + org.wso2.carbon.core + + + javax.websocket + javax.websocket-api + + + org.wso2.carbon.analytics-common + org.wso2.carbon.databridge.commons + + + com.google.code.gson + gson + + + org.wso2.carbon.analytics-common + org.wso2.carbon.event.stream.core + + + + + + + org.apache.felix + maven-scr-plugin + + + generate-scr-descriptor + + scr + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.apache.felix + maven-bundle-plugin + true + + + ${project.artifactId} + ${project.artifactId} + + org.wso2.carbon.event.output.adapter.extensions.ui.internal, + org.wso2.carbon.event.output.adapter.extensions.ui.internal.* + + + !org.wso2.carbon.event.output.adapter.extensions.ui.internal, + !org.wso2.carbon.event.output.adapter.extensions.ui.internal.*, + org.wso2.carbon.event.output.adapter.extensions.ui.* + + + org.wso2.carbon.event.output.adapter.core.*, + javax.xml.namespace; version=0.0.0, + org.apache.axis2, + org.apache.axis2.client, + org.apache.axis2.context, + org.apache.axis2.transport.http, + org.apache.commons.httpclient, + org.apache.commons.httpclient.contrib.ssl, + org.apache.commons.httpclient.params, + org.apache.commons.httpclient.protocol, + org.apache.commons.pool, + org.apache.commons.pool.impl, + org.apache.log4j, + com.google.gson, + javax.websocket, + org.apache.commons.logging, + org.osgi.framework, + org.osgi.service.component, + org.wso2.carbon.context, + org.wso2.carbon.databridge.commons, + org.wso2.carbon.event.stream.core, + org.wso2.carbon.event.stream.core.exception, + org.wso2.carbon.utils + + + + + + + + + + + diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/UIEventAdapter.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/UIEventAdapter.java new file mode 100644 index 000000000..f34ee03df --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/UIEventAdapter.java @@ -0,0 +1,459 @@ +/* + * + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.wso2.carbon.event.output.adapter.extensions.ui; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.context.CarbonContext; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.databridge.commons.Attribute; +import org.wso2.carbon.databridge.commons.Event; +import org.wso2.carbon.databridge.commons.StreamDefinition; +import org.wso2.carbon.event.output.adapter.core.EventAdapterUtil; +import org.wso2.carbon.event.output.adapter.core.OutputEventAdapter; +import org.wso2.carbon.event.output.adapter.core.OutputEventAdapterConfiguration; +import org.wso2.carbon.event.output.adapter.core.exception.OutputEventAdapterException; +import org.wso2.carbon.event.output.adapter.core.exception.OutputEventAdapterRuntimeException; +import org.wso2.carbon.event.output.adapter.core.exception.TestConnectionNotSupportedException; +import org.wso2.carbon.event.output.adapter.extensions.ui.internal.ds.OutputAdaptorEventStreamServiceValueHolder; +import org.wso2.carbon.event.output.adapter.extensions.ui.internal.util.UIEventAdapterConstants; +import org.wso2.carbon.event.output.adapter.extensions.ui.internal.UIOutputCallbackControllerServiceImpl; +import org.wso2.carbon.event.output.adapter.extensions.ui.internal.ds.UIEventAdaptorServiceInternalValueHolder; +import org.wso2.carbon.event.output.adapter.extensions.ui.internal.util.CEPWebSocketSession; +import org.wso2.carbon.event.stream.core.EventStreamService; +import org.wso2.carbon.event.stream.core.exception.EventStreamConfigurationException; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Contains the life cycle of executions regarding the UI Adapter + */ + +public class UIEventAdapter implements OutputEventAdapter { + + private static final Log log = LogFactory.getLog(UIEventAdapter.class); + private OutputEventAdapterConfiguration eventAdapterConfiguration; + private Map globalProperties; + private int queueSize; + private LinkedBlockingDeque streamSpecificEvents; + private static ThreadPoolExecutor executorService; + private int tenantId; + private boolean doLogDroppedMessage; + + private String streamId; + private StreamDefinition streamDefinition; + private List streamMetaAttributes; + private List streamCorrelationAttributes; + private List streamPayloadAttributes; + + public UIEventAdapter(OutputEventAdapterConfiguration eventAdapterConfiguration, Map globalProperties) { + this.eventAdapterConfiguration = eventAdapterConfiguration; + this.globalProperties = globalProperties; + this.doLogDroppedMessage = true; + } + + @Override + public void init() throws OutputEventAdapterException { + + tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId(); + + //ExecutorService will be assigned if it is null + if (executorService == null) { + int minThread; + int maxThread; + long defaultKeepAliveTime; + int jobQueSize; + + //If global properties are available those will be assigned else constant values will be assigned + if (globalProperties.get(UIEventAdapterConstants.ADAPTER_MIN_THREAD_POOL_SIZE_NAME) != null) { + minThread = Integer.parseInt(globalProperties.get( + UIEventAdapterConstants.ADAPTER_MIN_THREAD_POOL_SIZE_NAME)); + } else { + minThread = UIEventAdapterConstants.ADAPTER_MIN_THREAD_POOL_SIZE; + } + + if (globalProperties.get(UIEventAdapterConstants.ADAPTER_MAX_THREAD_POOL_SIZE_NAME) != null) { + maxThread = Integer.parseInt(globalProperties.get( + UIEventAdapterConstants.ADAPTER_MAX_THREAD_POOL_SIZE_NAME)); + } else { + maxThread = UIEventAdapterConstants.ADAPTER_MAX_THREAD_POOL_SIZE; + } + + if (globalProperties.get(UIEventAdapterConstants.ADAPTER_KEEP_ALIVE_TIME_NAME) != null) { + defaultKeepAliveTime = Integer.parseInt(globalProperties.get( + UIEventAdapterConstants.ADAPTER_KEEP_ALIVE_TIME_NAME)); + } else { + defaultKeepAliveTime = UIEventAdapterConstants.DEFAULT_KEEP_ALIVE_TIME_IN_MILLIS; + } + + if (globalProperties.get(UIEventAdapterConstants.ADAPTER_EXECUTOR_JOB_QUEUE_SIZE_NAME) != null) { + jobQueSize = Integer.parseInt(globalProperties.get( + UIEventAdapterConstants.ADAPTER_EXECUTOR_JOB_QUEUE_SIZE_NAME)); + } else { + jobQueSize = UIEventAdapterConstants.ADAPTER_EXECUTOR_JOB_QUEUE_SIZE; + } + + executorService = new ThreadPoolExecutor(minThread, maxThread, defaultKeepAliveTime, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(jobQueSize)); + } + + streamId = eventAdapterConfiguration.getOutputStreamIdOfWso2eventMessageFormat(); + if (streamId == null || streamId.isEmpty()) { + throw new OutputEventAdapterRuntimeException("UI event adapter needs a output stream id"); + } + + // fetch the "streamDefinition" corresponding to the "streamId" and then fetch the different attribute types + // of the streamDefinition corresponding to the event's streamId. They are required when validating values in + // the events against the streamDef attributes. + streamDefinition = getStreamDefinition(streamId); + streamMetaAttributes = streamDefinition.getMetaData(); + streamCorrelationAttributes = streamDefinition.getCorrelationData(); + streamPayloadAttributes = streamDefinition.getPayloadData(); + + ConcurrentHashMap> tenantSpecifcEventOutputAdapterMap = + UIEventAdaptorServiceInternalValueHolder.getTenantSpecificOutputEventStreamAdapterMap(); + + ConcurrentHashMap streamSpecifAdapterMap = tenantSpecifcEventOutputAdapterMap.get(tenantId); + + if (streamSpecifAdapterMap == null) { + streamSpecifAdapterMap = new ConcurrentHashMap(); + if (null != tenantSpecifcEventOutputAdapterMap.putIfAbsent(tenantId, streamSpecifAdapterMap)) { + streamSpecifAdapterMap = tenantSpecifcEventOutputAdapterMap.get(tenantId); + } + } + + String adapterName = streamSpecifAdapterMap.get(streamId); + + if (adapterName != null) { + throw new OutputEventAdapterException(("An Output ui event adapter \"" + adapterName + "\" is already" + + " exist for stream id \"" + streamId + "\"")); + } else { + streamSpecifAdapterMap.put(streamId, eventAdapterConfiguration.getName()); + + ConcurrentHashMap>> tenantSpecificStreamMap = + UIEventAdaptorServiceInternalValueHolder.getTenantSpecificStreamEventMap(); + ConcurrentHashMap> streamSpecificEventsMap = + tenantSpecificStreamMap.get(tenantId); + + if (streamSpecificEventsMap == null) { + streamSpecificEventsMap = new ConcurrentHashMap>(); + if (null != tenantSpecificStreamMap.putIfAbsent(tenantId, streamSpecificEventsMap)) { + streamSpecificEventsMap = tenantSpecificStreamMap.get(tenantId); + } + } + streamSpecificEvents = streamSpecificEventsMap.get(streamId); + + if (streamSpecificEvents == null) { + streamSpecificEvents = new LinkedBlockingDeque(); + if (null != streamSpecificEventsMap.putIfAbsent(streamId, streamSpecificEvents)) { + streamSpecificEvents = streamSpecificEventsMap.get(streamId); + } + } + } + + if (globalProperties.get(UIEventAdapterConstants.ADAPTER_EVENT_QUEUE_SIZE_NAME) != null) { + try { + queueSize = Integer.parseInt( + globalProperties.get(UIEventAdapterConstants.ADAPTER_EVENT_QUEUE_SIZE_NAME)); + } catch (NumberFormatException e) { + log.error("String does not have the appropriate format for conversion." + e.getMessage()); + queueSize = UIEventAdapterConstants.EVENTS_QUEUE_SIZE; + } + } else { + queueSize = UIEventAdapterConstants.EVENTS_QUEUE_SIZE; + } + } + + @Override + public void testConnect() throws TestConnectionNotSupportedException { + throw new TestConnectionNotSupportedException("Test connection is not available"); + } + + @Override + public void connect() { + //Not needed + } + + @Override + public void publish(Object message, Map dynamicProperties) { + + Event event = (Event) message; + StringBuilder eventBuilder = new StringBuilder("["); + + if (streamSpecificEvents.size() == queueSize) { + streamSpecificEvents.removeFirst(); + } + + eventBuilder.append(event.getTimeStamp()); + + if (event.getMetaData() != null) { + eventBuilder.append(","); + Object[] metaData = event.getMetaData(); + for (int i = 0; i < metaData.length; i++) { + eventBuilder.append("\""); + eventBuilder.append(metaData[i]); + eventBuilder.append("\""); + if (i != (metaData.length - 1)) { + eventBuilder.append(","); + } + } + } + + if (event.getCorrelationData() != null) { + Object[] correlationData = event.getCorrelationData(); + + eventBuilder.append(","); + + for (int i = 0; i < correlationData.length; i++) { + eventBuilder.append("\""); + eventBuilder.append(correlationData[i]); + eventBuilder.append("\""); + if (i != (correlationData.length - 1)) { + eventBuilder.append(","); + } + } + } + + if (event.getPayloadData() != null) { + Object[] payloadData = event.getPayloadData(); + eventBuilder.append(","); + for (int i = 0; i < payloadData.length; i++) { + eventBuilder.append("\""); + eventBuilder.append(payloadData[i]); + eventBuilder.append("\""); + if (i != (payloadData.length - 1)) { + eventBuilder.append(","); + } + } + } + + eventBuilder.append("]"); + String eventString = eventBuilder.toString(); + Object[] eventValues = new Object[UIEventAdapterConstants.INDEX_TWO]; + eventValues[UIEventAdapterConstants.INDEX_ZERO] = eventString; + eventValues[UIEventAdapterConstants.INDEX_ONE] = System.currentTimeMillis(); + streamSpecificEvents.add(eventValues); + + // fetch all valid sessions checked against any queryParameters provided when subscribing. + CopyOnWriteArrayList validSessions = getValidSessions(event); + + try { + executorService.execute(new WebSocketSender(validSessions, eventString)); + } catch (RejectedExecutionException e) { + EventAdapterUtil.logAndDrop(eventAdapterConfiguration.getName(), message, "Job queue is full", e, log, + tenantId); + } + + } + + @Override + public void disconnect() { + //Not needed + } + + @Override + public void destroy() { + + int tenantId = CarbonContext.getThreadLocalCarbonContext().getTenantId(); + + ConcurrentHashMap tenantSpecificAdapterMap = UIEventAdaptorServiceInternalValueHolder + .getTenantSpecificOutputEventStreamAdapterMap().get(tenantId); + if (tenantSpecificAdapterMap != null && streamId != null) { + tenantSpecificAdapterMap.remove(streamId); //Removing outputadapter and streamId + } + + ConcurrentHashMap> tenantSpecificStreamEventMap = + UIEventAdaptorServiceInternalValueHolder.getTenantSpecificStreamEventMap().get(tenantId); + if (tenantSpecificStreamEventMap != null && streamId != null) { + //Removing the streamId and events registered for the output adapter + tenantSpecificStreamEventMap.remove(streamId); + } + } + + @Override + public boolean isPolled() { + return true; + } + + /** + * Fetch the StreamDefinition corresponding to the given StreamId from the EventStreamService. + * + * @param streamId the streamId of this UIEventAdaptor. + * @return the "StreamDefinition" object corresponding to the streamId of this EventAdaptor. + * @throws OutputEventAdapterException if the "EventStreamService" OSGI service is unavailable/unregistered or if + * the matching Steam-Definition for the given StreamId cannot be retrieved. + */ + private StreamDefinition getStreamDefinition(String streamId) throws OutputEventAdapterException { + EventStreamService eventStreamService = OutputAdaptorEventStreamServiceValueHolder.getEventStreamService(); + if (eventStreamService != null) { + try { + return eventStreamService.getStreamDefinition(streamId); + } catch (EventStreamConfigurationException e) { + String adaptorType = eventAdapterConfiguration.getType(); + String adaptorName = eventAdapterConfiguration.getName(); + String errorMsg = "Error while retrieving Stream-Definition for Stream with id [" + streamId + "] " + + "for Adaptor [" + adaptorName + "] of type [" + adaptorType + "]."; + log.error(errorMsg); + throw new OutputEventAdapterException(errorMsg, e); + } + } + throw new OutputEventAdapterException( + "Could not retrieve the EventStreamService whilst trying to fetch the Stream-Definition of Stream " + + "with Id [" + streamId + "]."); + } + + /** + * Fetches all valid web-socket sessions from the entire pool of subscribed sessions. The validity is checked + * against any queryString provided when subscribing to the web-socket endpoint. + * + * @param event the current event received and that which needs to be published to subscribed sessions. + * @return a list of all validated web-socket sessions against the queryString values. + */ + private CopyOnWriteArrayList getValidSessions(Event event) { + CopyOnWriteArrayList validSessions = new CopyOnWriteArrayList<>(); + UIOutputCallbackControllerServiceImpl uiOutputCallbackControllerServiceImpl = + UIEventAdaptorServiceInternalValueHolder.getUIOutputCallbackRegisterServiceImpl(); + // get all subscribed web-socket sessions. + CopyOnWriteArrayList cepWebSocketSessions = + uiOutputCallbackControllerServiceImpl.getSessions(tenantId, streamId); + + for (CEPWebSocketSession cepWebSocketSession : cepWebSocketSessions) { + boolean isValidSession = validateEventAgainstSessionFilters(event, cepWebSocketSession); + if (isValidSession) { + validSessions.add(cepWebSocketSession); + } + } + return validSessions; + } + + + /** + * Processes the given session's validity to receive the current "event" against any queryParams that was used at + * the time when the web-socket-session is subscribed. This method can be extended to validate the event against + * any additional attribute of the given session too. + * + * @param event the current event received and that which needs to be published to subscribed + * sessions. + * @param cepWebSocketSession the session which needs validated for its authenticity to receive this event. + * @return "true" if the session is valid to receive the event else "false". + */ + private boolean validateEventAgainstSessionFilters(Event event, CEPWebSocketSession cepWebSocketSession) { + + // fetch the queryString Key:Value pair map of the given session. + Map queryParamValuePairs = cepWebSocketSession.getQueryParamValuePairs(); + if (queryParamValuePairs != null) { + // fetch the different attribute values received as part of the current event. + Object[] eventMetaData = event.getMetaData(); + Object[] eventCorrelationData = event.getCorrelationData(); + Object[] eventPayloadData = event.getPayloadData(); + + if (streamMetaAttributes != null) { + for (int i = 0; i < streamMetaAttributes.size(); i++) { + String attributeName = streamMetaAttributes.get(i).getName(); + String queryValue = queryParamValuePairs.get(attributeName); + + if (queryValue != null && + (eventMetaData == null || !eventMetaData[i].toString().equals(queryValue))) { + return false; + } + } + } + + if (streamCorrelationAttributes != null) { + for (int i = 0; i < streamCorrelationAttributes.size(); i++) { + String attributeName = streamCorrelationAttributes.get(i).getName(); + String queryValue = queryParamValuePairs.get(attributeName); + + if (queryValue != null && + (eventCorrelationData == null || !eventCorrelationData[i].toString().equals(queryValue))) { + return false; + } + } + } + + if (streamPayloadAttributes != null) { + for (int i = 0; i < streamPayloadAttributes.size(); i++) { + String attributeName = streamPayloadAttributes.get(i).getName(); + String queryValue = queryParamValuePairs.get(attributeName); + + if (queryValue != null && (eventPayloadData == null || !eventPayloadData[i].toString().equals( + queryValue))) { + return false; + } + } + } + } + return true; + } + + private class WebSocketSender implements Runnable { + + private String message; + private CopyOnWriteArrayList cepWebSocketSessions; + + public WebSocketSender(CopyOnWriteArrayList cepWebSocketSessions, String message) { + this.cepWebSocketSessions = cepWebSocketSessions; + this.message = message; + } + + /** + * When an object implementing interface Runnable is used + * to create a thread, starting the thread causes the object's + * run method to be called in that separately executing + * thread. + *

+ * The general contract of the method run is that it may + * take any action whatsoever. + * + * @see Thread#run() + */ + @Override + public void run() { + if (cepWebSocketSessions != null) { + doLogDroppedMessage = true; + for (CEPWebSocketSession cepWebSocketSession : cepWebSocketSessions) { + synchronized (cepWebSocketSession) { + try { + cepWebSocketSession.getSession().getBasicRemote().sendText(message); + } catch (IOException e) { + EventAdapterUtil.logAndDrop(eventAdapterConfiguration.getName(), message, + "Cannot send to endpoint", e, log, tenantId); + } + } + } + } else if (doLogDroppedMessage) { + EventAdapterUtil.logAndDrop(eventAdapterConfiguration.getName(), message, "No clients registered", log, + tenantId); + doLogDroppedMessage = false; + } + } + } +} + diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/UIEventAdapterFactory.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/UIEventAdapterFactory.java new file mode 100644 index 000000000..c28f982f3 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/UIEventAdapterFactory.java @@ -0,0 +1,89 @@ +/* + * + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.wso2.carbon.event.output.adapter.extensions.ui; + +import org.wso2.carbon.event.output.adapter.core.MessageType; +import org.wso2.carbon.event.output.adapter.core.OutputEventAdapter; +import org.wso2.carbon.event.output.adapter.core.OutputEventAdapterConfiguration; +import org.wso2.carbon.event.output.adapter.core.OutputEventAdapterFactory; +import org.wso2.carbon.event.output.adapter.core.Property; +import org.wso2.carbon.event.output.adapter.extensions.ui.internal.util.UIEventAdapterConstants; +import org.wso2.carbon.utils.CarbonUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.ResourceBundle; + +/** + * The UI event adapter factory class to create a UI output adapter + */ +public class UIEventAdapterFactory extends OutputEventAdapterFactory { + + private ResourceBundle resourceBundle = ResourceBundle.getBundle("org.wso2.carbon.event.output.adapter.extensions.ui.i18n" + + ".Resources", Locale.getDefault()); + private int httpPort; + private int httpsPort; + private int portOffset; + + public UIEventAdapterFactory() { + portOffset = getPortOffset(); + httpPort = UIEventAdapterConstants.DEFAULT_HTTP_PORT + portOffset; + httpsPort = UIEventAdapterConstants.DEFAULT_HTTPS_PORT + portOffset; + } + + @Override + public String getType() { + return UIEventAdapterConstants.ADAPTER_TYPE_UI; + } + + @Override + public List getSupportedMessageFormats() { + List supportedMessageFormats = new ArrayList(); + supportedMessageFormats.add(MessageType.WSO2EVENT); + return supportedMessageFormats; + } + + @Override + public List getStaticPropertyList() { + return null; + } + + @Override + public List getDynamicPropertyList() { + return null; + } + + @Override + public String getUsageTips() { + return resourceBundle.getString(UIEventAdapterConstants.ADAPTER_USAGE_TIPS_PREFIX) + " " + resourceBundle.getString(UIEventAdapterConstants.ADAPTER_USAGE_TIPS_POSTFIX); + } + + @Override + public OutputEventAdapter createEventAdapter(OutputEventAdapterConfiguration eventAdapterConfiguration, + Map globalProperties) { + return new UIEventAdapter(eventAdapterConfiguration, globalProperties); + } + + private int getPortOffset() { + return CarbonUtils.getPortFromServerConfig(UIEventAdapterConstants.CARBON_CONFIG_PORT_OFFSET_NODE) + 1; + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/UIOutputCallbackControllerService.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/UIOutputCallbackControllerService.java new file mode 100644 index 000000000..9b9375271 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/UIOutputCallbackControllerService.java @@ -0,0 +1,61 @@ +/* + * + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ + +package org.wso2.carbon.event.output.adapter.extensions.ui; + +import com.google.gson.JsonObject; + +import javax.websocket.Session; + +/** + * This interface is exposed as an OSGI service, which will be invoked by the local websocket endpoint to inform new subscriptions; and do un-subscriptions.. + */ +public interface UIOutputCallbackControllerService { + + /** + * Used to subscribe the session id and stream id for later web socket connectivity + * + * @param streamName - Stream name which user register to. + * @param version - Stream version which user uses. + * @param session - Session which user registered. + * @return + */ + public void subscribeWebsocket(String streamName, String version, Session session); + + /** + * Used to return events per streamId + * + * @param streamName - Stream name which user register to. + * @param version - Stream version which user uses. + * @param session - Session which user subscribed to. + * @return the events list. + */ + public void unsubscribeWebsocket(String streamName, String version, Session session); + + /** + * Used to return events per http GET request. + * + * @param streamName - Stream name which user register to. + * @param version - Stream version which user uses. + * @param lastUpdatedTime - Last dispatched events time. + * @return the events list. + */ + public JsonObject retrieveEvents(String streamName, String version, String lastUpdatedTime); +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/UIOutputCallbackControllerServiceImpl.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/UIOutputCallbackControllerServiceImpl.java new file mode 100644 index 000000000..9da8578bc --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/UIOutputCallbackControllerServiceImpl.java @@ -0,0 +1,210 @@ +/* + * + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.wso2.carbon.event.output.adapter.extensions.ui.internal; + +import com.google.gson.JsonObject; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.event.output.adapter.extensions.ui.UIOutputCallbackControllerService; +import org.wso2.carbon.event.output.adapter.extensions.ui.internal.util.CEPWebSocketSession; +import org.wso2.carbon.event.output.adapter.extensions.ui.internal.util.UIEventAdapterConstants; +import org.wso2.carbon.event.output.adapter.extensions.ui.internal.ds.UIEventAdaptorServiceInternalValueHolder; + +import javax.websocket.Session; +import java.util.Iterator; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.LinkedBlockingDeque; + +/** + * Service implementation class which exposes to front end + */ +public class UIOutputCallbackControllerServiceImpl implements UIOutputCallbackControllerService { + + private ConcurrentHashMap>> + outputEventAdaptorSessionMap; + + public UIOutputCallbackControllerServiceImpl() { + outputEventAdaptorSessionMap = + new ConcurrentHashMap>>(); + } + + + /** + * Used to subscribe the session id and stream id for later web socket connectivity + * + * @param streamName - Stream name which user register to. + * @param version - Stream version which user uses. + * @param session - Session which user registered. + * @return + */ + public void subscribeWebsocket(String streamName, String version, Session session) { + + int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId(); + + if (version == null || " ".equals(version)) { + version = UIEventAdapterConstants.ADAPTER_UI_DEFAULT_OUTPUT_STREAM_VERSION; + } + String streamId = streamName + UIEventAdapterConstants.ADAPTER_UI_COLON + version; + ConcurrentHashMap> tenantSpecificAdaptorMap = + outputEventAdaptorSessionMap.get(tenantId); + if (tenantSpecificAdaptorMap == null) { + tenantSpecificAdaptorMap = new ConcurrentHashMap>(); + if (null != outputEventAdaptorSessionMap.putIfAbsent(tenantId, tenantSpecificAdaptorMap)) { + tenantSpecificAdaptorMap = outputEventAdaptorSessionMap.get(tenantId); + } + } + CopyOnWriteArrayList adapterSpecificSessions = tenantSpecificAdaptorMap.get(streamId); + if (adapterSpecificSessions == null) { + adapterSpecificSessions = new CopyOnWriteArrayList(); + if (null != tenantSpecificAdaptorMap.putIfAbsent(streamId, adapterSpecificSessions)) { + adapterSpecificSessions = tenantSpecificAdaptorMap.get(streamId); + } + } + + CEPWebSocketSession cepWebSocketSession = new CEPWebSocketSession(session); + adapterSpecificSessions.add(cepWebSocketSession); + } + + /** + * Used to return registered sessions per streamId + * + * @param tenantId - Tenant id of the user. + * @param streamId - Stream name and version which user register to. + * @return the sessions list. + */ + public CopyOnWriteArrayList getSessions(int tenantId, String streamId) { + ConcurrentHashMap> tenantSpecificAdaptorMap = outputEventAdaptorSessionMap.get(tenantId); + if (tenantSpecificAdaptorMap != null) { + return tenantSpecificAdaptorMap.get(streamId); + } + return null; + } + + /** + * Used to return events per streamId + * + * @param tenanId - Tenant id of the user. + * @param streamName - Stream name which user register to. + * @param version - Stream version which user uses. + * @return the events list. + */ + public LinkedBlockingDeque getEvents(int tenanId, String streamName, String version) { + + ConcurrentHashMap> tenantSpecificStreamMap = + UIEventAdaptorServiceInternalValueHolder.getTenantSpecificStreamEventMap().get(tenanId); + + if (tenantSpecificStreamMap != null) { + String streamId = streamName + UIEventAdapterConstants.ADAPTER_UI_COLON + version; + return tenantSpecificStreamMap.get(streamId); + } + return null; + } + + /** + * Used to return events per streamId + * + * @param streamName - Stream name which user register to. + * @param version - Stream version which user uses. + * @param session - Session which user subscribed to. + * @return the events list. + */ + public void unsubscribeWebsocket(String streamName, String version, Session session) { + + int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId(); + + if (version == null || " ".equals(version)) { + version = UIEventAdapterConstants.ADAPTER_UI_DEFAULT_OUTPUT_STREAM_VERSION; + } + String id = streamName + UIEventAdapterConstants.ADAPTER_UI_COLON + version; + ConcurrentHashMap> tenantSpecificAdaptorMap = outputEventAdaptorSessionMap.get(tenantId); + if (tenantSpecificAdaptorMap != null) { + CopyOnWriteArrayList adapterSpecificSessions = tenantSpecificAdaptorMap.get(id); + if (adapterSpecificSessions != null) { + CEPWebSocketSession sessionToRemove = null; + for (Iterator iterator = adapterSpecificSessions.iterator(); iterator.hasNext(); ) { + CEPWebSocketSession cepWebSocketSession = iterator.next(); + if (session.getId().equals(cepWebSocketSession.getSession().getId())) { + sessionToRemove = cepWebSocketSession; + break; + } + } + if (sessionToRemove != null) { + adapterSpecificSessions.remove(sessionToRemove); + } + } + } + } + + /** + * Used to return events per http GET request. + * + * @param streamName - Stream name which user register to. + * @param version - Stream version which user uses. + * @param lastUpdatedTime - Last dispatched events time. + * @return the events list. + */ + @Override + public JsonObject retrieveEvents(String streamName, String version, String lastUpdatedTime) { + + int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId(); + LinkedBlockingDeque allEvents = getEvents(tenantId, streamName, version); + //List eventsListToBeSent; + Object lastEventTime = null; + JsonObject eventsData; + + if (allEvents != null) { + eventsData = new JsonObject(); + + Boolean firstFilteredValue = true; + long sentTimeStamp = Long.parseLong(lastUpdatedTime); + //eventsListToBeSent = new ArrayList(); + + StringBuilder allEventsAsString = new StringBuilder("["); + // set Iterator as descending + Iterator iterator = allEvents.descendingIterator(); + + while (iterator.hasNext()) { + + Object[] eventValues = (Object[]) iterator.next(); + long eventTimeStamp = (Long) eventValues[UIEventAdapterConstants.INDEX_ONE]; + if (sentTimeStamp < eventTimeStamp) { + + if (!firstFilteredValue) { + allEventsAsString.append(","); + } + firstFilteredValue = false; + String eventString = (String) eventValues[UIEventAdapterConstants.INDEX_ZERO]; + allEventsAsString.append(eventString); + } + } + allEventsAsString.append("]"); + + if (allEvents.size() != 0) { + Object[] lastObj = (Object[]) allEvents.getLast(); + lastEventTime = lastObj[UIEventAdapterConstants.INDEX_ONE]; + eventsData.addProperty("lastEventTime", String.valueOf(lastEventTime)); + } + eventsData.addProperty("events", allEventsAsString.toString()); + + return eventsData; + } + return null; + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/ds/OutputAdaptorEventStreamServiceValueHolder.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/ds/OutputAdaptorEventStreamServiceValueHolder.java new file mode 100644 index 000000000..99515467a --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/ds/OutputAdaptorEventStreamServiceValueHolder.java @@ -0,0 +1,23 @@ +package org.wso2.carbon.event.output.adapter.extensions.ui.internal.ds; + +import org.wso2.carbon.event.stream.core.EventStreamService; + +/** + * This class holds a reference to the current EventStream OSGI service component "eventStreamService.component" in the + * OSGI runtime. + */ +public class OutputAdaptorEventStreamServiceValueHolder { + private static EventStreamService eventStreamService; + + public static void registerEventStreamService(EventStreamService eventBuilderService) { + OutputAdaptorEventStreamServiceValueHolder.eventStreamService = eventBuilderService; + } + + public static EventStreamService getEventStreamService() { + return OutputAdaptorEventStreamServiceValueHolder.eventStreamService; + } + + public static void unregisterEventStreamService(EventStreamService eventStreamService) { + OutputAdaptorEventStreamServiceValueHolder.eventStreamService = null; + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/ds/UIEventAdaptorServiceInternalValueHolder.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/ds/UIEventAdaptorServiceInternalValueHolder.java new file mode 100644 index 000000000..145b320d6 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/ds/UIEventAdaptorServiceInternalValueHolder.java @@ -0,0 +1,56 @@ +/* + * + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.wso2.carbon.event.output.adapter.extensions.ui.internal.ds; + +import org.wso2.carbon.event.output.adapter.extensions.ui.internal.UIOutputCallbackControllerServiceImpl; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingDeque; + +/** + * Creates a holder of type UIOutputCallbackRegisterServiceImpl. + */ +public final class UIEventAdaptorServiceInternalValueHolder { + + private static UIOutputCallbackControllerServiceImpl UIOutputCallbackRegisterServiceImpl; + private static ConcurrentHashMap> tenantSpecificOutputEventStreamAdapterMap = new + ConcurrentHashMap>(); + private static ConcurrentHashMap>> tenantSpecificStreamEventMap + = new ConcurrentHashMap>>(); + + public static void registerUIOutputCallbackRegisterServiceInternal( + UIOutputCallbackControllerServiceImpl UIOutputCallbackRegisterServiceImpl) { + UIEventAdaptorServiceInternalValueHolder.UIOutputCallbackRegisterServiceImpl = + UIOutputCallbackRegisterServiceImpl; + } + + public static UIOutputCallbackControllerServiceImpl getUIOutputCallbackRegisterServiceImpl() { + return UIEventAdaptorServiceInternalValueHolder.UIOutputCallbackRegisterServiceImpl; + } + + public static ConcurrentHashMap> getTenantSpecificOutputEventStreamAdapterMap() { + return tenantSpecificOutputEventStreamAdapterMap; + } + + public static ConcurrentHashMap>> + getTenantSpecificStreamEventMap() { + return tenantSpecificStreamEventMap; + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/ds/UILocalEventAdapterDS.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/ds/UILocalEventAdapterDS.java new file mode 100644 index 000000000..be43fc955 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/ds/UILocalEventAdapterDS.java @@ -0,0 +1,80 @@ +/* + * + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.wso2.carbon.event.output.adapter.extensions.ui.internal.ds; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.osgi.service.component.ComponentContext; +import org.wso2.carbon.event.output.adapter.core.OutputEventAdapterFactory; +import org.wso2.carbon.event.output.adapter.extensions.ui.UIEventAdapterFactory; +import org.wso2.carbon.event.output.adapter.extensions.ui.UIOutputCallbackControllerService; +import org.wso2.carbon.event.output.adapter.extensions.ui.internal.UIOutputCallbackControllerServiceImpl; +import org.wso2.carbon.event.stream.core.EventStreamService; + +/** + * @scr.component component.name="output.extensions.Ui.AdapterService.component" immediate="true" + * @scr.reference name="eventStreamService.service" + * interface="org.wso2.carbon.event.stream.core.EventStreamService" cardinality="1..1" + * policy="dynamic" bind="setEventStreamService" unbind="unsetEventStreamService" + */ +public class UILocalEventAdapterDS { + + private static final Log log = LogFactory.getLog(UILocalEventAdapterDS.class); + + /** + * initialize the ui adapter service here service here. + * + * @param context + */ + protected void activate(ComponentContext context) { + + try { + OutputEventAdapterFactory uiEventAdapterFactory = new UIEventAdapterFactory(); + context.getBundleContext().registerService(OutputEventAdapterFactory.class.getName(), uiEventAdapterFactory, null); + + UIOutputCallbackControllerServiceImpl UIOutputCallbackRegisterServiceImpl = new UIOutputCallbackControllerServiceImpl(); + context.getBundleContext().registerService(UIOutputCallbackControllerService.class.getName(), + UIOutputCallbackRegisterServiceImpl, null); + + UIEventAdaptorServiceInternalValueHolder.registerUIOutputCallbackRegisterServiceInternal( + UIOutputCallbackRegisterServiceImpl); + + if (log.isDebugEnabled()) { + log.debug("Successfully deployed the output ui adapter service"); + } + } catch (RuntimeException e) { + log.error("Can not create the output ui adapter service ", e); + } + } + + protected void setEventStreamService(EventStreamService eventStreamService) { + if (log.isDebugEnabled()) { + log.debug("Setting the EventStreamService reference for the UILocalEventAdaptor Service"); + } + OutputAdaptorEventStreamServiceValueHolder.registerEventStreamService(eventStreamService); + } + + protected void unsetEventStreamService(EventStreamService eventStreamService) { + if (log.isDebugEnabled()) { + log.debug("Un-Setting the EventStreamService reference for the UILocalEventAdaptor Service"); + } + OutputAdaptorEventStreamServiceValueHolder.unregisterEventStreamService(eventStreamService); + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/util/CEPWebSocketSession.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/util/CEPWebSocketSession.java new file mode 100644 index 000000000..751631b38 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/util/CEPWebSocketSession.java @@ -0,0 +1,62 @@ +package org.wso2.carbon.event.output.adapter.extensions.ui.internal.util; + + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import javax.websocket.Session; +import java.util.HashMap; +import java.util.Map; + +/** + * This is wrapper class over the javax.websocket.Session implementation. This class contains additional attributes + * of the Session object derived from processing some of the (default) existing attributes. + * Ex: Query-String's [Key:Value] Map derived from the queryString attribute of the original class. + */ +public class CEPWebSocketSession { + private static final Log log = LogFactory.getLog(CEPWebSocketSession.class); + + private static final String QUERY_STRING_SEPERATOR = "&"; + private static final String QUERY_KEY_VALUE_SEPERATOR = "="; + private Map queryParamValuePairs = null; + private Session session; + + public CEPWebSocketSession(Session session) { + this.session = session; + setQueryParamValuePairs(); + } + + public Map getQueryParamValuePairs() { + return queryParamValuePairs; + } + + public Session getSession() { + return session; + } + + /** + * Processes the queryString from the current instance's Session attribute and constructs a map of Query + * Key:Value pair. + */ + private void setQueryParamValuePairs() { + if (session.getQueryString() != null) { + String queryString = session.getQueryString(); + String[] allQueryParamPairs = queryString.split(QUERY_STRING_SEPERATOR); + + for (String keyValuePair : allQueryParamPairs) { + String[] thisQueryParamPair = keyValuePair.split(QUERY_KEY_VALUE_SEPERATOR); + + if (thisQueryParamPair.length != 2) { + log.warn("Invalid query string [" + queryString + "] passed in."); + break; + } + + if (queryParamValuePairs == null) { + queryParamValuePairs = new HashMap<>(); + } + + queryParamValuePairs.put(thisQueryParamPair[0], thisQueryParamPair[1]); + } + } + } +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/util/UIEventAdapterConstants.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/util/UIEventAdapterConstants.java new file mode 100644 index 000000000..9c1ea3ad5 --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/java/org/wso2/carbon/event/output/adapter/extensions/ui/internal/util/UIEventAdapterConstants.java @@ -0,0 +1,54 @@ +/* + * + * Copyright (c) 2014-2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.wso2.carbon.event.output.adapter.extensions.ui.internal.util; + +/** + * This class contains the constants related to ui Output Event Adaptor. + */ +public class UIEventAdapterConstants { + + private UIEventAdapterConstants() { + } + + public static final String ADAPTER_TYPE_UI = "iot-ui"; + public static final String ADAPTER_USAGE_TIPS_PREFIX = "ui.usage.tips_prefix"; + public static final String ADAPTER_USAGE_TIPS_POSTFIX = "ui.usage.tips_postfix"; + public static final String ADAPTER_UI_DEFAULT_OUTPUT_STREAM_VERSION = "1.0.0"; + public static final String ADAPTER_UI_COLON = ":"; + public static final int INDEX_ZERO = 0; + public static final int INDEX_ONE = 1; + public static final int INDEX_TWO = 2; + + public static final int ADAPTER_MIN_THREAD_POOL_SIZE = 8; + public static final int ADAPTER_MAX_THREAD_POOL_SIZE = 100; + public static final int ADAPTER_EXECUTOR_JOB_QUEUE_SIZE = 2000; + public static final long DEFAULT_KEEP_ALIVE_TIME_IN_MILLIS = 20000; + public static final String ADAPTER_MIN_THREAD_POOL_SIZE_NAME = "minThread"; + public static final String ADAPTER_MAX_THREAD_POOL_SIZE_NAME = "maxThread"; + public static final String ADAPTER_KEEP_ALIVE_TIME_NAME = "keepAliveTimeInMillis"; + public static final String ADAPTER_EXECUTOR_JOB_QUEUE_SIZE_NAME = "jobQueueSize"; + + public static final String ADAPTER_EVENT_QUEUE_SIZE_NAME = "eventQueueSize"; + public static final int EVENTS_QUEUE_SIZE = 30; + + public static final String CARBON_CONFIG_PORT_OFFSET_NODE = "Ports.Offset"; + public static final int DEFAULT_HTTP_PORT = 9763; + public static final int DEFAULT_HTTPS_PORT = 9443; +} diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/resources/org/wso2/carbon/event/output/adapter/extensions/ui/i18n/Resources.properties b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/resources/org/wso2/carbon/event/output/adapter/extensions/ui/i18n/Resources.properties new file mode 100644 index 000000000..4a3dcba8e --- /dev/null +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui/src/main/resources/org/wso2/carbon/event/output/adapter/extensions/ui/i18n/Resources.properties @@ -0,0 +1,22 @@ +# +# Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +# +# WSO2 Inc. licenses this file to you under the Apache License, +# Version 2.0 (the "License"); you may not use this file except +# in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +output.event.stream.name=Output Stream Name +output.event.stream.version=Output Stream Version +ui.usage.tips_prefix=There must be an UI output adaptor for each stream to be visualized +ui.usage.tips_postfix= via Analytics Dashboard. diff --git a/components/iot-plugins/das-extensions/pom.xml b/components/iot-plugins/das-extensions/pom.xml new file mode 100644 index 000000000..7c1f0be17 --- /dev/null +++ b/components/iot-plugins/das-extensions/pom.xml @@ -0,0 +1,40 @@ + + + + + + + org.wso2.carbon.devicemgt-plugins + iot-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + das-extensions + pom + This contains das extension + http://wso2.org + + + org.wso2.carbon.event.input.adapter.extensions + org.wso2.carbon.event.output.adapter.extensions.ui + org.wso2.carbon.event.output.adapter.extensions.ui.endpoint + + diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/service/impl/DigitalDisplayManagerServiceImpl.java b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/service/impl/DigitalDisplayManagerServiceImpl.java index 382a22c6f..d0e5f332c 100644 --- a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/service/impl/DigitalDisplayManagerServiceImpl.java +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/service/impl/DigitalDisplayManagerServiceImpl.java @@ -35,7 +35,6 @@ import org.wso2.carbon.device.mgt.iot.exception.DeviceControllerException; import org.wso2.carbon.device.mgt.iot.util.ZipArchive; import org.wso2.carbon.device.mgt.iot.util.ZipUtil; import org.wso2.carbon.identity.jwt.client.extension.JWTClient; -import org.wso2.carbon.identity.jwt.client.extension.JWTClientManager; import org.wso2.carbon.identity.jwt.client.extension.dto.AccessTokenInfo; import org.wso2.carbon.identity.jwt.client.extension.exception.JWTClientException; import org.wso2.carbon.user.api.UserStoreException; @@ -169,16 +168,13 @@ public class DigitalDisplayManagerServiceImpl implements DigitalDisplayManagerSe apiApplicationKey = apiManagementProviderService.generateAndRetrieveApplicationKeys( DigitalDisplayConstants.DEVICE_TYPE, tags, KEY_TYPE, applicationUsername, true); } - JWTClient jwtClient = JWTClientManager.getInstance().getJWTClient(); + JWTClient jwtClient = APIUtil.getJWTClientManagerService().getJWTClient(); String scopes = "device_type_" + DigitalDisplayConstants.DEVICE_TYPE + " device_" + deviceId; AccessTokenInfo accessTokenInfo = jwtClient.getAccessToken(apiApplicationKey.getConsumerKey(), apiApplicationKey.getConsumerSecret(), owner, scopes); - //create token - String accessToken = accessTokenInfo.getAccess_token(); - String refreshToken = accessTokenInfo.getRefresh_token(); - //adding registering data + String accessToken = accessTokenInfo.getAccessToken(); + String refreshToken = accessTokenInfo.getRefreshToken(); boolean status; - //Register the device with CDMF status = register(deviceId, deviceName); if (!status) { String msg = "Error occurred while registering the device with " + "id: " + deviceId + " owner:" + owner; diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/service/impl/util/APIUtil.java b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/service/impl/util/APIUtil.java index 51b41cd72..1c8855a32 100644 --- a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/service/impl/util/APIUtil.java +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/service/impl/util/APIUtil.java @@ -5,6 +5,7 @@ import org.apache.commons.logging.LogFactory; import org.wso2.carbon.apimgt.application.extension.APIManagementProviderService; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; +import org.wso2.carbon.identity.jwt.client.extension.service.JWTClientManagerService; /** * This class provides utility functions used by REST-API. @@ -52,4 +53,17 @@ public class APIUtil { } return apiManagementProviderService; } + + public static JWTClientManagerService getJWTClientManagerService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + JWTClientManagerService jwtClientManagerService = + (JWTClientManagerService) ctx.getOSGiService(JWTClientManagerService.class, null); + if (jwtClientManagerService == null) { + String msg = "JWT Client manager service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return jwtClientManagerService; + } + } diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/service/impl/DroneManagerServiceImpl.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/service/impl/DroneManagerServiceImpl.java index d9fbfb44b..60cb5520a 100644 --- a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/service/impl/DroneManagerServiceImpl.java +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/service/impl/DroneManagerServiceImpl.java @@ -36,7 +36,6 @@ import org.wso2.carbon.device.mgt.iot.exception.DeviceControllerException; import org.wso2.carbon.device.mgt.iot.util.ZipArchive; import org.wso2.carbon.device.mgt.iot.util.ZipUtil; import org.wso2.carbon.identity.jwt.client.extension.JWTClient; -import org.wso2.carbon.identity.jwt.client.extension.JWTClientManager; import org.wso2.carbon.identity.jwt.client.extension.dto.AccessTokenInfo; import org.wso2.carbon.identity.jwt.client.extension.exception.JWTClientException; import org.wso2.carbon.user.api.UserStoreException; @@ -209,14 +208,14 @@ public class DroneManagerServiceImpl implements DroneManagerService { apiApplicationKey = apiManagementProviderService.generateAndRetrieveApplicationKeys( DroneConstants.DEVICE_TYPE, tags, KEY_TYPE, applicationUsername, true); } - JWTClient jwtClient = JWTClientManager.getInstance().getJWTClient(); + JWTClient jwtClient = APIUtil.getJWTClientManagerService().getJWTClient(); String owner = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUsername(); String scopes = "device_type_" + DroneConstants.DEVICE_TYPE + " device_" + deviceId; AccessTokenInfo accessTokenInfo = jwtClient.getAccessToken(apiApplicationKey.getConsumerKey(), apiApplicationKey.getConsumerSecret(), owner, scopes); //create token - String accessToken = accessTokenInfo.getAccess_token(); - String refreshToken = accessTokenInfo.getRefresh_token(); + String accessToken = accessTokenInfo.getAccessToken(); + String refreshToken = accessTokenInfo.getRefreshToken(); //adding registering data XmppAccount newXmppAccount = new XmppAccount(); newXmppAccount.setAccountName(APIUtil.getAuthenticatedUser() + "_" + deviceId); diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/service/impl/util/APIUtil.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/service/impl/util/APIUtil.java index 4af82a9ba..6d8bb627d 100644 --- a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/service/impl/util/APIUtil.java +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/service/impl/util/APIUtil.java @@ -5,6 +5,7 @@ import org.apache.commons.logging.LogFactory; import org.wso2.carbon.apimgt.application.extension.APIManagementProviderService; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; +import org.wso2.carbon.identity.jwt.client.extension.service.JWTClientManagerService; /** * This class provides utility functions used by REST-API. @@ -52,4 +53,16 @@ public class APIUtil { } return apiManagementProviderService; } + + public static JWTClientManagerService getJWTClientManagerService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + JWTClientManagerService jwtClientManagerService = + (JWTClientManagerService) ctx.getOSGiService(JWTClientManagerService.class, null); + if (jwtClientManagerService == null) { + String msg = "JWT Client manager service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return jwtClientManagerService; + } } diff --git a/components/iot-plugins/pom.xml b/components/iot-plugins/pom.xml index 5738bf180..039f5bddd 100644 --- a/components/iot-plugins/pom.xml +++ b/components/iot-plugins/pom.xml @@ -40,6 +40,7 @@ raspberrypi-plugin virtual-fire-alarm-plugin iot-base-plugin + das-extensions diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/pom.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/pom.xml index 3a0095939..3c7921123 100644 --- a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/pom.xml +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/pom.xml @@ -141,6 +141,11 @@ org.wso2.carbon.apimgt.application.extension provided + + org.wso2.carbon.analytics + org.wso2.carbon.analytics.api + provided + diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/RaspberryPiControllerServiceImpl.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/RaspberryPiControllerServiceImpl.java index 81b906a59..ca7ce2487 100644 --- a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/RaspberryPiControllerServiceImpl.java +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/RaspberryPiControllerServiceImpl.java @@ -20,20 +20,19 @@ package org.wso2.carbon.device.mgt.iot.raspberrypi.service.impl; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.wso2.carbon.context.PrivilegedCarbonContext; -import org.wso2.carbon.device.mgt.analytics.data.publisher.AnalyticsDataRecord; -import org.wso2.carbon.device.mgt.analytics.data.publisher.exception.DeviceManagementAnalyticsException; -import org.wso2.carbon.device.mgt.analytics.data.publisher.service.DeviceAnalyticsService; +import org.wso2.carbon.analytics.dataservice.commons.SORT; +import org.wso2.carbon.analytics.dataservice.commons.SortByField; +import org.wso2.carbon.analytics.datasource.commons.exception.AnalyticsException; import org.wso2.carbon.device.mgt.common.DeviceManagementException; import org.wso2.carbon.device.mgt.iot.controlqueue.mqtt.MqttConfig; import org.wso2.carbon.device.mgt.iot.exception.DeviceControllerException; import org.wso2.carbon.device.mgt.iot.raspberrypi.service.impl.dto.DeviceData; -import org.wso2.carbon.device.mgt.iot.raspberrypi.service.impl.dto.SensorData; +import org.wso2.carbon.device.mgt.iot.raspberrypi.service.impl.dto.SensorRecord; import org.wso2.carbon.device.mgt.iot.raspberrypi.service.impl.transport.RaspberryPiMQTTConnector; +import org.wso2.carbon.device.mgt.iot.raspberrypi.service.impl.util.APIUtil; import org.wso2.carbon.device.mgt.iot.raspberrypi.service.impl.util.RaspberrypiServiceUtils; import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.constants.RaspberrypiConstants; import org.wso2.carbon.device.mgt.iot.sensormgt.SensorDataManager; -import org.wso2.carbon.device.mgt.iot.sensormgt.SensorRecord; import org.wso2.carbon.device.mgt.iot.service.IoTServerStartupListener; import javax.servlet.http.HttpServletRequest; @@ -41,8 +40,6 @@ import javax.ws.rs.PathParam; import javax.ws.rs.core.Response; import java.util.ArrayList; import java.util.Calendar; -import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @@ -123,7 +120,7 @@ public class RaspberryPiControllerServiceImpl implements RaspberryPiControllerSe } public Response requestTemperature(@PathParam("deviceId") String deviceId) { - SensorRecord sensorRecord = null; + org.wso2.carbon.device.mgt.iot.sensormgt.SensorRecord sensorRecord = null; if (log.isDebugEnabled()) { log.debug("Sending request to read raspberrypi-temperature of device [" + deviceId + "] via "); } @@ -179,42 +176,19 @@ public class RaspberryPiControllerServiceImpl implements RaspberryPiControllerSe public Response getArduinoTemperatureStats(String deviceId, String user, long from, long to) { String fromDate = String.valueOf(from); String toDate = String.valueOf(to); - List sensorDatas = new ArrayList<>(); - PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); - DeviceAnalyticsService deviceAnalyticsService = (DeviceAnalyticsService) ctx - .getOSGiService(DeviceAnalyticsService.class, null); String query = "owner:" + user + " AND deviceId:" + deviceId + " AND deviceType:" + RaspberrypiConstants.DEVICE_TYPE + " AND time : [" + fromDate + " TO " + toDate + "]"; String sensorTableName = RaspberrypiConstants.TEMPERATURE_EVENT_TABLE; try { - List records = deviceAnalyticsService.getAllEventsForDevice(sensorTableName, query); - Collections.sort(records, new Comparator() { - @Override - public int compare(AnalyticsDataRecord o1, AnalyticsDataRecord o2) { - long t1 = (Long) o1.getValue("time"); - long t2 = (Long) o2.getValue("time"); - if (t1 < t2) { - return -1; - } else if (t1 > t2) { - return 1; - } else { - return 0; - } - } - }); - for (AnalyticsDataRecord record : records) { - SensorData sensorData = new SensorData(); - sensorData.setTime((long) record.getValue("time")); - sensorData.setValue("" + (float) record.getValue(RaspberrypiConstants.SENSOR_TEMPERATURE)); - sensorDatas.add(sensorData); - } - SensorData[] sensorDetails = sensorDatas.toArray(new SensorData[sensorDatas.size()]); - return Response.ok().entity(sensorDetails).build(); - } catch (DeviceManagementAnalyticsException e) { + List sortByFields = new ArrayList<>(); + SortByField sortByField = new SortByField("time", SORT.ASC, false); + sortByFields.add(sortByField); + List sensorRecords = APIUtil.getAllEventsForDevice(sensorTableName, query, sortByFields); + return Response.status(Response.Status.OK.getStatusCode()).entity(sensorRecords).build(); + } catch (AnalyticsException e) { String errorMsg = "Error on retrieving stats on table " + sensorTableName + " with query " + query; log.error(errorMsg); - SensorData[] sensorDetails = sensorDatas.toArray(new SensorData[sensorDatas.size()]); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()).entity(sensorDetails).build(); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()).entity(errorMsg).build(); } } diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/RaspberryPiManagerServiceImpl.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/RaspberryPiManagerServiceImpl.java index 5151a6fa9..edb966d82 100644 --- a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/RaspberryPiManagerServiceImpl.java +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/RaspberryPiManagerServiceImpl.java @@ -38,7 +38,6 @@ import org.wso2.carbon.device.mgt.iot.raspberrypi.service.impl.util.APIUtil; import org.wso2.carbon.device.mgt.iot.util.ZipArchive; import org.wso2.carbon.device.mgt.iot.util.ZipUtil; import org.wso2.carbon.identity.jwt.client.extension.JWTClient; -import org.wso2.carbon.identity.jwt.client.extension.JWTClientManager; import org.wso2.carbon.identity.jwt.client.extension.dto.AccessTokenInfo; import org.wso2.carbon.identity.jwt.client.extension.exception.JWTClientException; import org.wso2.carbon.user.api.UserStoreException; @@ -213,13 +212,13 @@ public class RaspberryPiManagerServiceImpl implements RaspberryPiManagerService apiApplicationKey = apiManagementProviderService.generateAndRetrieveApplicationKeys( RaspberrypiConstants.DEVICE_TYPE, tags, KEY_TYPE, applicationUsername, true); } - JWTClient jwtClient = JWTClientManager.getInstance().getJWTClient(); + JWTClient jwtClient = APIUtil.getJWTClientManagerService().getJWTClient(); String scopes = "device_type_" + RaspberrypiConstants.DEVICE_TYPE + " device_" + deviceId; AccessTokenInfo accessTokenInfo = jwtClient.getAccessToken(apiApplicationKey.getConsumerKey(), apiApplicationKey.getConsumerSecret(), owner, scopes); //create token - String accessToken = accessTokenInfo.getAccess_token(); - String refreshToken = accessTokenInfo.getRefresh_token(); + String accessToken = accessTokenInfo.getAccessToken(); + String refreshToken = accessTokenInfo.getRefreshToken(); //adding registering data XmppAccount newXmppAccount = new XmppAccount(); newXmppAccount.setAccountName(owner + "_" + deviceId); diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/dto/SensorRecord.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/dto/SensorRecord.java new file mode 100644 index 000000000..18b01f29d --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/dto/SensorRecord.java @@ -0,0 +1,68 @@ +package org.wso2.carbon.device.mgt.iot.raspberrypi.service.impl.dto; + +import org.codehaus.jackson.annotate.JsonIgnoreProperties; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@XmlRootElement +/** + * This stores sensor event data for android sense. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SensorRecord { + + @XmlElementWrapper(required = true, name = "values") + private Map values; + + /** The id. */ + @XmlElement(required = false, name = "id") + private String id; + + /** + * Gets the values. + * @return the values + */ + public Map getValues() { + return values; + } + + /** + * Sets the values. + * @param values the values + */ + public void setValues(Map values) { + this.values = values; + } + + /** + * Sets the id. + * @param id the new id + */ + public void setId(String id) { + this.id = id; + } + + /** + * Gets the id. + * @return the id + */ + public String getId() { + return id; + } + + @Override + public String toString(){ + List valueList = new ArrayList(); + for (Map.Entry entry : values.entrySet()) { + valueList.add(entry.getKey() + ":" + entry.getValue()); + } + return valueList.toString(); + + } + +} diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/util/APIUtil.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/util/APIUtil.java index b0e91eb49..9cdb687bb 100644 --- a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/util/APIUtil.java +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/util/APIUtil.java @@ -2,9 +2,25 @@ package org.wso2.carbon.device.mgt.iot.raspberrypi.service.impl.util; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.analytics.api.AnalyticsDataAPI; +import org.wso2.carbon.analytics.dataservice.commons.AnalyticsDataResponse; +import org.wso2.carbon.analytics.dataservice.commons.AnalyticsDrillDownRequest; +import org.wso2.carbon.analytics.dataservice.commons.SearchResultEntry; +import org.wso2.carbon.analytics.dataservice.commons.SortByField; +import org.wso2.carbon.analytics.dataservice.core.AnalyticsDataServiceUtils; +import org.wso2.carbon.analytics.datasource.commons.Record; +import org.wso2.carbon.analytics.datasource.commons.exception.AnalyticsException; import org.wso2.carbon.apimgt.application.extension.APIManagementProviderService; +import org.wso2.carbon.context.CarbonContext; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; +import org.wso2.carbon.device.mgt.iot.raspberrypi.service.impl.dto.SensorRecord; +import org.wso2.carbon.identity.jwt.client.extension.service.JWTClientManagerService; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * This class provides utility functions used by REST-API. @@ -23,12 +39,6 @@ public class APIUtil { return username; } - public static String getTenantDomainOftheUser() { - PrivilegedCarbonContext threadLocalCarbonContext = PrivilegedCarbonContext.getThreadLocalCarbonContext(); - String tenantDomain = threadLocalCarbonContext.getTenantDomain(); - return tenantDomain; - } - public static DeviceManagementProviderService getDeviceManagementService() { PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); DeviceManagementProviderService deviceManagementProviderService = @@ -41,10 +51,90 @@ public class APIUtil { return deviceManagementProviderService; } + public static AnalyticsDataAPI getAnalyticsDataAPI() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + AnalyticsDataAPI analyticsDataAPI = + (AnalyticsDataAPI) ctx.getOSGiService(AnalyticsDataAPI.class, null); + if (analyticsDataAPI == null) { + String msg = "Analytics api service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return analyticsDataAPI; + } + + public static List getAllEventsForDevice(String tableName, String query, List sortByFields) throws AnalyticsException { + int tenantId = CarbonContext.getThreadLocalCarbonContext().getTenantId(); + AnalyticsDataAPI analyticsDataAPI = getAnalyticsDataAPI(); + int eventCount = analyticsDataAPI.searchCount(tenantId, tableName, query); + if (eventCount == 0) { + return null; + } + AnalyticsDrillDownRequest drillDownRequest = new AnalyticsDrillDownRequest(); + drillDownRequest.setQuery(query); + drillDownRequest.setTableName(tableName); + drillDownRequest.setRecordCount(eventCount); + if (sortByFields != null) { + drillDownRequest.setSortByFields(sortByFields); + } + List resultEntries = analyticsDataAPI.drillDownSearch(tenantId, drillDownRequest); + List recordIds = getRecordIds(resultEntries); + AnalyticsDataResponse response = analyticsDataAPI.get(tenantId, tableName, 1, null, recordIds); + Map sensorDatas = createSensorData(AnalyticsDataServiceUtils.listRecords( + analyticsDataAPI, response)); + List sortedSensorData = getSortedSensorData(sensorDatas, resultEntries); + return sortedSensorData; + } + + private static List getRecordIds(List searchResults) { + List ids = new ArrayList<>(); + for (SearchResultEntry searchResult : searchResults) { + ids.add(searchResult.getId()); + } + return ids; + } + + public static List getSortedSensorData(Map sensorDatas, + List searchResults) { + List sortedRecords = new ArrayList<>(); + for (SearchResultEntry searchResultEntry : searchResults) { + sortedRecords.add(sensorDatas.get(searchResultEntry.getId())); + } + return sortedRecords; + } + + /** + * Creates the SensorDatas from records. + * + * @param records the records + * @return the Map of SensorRecord + */ + public static Map createSensorData(List records) { + Map sensorDatas = new HashMap<>(); + for (Record record : records) { + SensorRecord sensorData = createSensorData(record); + sensorDatas.put(sensorData.getId(), sensorData); + } + return sensorDatas; + } + + /** + * Create a SensorRecord object out of a Record object + * + * @param record the record object + * @return SensorRecord object + */ + public static SensorRecord createSensorData(Record record) { + SensorRecord recordBean = new SensorRecord(); + recordBean.setId(record.getId()); + recordBean.setValues(record.getValues()); + return recordBean; + } + public static APIManagementProviderService getAPIManagementProviderService() { PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); APIManagementProviderService apiManagementProviderService = - (APIManagementProviderService) ctx.getOSGiService(DeviceManagementProviderService.class, null); + (APIManagementProviderService) ctx.getOSGiService(APIManagementProviderService.class, null); if (apiManagementProviderService == null) { String msg = "API management provider service has not initialized."; log.error(msg); @@ -52,4 +142,21 @@ public class APIUtil { } return apiManagementProviderService; } + + public static JWTClientManagerService getJWTClientManagerService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + JWTClientManagerService jwtClientManagerService = + (JWTClientManagerService) ctx.getOSGiService(JWTClientManagerService.class, null); + if (jwtClientManagerService == null) { + String msg = "JWT Client manager service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return jwtClientManagerService; + } + + public static String getTenantDomainOftheUser() { + PrivilegedCarbonContext threadLocalCarbonContext = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + return threadLocalCarbonContext.getTenantDomain(); + } } diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/util/RaspberrypiServiceUtils.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/util/RaspberrypiServiceUtils.java index cb4d52c48..1992a0e01 100644 --- a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/util/RaspberrypiServiceUtils.java +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/util/RaspberrypiServiceUtils.java @@ -27,7 +27,7 @@ import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; import org.apache.http.impl.nio.client.HttpAsyncClients; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.device.mgt.analytics.data.publisher.exception.DataPublisherConfigurationException; -import org.wso2.carbon.device.mgt.analytics.data.publisher.service.DeviceAnalyticsService; +import org.wso2.carbon.device.mgt.analytics.data.publisher.service.EventsPublisherService; import org.wso2.carbon.device.mgt.common.DeviceManagementException; import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.constants.RaspberrypiConstants; @@ -211,8 +211,8 @@ public class RaspberrypiServiceUtils { public static boolean publishToDAS(String deviceId, float temperature) { PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); - DeviceAnalyticsService deviceAnalyticsService = (DeviceAnalyticsService) ctx.getOSGiService( - DeviceAnalyticsService.class, null); + EventsPublisherService deviceAnalyticsService = (EventsPublisherService) ctx.getOSGiService( + EventsPublisherService.class, null); String owner = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUsername(); Object metdaData[] = {owner, RaspberrypiConstants.DEVICE_TYPE, deviceId, System.currentTimeMillis()}; Object payloadData[] = {temperature}; diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/pom.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/pom.xml index e50829cf8..291381f85 100644 --- a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/pom.xml +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/pom.xml @@ -245,6 +245,11 @@ org.wso2.carbon.apimgt.application.extension provided + + org.wso2.carbon.analytics + org.wso2.carbon.analytics.api + provided + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/VirtualFireAlarmControllerServiceImpl.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/VirtualFireAlarmControllerServiceImpl.java index 60fb360d3..b59887fe3 100644 --- a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/VirtualFireAlarmControllerServiceImpl.java +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/VirtualFireAlarmControllerServiceImpl.java @@ -20,26 +20,25 @@ package org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.analytics.dataservice.commons.SORT; +import org.wso2.carbon.analytics.dataservice.commons.SortByField; +import org.wso2.carbon.analytics.datasource.commons.exception.AnalyticsException; import org.wso2.carbon.certificate.mgt.core.dto.SCEPResponse; import org.wso2.carbon.certificate.mgt.core.exception.KeystoreException; import org.wso2.carbon.certificate.mgt.core.service.CertificateManagementService; -import org.wso2.carbon.context.PrivilegedCarbonContext; -import org.wso2.carbon.device.mgt.analytics.data.publisher.AnalyticsDataRecord; -import org.wso2.carbon.device.mgt.analytics.data.publisher.exception.DeviceManagementAnalyticsException; -import org.wso2.carbon.device.mgt.analytics.data.publisher.service.DeviceAnalyticsService; import org.wso2.carbon.device.mgt.common.DeviceManagementException; import org.wso2.carbon.device.mgt.iot.controlqueue.mqtt.MqttConfig; import org.wso2.carbon.device.mgt.iot.controlqueue.xmpp.XmppConfig; import org.wso2.carbon.device.mgt.iot.exception.DeviceControllerException; import org.wso2.carbon.device.mgt.iot.sensormgt.SensorDataManager; -import org.wso2.carbon.device.mgt.iot.sensormgt.SensorRecord; import org.wso2.carbon.device.mgt.iot.service.IoTServerStartupListener; import org.wso2.carbon.device.mgt.iot.transport.TransportHandlerException; import org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.dto.DeviceData; -import org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.dto.SensorData; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.dto.SensorRecord; import org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.exception.VirtualFireAlarmException; import org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.transport.VirtualFireAlarmMQTTConnector; import org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.transport.VirtualFireAlarmXMPPConnector; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.util.APIUtil; import org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.util.SecurityManager; import org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.util.VirtualFireAlarmServiceUtils; import org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.util.scep.ContentType; @@ -52,8 +51,6 @@ import javax.ws.rs.core.Response; import java.io.InputStream; import java.util.ArrayList; import java.util.Calendar; -import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @@ -253,7 +250,7 @@ public class VirtualFireAlarmControllerServiceImpl implements VirtualFireAlarmCo } public Response requestTemperature(String deviceId, String protocol) { - SensorRecord sensorRecord = null; + org.wso2.carbon.device.mgt.iot.sensormgt.SensorRecord sensorRecord = null; String protocolString = protocol.toUpperCase(); if (log.isDebugEnabled()) { log.debug("Sending request to read virtual-firealarm-temperature of device " + @@ -405,45 +402,23 @@ public class VirtualFireAlarmControllerServiceImpl implements VirtualFireAlarmCo } public Response getVirtualFirealarmStats(String deviceId, String sensor, String user, long from, long to) { + String fromDate = String.valueOf(from); + String toDate = String.valueOf(to); + String query = "owner:" + user + " AND deviceId:" + deviceId + " AND deviceType:" + + VirtualFireAlarmConstants.DEVICE_TYPE + " AND time : [" + fromDate + " TO " + toDate + "]"; + String sensorTableName = getSensorEventTableName(sensor); try { - String fromDate = String.valueOf(from); - String toDate = String.valueOf(to); - List sensorDatas = new ArrayList<>(); - PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); - DeviceAnalyticsService deviceAnalyticsService = (DeviceAnalyticsService) ctx - .getOSGiService(DeviceAnalyticsService.class, null); - String query = "owner:" + user + " AND deviceId:" + deviceId + " AND deviceType:" + - VirtualFireAlarmConstants.DEVICE_TYPE + " AND time : [" + fromDate + " TO " + toDate + "]"; - String sensorTableName = getSensorEventTableName(sensor); if (sensorTableName != null) { - List records = deviceAnalyticsService.getAllEventsForDevice(sensorTableName, query); - Collections.sort(records, new Comparator() { - @Override - public int compare(AnalyticsDataRecord o1, AnalyticsDataRecord o2) { - long t1 = (Long) o1.getValue("time"); - long t2 = (Long) o2.getValue("time"); - if (t1 < t2) { - return -1; - } else if (t1 > t2) { - return 1; - } else { - return 0; - } - } - }); - for (AnalyticsDataRecord record : records) { - SensorData sensorData = new SensorData(); - sensorData.setTime((long) record.getValue("time")); - sensorData.setValue("" + (float) record.getValue(sensor)); - sensorDatas.add(sensorData); - } - SensorData[] sensorDetails = sensorDatas.toArray(new SensorData[sensorDatas.size()]); - return Response.ok().entity(sensorDetails).build(); + List sortByFields = new ArrayList<>(); + SortByField sortByField = new SortByField("time", SORT.ASC, false); + sortByFields.add(sortByField); + List sensorRecords = APIUtil.getAllEventsForDevice(sensorTableName, query, sortByFields); + return Response.status(Response.Status.OK.getStatusCode()).entity(sensorRecords).build(); } - } catch (DeviceManagementAnalyticsException e) { - String errorMsg = "Error on retrieving stats on table for sensor name" + sensor; + } catch (AnalyticsException e) { + String errorMsg = "Error on retrieving stats on table " + sensorTableName + " with query " + query; log.error(errorMsg); - return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()).entity(errorMsg).build(); } return Response.status(Response.Status.BAD_REQUEST).build(); } diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/VirtualFireAlarmManagerServiceImpl.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/VirtualFireAlarmManagerServiceImpl.java index f230dac1a..9e226acc0 100644 --- a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/VirtualFireAlarmManagerServiceImpl.java +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/VirtualFireAlarmManagerServiceImpl.java @@ -36,7 +36,6 @@ import org.wso2.carbon.device.mgt.iot.util.ZipUtil; import org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.util.APIUtil; import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.constants.VirtualFireAlarmConstants; import org.wso2.carbon.identity.jwt.client.extension.JWTClient; -import org.wso2.carbon.identity.jwt.client.extension.JWTClientManager; import org.wso2.carbon.identity.jwt.client.extension.dto.AccessTokenInfo; import org.wso2.carbon.identity.jwt.client.extension.exception.JWTClientException; import org.wso2.carbon.user.api.UserStoreException; @@ -210,12 +209,12 @@ public class VirtualFireAlarmManagerServiceImpl implements VirtualFireAlarmManag apiApplicationKey = apiManagementProviderService.generateAndRetrieveApplicationKeys( VirtualFireAlarmConstants.DEVICE_TYPE, tags, KEY_TYPE, applicationUsername, true); } - JWTClient jwtClient = JWTClientManager.getInstance().getJWTClient(); + JWTClient jwtClient = APIUtil.getJWTClientManagerService().getJWTClient(); String scopes = "device_type_" + VirtualFireAlarmConstants.DEVICE_TYPE + " device_" + deviceId; AccessTokenInfo accessTokenInfo = jwtClient.getAccessToken(apiApplicationKey.getConsumerKey(), apiApplicationKey.getConsumerSecret(), owner, scopes); - String accessToken = accessTokenInfo.getAccess_token(); - String refreshToken = accessTokenInfo.getRefresh_token(); + String accessToken = accessTokenInfo.getAccessToken(); + String refreshToken = accessTokenInfo.getRefreshToken(); //adding registering data XmppAccount newXmppAccount = new XmppAccount(); newXmppAccount.setAccountName(owner + "_" + deviceId); diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/dto/SensorData.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/dto/SensorData.java deleted file mode 100644 index e5e99609f..000000000 --- a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/dto/SensorData.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.dto; - -import org.codehaus.jackson.annotate.JsonIgnoreProperties; - -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -@XmlRootElement -/** - * This stores sensor event data for the device type. - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class SensorData { - - @XmlElement public Long time; - @XmlElement public String key; - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } - - public Long getTime() { - return time; - } - - public void setTime(Long time) { - this.time = time; - } - - @XmlElement public String value; - -} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/dto/SensorRecord.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/dto/SensorRecord.java new file mode 100644 index 000000000..e1f46d2d7 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/dto/SensorRecord.java @@ -0,0 +1,68 @@ +package org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.dto; + +import org.codehaus.jackson.annotate.JsonIgnoreProperties; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@XmlRootElement +/** + * This stores sensor event data for android sense. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class SensorRecord { + + @XmlElementWrapper(required = true, name = "values") + private Map values; + + /** The id. */ + @XmlElement(required = false, name = "id") + private String id; + + /** + * Gets the values. + * @return the values + */ + public Map getValues() { + return values; + } + + /** + * Sets the values. + * @param values the values + */ + public void setValues(Map values) { + this.values = values; + } + + /** + * Sets the id. + * @param id the new id + */ + public void setId(String id) { + this.id = id; + } + + /** + * Gets the id. + * @return the id + */ + public String getId() { + return id; + } + + @Override + public String toString(){ + List valueList = new ArrayList(); + for (Map.Entry entry : values.entrySet()) { + valueList.add(entry.getKey() + ":" + entry.getValue()); + } + return valueList.toString(); + + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/util/APIUtil.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/util/APIUtil.java index 8f92c500a..ed6f9892c 100644 --- a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/util/APIUtil.java +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/util/APIUtil.java @@ -2,9 +2,25 @@ package org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.util; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.analytics.api.AnalyticsDataAPI; +import org.wso2.carbon.analytics.dataservice.commons.AnalyticsDataResponse; +import org.wso2.carbon.analytics.dataservice.commons.AnalyticsDrillDownRequest; +import org.wso2.carbon.analytics.dataservice.commons.SearchResultEntry; +import org.wso2.carbon.analytics.dataservice.commons.SortByField; +import org.wso2.carbon.analytics.dataservice.core.AnalyticsDataServiceUtils; +import org.wso2.carbon.analytics.datasource.commons.Record; +import org.wso2.carbon.analytics.datasource.commons.exception.AnalyticsException; import org.wso2.carbon.apimgt.application.extension.APIManagementProviderService; +import org.wso2.carbon.context.CarbonContext; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.dto.SensorRecord; +import org.wso2.carbon.identity.jwt.client.extension.service.JWTClientManagerService; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * This class provides utility functions used by REST-API. @@ -23,12 +39,6 @@ public class APIUtil { return username; } - public static String getTenantDomainOftheUser() { - PrivilegedCarbonContext threadLocalCarbonContext = PrivilegedCarbonContext.getThreadLocalCarbonContext(); - String tenantDomain = threadLocalCarbonContext.getTenantDomain(); - return tenantDomain; - } - public static DeviceManagementProviderService getDeviceManagementService() { PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); DeviceManagementProviderService deviceManagementProviderService = @@ -41,6 +51,86 @@ public class APIUtil { return deviceManagementProviderService; } + public static AnalyticsDataAPI getAnalyticsDataAPI() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + AnalyticsDataAPI analyticsDataAPI = + (AnalyticsDataAPI) ctx.getOSGiService(AnalyticsDataAPI.class, null); + if (analyticsDataAPI == null) { + String msg = "Analytics api service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return analyticsDataAPI; + } + + public static List getAllEventsForDevice(String tableName, String query, List sortByFields) throws AnalyticsException { + int tenantId = CarbonContext.getThreadLocalCarbonContext().getTenantId(); + AnalyticsDataAPI analyticsDataAPI = getAnalyticsDataAPI(); + int eventCount = analyticsDataAPI.searchCount(tenantId, tableName, query); + if (eventCount == 0) { + return null; + } + AnalyticsDrillDownRequest drillDownRequest = new AnalyticsDrillDownRequest(); + drillDownRequest.setQuery(query); + drillDownRequest.setTableName(tableName); + drillDownRequest.setRecordCount(eventCount); + if (sortByFields != null) { + drillDownRequest.setSortByFields(sortByFields); + } + List resultEntries = analyticsDataAPI.drillDownSearch(tenantId, drillDownRequest); + List recordIds = getRecordIds(resultEntries); + AnalyticsDataResponse response = analyticsDataAPI.get(tenantId, tableName, 1, null, recordIds); + Map sensorDatas = createSensorData(AnalyticsDataServiceUtils.listRecords( + analyticsDataAPI, response)); + List sortedSensorData = getSortedSensorData(sensorDatas, resultEntries); + return sortedSensorData; + } + + private static List getRecordIds(List searchResults) { + List ids = new ArrayList<>(); + for (SearchResultEntry searchResult : searchResults) { + ids.add(searchResult.getId()); + } + return ids; + } + + public static List getSortedSensorData(Map sensorDatas, + List searchResults) { + List sortedRecords = new ArrayList<>(); + for (SearchResultEntry searchResultEntry : searchResults) { + sortedRecords.add(sensorDatas.get(searchResultEntry.getId())); + } + return sortedRecords; + } + + /** + * Creates the SensorDatas from records. + * + * @param records the records + * @return the Map of SensorRecord + */ + public static Map createSensorData(List records) { + Map sensorDatas = new HashMap<>(); + for (Record record : records) { + SensorRecord sensorData = createSensorData(record); + sensorDatas.put(sensorData.getId(), sensorData); + } + return sensorDatas; + } + + /** + * Create a SensorRecord object out of a Record object + * + * @param record the record object + * @return SensorRecord object + */ + public static SensorRecord createSensorData(Record record) { + SensorRecord recordBean = new SensorRecord(); + recordBean.setId(record.getId()); + recordBean.setValues(record.getValues()); + return recordBean; + } + public static APIManagementProviderService getAPIManagementProviderService() { PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); APIManagementProviderService apiManagementProviderService = @@ -52,4 +142,21 @@ public class APIUtil { } return apiManagementProviderService; } + + public static JWTClientManagerService getJWTClientManagerService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + JWTClientManagerService jwtClientManagerService = + (JWTClientManagerService) ctx.getOSGiService(JWTClientManagerService.class, null); + if (jwtClientManagerService == null) { + String msg = "JWT Client manager service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return jwtClientManagerService; + } + + public static String getTenantDomainOftheUser() { + PrivilegedCarbonContext threadLocalCarbonContext = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + return threadLocalCarbonContext.getTenantDomain(); + } } diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/util/VirtualFireAlarmServiceUtils.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/util/VirtualFireAlarmServiceUtils.java index 4b8f1d2ad..21f27acf9 100644 --- a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/util/VirtualFireAlarmServiceUtils.java +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/util/VirtualFireAlarmServiceUtils.java @@ -30,7 +30,7 @@ import org.wso2.carbon.certificate.mgt.core.exception.KeystoreException; import org.wso2.carbon.certificate.mgt.core.service.CertificateManagementService; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.device.mgt.analytics.data.publisher.exception.DataPublisherConfigurationException; -import org.wso2.carbon.device.mgt.analytics.data.publisher.service.DeviceAnalyticsService; +import org.wso2.carbon.device.mgt.analytics.data.publisher.service.EventsPublisherService; import org.wso2.carbon.device.mgt.common.DeviceManagementException; import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.constants.VirtualFireAlarmConstants; import org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.exception.VirtualFireAlarmException; @@ -242,8 +242,8 @@ public class VirtualFireAlarmServiceUtils { public static boolean publishToDAS(String deviceId, float temperature) { PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); - DeviceAnalyticsService deviceAnalyticsService = (DeviceAnalyticsService) ctx.getOSGiService( - DeviceAnalyticsService.class, null); + EventsPublisherService deviceAnalyticsService = (EventsPublisherService) ctx.getOSGiService( + EventsPublisherService.class, null); if (deviceAnalyticsService != null) { String owner = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUsername(); Object metdaData[] = {owner, VirtualFireAlarmConstants.DEVICE_TYPE, deviceId, System.currentTimeMillis()}; diff --git a/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/pom.xml b/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/pom.xml new file mode 100644 index 000000000..088e9babc --- /dev/null +++ b/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/pom.xml @@ -0,0 +1,120 @@ + + + + + + + + org.wso2.carbon.devicemgt-plugins + das-extensions-feature + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.event.adapter.extensions.server.feature + pom + WSO2 Carbon - Event Input Adapter Server Feature + http://wso2.org + This feature contains the bundles required for Input Event Adapter functionality + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.event.input.adapter.extensions + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.event.output.adapter.extensions.ui + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy + package + + copy + + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.event.output.adapter.extensions.ui.endpoint + ${carbon.devicemgt.plugins.version} + war + true + ${project.build.directory}/maven-shared-archive-resources/webapps/ + secured-outputui.war + + + + + + + + org.wso2.maven + carbon-p2-plugin + ${carbon.p2.plugin.version} + + + 4-p2-feature-generation + package + + p2-feature-gen + + + org.wso2.carbon.event.adapter.extensions.server + ../../etc/feature.properties + + + org.wso2.carbon.p2.category.type:server + org.eclipse.equinox.p2.type.group:true + + + + + org.wso2.carbon.devicemgt-plugins:org.wso2.carbon.event.input.adapter.extensions:${carbon.devicemgt.plugins.version} + + + org.wso2.carbon.devicemgt-plugins:org.wso2.carbon.event.output.adapter.extensions.ui:${carbon.devicemgt.plugins.version} + + + com.jayway.jsonpath.wso2:json-path:${json.path.version} + + + net.minidev.wso2:json-smart:${json.smart.version} + + + + org.wso2.carbon.core.server:${carbon.kernel.version} + + + + + + + + + + diff --git a/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/src/main/resources/build.properties b/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/src/main/resources/build.properties new file mode 100644 index 000000000..6448563e3 --- /dev/null +++ b/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/src/main/resources/build.properties @@ -0,0 +1,20 @@ +# +# Copyright (c) 2005-2014, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. +# +# WSO2 Inc. licenses this file to you under the Apache License, +# Version 2.0 (the "License"); you may not use this file except +# in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +custom = true +root.agent=conf diff --git a/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/src/main/resources/p2.inf b/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/src/main/resources/p2.inf new file mode 100644 index 000000000..948ab4471 --- /dev/null +++ b/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/src/main/resources/p2.inf @@ -0,0 +1,4 @@ +instructions.configure = \ +org.eclipse.equinox.p2.touchpoint.natives.mkdir(path:${installFolder}/../../deployment/server/webapps/);\ +org.eclipse.equinox.p2.touchpoint.natives.copy(source:${installFolder}/../features/org.wso2.carbon.event.adapter.extensions.server_${feature.version}/webapps/,target:${installFolder}/../../deployment/server/webapps/,overwrite:true);\ +org.eclipse.equinox.p2.touchpoint.natives.copy(source:${installFolder}/../features/org.wso2.carbon.event.adapter.extensions.server_${feature.version}/websocket-validation.properties,target:${installFolder}/../../conf/etc/websocket-validation.properties,overwrite:true);\ diff --git a/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/src/main/resources/websocket-validation.properties b/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/src/main/resources/websocket-validation.properties new file mode 100644 index 000000000..ac35d4733 --- /dev/null +++ b/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/src/main/resources/websocket-validation.properties @@ -0,0 +1,5 @@ +tokenValidationEndpoint=https://localhost:9443/services/OAuth2TokenValidationService +username=admin +password=admin +maxConnectionsPerHost=2 +maxTotalConnections=100 \ No newline at end of file diff --git a/features/iot-plugins-feature/das-extensions-feature/pom.xml b/features/iot-plugins-feature/das-extensions-feature/pom.xml new file mode 100644 index 000000000..8d55a17c7 --- /dev/null +++ b/features/iot-plugins-feature/das-extensions-feature/pom.xml @@ -0,0 +1,40 @@ + + + + + + + org.wso2.carbon.devicemgt-plugins + iot-plugins-feature + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + das-extensions-feature + 2.1.0-SNAPSHOT + pom + WSO2 Carbon - Device Management, DAS Extensions Feature + http://wso2.org + + + org.wso2.carbon.event.adapter.extensions.server.feature + + + diff --git a/features/iot-plugins-feature/pom.xml b/features/iot-plugins-feature/pom.xml index 10a0ac19a..5f7681d07 100644 --- a/features/iot-plugins-feature/pom.xml +++ b/features/iot-plugins-feature/pom.xml @@ -41,6 +41,7 @@ raspberrypi-plugin-feature virtual-fire-alarm-plugin-feature iot-base-plugin-feature + das-extensions-feature diff --git a/pom.xml b/pom.xml index c0df257a1..d72b551cc 100644 --- a/pom.xml +++ b/pom.xml @@ -256,7 +256,6 @@ org.wso2.carbon.devicemgt org.wso2.carbon.device.mgt.analytics.data.publisher ${carbon.device.mgt.version} - provided org.wso2.carbon.devicemgt @@ -294,6 +293,16 @@ org.wso2.carbon.databridge.core ${carbon.analytics.common.version} + + org.wso2.carbon.analytics-common + org.wso2.carbon.event.output.adapter.core + ${carbon.analytics.common.version} + + + org.wso2.carbon.analytics-common + org.wso2.carbon.event.stream.core + ${carbon.analytics.common.version} + org.wso2.carbon.analytics-common org.wso2.carbon.databridge.agent @@ -310,6 +319,11 @@ org.wso2.carbon.databridge.commons ${carbon.analytics.common.version} + + org.wso2.carbon.analytics + org.wso2.carbon.analytics.api + ${carbon.analytics.version} + @@ -441,12 +455,28 @@ + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.event.input.adapter.extensions + ${carbon.devicemgt.plugins.version} + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.event.output.adapter.extensions.ui + ${carbon.devicemgt.plugins.version} + + org.wso2.carbon.analytics org.wso2.carbon.analytics.datasource.commons ${carbon.analytics.version} + + org.wso2.carbon.analytics-common + org.wso2.carbon.event.input.adapter.core + ${carbon.analytics.common.version} + @@ -1005,6 +1035,45 @@ ${junit.version} + + + org.apache.httpcomponents.wso2 + httpcore + ${httpcore.version} + + + org.wso2.orbit.org.apache.httpcomponents + httpclient + ${httpclient.version} + + + com.googlecode.json-simple.wso2 + json-simple + ${json-simple.version} + + + com.jayway.jsonpath.wso2 + json-path + ${json.path.version} + + + + + javax.websocket + javax.websocket-api + ${javax.websocket.version} + + + org.apache.tomcat + tomcat-websocket-api + ${tomcat.websocket.version} + provided + + + javax.ws.rs + javax.ws.rs-api + ${javax.version} + @@ -1042,7 +1111,7 @@ 7.0.34.wso2v2 - 2.7.16 + 2.6.1 2.5.11 1.9.0 1.1.1 @@ -1061,6 +1130,7 @@ 5.0.7 + [5.0.7, 6.0.0) 4.5.0 @@ -1092,7 +1162,7 @@ 5.0.11 [5.0.11,6.0.0) - 1.0.5 + 1.0.6-ALPHA [1.0.5,2.0.0] @@ -1126,7 +1196,18 @@ 1.8 7.0.59.wso2v1 - + + 4.3.1.wso2v2 + [4.3.1, 5.0.0) + 4.3.3.wso2v1 + + 1.1.wso2v1 + 1.2.0.wso2v1 + 2.1.0.wso2v1 + + 7.0.54 + 1.0 + 2.0 github-scm From cdb1f62ba977791b11999089922d3f84dcf9ae6f Mon Sep 17 00:00:00 2001 From: ayyoob Date: Fri, 15 Apr 2016 23:10:48 +0530 Subject: [PATCH 2/6] Made device plugins to connect securely with the MQTT broker --- .../pom.xml | 29 +++------ .../AndroidSenseControllerServiceImpl.java | 10 ++- .../transport/AndroidSenseMQTTConnector.java | 36 +++++++++++ .../service/impl/util/APIUtil.java | 26 ++++++++ .../constants/AndroidSenseConstants.java | 1 + .../impl/AndroidSenseManagerService.java | 3 +- .../pom.xml | 2 +- .../http/HTTPEventAdapterFactory.java | 2 - .../http/util/HTTPContentValidator.java | 1 - .../adapter/extensions/mqtt/Constants.java | 2 +- .../extensions/mqtt/MQTTEventAdapter.java | 9 ++- .../mqtt/MQTTEventAdapterFactory.java | 40 ++++-------- .../mqtt/util/MQTTAdapterListener.java | 23 ++++--- .../java/SuperTenantSubscriptionEndpoint.java | 11 +++- .../main/java/TenantSubscriptionEndpoint.java | 7 +++ .../main/java/oauth/OAuthTokenValdiator.java | 24 ++++--- .../src/main/java/util/UIConstants.java | 2 +- .../DigitalDisplayControllerServiceImpl.java | 13 +++- .../util/DigitalDisplayMQTTConnector.java | 48 +++++++++++--- .../constants/DigitalDisplayConstants.java | 1 + .../impl/DigitalDisplayManagerService.java | 3 +- .../RaspberryPiControllerServiceImpl.java | 12 +++- .../transport/RaspberryPiMQTTConnector.java | 62 ++++++++++++++----- .../constants/RaspberrypiConstants.java | 1 + .../impl/RaspberrypiManagerService.java | 3 +- ...VirtualFireAlarmControllerServiceImpl.java | 10 ++- .../VirtualFireAlarmMQTTConnector.java | 48 +++++++++++--- .../constants/VirtualFireAlarmConstants.java | 1 + .../impl/VirtualFireAlarmManagerService.java | 3 +- .../pom.xml | 6 -- .../resources/websocket-validation.properties | 4 +- pom.xml | 5 +- 32 files changed, 310 insertions(+), 138 deletions(-) diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/pom.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/pom.xml index 52489251b..3a2404ddf 100644 --- a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/pom.xml +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/pom.xml @@ -46,24 +46,14 @@ cxf-rt-frontend-jaxrs provided - - org.apache.cxf - cxf-rt-transports-http - provided - org.eclipse.paho org.eclipse.paho.client.mqttv3 + provided - - org.apache.httpcomponents - httpasyncclient - 4.1 - provided - org.wso2.carbon.devicemgt-plugins org.wso2.carbon.device.mgt.iot @@ -100,12 +90,6 @@ provided - - commons-httpclient.wso2 - commons-httpclient - provided - - org.wso2.carbon org.wso2.carbon.utils @@ -174,14 +158,19 @@ org.wso2.carbon.apimgt.annotations provided + + org.wso2.carbon.analytics + org.wso2.carbon.analytics.api + provided + org.wso2.carbon.devicemgt - org.wso2.carbon.apimgt.webapp.publisher + org.wso2.carbon.identity.jwt.client.extension provided - org.wso2.carbon.analytics - org.wso2.carbon.analytics.api + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.application.extension provided diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/AndroidSenseControllerServiceImpl.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/AndroidSenseControllerServiceImpl.java index 312a9d9aa..9849aa8a7 100644 --- a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/AndroidSenseControllerServiceImpl.java +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/AndroidSenseControllerServiceImpl.java @@ -74,9 +74,17 @@ public class AndroidSenseControllerServiceImpl implements AndroidSenseController if (waitForServerStartup()) { return; } + //The delay is added till the server starts up. + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } AndroidSenseControllerServiceImpl.androidSenseMQTTConnector = androidSenseMQTTConnector; if (MqttConfig.getInstance().isEnabled()) { - androidSenseMQTTConnector.connect(); + synchronized (androidSenseMQTTConnector) { + androidSenseMQTTConnector.connect(); + } } else { log.warn("MQTT disabled in 'devicemgt-config.xml'. Hence, VirtualFireAlarmMQTTConnector not started."); } diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/transport/AndroidSenseMQTTConnector.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/transport/AndroidSenseMQTTConnector.java index bd86bca26..701e0e21c 100644 --- a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/transport/AndroidSenseMQTTConnector.java +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/transport/AndroidSenseMQTTConnector.java @@ -24,6 +24,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.wso2.carbon.apimgt.application.extension.APIManagementProviderService; +import org.wso2.carbon.apimgt.application.extension.dto.ApiApplicationKey; +import org.wso2.carbon.apimgt.application.extension.exception.APIManagerException; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.device.mgt.analytics.data.publisher.exception.DataPublisherConfigurationException; import org.wso2.carbon.device.mgt.analytics.data.publisher.service.EventsPublisherService; @@ -31,6 +34,7 @@ import org.wso2.carbon.device.mgt.common.Device; import org.wso2.carbon.device.mgt.common.DeviceIdentifier; import org.wso2.carbon.device.mgt.common.DeviceManagementException; import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; +import org.wso2.carbon.device.mgt.iot.androidsense.service.impl.util.APIUtil; import org.wso2.carbon.device.mgt.iot.androidsense.service.impl.util.DeviceData; import org.wso2.carbon.device.mgt.iot.androidsense.service.impl.util.SensorData; import org.wso2.carbon.device.mgt.iot.androidsense.plugin.constants.AndroidSenseConstants; @@ -38,6 +42,10 @@ import org.wso2.carbon.device.mgt.iot.controlqueue.mqtt.MqttConfig; import org.wso2.carbon.device.mgt.iot.sensormgt.SensorDataManager; import org.wso2.carbon.device.mgt.iot.transport.TransportHandlerException; import org.wso2.carbon.device.mgt.iot.transport.mqtt.MQTTTransportHandler; +import org.wso2.carbon.identity.jwt.client.extension.JWTClient; +import org.wso2.carbon.identity.jwt.client.extension.dto.AccessTokenInfo; +import org.wso2.carbon.identity.jwt.client.extension.exception.JWTClientException; +import org.wso2.carbon.user.api.UserStoreException; import org.wso2.carbon.utils.multitenancy.MultitenantUtils; import java.nio.charset.StandardCharsets; @@ -48,6 +56,8 @@ public class AndroidSenseMQTTConnector extends MQTTTransportHandler { private static Log log = LogFactory.getLog(AndroidSenseMQTTConnector.class); private static String subscribeTopic = AndroidSenseConstants.MQTT_SUBSCRIBE_WORDS_TOPIC; private static String iotServerSubscriber = UUID.randomUUID().toString().substring(0, 5); + private static final String KEY_TYPE = "PRODUCTION"; + private static final String EMPTY_STRING = ""; private AndroidSenseMQTTConnector() { super(iotServerSubscriber, AndroidSenseConstants.DEVICE_TYPE, @@ -59,7 +69,25 @@ public class AndroidSenseMQTTConnector extends MQTTTransportHandler { Runnable connector = new Runnable() { public void run() { while (!isConnected()) { + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantDomain( + AndroidSenseConstants.DEVICE_TYPE_PROVIDER_DOMAIN, true); try { + String applicationUsername = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUserRealm() + .getRealmConfiguration().getAdminUserName(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setUsername(applicationUsername); + APIManagementProviderService apiManagementProviderService = APIUtil + .getAPIManagementProviderService(); + String[] tags = {AndroidSenseConstants.DEVICE_TYPE}; + ApiApplicationKey apiApplicationKey = apiManagementProviderService.generateAndRetrieveApplicationKeys( + AndroidSenseConstants.DEVICE_TYPE, tags, KEY_TYPE, applicationUsername, true); + JWTClient jwtClient = APIUtil.getJWTClientManagerService().getJWTClient(); + String scopes = "device_type_" + AndroidSenseConstants.DEVICE_TYPE + " device_mqtt_connector"; + AccessTokenInfo accessTokenInfo = jwtClient.getAccessToken(apiApplicationKey.getConsumerKey(), + apiApplicationKey.getConsumerSecret(), applicationUsername, scopes); + //create token + String accessToken = accessTokenInfo.getAccessToken(); + setUsernameAndPassword(accessToken, EMPTY_STRING); connectToQueue(); subscribeToQueue(); } catch (TransportHandlerException e) { @@ -69,6 +97,14 @@ public class AndroidSenseMQTTConnector extends MQTTTransportHandler { } catch (InterruptedException ex) { log.error("MQTT-Subscriber: Thread Sleep Interrupt Exception.", ex); } + }catch (JWTClientException e) { + log.error("Failed to retrieve token from JWT Client.", e); + } catch (UserStoreException e) { + log.error("Failed to retrieve the user.", e); + } catch (APIManagerException e) { + log.error("Failed to create an application and generate keys.", e); + } finally { + PrivilegedCarbonContext.endTenantFlow(); } } } diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/util/APIUtil.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/util/APIUtil.java index c8d90a1c1..022e72b15 100644 --- a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/util/APIUtil.java +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/service/impl/util/APIUtil.java @@ -10,9 +10,11 @@ import org.wso2.carbon.analytics.dataservice.commons.SortByField; import org.wso2.carbon.analytics.dataservice.core.AnalyticsDataServiceUtils; import org.wso2.carbon.analytics.datasource.commons.Record; import org.wso2.carbon.analytics.datasource.commons.exception.AnalyticsException; +import org.wso2.carbon.apimgt.application.extension.APIManagementProviderService; import org.wso2.carbon.context.CarbonContext; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; +import org.wso2.carbon.identity.jwt.client.extension.service.JWTClientManagerService; import java.util.ArrayList; import java.util.HashMap; @@ -127,4 +129,28 @@ public class APIUtil { recordBean.setValues(record.getValues()); return recordBean; } + + public static APIManagementProviderService getAPIManagementProviderService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + APIManagementProviderService apiManagementProviderService = + (APIManagementProviderService) ctx.getOSGiService(APIManagementProviderService.class, null); + if (apiManagementProviderService == null) { + String msg = "API management provider service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return apiManagementProviderService; + } + + public static JWTClientManagerService getJWTClientManagerService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + JWTClientManagerService jwtClientManagerService = + (JWTClientManagerService) ctx.getOSGiService(JWTClientManagerService.class, null); + if (jwtClientManagerService == null) { + String msg = "JWT Client manager service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return jwtClientManagerService; + } } diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/plugin/constants/AndroidSenseConstants.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/plugin/constants/AndroidSenseConstants.java index 0ed7a9dd7..e68e50a43 100644 --- a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/plugin/constants/AndroidSenseConstants.java +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/plugin/constants/AndroidSenseConstants.java @@ -49,4 +49,5 @@ public class AndroidSenseConstants { //MQTT Subscribe topic public static final String MQTT_SUBSCRIBE_WORDS_TOPIC = "wso2/android_sense/+/data"; public static final String DATA_SOURCE_NAME = "jdbc/AndroidSenseDM_DB"; + public final static String DEVICE_TYPE_PROVIDER_DOMAIN = "carbon.super"; } diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/plugin/impl/AndroidSenseManagerService.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/plugin/impl/AndroidSenseManagerService.java index a57c4e5e1..a82a3ff96 100644 --- a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/plugin/impl/AndroidSenseManagerService.java +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/plugin/impl/AndroidSenseManagerService.java @@ -30,7 +30,6 @@ import java.util.List; public class AndroidSenseManagerService implements DeviceManagementService { private DeviceManager deviceManager; - private static final String SUPER_TENANT_DOMAIN = "carbon.super"; @Override public String getType() { @@ -39,7 +38,7 @@ public class AndroidSenseManagerService implements DeviceManagementService { @Override public String getProviderTenantDomain() { - return SUPER_TENANT_DOMAIN; + return AndroidSenseConstants.DEVICE_TYPE_PROVIDER_DOMAIN; } @Override diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/pom.xml b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/pom.xml index 1c27e77a1..407d71a13 100644 --- a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/pom.xml +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/pom.xml @@ -65,7 +65,7 @@ org.wso2.carbon.identity.jwt.client.extension - com.jayway.jsonpath.wso2 + com.jayway.jsonpath json-path diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/HTTPEventAdapterFactory.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/HTTPEventAdapterFactory.java index 4a810d7d6..7a8a42163 100644 --- a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/HTTPEventAdapterFactory.java +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/HTTPEventAdapterFactory.java @@ -54,9 +54,7 @@ public class HTTPEventAdapterFactory extends InputEventAdapterFactory { @Override public List getSupportedMessageFormats() { List supportInputMessageTypes = new ArrayList(); - supportInputMessageTypes.add(MessageType.XML); supportInputMessageTypes.add(MessageType.JSON); - supportInputMessageTypes.add(MessageType.TEXT); return supportInputMessageTypes; } diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/HTTPContentValidator.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/HTTPContentValidator.java index d6a163e87..989f0f282 100644 --- a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/HTTPContentValidator.java +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/HTTPContentValidator.java @@ -32,7 +32,6 @@ public class HTTPContentValidator implements ContentValidator { @Override public ContentInfo validate(Map paramMap) { String deviceId = paramMap.get("deviceId"); - String msg = paramMap.get(HTTPEventAdapterConstants.PAYLOAD_TAG); String deviceIdJsonPath = paramMap.get(HTTPEventAdapterConstants.DEVICE_ID_JSON_PATH); Object res = JsonPath.read(msg, deviceIdJsonPath); diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/Constants.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/Constants.java index 284397cb1..9861c1c7e 100644 --- a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/Constants.java +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/Constants.java @@ -30,7 +30,7 @@ public class Constants { public static final String CLIENT_NAME = "client_name"; public static final String DEFAULT = "default"; public static final String MQTT_CONTENT_VALIDATION_DEFAULT_PARAMETERS = - "device_id_json_path:meta_deviceId,device_id_topic_hierarchy_index:2"; + "device_id_json_path:event.metaData.deviceId,device_id_topic_hierarchy_index:2"; public static final String TOPIC = "topic"; public static final String PAYLOAD = "payload"; public static final String DEVICE_ID_JSON_PATH = "device_id_json_path"; diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/MQTTEventAdapter.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/MQTTEventAdapter.java index c273e5af0..e95cac9e9 100644 --- a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/MQTTEventAdapter.java +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/MQTTEventAdapter.java @@ -66,7 +66,7 @@ public class MQTTEventAdapter implements InputEventAdapter { String params[] = contentValidationParams.split(","); Map paramsMap = new HashMap<>(); for (String param: params) { - String paramsKeyAndValue[] = param.split("/_(.+)?/"); + String paramsKeyAndValue[] = splitOnFirst(param, ':'); if (paramsKeyAndValue.length != 2) { throw new InputEventAdapterException("Invalid parameters for content validation - " + param); } @@ -95,6 +95,13 @@ public class MQTTEventAdapter implements InputEventAdapter { } } + private String[] splitOnFirst(String str, char c) { + int idx = str.indexOf(c); + String head = str.substring(0, idx); + String tail = str.substring(idx + 1); + return new String[] { head, tail} ; + } + @Override public void testConnect() throws TestConnectionNotSupportedException { throw new TestConnectionNotSupportedException("not-supported"); diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/MQTTEventAdapterFactory.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/MQTTEventAdapterFactory.java index be1111bce..a8caa23b2 100644 --- a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/MQTTEventAdapterFactory.java +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/MQTTEventAdapterFactory.java @@ -38,11 +38,7 @@ public class MQTTEventAdapterFactory extends InputEventAdapterFactory { @Override public List getSupportedMessageFormats() { List supportInputMessageTypes = new ArrayList(); - - supportInputMessageTypes.add(MessageType.TEXT); supportInputMessageTypes.add(MessageType.JSON); - supportInputMessageTypes.add(MessageType.XML); - return supportInputMessageTypes; } @@ -52,28 +48,23 @@ public class MQTTEventAdapterFactory extends InputEventAdapterFactory { // set topic Property topicProperty = new Property(MQTTEventAdapterConstants.ADAPTER_MESSAGE_TOPIC); - topicProperty.setDisplayName( - resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_MESSAGE_TOPIC)); + topicProperty.setDisplayName(resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_MESSAGE_TOPIC)); topicProperty.setRequired(true); - topicProperty.setHint( - resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_MESSAGE_TOPIC_HINT)); + topicProperty.setHint(resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_MESSAGE_TOPIC_HINT)); propertyList.add(topicProperty); //Broker Url Property brokerUrl = new Property(MQTTEventAdapterConstants.ADAPTER_CONF_URL); - brokerUrl.setDisplayName( - resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_URL)); + brokerUrl.setDisplayName(resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_URL)); brokerUrl.setRequired(true); brokerUrl.setHint(resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_URL_HINT)); propertyList.add(brokerUrl); //DCR endpoint details Property dcrUrl = new Property(MQTTEventAdapterConstants.ADAPTER_CONF_DCR_URL); - dcrUrl.setDisplayName( - resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_DCR_URL)); + dcrUrl.setDisplayName(resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_DCR_URL)); dcrUrl.setRequired(false); - dcrUrl.setHint( - resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_DCR_URL_HINT)); + dcrUrl.setHint(resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_DCR_URL_HINT)); propertyList.add(dcrUrl); //Content Validator details @@ -83,7 +74,6 @@ public class MQTTEventAdapterFactory extends InputEventAdapterFactory { contentValidator.setRequired(false); contentValidator.setHint( resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_CONTENT_VALIDATOR_CLASSNAME_HINT)); - propertyList.add(contentValidator); contentValidator.setDefaultValue(Constants.DEFAULT); propertyList.add(contentValidator); @@ -94,7 +84,6 @@ public class MQTTEventAdapterFactory extends InputEventAdapterFactory { contentValidatorParams.setRequired(false); contentValidatorParams.setHint( resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_CONTENT_VALIDATOR_PARAMS_HINT)); - propertyList.add(contentValidatorParams); contentValidatorParams.setDefaultValue(Constants.MQTT_CONTENT_VALIDATION_DEFAULT_PARAMETERS); propertyList.add(contentValidatorParams); @@ -103,9 +92,7 @@ public class MQTTEventAdapterFactory extends InputEventAdapterFactory { userName.setDisplayName( resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_USERNAME)); userName.setRequired(false); - userName.setHint( - resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_USERNAME_HINT)); - propertyList.add(userName); + userName.setHint(resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_USERNAME_HINT)); propertyList.add(userName); //Broker Required Scopes. @@ -113,28 +100,23 @@ public class MQTTEventAdapterFactory extends InputEventAdapterFactory { scopes.setDisplayName( resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_SCOPES)); scopes.setRequired(false); - scopes.setHint( - resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_SCOPES_HINT)); + scopes.setHint(resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_SCOPES_HINT)); propertyList.add(scopes); //Broker clear session Property clearSession = new Property(MQTTEventAdapterConstants.ADAPTER_CONF_CLEAN_SESSION); - clearSession.setDisplayName( - resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_CLEAN_SESSION)); + clearSession.setDisplayName(resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_CLEAN_SESSION)); clearSession.setRequired(false); clearSession.setOptions(new String[]{"true", "false"}); clearSession.setDefaultValue("true"); - clearSession.setHint( - resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_CLEAN_SESSION_HINT)); + clearSession.setHint(resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_CLEAN_SESSION_HINT)); propertyList.add(clearSession); // set clientId Property clientId = new Property(MQTTEventAdapterConstants.ADAPTER_CONF_CLIENTID); - clientId.setDisplayName( - resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_CLIENTID)); + clientId.setDisplayName(resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_CLIENTID)); clientId.setRequired(false); - clientId.setHint( - resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_CLIENTID_HINT)); + clientId.setHint(resourceBundle.getString(MQTTEventAdapterConstants.ADAPTER_CONF_CLIENTID_HINT)); propertyList.add(clientId); return propertyList; diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTAdapterListener.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTAdapterListener.java index 0da5ee21d..8698bd03d 100644 --- a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTAdapterListener.java +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTAdapterListener.java @@ -143,6 +143,9 @@ public class MQTTAdapterListener implements MqttCallback, Runnable { String scopes = this.mqttBrokerConnectionConfiguration.getBrokerScopes(); //getJWT Client Parameters. if (dcrUrlString != null && !dcrUrlString.isEmpty()) { + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantId(tenantId, true); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setUsername(username); try { URL dcrUrl = new URL(dcrUrlString); HttpClient httpClient = MQTTUtil.getHttpClient(dcrUrl.getProtocol()); @@ -153,7 +156,6 @@ public class MQTTAdapterListener implements MqttCallback, Runnable { registrationProfile.setOwner(username); registrationProfile.setTokenScope(Constants.TOKEN_SCOPE); registrationProfile.setApplicationType(Constants.APPLICATION_TYPE); - int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId(true); registrationProfile.setClientName(username + "_" + tenantId); String jsonString = registrationProfile.toJSON(); StringEntity requestEntity = new StringEntity(jsonString, ContentType.APPLICATION_JSON); @@ -180,6 +182,8 @@ public class MQTTAdapterListener implements MqttCallback, Runnable { log.error("Invalid dcrUrl : " + dcrUrlString); } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | IOException e) { log.error("Failed to create an https connection.", e); + } finally { + PrivilegedCarbonContext.endTenantFlow(); } } } @@ -231,15 +235,16 @@ public class MQTTAdapterListener implements MqttCallback, Runnable { if (log.isDebugEnabled()) { log.debug("Event received in MQTT Event Adapter - " + msgText); } - ContentInfo contentInfo; - synchronized (contentValidationParams) { - contentValidationParams.put(Constants.TOPIC, topic); - contentValidationParams.put(Constants.PAYLOAD, msgText); - contentInfo = contentValidator.validate(contentValidationParams); - contentValidationParams.remove(Constants.TOPIC); - contentValidationParams.remove(Constants.PAYLOAD); - } + if (contentValidator != null) { + ContentInfo contentInfo; + synchronized (contentValidationParams) { + contentValidationParams.put(Constants.TOPIC, topic); + contentValidationParams.put(Constants.PAYLOAD, msgText); + contentInfo = contentValidator.validate(contentValidationParams); + contentValidationParams.remove(Constants.TOPIC); + contentValidationParams.remove(Constants.PAYLOAD); + } if (contentInfo != null && contentInfo.isValidContent()) { eventAdapterListener.onEvent(contentInfo.getMsgText()); } diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/SuperTenantSubscriptionEndpoint.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/SuperTenantSubscriptionEndpoint.java index 674b32447..f6c27536f 100644 --- a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/SuperTenantSubscriptionEndpoint.java +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/SuperTenantSubscriptionEndpoint.java @@ -32,6 +32,7 @@ import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; +import java.io.IOException; /** * Connect to web socket with Super tenant @@ -61,13 +62,17 @@ public class SuperTenantSubscriptionEndpoint extends SubscriptionEndpoint { try { PrivilegedCarbonContext.startTenantFlow(); PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantId(MultitenantConstants.SUPER_TENANT_ID); - ServiceHolder.getInstance().getUiOutputCallbackControllerService().subscribeWebsocket(streamName, - version, - session); + version, session); } finally { PrivilegedCarbonContext.endTenantFlow(); } + } else { + try { + session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "Unauthorized Access")); + } catch (IOException e) { + log.error("Failed to disconnect the unauthorized client."); + } } } diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/TenantSubscriptionEndpoint.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/TenantSubscriptionEndpoint.java index 640fb0de1..d0ff600b6 100644 --- a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/TenantSubscriptionEndpoint.java +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/TenantSubscriptionEndpoint.java @@ -31,6 +31,7 @@ import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; +import java.io.IOException; /** * Connect to web socket with a tenant @@ -66,6 +67,12 @@ public class TenantSubscriptionEndpoint extends SubscriptionEndpoint { } finally { PrivilegedCarbonContext.endTenantFlow(); } + } else { + try { + session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "Unauthorized Access")); + } catch (IOException e) { + log.error("Failed to disconnect the unauthorized client."); + } } } diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/oauth/OAuthTokenValdiator.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/oauth/OAuthTokenValdiator.java index 1abe5f179..0c2a4c144 100644 --- a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/oauth/OAuthTokenValdiator.java +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/oauth/OAuthTokenValdiator.java @@ -16,6 +16,7 @@ import util.AuthenticationInfo; import javax.websocket.Session; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.rmi.RemoteException; @@ -32,16 +33,22 @@ public class OAuthTokenValdiator { private static final String QUERY_KEY_VALUE_SEPERATOR = "="; private static final String TOKEN_TYPE = "bearer"; private static final String TOKEN_IDENTIFIER = "token"; - private static OAuthTokenValdiator oAuthTokenValdiator = new OAuthTokenValdiator(); + private static OAuthTokenValdiator oAuthTokenValdiator; public static OAuthTokenValdiator getInstance() { - return oAuthTokenValdiator; + if (oAuthTokenValdiator == null) { + synchronized (OAuthTokenValdiator.class) { + if (oAuthTokenValdiator == null) { + oAuthTokenValdiator = new OAuthTokenValdiator(); + } + } + } + return oAuthTokenValdiator; } private OAuthTokenValdiator() { - Properties properties = null; try { - properties = getWebSocketConfig(); + Properties properties = getWebSocketConfig(); this.stubs = new GenericObjectPool(new OAuthTokenValidaterStubFactory(properties)); } catch (IOException e) { log.error("Failed to parse the web socket config file " + WEBSOCKET_CONFIG_LOCATION); @@ -145,9 +152,12 @@ public class OAuthTokenValdiator { */ private Properties getWebSocketConfig() throws IOException { Properties properties = new Properties(); - InputStream inputStream = getClass().getClassLoader().getResourceAsStream(WEBSOCKET_CONFIG_LOCATION); - if (inputStream != null) { - properties.load(inputStream); + File configFile =new File(WEBSOCKET_CONFIG_LOCATION); + if (configFile.exists()) { + InputStream fileInputStream = new FileInputStream(configFile); + if (fileInputStream != null) { + properties.load(fileInputStream); + } } return properties; } diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/util/UIConstants.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/util/UIConstants.java index 52f61f020..2db13ce81 100644 --- a/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/util/UIConstants.java +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/util/UIConstants.java @@ -32,7 +32,7 @@ public class UIConstants { public static final String ADAPTER_UI_COLON = ":"; public static final String MAXIMUM_TOTAL_HTTP_CONNECTION = "maximumTotalHttpConnection"; public static final String MAXIMUM_HTTP_CONNECTION_PER_HOST = "maximumHttpConnectionPerHost"; - public static final String TOKEN_VALIDATION_ENDPOINT_URL = "tokenValidationEndpointUrl"; + public static final String TOKEN_VALIDATION_ENDPOINT_URL = "tokenValidationEndpoint"; public static final String USERNAME = "username"; public static final String PASSWORD = "password"; } diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/service/impl/DigitalDisplayControllerServiceImpl.java b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/service/impl/DigitalDisplayControllerServiceImpl.java index 0bb084db6..1042a6b2c 100644 --- a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/service/impl/DigitalDisplayControllerServiceImpl.java +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/service/impl/DigitalDisplayControllerServiceImpl.java @@ -70,11 +70,18 @@ public class DigitalDisplayControllerServiceImpl implements DigitalDisplayContro return; } DigitalDisplayControllerServiceImpl.digitalDisplayMQTTConnector = digitalDisplayMQTTConnector; + //The delay is added for the server starts up. + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } if (MqttConfig.getInstance().isEnabled()) { - digitalDisplayMQTTConnector.connect(); + synchronized (digitalDisplayMQTTConnector) { + digitalDisplayMQTTConnector.connect(); + } } else { - log.warn("MQTT disabled in 'devicemgt-config.xml'. " + - "Hence, DigitalDisplayMQTTConnector not started."); + log.warn("MQTT disabled in 'devicemgt-config.xml'. Hence, not started."); } } }; diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/service/impl/util/DigitalDisplayMQTTConnector.java b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/service/impl/util/DigitalDisplayMQTTConnector.java index 4c4c5b257..2950a66dd 100644 --- a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/service/impl/util/DigitalDisplayMQTTConnector.java +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/service/impl/util/DigitalDisplayMQTTConnector.java @@ -5,12 +5,21 @@ import org.apache.commons.logging.LogFactory; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; import org.json.JSONObject; +import org.wso2.carbon.apimgt.application.extension.APIManagementProviderService; +import org.wso2.carbon.apimgt.application.extension.dto.ApiApplicationKey; +import org.wso2.carbon.apimgt.application.extension.exception.APIManagerException; +import org.wso2.carbon.base.MultitenantConstants; +import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.device.mgt.iot.controlqueue.mqtt.MqttConfig; import org.wso2.carbon.device.mgt.iot.digitaldisplay.service.impl.model.ScreenShotModel; import org.wso2.carbon.device.mgt.iot.digitaldisplay.service.impl.websocket.DigitalDisplayWebSocketServerEndPoint; import org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.constants.DigitalDisplayConstants; import org.wso2.carbon.device.mgt.iot.transport.TransportHandlerException; import org.wso2.carbon.device.mgt.iot.transport.mqtt.MQTTTransportHandler; +import org.wso2.carbon.identity.jwt.client.extension.JWTClient; +import org.wso2.carbon.identity.jwt.client.extension.dto.AccessTokenInfo; +import org.wso2.carbon.identity.jwt.client.extension.exception.JWTClientException; +import org.wso2.carbon.user.api.UserStoreException; import java.util.HashMap; import java.util.Map; @@ -24,6 +33,8 @@ public class DigitalDisplayMQTTConnector extends MQTTTransportHandler { private static final String MQTT_TOPIC_APPENDER = "wso2/iot"; private static final String subscribeTopic = MQTT_TOPIC_APPENDER + "/" + DigitalDisplayConstants.DEVICE_TYPE + "/+/digital_display_publisher"; + private static final String KEY_TYPE = "PRODUCTION"; + private static final String EMPTY_STRING = ""; private static String iotServerSubscriber = UUID.randomUUID().toString().substring(0, 5); @@ -41,24 +52,41 @@ public class DigitalDisplayMQTTConnector extends MQTTTransportHandler { Runnable connector = new Runnable() { public void run() { while (!isConnected()) { + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantDomain( + DigitalDisplayConstants.DEVICE_TYPE_PROVIDER_DOMAIN, true); try { - String brokerUsername = MqttConfig.getInstance().getMqttQueueUsername(); - String brokerPassword = MqttConfig.getInstance().getMqttQueuePassword(); - setUsernameAndPassword(brokerUsername, brokerPassword); + String applicationUsername = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUserRealm() + .getRealmConfiguration().getAdminUserName(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setUsername(applicationUsername); + APIManagementProviderService apiManagementProviderService = APIUtil.getAPIManagementProviderService(); + String[] tags = {DigitalDisplayConstants.DEVICE_TYPE}; + ApiApplicationKey apiApplicationKey = apiManagementProviderService.generateAndRetrieveApplicationKeys( + DigitalDisplayConstants.DEVICE_TYPE, tags, KEY_TYPE, applicationUsername, true); + JWTClient jwtClient = APIUtil.getJWTClientManagerService().getJWTClient(); + String scopes = "device_type_" + DigitalDisplayConstants.DEVICE_TYPE + " device_mqtt_connector"; + AccessTokenInfo accessTokenInfo = jwtClient.getAccessToken(apiApplicationKey.getConsumerKey(), + apiApplicationKey.getConsumerSecret(), applicationUsername, scopes); + //create token + String accessToken = accessTokenInfo.getAccessToken(); + setUsernameAndPassword(accessToken, EMPTY_STRING); connectToQueue(); + subscribeToQueue(); } catch (TransportHandlerException e) { - log.error("Connection to MQTT Broker at: " + mqttBrokerEndPoint + " failed", e); + log.error("Connection/Subscription to MQTT Broker at: " + mqttBrokerEndPoint + " failed", e); try { Thread.sleep(timeoutInterval); } catch (InterruptedException ex) { log.error("MQTT-Connector: Thread Sleep Interrupt Exception.", ex); } - } - - try { - subscribeToQueue(); - } catch (TransportHandlerException e) { - log.warn("Subscription to MQTT Broker at: " + mqttBrokerEndPoint + " failed", e); + } catch (JWTClientException e) { + log.error("Failed to retrieve token from JWT Client.", e); + } catch (UserStoreException e) { + log.error("Failed to retrieve the user.", e); + } catch (APIManagerException e) { + log.error("Failed to create an application and generate keys.", e); + } finally { + PrivilegedCarbonContext.endTenantFlow(); } } } diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/plugin/constants/DigitalDisplayConstants.java b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/plugin/constants/DigitalDisplayConstants.java index 00ef15972..cbfb6a256 100644 --- a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/plugin/constants/DigitalDisplayConstants.java +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/plugin/constants/DigitalDisplayConstants.java @@ -34,4 +34,5 @@ public class DigitalDisplayConstants { public final static String GET_DEVICE_STATUS_CONSTANT = "get_device_status"; public final static String PUBLISH_TOPIC = "wso2/iot/digital_display/%s/digital_display_subscriber"; public static final String DATA_SOURCE_NAME = "jdbc/DigitalDisplayDM_DB"; + public final static String DEVICE_TYPE_PROVIDER_DOMAIN = "carbon.super"; } diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/plugin/impl/DigitalDisplayManagerService.java b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/plugin/impl/DigitalDisplayManagerService.java index 593b16e91..01681c8a3 100644 --- a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/plugin/impl/DigitalDisplayManagerService.java +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/plugin/impl/DigitalDisplayManagerService.java @@ -30,7 +30,6 @@ import java.util.List; public class DigitalDisplayManagerService implements DeviceManagementService{ private DeviceManager deviceManager; - private final static String DEVICE_TYPE_PROVIDER_DOMAIN = "carbon.super"; @Override public String getType() { @@ -39,7 +38,7 @@ public class DigitalDisplayManagerService implements DeviceManagementService{ @Override public String getProviderTenantDomain() { - return DEVICE_TYPE_PROVIDER_DOMAIN; + return DigitalDisplayConstants.DEVICE_TYPE_PROVIDER_DOMAIN; } @Override diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/RaspberryPiControllerServiceImpl.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/RaspberryPiControllerServiceImpl.java index ca7ce2487..13cb9ee46 100644 --- a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/RaspberryPiControllerServiceImpl.java +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/RaspberryPiControllerServiceImpl.java @@ -72,10 +72,18 @@ public class RaspberryPiControllerServiceImpl implements RaspberryPiControllerSe return; } RaspberryPiControllerServiceImpl.this.raspberryPiMQTTConnector = raspberryPiMQTTConnector; + //The delay is added for the server starts up. + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } if (MqttConfig.getInstance().isEnabled()) { - raspberryPiMQTTConnector.connect(); + synchronized (raspberryPiMQTTConnector) { + raspberryPiMQTTConnector.connect(); + } } else { - log.warn("MQTT disabled in 'devicemgt-config.xml'. Hence, RaspberryPiMQTTConnector not started."); + log.warn("MQTT disabled in 'devicemgt-config.xml'. Hence, not started."); } } }; diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/transport/RaspberryPiMQTTConnector.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/transport/RaspberryPiMQTTConnector.java index 1d01f36dd..30c9fb2bf 100644 --- a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/transport/RaspberryPiMQTTConnector.java +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.api/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/service/impl/transport/RaspberryPiMQTTConnector.java @@ -22,17 +22,26 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.wso2.carbon.apimgt.application.extension.APIManagementProviderService; +import org.wso2.carbon.apimgt.application.extension.dto.ApiApplicationKey; +import org.wso2.carbon.apimgt.application.extension.exception.APIManagerException; +import org.wso2.carbon.base.MultitenantConstants; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.device.mgt.common.Device; import org.wso2.carbon.device.mgt.common.DeviceIdentifier; import org.wso2.carbon.device.mgt.common.DeviceManagementException; import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; import org.wso2.carbon.device.mgt.iot.controlqueue.mqtt.MqttConfig; +import org.wso2.carbon.device.mgt.iot.raspberrypi.service.impl.util.APIUtil; import org.wso2.carbon.device.mgt.iot.raspberrypi.service.impl.util.RaspberrypiServiceUtils; import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.constants.RaspberrypiConstants; import org.wso2.carbon.device.mgt.iot.sensormgt.SensorDataManager; import org.wso2.carbon.device.mgt.iot.transport.TransportHandlerException; import org.wso2.carbon.device.mgt.iot.transport.mqtt.MQTTTransportHandler; +import org.wso2.carbon.identity.jwt.client.extension.JWTClient; +import org.wso2.carbon.identity.jwt.client.extension.dto.AccessTokenInfo; +import org.wso2.carbon.identity.jwt.client.extension.exception.JWTClientException; +import org.wso2.carbon.user.api.UserStoreException; import org.wso2.carbon.utils.multitenancy.MultitenantUtils; import java.util.Calendar; @@ -41,6 +50,8 @@ import java.util.UUID; public class RaspberryPiMQTTConnector extends MQTTTransportHandler { private static Log log = LogFactory.getLog(RaspberryPiMQTTConnector.class); private static final String subscribeTopic = "wso2/" + RaspberrypiConstants.DEVICE_TYPE + "/+/publisher"; + private static final String KEY_TYPE = "PRODUCTION"; + private static final String EMPTY_STRING = ""; private static final String iotServerSubscriber = UUID.randomUUID().toString().substring(0, 5); @@ -54,25 +65,42 @@ public class RaspberryPiMQTTConnector extends MQTTTransportHandler { Runnable connector = new Runnable() { public void run() { while (!isConnected()) { - try { - String brokerUsername = MqttConfig.getInstance().getMqttQueueUsername(); - String brokerPassword = MqttConfig.getInstance().getMqttQueuePassword(); - setUsernameAndPassword(brokerUsername, brokerPassword); + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantDomain( + RaspberrypiConstants.DEVICE_TYPE_PROVIDER_DOMAIN, true); + try { + String applicationUsername = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUserRealm() + .getRealmConfiguration().getAdminUserName(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setUsername(applicationUsername); + APIManagementProviderService apiManagementProviderService = APIUtil.getAPIManagementProviderService(); + String[] tags = {RaspberrypiConstants.DEVICE_TYPE}; + ApiApplicationKey apiApplicationKey = apiManagementProviderService.generateAndRetrieveApplicationKeys( + RaspberrypiConstants.DEVICE_TYPE, tags, KEY_TYPE, applicationUsername, true); + JWTClient jwtClient = APIUtil.getJWTClientManagerService().getJWTClient(); + String scopes = "device_type_" + RaspberrypiConstants.DEVICE_TYPE + " device_mqtt_connector"; + AccessTokenInfo accessTokenInfo = jwtClient.getAccessToken(apiApplicationKey.getConsumerKey(), + apiApplicationKey.getConsumerSecret(), applicationUsername, scopes); + //create token + String accessToken = accessTokenInfo.getAccessToken(); + setUsernameAndPassword(accessToken, EMPTY_STRING); connectToQueue(); - } catch (TransportHandlerException e) { - log.error("Connection to MQTT Broker at: " + mqttBrokerEndPoint + " failed", e); - try { - Thread.sleep(timeoutInterval); - } catch (InterruptedException ex) { - log.error("MQTT-Connector: Thread Sleep Interrupt Exception.", ex); - } - } - - try { subscribeToQueue(); - } catch (TransportHandlerException e) { - log.warn("Subscription to MQTT Broker at: " + mqttBrokerEndPoint + " failed", e); - } + } catch (TransportHandlerException e) { + log.error("Connection/Subscription to MQTT Broker at: " + mqttBrokerEndPoint + " failed", e); + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException ex) { + log.error("MQTT-Connector: Thread Sleep Interrupt Exception.", ex); + } + } catch (JWTClientException e) { + log.error("Failed to retrieve token from JWT Client.", e); + } catch (UserStoreException e) { + log.error("Failed to retrieve the user.", e); + } catch (APIManagerException e) { + log.error("Failed to create an application and generate keys.", e); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } } } }; diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/constants/RaspberrypiConstants.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/constants/RaspberrypiConstants.java index c6534e46f..a301ac6fb 100644 --- a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/constants/RaspberrypiConstants.java +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/constants/RaspberrypiConstants.java @@ -35,5 +35,6 @@ public class RaspberrypiConstants { //sensor events summerized table name public static final String TEMPERATURE_EVENT_TABLE = "ORG_WSO2_IOT_DEVICES_TEMPERATURE"; public static final String DATA_SOURCE_NAME = "jdbc/RaspberryPiDM_DB"; + public final static String DEVICE_TYPE_PROVIDER_DOMAIN = "carbon.super"; } diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/RaspberrypiManagerService.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/RaspberrypiManagerService.java index 963475d4c..bfbe62d14 100644 --- a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/RaspberrypiManagerService.java +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/RaspberrypiManagerService.java @@ -33,7 +33,6 @@ import java.util.List; public class RaspberrypiManagerService implements DeviceManagementService { private DeviceManager deviceManager; - private final static String DEVICE_TYPE_PROVIDER_DOMAIN = "carbon.super"; @Override public String getType() { @@ -42,7 +41,7 @@ public class RaspberrypiManagerService implements DeviceManagementService { @Override public String getProviderTenantDomain() { - return DEVICE_TYPE_PROVIDER_DOMAIN; + return RaspberrypiConstants.DEVICE_TYPE_PROVIDER_DOMAIN; } @Override diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/VirtualFireAlarmControllerServiceImpl.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/VirtualFireAlarmControllerServiceImpl.java index b59887fe3..cb1cf0a45 100644 --- a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/VirtualFireAlarmControllerServiceImpl.java +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/VirtualFireAlarmControllerServiceImpl.java @@ -185,8 +185,16 @@ public class VirtualFireAlarmControllerServiceImpl implements VirtualFireAlarmCo return; } VirtualFireAlarmControllerServiceImpl.this.virtualFireAlarmMQTTConnector = virtualFireAlarmMQTTConnector; + //The delay is added for the server starts up. + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } if (MqttConfig.getInstance().isEnabled()) { - virtualFireAlarmMQTTConnector.connect(); + synchronized (virtualFireAlarmMQTTConnector) { + virtualFireAlarmMQTTConnector.connect(); + } } else { log.warn("MQTT disabled in 'devicemgt-config.xml'. Hence, VirtualFireAlarmMQTTConnector not started."); } diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/transport/VirtualFireAlarmMQTTConnector.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/transport/VirtualFireAlarmMQTTConnector.java index e4e8e3cb8..0d7efef2b 100644 --- a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/transport/VirtualFireAlarmMQTTConnector.java +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.api/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/service/impl/transport/VirtualFireAlarmMQTTConnector.java @@ -22,6 +22,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.wso2.carbon.apimgt.application.extension.APIManagementProviderService; +import org.wso2.carbon.apimgt.application.extension.dto.ApiApplicationKey; +import org.wso2.carbon.apimgt.application.extension.exception.APIManagerException; +import org.wso2.carbon.base.MultitenantConstants; import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.device.mgt.common.Device; import org.wso2.carbon.device.mgt.common.DeviceIdentifier; @@ -32,9 +36,14 @@ import org.wso2.carbon.device.mgt.iot.sensormgt.SensorDataManager; import org.wso2.carbon.device.mgt.iot.transport.TransportHandlerException; import org.wso2.carbon.device.mgt.iot.transport.mqtt.MQTTTransportHandler; import org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.exception.VirtualFireAlarmException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.util.APIUtil; import org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.util.SecurityManager; import org.wso2.carbon.device.mgt.iot.virtualfirealarm.service.impl.util.VirtualFireAlarmServiceUtils; import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.constants.VirtualFireAlarmConstants; +import org.wso2.carbon.identity.jwt.client.extension.JWTClient; +import org.wso2.carbon.identity.jwt.client.extension.dto.AccessTokenInfo; +import org.wso2.carbon.identity.jwt.client.extension.exception.JWTClientException; +import org.wso2.carbon.user.api.UserStoreException; import org.wso2.carbon.utils.multitenancy.MultitenantUtils; import java.nio.charset.StandardCharsets; @@ -64,6 +73,8 @@ public class VirtualFireAlarmMQTTConnector extends MQTTTransportHandler { // wildcard (+) is in place for device_owner & device_id private static String subscribeTopic = "wso2/" + VirtualFireAlarmConstants.DEVICE_TYPE + "/+/publisher"; private static String iotServerSubscriber = UUID.randomUUID().toString().substring(0, 5); + private static final String KEY_TYPE = "PRODUCTION"; + private static final String EMPTY_STRING = ""; /** * Default constructor for the VirtualFirealarmMQTTConnector. @@ -83,24 +94,41 @@ public class VirtualFireAlarmMQTTConnector extends MQTTTransportHandler { Runnable connector = new Runnable() { public void run() { while (!isConnected()) { + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantDomain( + VirtualFireAlarmConstants.DEVICE_TYPE_PROVIDER_DOMAIN, true); try { - String brokerUsername = MqttConfig.getInstance().getMqttQueueUsername(); - String brokerPassword = MqttConfig.getInstance().getMqttQueuePassword(); - setUsernameAndPassword(brokerUsername, brokerPassword); + String applicationUsername = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUserRealm() + .getRealmConfiguration().getAdminUserName(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setUsername(applicationUsername); + APIManagementProviderService apiManagementProviderService = APIUtil.getAPIManagementProviderService(); + String[] tags = {VirtualFireAlarmConstants.DEVICE_TYPE}; + ApiApplicationKey apiApplicationKey = apiManagementProviderService.generateAndRetrieveApplicationKeys( + VirtualFireAlarmConstants.DEVICE_TYPE, tags, KEY_TYPE, applicationUsername, true); + JWTClient jwtClient = APIUtil.getJWTClientManagerService().getJWTClient(); + String scopes = "device_type_" + VirtualFireAlarmConstants.DEVICE_TYPE + " device_mqtt_connector"; + AccessTokenInfo accessTokenInfo = jwtClient.getAccessToken(apiApplicationKey.getConsumerKey(), + apiApplicationKey.getConsumerSecret(), applicationUsername, scopes); + //create token + String accessToken = accessTokenInfo.getAccessToken(); + setUsernameAndPassword(accessToken, EMPTY_STRING); connectToQueue(); + subscribeToQueue(); } catch (TransportHandlerException e) { - log.error("Connection to MQTT Broker at: " + mqttBrokerEndPoint + " failed", e); + log.error("Connection/Subscription to MQTT Broker at: " + mqttBrokerEndPoint + " failed", e); try { Thread.sleep(timeoutInterval); } catch (InterruptedException ex) { log.error("MQTT-Connector: Thread Sleep Interrupt Exception.", ex); } - } - - try { - subscribeToQueue(); - } catch (TransportHandlerException e) { - log.warn("Subscription to MQTT Broker at: " + mqttBrokerEndPoint + " failed", e); + } catch (JWTClientException e) { + log.error("Failed to retrieve token from JWT Client.", e); + } catch (UserStoreException e) { + log.error("Failed to retrieve the user.", e); + } catch (APIManagerException e) { + log.error("Failed to create an application and generate keys.", e); + } finally { + PrivilegedCarbonContext.endTenantFlow(); } } } diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/constants/VirtualFireAlarmConstants.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/constants/VirtualFireAlarmConstants.java index 3b8b5e2a8..5c4f03dd0 100644 --- a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/constants/VirtualFireAlarmConstants.java +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/constants/VirtualFireAlarmConstants.java @@ -37,4 +37,5 @@ public class VirtualFireAlarmConstants { //sensor events summerized table name for humidity public static final String HUMIDITY_EVENT_TABLE = "DEVICE_HUMIDITY_SUMMARY"; public static final String DATA_SOURCE_NAME = "jdbc/VirtualFireAlarmDM_DB"; + public final static String DEVICE_TYPE_PROVIDER_DOMAIN = "carbon.super"; } diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/VirtualFireAlarmManagerService.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/VirtualFireAlarmManagerService.java index 214afd849..1667619a0 100644 --- a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/VirtualFireAlarmManagerService.java +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/VirtualFireAlarmManagerService.java @@ -32,7 +32,6 @@ import java.util.List; public class VirtualFireAlarmManagerService implements DeviceManagementService{ private DeviceManager deviceManager; - private final static String DEVICE_TYPE_PROVIDER_DOMAIN = "carbon.super"; @Override public String getType() { @@ -42,7 +41,7 @@ public class VirtualFireAlarmManagerService implements DeviceManagementService{ @Override public String getProviderTenantDomain() { - return DEVICE_TYPE_PROVIDER_DOMAIN; + return VirtualFireAlarmConstants.DEVICE_TYPE_PROVIDER_DOMAIN; } @Override diff --git a/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/pom.xml b/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/pom.xml index 088e9babc..0b42e85ed 100644 --- a/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/pom.xml +++ b/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/pom.xml @@ -99,12 +99,6 @@ org.wso2.carbon.devicemgt-plugins:org.wso2.carbon.event.output.adapter.extensions.ui:${carbon.devicemgt.plugins.version} - - com.jayway.jsonpath.wso2:json-path:${json.path.version} - - - net.minidev.wso2:json-smart:${json.smart.version} - org.wso2.carbon.core.server:${carbon.kernel.version} diff --git a/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/src/main/resources/websocket-validation.properties b/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/src/main/resources/websocket-validation.properties index ac35d4733..be0f37472 100644 --- a/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/src/main/resources/websocket-validation.properties +++ b/features/iot-plugins-feature/das-extensions-feature/org.wso2.carbon.event.adapter.extensions.server.feature/src/main/resources/websocket-validation.properties @@ -1,5 +1,5 @@ tokenValidationEndpoint=https://localhost:9443/services/OAuth2TokenValidationService username=admin password=admin -maxConnectionsPerHost=2 -maxTotalConnections=100 \ No newline at end of file +maximumHttpConnectionPerHost=2 +maximumTotalHttpConnection=100 \ No newline at end of file diff --git a/pom.xml b/pom.xml index d72b551cc..835fb5401 100644 --- a/pom.xml +++ b/pom.xml @@ -1052,7 +1052,7 @@ ${json-simple.version} - com.jayway.jsonpath.wso2 + com.jayway.jsonpath json-path ${json.path.version} @@ -1202,8 +1202,7 @@ 4.3.3.wso2v1 1.1.wso2v1 - 1.2.0.wso2v1 - 2.1.0.wso2v1 + 0.9.1 7.0.54 1.0 From 3e1b0c54e91b47a1acf14a942061ad7504aa9f0b Mon Sep 17 00:00:00 2001 From: ayyoob Date: Sat, 16 Apr 2016 01:10:58 +0530 Subject: [PATCH 3/6] made content validator to process batch events --- .../http/util/HTTPContentValidator.java | 38 +++++++++++++++++-- .../mqtt/util/MQTTContentValidator.java | 37 +++++++++++++++++- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/HTTPContentValidator.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/HTTPContentValidator.java index 989f0f282..1afc511a4 100644 --- a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/HTTPContentValidator.java +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/http/util/HTTPContentValidator.java @@ -21,6 +21,9 @@ package org.wso2.carbon.event.input.adapter.extensions.http.util; import com.jayway.jsonpath.JsonPath; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.json.simple.JSONArray; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; import org.wso2.carbon.event.input.adapter.extensions.ContentInfo; import org.wso2.carbon.event.input.adapter.extensions.ContentValidator; @@ -28,17 +31,46 @@ import java.util.Map; public class HTTPContentValidator implements ContentValidator { private static final Log log = LogFactory.getLog(HTTPContentValidator.class); + private static String JSON_ARRAY_START_CHAR = "["; @Override public ContentInfo validate(Map paramMap) { String deviceId = paramMap.get("deviceId"); String msg = paramMap.get(HTTPEventAdapterConstants.PAYLOAD_TAG); String deviceIdJsonPath = paramMap.get(HTTPEventAdapterConstants.DEVICE_ID_JSON_PATH); + boolean status; + if (msg.startsWith(JSON_ARRAY_START_CHAR)) { + status = processMultipleEvents(msg, deviceId, deviceIdJsonPath); + } else { + status = processSingleEvent(msg, deviceId, deviceIdJsonPath); + } + return new ContentInfo(status, msg); + } + + private boolean processSingleEvent(String msg, String deviceIdFromTopic, String deviceIdJsonPath) { Object res = JsonPath.read(msg, deviceIdJsonPath); String deviceIdFromContent = (res != null) ? res.toString() : ""; - if (deviceIdFromContent.equals(deviceId)) { - return new ContentInfo(true, msg); + if (deviceIdFromContent.equals(deviceIdFromTopic)) { + return true; + } + return false; + } + + private boolean processMultipleEvents(String msg, String deviceIdFromTopic, String deviceIdJsonPath) { + try { + JSONParser jsonParser = new JSONParser(); + JSONArray jsonArray = (JSONArray) jsonParser.parse(msg); + boolean status = false; + for (int i = 0; i < jsonArray.size(); i++) { + status = processSingleEvent(jsonArray.get(i).toString(), deviceIdFromTopic, deviceIdJsonPath); + if (!status) { + return status; + } + } + return status; + } catch (ParseException e) { + log.error("Invalid input " + msg, e); + return false; } - return new ContentInfo(false, msg); } } diff --git a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTContentValidator.java b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTContentValidator.java index 96c1eee16..fb14e9ed7 100644 --- a/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTContentValidator.java +++ b/components/iot-plugins/das-extensions/org.wso2.carbon.event.input.adapter.extensions/src/main/java/org/wso2/carbon/event/input/adapter/extensions/mqtt/util/MQTTContentValidator.java @@ -21,6 +21,10 @@ package org.wso2.carbon.event.input.adapter.extensions.mqtt.util; import com.jayway.jsonpath.JsonPath; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; import org.wso2.carbon.event.input.adapter.extensions.ContentInfo; import org.wso2.carbon.event.input.adapter.extensions.ContentValidator; import org.wso2.carbon.event.input.adapter.extensions.mqtt.Constants; @@ -28,6 +32,7 @@ import org.wso2.carbon.event.input.adapter.extensions.mqtt.Constants; import java.util.Map; public class MQTTContentValidator implements ContentValidator { + private static String JSON_ARRAY_START_CHAR = "["; private static final Log log = LogFactory.getLog(MQTTContentValidator.class); @Override @@ -43,11 +48,39 @@ public class MQTTContentValidator implements ContentValidator { deviceIdInTopicHierarchyLevelIndex = Integer.parseInt(deviceIdInTopicHierarchyLevel); } String deviceIdFromTopic = topics[deviceIdInTopicHierarchyLevelIndex]; + boolean status; + if (msg.startsWith(JSON_ARRAY_START_CHAR)) { + status = processMultipleEvents(msg, deviceIdFromTopic, deviceIdJsonPath); + } else { + status = processSingleEvent(msg, deviceIdFromTopic, deviceIdJsonPath); + } + return new ContentInfo(status, msg); + } + + private boolean processSingleEvent(String msg, String deviceIdFromTopic, String deviceIdJsonPath) { Object res = JsonPath.read(msg, deviceIdJsonPath); String deviceIdFromContent = (res != null) ? res.toString() : ""; if (deviceIdFromContent.equals(deviceIdFromTopic)) { - return new ContentInfo(true, msg); + return true; + } + return false; + } + + private boolean processMultipleEvents(String msg, String deviceIdFromTopic, String deviceIdJsonPath) { + try { + JSONParser jsonParser = new JSONParser(); + JSONArray jsonArray = (JSONArray) jsonParser.parse(msg); + boolean status = false; + for (int i = 0; i < jsonArray.size(); i++) { + status = processSingleEvent(jsonArray.get(i).toString(), deviceIdFromTopic, deviceIdJsonPath); + if (!status) { + return status; + } + } + return status; + } catch (ParseException e) { + log.error("Invalid input " + msg, e); + return false; } - return new ContentInfo(false, msg); } } From 0b67c0f74acdbf0dece463d2c875bd64106ffb7d Mon Sep 17 00:00:00 2001 From: ayyoob Date: Sun, 17 Apr 2016 12:18:25 +0530 Subject: [PATCH 4/6] added android sense agent code --- .gitignore | 7 + .../app/build.gradle | 46 +++ .../app/src/main/AndroidManifest.xml | 72 ++++ .../iot/android/sense/RegisterActivity.java | 183 +++++++++ .../sense/constants/SenseConstants.java | 33 ++ .../data/publisher/DataPublisherReceiver.java | 36 ++ .../data/publisher/DataPublisherService.java | 171 ++++++++ .../android/sense/data/publisher/Event.java | 219 ++++++++++ .../mqtt/AndroidSenseMQTTHandler.java | 241 +++++++++++ .../mqtt/transport/MQTTTransportHandler.java | 385 ++++++++++++++++++ .../mqtt/transport/TransportHandler.java | 108 +++++ .../transport/TransportHandlerException.java | 56 +++ .../sense/event/SenseScheduleReceiver.java | 42 ++ .../iot/android/sense/event/SenseService.java | 63 +++ .../sense/event/streams/DataReader.java | 21 + .../event/streams/Location/LocationData.java | 56 +++ .../streams/Location/LocationDataReader.java | 169 ++++++++ .../event/streams/SenseDataCollector.java | 44 ++ .../event/streams/Sensor/SensorData.java | 102 +++++ .../streams/Sensor/SensorDataReader.java | 115 ++++++ .../event/streams/battery/BatteryData.java | 124 ++++++ .../streams/battery/BatteryDataReceiver.java | 38 ++ .../realtimeviewer/ActivitySelectSensor.java | 280 +++++++++++++ .../realtimeviewer/datastore/TempStore.java | 37 ++ .../event/RealTimeSensorChangeReceiver.java | 41 ++ .../event/realtimesensor/RealTimeSensor.java | 88 ++++ .../realtimesensor/RealTimeSensorReader.java | 62 +++ .../AvailableSensorsInDevice.java | 65 +++ .../sensorlisting/SupportedSensors.java | 120 ++++++ .../view/adaptor/SensorViewAdaptor.java | 97 +++++ .../sensor/selector/SelectSensorDialog.java | 147 +++++++ .../sense/speech/detector/IVoiceControl.java | 32 ++ .../detector/WordRecognitionActivity.java | 132 ++++++ .../detector/util/ListeningActivity.java | 125 ++++++ .../speech/detector/util/ProcessWords.java | 193 +++++++++ .../detector/util/StringSimilarity.java | 67 +++ .../util/VoiceRecognitionListener.java | 80 ++++ .../sense/speech/detector/util/WordData.java | 47 +++ .../iot/android/sense/util/LocalRegistry.java | 225 ++++++++++ .../iot/android/sense/util/SenseClient.java | 85 ++++ .../sense/util/SenseClientAsyncExecutor.java | 163 ++++++++ .../android/sense/util/SenseDataHolder.java | 78 ++++ .../iot/android/sense/util/SenseUtils.java | 46 +++ .../iot/android/sense/util/SenseWakeLock.java | 47 +++ .../sense/util/dto/AccessTokenInfo.java | 40 ++ .../util/dto/AndroidSenseManagerService.java | 13 + .../ApiApplicationRegistrationService.java | 25 ++ .../util/dto/ApiRegistrationProfile.java | 64 +++ .../dto/DynamicClientRegistrationService.java | 40 ++ .../sense/util/dto/OAuthApplicationInfo.java | 44 ++ .../util/dto/OAuthRequestInterceptor.java | 26 ++ .../sense/util/dto/RegistrationProfile.java | 67 +++ .../sense/util/dto/TokenIssuerService.java | 16 + .../app/src/main/res/drawable/mic.png | Bin 0 -> 26775 bytes .../src/main/res/drawable/pushtoserver.png | Bin 0 -> 57808 bytes .../app/src/main/res/drawable/sensor.png | Bin 0 -> 13854 bytes .../src/main/res/drawable/side_nav_bar.xml | 9 + .../activity_activity_select_sensor.xml | 18 + .../src/main/res/layout/activity_register.xml | 79 ++++ .../res/layout/activity_sense_settings.xml | 16 + .../res/layout/activity_speech_sense_main.xml | 86 ++++ .../layout/app_bar_activity_select_sensor.xml | 53 +++ .../layout/content_activity_select_sensor.xml | 28 ++ .../main/res/layout/display_sensor_values.xml | 56 +++ .../layout/fragment_select_sensor_dialog.xml | 10 + .../nav_header_activity_select_sensor.xml | 20 + ...activity_activity_select_sensor_drawer.xml | 22 + .../main/res/menu/activity_select_sensor.xml | 6 + .../src/main/res/menu/menu_sense_settings.xml | 7 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes .../app/src/main/res/mipmap-hdpi/wso2logo.png | Bin 0 -> 30848 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes .../src/main/res/raw/client_truststore.bks | Bin 0 -> 36583 bytes .../app/src/main/res/raw/wso2carbon.cer | Bin 0 -> 569 bytes .../app/src/main/res/values-v21/styles.xml | 8 + .../app/src/main/res/values-w820dp/dimens.xml | 6 + .../app/src/main/res/values/device.xml | 4 + .../app/src/main/res/values/dimens.xml | 9 + .../app/src/main/res/values/strings.xml | 14 + .../res/values/strings_activity_register.xml | 13 + .../app/src/main/res/values/styles.xml | 14 + .../build.gradle | 21 + .../gradle/wrapper/gradle-wrapper.properties | 6 + .../gradlew | 164 ++++++++ .../gradlew.bat | 90 ++++ .../pom.xml | 54 +++ .../settings.gradle | 1 + .../impl/AndroidSenseControllerService.java | 163 +------- .../AndroidSenseControllerServiceImpl.java | 323 +++------------ .../impl/AndroidSenseManagerService.java | 6 +- .../impl/AndroidSenseManagerServiceImpl.java | 17 +- .../transport/AndroidSenseMQTTConnector.java | 199 +-------- .../src/main/webapp/META-INF/permissions.xml | 119 +----- .../src/main/webapp/WEB-INF/web.xml | 14 +- .../src/main/java/SubscriptionEndpoint.java | 3 +- .../java/SuperTenantSubscriptionEndpoint.java | 2 +- .../main/java/TenantSubscriptionEndpoint.java | 2 +- .../SuperTenantEventRetrievalEndpoint.java | 85 ---- .../servlet/TenantEventRetrievalEndpoint.java | 89 ---- .../servlet => util}/ServiceHolder.java | 5 +- .../src/main/webapp/WEB-INF/web.xml | 17 - components/iot-plugins/das-extensions/pom.xml | 2 +- .../util/DigitalDisplayMQTTConnector.java | 3 + .../transport/mqtt/MQTTTransportHandler.java | 9 +- .../transport/RaspberryPiMQTTConnector.java | 3 + .../VirtualFireAlarmMQTTConnector.java | 3 + 108 files changed, 5859 insertions(+), 942 deletions(-) create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/build.gradle create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/AndroidManifest.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/RegisterActivity.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/constants/SenseConstants.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/DataPublisherReceiver.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/DataPublisherService.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/Event.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/AndroidSenseMQTTHandler.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/MQTTTransportHandler.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/TransportHandler.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/TransportHandlerException.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/SenseScheduleReceiver.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/SenseService.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/DataReader.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Location/LocationData.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Location/LocationDataReader.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/SenseDataCollector.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Sensor/SensorData.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Sensor/SensorDataReader.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/battery/BatteryData.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/battery/BatteryDataReceiver.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/ActivitySelectSensor.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/datastore/TempStore.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/RealTimeSensorChangeReceiver.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/realtimesensor/RealTimeSensor.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/realtimesensor/RealTimeSensorReader.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/sensorlisting/AvailableSensorsInDevice.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/sensorlisting/SupportedSensors.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/view/adaptor/SensorViewAdaptor.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/view/sensor/selector/SelectSensorDialog.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/IVoiceControl.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/WordRecognitionActivity.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/ListeningActivity.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/ProcessWords.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/StringSimilarity.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/VoiceRecognitionListener.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/WordData.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/LocalRegistry.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseClient.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseClientAsyncExecutor.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseDataHolder.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseUtils.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseWakeLock.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/AccessTokenInfo.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/AndroidSenseManagerService.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/ApiApplicationRegistrationService.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/ApiRegistrationProfile.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/DynamicClientRegistrationService.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/OAuthApplicationInfo.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/OAuthRequestInterceptor.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/RegistrationProfile.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/TokenIssuerService.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/drawable/mic.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/drawable/pushtoserver.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/drawable/sensor.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/drawable/side_nav_bar.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_activity_select_sensor.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_register.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_sense_settings.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_speech_sense_main.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/app_bar_activity_select_sensor.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/content_activity_select_sensor.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/display_sensor_values.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/fragment_select_sensor_dialog.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/nav_header_activity_select_sensor.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/menu/activity_activity_select_sensor_drawer.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/menu/activity_select_sensor.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/menu/menu_sense_settings.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/mipmap-hdpi/wso2logo.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/raw/client_truststore.bks create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/raw/wso2carbon.cer create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/values-v21/styles.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/values-w820dp/dimens.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/values/device.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/values/dimens.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/values/strings.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/values/strings_activity_register.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/values/styles.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/build.gradle create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/gradle/wrapper/gradle-wrapper.properties create mode 100755 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/gradlew create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/gradlew.bat create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/pom.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/settings.gradle delete mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/org/wso2/carbon/servlet/SuperTenantEventRetrievalEndpoint.java delete mode 100644 components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/org/wso2/carbon/servlet/TenantEventRetrievalEndpoint.java rename components/iot-plugins/das-extensions/org.wso2.carbon.event.output.adapter.extensions.ui.endpoint/src/main/java/{org/wso2/carbon/servlet => util}/ServiceHolder.java (82%) diff --git a/.gitignore b/.gitignore index cd8838ac6..ac1222119 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,10 @@ target # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* + +# Android Studio +.gradle +build/ + +# Local configuration file (sdk path, etc) +local.properties diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/build.gradle b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/build.gradle new file mode 100644 index 000000000..1fd0aa818 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/build.gradle @@ -0,0 +1,46 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 22 + buildToolsVersion '22.0.1' + defaultConfig { + applicationId "agent.sense.android.iot.carbon.wso2.org.wso2_senseagent" + minSdkVersion 19 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + applicationVariants.all { variant -> + variant.outputs.each { output -> + def newName = output.outputFile.name + newName = newName.replace("app-", "androidsense") + newName = newName.replace("release", "") + //noinspection GroovyAssignabilityCheck + output.outputFile = new File(output.outputFile.parent, newName) + } + } + } + } + packagingOptions { + exclude 'META-INF/NOTICE' + exclude 'META-INF/LICENSE' + } + productFlavors { + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:22.2.1' + compile 'com.android.support:design:22.2.1' + compile 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.0.2' + compile 'com.github.rholder:snowball-stemmer:1.3.0.581.1' + compile 'commons-codec:commons-codec:1.4' + compile 'com.netflix.feign:feign-jaxrs:8.16.0' + compile 'com.netflix.feign:feign-jackson:8.16.0' + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/AndroidManifest.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..5a799fbda --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/AndroidManifest.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/RegisterActivity.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/RegisterActivity.java new file mode 100644 index 000000000..fceeeeede --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/RegisterActivity.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.KeyEvent; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.EditText; + +import org.wso2.carbon.iot.android.sense.data.publisher.DataPublisherReceiver; +import org.wso2.carbon.iot.android.sense.data.publisher.mqtt.AndroidSenseMQTTHandler; +import org.wso2.carbon.iot.android.sense.data.publisher.mqtt.transport.MQTTTransportHandler; +import org.wso2.carbon.iot.android.sense.event.SenseScheduleReceiver; +import org.wso2.carbon.iot.android.sense.realtimeviewer.ActivitySelectSensor; +import org.wso2.carbon.iot.android.sense.realtimeviewer.sensorlisting.AvailableSensorsInDevice; +import org.wso2.carbon.iot.android.sense.realtimeviewer.sensorlisting.SupportedSensors; +import org.wso2.carbon.iot.android.sense.util.LocalRegistry; +import org.wso2.carbon.iot.android.sense.util.SenseClient; +import org.wso2.carbon.iot.android.sense.util.SenseUtils; +import agent.sense.android.iot.carbon.wso2.org.wso2_senseagent.R; + + +/** + * A login screen that offers to register the device. + */ +public class RegisterActivity extends Activity { + + private EditText mUsernameView; + private EditText mPasswordView; + private EditText mHostView; + private EditText mMqttPortView; + private View mProgressView; + private View mLoginFormView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + getSharedPreferences(SupportedSensors.SELECTED_SENSORS, 0).edit().clear().apply(); + + if (LocalRegistry.isExist(getApplicationContext())) { + Intent intent = new Intent(getApplicationContext(), ActivitySelectSensor.class); + startActivity(intent); + } + setContentView(R.layout.activity_register); + mUsernameView = (EditText) findViewById(R.id.username); + mPasswordView = (EditText) findViewById(R.id.password); + mHostView = (EditText) findViewById(R.id.hostname); + mMqttPortView = (EditText) findViewById(R.id.mqttPort); + AvailableSensorsInDevice availableSensorsInDevice = new AvailableSensorsInDevice(getApplicationContext()); + availableSensorsInDevice.setContent(); + + Button deviceRegisterButton = (Button) findViewById(R.id.device_register_button); + + + deviceRegisterButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + attemptLogin(); + } + }); + + mLoginFormView = findViewById(R.id.login_form); + mProgressView = findViewById(R.id.login_progress); + } + + public void attemptLogin() { + showProgress(true); + // Reset errors. + mUsernameView.setError(null); + mPasswordView.setError(null); + + // Store values at the time of the login attempt. + String username = mUsernameView.getText().toString(); + String password = mPasswordView.getText().toString(); + String hostname = mHostView.getText().toString(); + String mqttPort = mMqttPortView.getText().toString(); + boolean cancel = false; + View focusView = null; + + // Check for a valid password, if the user entered one. + if (!TextUtils.isEmpty(password)) { + // mPasswordView.setError(getString(R.string.error_invalid_password)); + focusView = mPasswordView; + //cancel = true; + } + // Check for a valid username . + if (TextUtils.isEmpty(username)) { + mUsernameView.setError(getString(R.string.error_field_required)); + focusView = mUsernameView; + cancel = true; + } + if (TextUtils.isEmpty(username)) { + mHostView.setError(getString(R.string.error_field_required)); + focusView = mHostView; + cancel = true; + } + + if (cancel) { + focusView.requestFocus(); + } else { + SenseClient client = new SenseClient(getApplicationContext()); + LocalRegistry.addServerURL(getBaseContext(), hostname); + String deviceId = SenseUtils.generateDeviceId(getBaseContext(), getContentResolver()); + boolean registerStatus = client.register(username, password, deviceId); + if (registerStatus) { + LocalRegistry.addUsername(getApplicationContext(), username); + LocalRegistry.addDeviceId(getApplicationContext(), deviceId); + LocalRegistry.addMqttPort(getApplicationContext(), Integer.parseInt(mqttPort)); + MQTTTransportHandler mqttTransportHandler = AndroidSenseMQTTHandler.getInstance(this); + if (!mqttTransportHandler.isConnected()) { + mqttTransportHandler.connect(); + } + SenseScheduleReceiver senseScheduleReceiver = new SenseScheduleReceiver(); + senseScheduleReceiver.clearAbortBroadcast(); + senseScheduleReceiver.onReceive(this, null); + + DataPublisherReceiver dataUploaderReceiver = new DataPublisherReceiver(); + dataUploaderReceiver.clearAbortBroadcast(); + dataUploaderReceiver.onReceive(this, null); + + Intent intent = new Intent(getApplicationContext(), ActivitySelectSensor.class); + startActivity(intent); + } + showProgress(false); + } + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2) + public void showProgress(final boolean show) { + // On Honeycomb MR2 we have the ViewPropertyAnimator APIs, which allow + // for very easy animations. If available, use these APIs to fade-in + // the progress spinner. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { + int shortAnimTime = getResources().getInteger(android.R.integer.config_shortAnimTime); + + mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); + mLoginFormView.animate().setDuration(shortAnimTime).alpha( + show ? 0 : 1).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); + } + }); + + mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); + mProgressView.animate().setDuration(shortAnimTime).alpha( + show ? 1 : 0).setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); + } + }); + } else { + // The ViewPropertyAnimator APIs are not available, so simply show + // and hide the relevant UI components. + mProgressView.setVisibility(show ? View.VISIBLE : View.GONE); + mLoginFormView.setVisibility(show ? View.GONE : View.VISIBLE); + } + } + +} + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/constants/SenseConstants.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/constants/SenseConstants.java new file mode 100644 index 000000000..70d847802 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/constants/SenseConstants.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.constants; + +public class SenseConstants { + public final static String DEVICE_TYPE = "android_sense"; + public final static String REGISTER_CONTEXT = "/android_sense_mgt"; + public final static String DCR_CONTEXT = "/dynamic-client-web"; + public final static String TOKEN_ISSUER_CONTEXT = "/oauth2"; + public final static String API_APPLICATION_REGISTRATION_CONTEXT = "/api-application-registration"; + + public static final int MQTT_BROKER_PORT = 1883; + public static final String EVENT_LISTENER_STARTED = "xxStartedxx"; + public static final String EVENT_LISTENER_FINISHED = "xxFinishedxx"; + public static final String EVENT_LISTENER_ONGOING = "xxOngoingxx"; + + public final class Request { + public final static String REQUEST_SUCCESSFUL = "200"; + public final static String REQUEST_CONFLICT = "409"; + public final static int MAX_ATTEMPTS = 2; + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/DataPublisherReceiver.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/DataPublisherReceiver.java new file mode 100644 index 000000000..b09d15a88 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/DataPublisherReceiver.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.data.publisher; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * This creates and AlarmManagerService that triggers the data uploader service with a 30 seconds interval. + */ +public class DataPublisherReceiver extends BroadcastReceiver { + private static int ALARM_INTERVAL = 30000; + + @Override + public void onReceive(Context context, Intent intent) { + AlarmManager service = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + Intent i = new Intent(context, DataPublisherService.class); + PendingIntent pending = PendingIntent.getService(context, 0, i, 0); + service.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), ALARM_INTERVAL, pending); + } + +} \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/DataPublisherService.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/DataPublisherService.java new file mode 100644 index 000000000..41853f8cd --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/DataPublisherService.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.data.publisher; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.hardware.Sensor; +import android.os.IBinder; +import android.support.annotation.Nullable; +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.wso2.carbon.iot.android.sense.data.publisher.mqtt.AndroidSenseMQTTHandler; +import org.wso2.carbon.iot.android.sense.data.publisher.mqtt.transport.MQTTTransportHandler; +import org.wso2.carbon.iot.android.sense.data.publisher.mqtt.transport.TransportHandlerException; +import org.wso2.carbon.iot.android.sense.constants.SenseConstants; +import org.wso2.carbon.iot.android.sense.event.streams.Location.LocationData; +import org.wso2.carbon.iot.android.sense.event.streams.Sensor.SensorData; +import org.wso2.carbon.iot.android.sense.event.streams.battery.BatteryData; +import org.wso2.carbon.iot.android.sense.speech.detector.util.ProcessWords; +import org.wso2.carbon.iot.android.sense.speech.detector.util.WordData; +import org.wso2.carbon.iot.android.sense.util.SenseDataHolder; +import org.wso2.carbon.iot.android.sense.util.LocalRegistry; +//import org.wso2.carbon.iot.android.sense.util.SenseClient; + +import java.util.ArrayList; +import java.util.List; + +/** + * This is an android service which publishes the data to the server. + */ +public class DataPublisherService extends Service { + private static final String TAG = "Data Publisher"; + private static String KEY_TAG = "key"; + private static String TIME_TAG = "time"; + private static String VALUE_TAG = "value"; + public static Context context; + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + context = this; + Log.d(TAG, "service started"); + Runnable runnable = new Runnable() { + @Override + public void run() { + try { + List events = new ArrayList<>(); + //retreive sensor data. + List sensorDataMap = SenseDataHolder.getSensorDataHolder(); + for (SensorData sensorData : sensorDataMap) { + Event event = new Event(); + event.setTimestamp(sensorData.getTimestamp()); + + switch (sensorData.getSensorType()) { + case Sensor.TYPE_ACCELEROMETER: + event.setAccelerometer(sensorData.getSensorValues()); + break; + case Sensor.TYPE_MAGNETIC_FIELD: + event.setMagnetic(sensorData.getSensorValues()); + break; + case Sensor.TYPE_GYROSCOPE: + event.setGyroscope(sensorData.getSensorValues()); + break; + case Sensor.TYPE_LIGHT: + event.setLight(sensorData.getSensorValues()[0]); + break; + case Sensor.TYPE_PRESSURE: + event.setPressure(sensorData.getSensorValues()[0]); + break; + case Sensor.TYPE_PROXIMITY: + event.setProximity(sensorData.getSensorValues()[0]); + break; + case Sensor.TYPE_GRAVITY: + event.setGravity(sensorData.getSensorValues()); + break; + case Sensor.TYPE_ROTATION_VECTOR: + event.setGravity(sensorData.getSensorValues()); + break; + } + events.add(event); + } + SenseDataHolder.resetSensorDataHolder(); + + //retreive batter data. + List batteryDataMap = SenseDataHolder.getBatteryDataHolder(); + for (BatteryData batteryData : batteryDataMap) { + Event event = new Event(); + event.setTimestamp(batteryData.getTimestamp()); + event.setBattery(batteryData.getLevel()); + events.add(event); + } + SenseDataHolder.resetBatteryDataHolder(); + //retreive location data. + List locationDataMap = SenseDataHolder.getLocationDataHolder(); + for (LocationData locationData : locationDataMap) { + Event event = new Event(); + event.setTimestamp(locationData.getTimeStamp()); + event.setGps(new double[]{locationData.getLatitude(), locationData.getLongitude()}); + events.add(event); + } + SenseDataHolder.resetLocationDataHolder(); + + //retreive words + ProcessWords.cleanAndPushToWordMap(); + List wordDatMap = SenseDataHolder.getWordDataHolder(); + for (WordData wordData : wordDatMap) { + if (wordData.getOccurences() == 0) { + continue; + } + for (int i = 0; i < wordData.getOccurences(); i++) { + Event event = new Event(); + event.setTimestamp(wordData.getTimestamp()); + event.setWord(wordData.getWord()); + String word = wordData.getWord(); + String status = word; + if ((!word.equals(SenseConstants.EVENT_LISTENER_STARTED)) && (!word.equals(SenseConstants + .EVENT_LISTENER_FINISHED))) { + status = SenseConstants.EVENT_LISTENER_ONGOING; + } + event.setWordStatus(status); + events.add(event); + } + } + SenseDataHolder.resetWordDataHolder(); + //publish the data + if (events.size() > 0) { + String user = LocalRegistry.getUsername(context); + String deviceId = LocalRegistry.getDeviceId(context); + JSONArray jsonArray = new JSONArray(); + for (Event event : events) { + event.setOwner(user); + event.setDeviceId(deviceId); + jsonArray.put(event.getEvent()); + } + MQTTTransportHandler mqttTransportHandler = AndroidSenseMQTTHandler.getInstance(context); + if (!mqttTransportHandler.isConnected()) { + mqttTransportHandler.connect(); + } + mqttTransportHandler.publishDeviceData(user, deviceId, jsonArray.toString()); + } + } catch (JSONException e) { + Log.e(TAG, "Json Data Parsing Exception", e); + } catch (TransportHandlerException e) { + Log.e(TAG, "Data Publish Failed", e); + } + } + }; + Thread dataUploaderThread = new Thread(runnable); + dataUploaderThread.start(); + return Service.START_NOT_STICKY; + } +} \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/Event.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/Event.java new file mode 100644 index 000000000..e7ceea063 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/Event.java @@ -0,0 +1,219 @@ +package org.wso2.carbon.iot.android.sense.data.publisher; + +import org.json.JSONException; +import org.json.JSONObject; + +public class Event { + + private String owner; + private String deviceId; + private String type; + private float battery; + private double gps[]; //lat,long + private float accelerometer[]; //x,y,z + private float magnetic[]; //x,y,z + private float gyroscope[]; //x,y,z + private float light; + private float pressure; + private float proximity; + private float gravity[]; + private float rotation[]; + private String wordSessionId; + private String word; + private String wordStatus; + private long timestamp; + + private float getBattery() { + return battery; + } + + public void setBattery(float battery) { + this.type = "battery"; + this.battery = battery; + } + + private double[] getGps() { + return gps != null ? gps : new double[]{0, 0}; + } + + public void setGps(double[] gps) { + this.type = "gps"; + this.gps = gps; + } + + private float[] getAccelerometer() { + return accelerometer != null ? accelerometer : new float[]{0, 0, 0}; + } + + public void setAccelerometer(float[] accelerometer) { + this.type = "accelerometer"; + this.accelerometer = accelerometer; + } + + private float[] getMagnetic() { + return magnetic != null ? magnetic : new float[]{0, 0, 0}; + } + + public void setMagnetic(float[] magnetic) { + this.type = "magnetic"; + this.magnetic = magnetic; + } + + private float[] getGyroscope() { + return gyroscope != null ? gyroscope : new float[]{0, 0, 0}; + } + + public void setGyroscope(float[] gyroscope) { + this.type = "gyroscope"; + this.gyroscope = gyroscope; + } + + public float getLight() { + return light; + } + + public void setLight(float light) { + this.type = "light"; + this.light = light; + } + + public float getPressure() { + return pressure; + } + + public void setPressure(float pressure) { + this.type = "pressure"; + this.pressure = pressure; + } + + public float getProximity() { + return proximity; + } + + public void setProximity(float proximity) { + this.type = "proximity"; + this.proximity = proximity; + } + + private float[] getGravity() { + return gravity != null ? gravity : new float[]{0, 0, 0}; + } + + public void setGravity(float gravity[]) { + this.type = "gravity"; + this.gravity = gravity; + } + + private float[] getRotation() { + return rotation != null ? rotation : new float[]{0, 0, 0}; + } + + public void setRotation(float rotation[]) { + this.type = "rotation"; + this.rotation = rotation; + } + + private String getWordSessionId() { + return wordSessionId != null ? wordSessionId : ""; + } + + public void setWordSessionId(String wordSessionId) { + this.wordSessionId = wordSessionId; + } + + private String getWord() { + return word != null ? word : ""; + } + + public void setWord(String word) { + this.type = "word"; + this.word = word; + } + + private long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + private String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + private String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getWordStatus() { + return wordStatus != null ? wordStatus : ""; + } + + public void setWordStatus(String wordStatus) { + this.wordStatus = wordStatus; + } + + public JSONObject getEvent() throws JSONException { + JSONObject jsonEvent = new JSONObject(); + JSONObject jsonMetaData = new JSONObject(); + jsonMetaData.put("owner", getOwner()); + jsonMetaData.put("deviceId", getDeviceId()); + jsonMetaData.put("type", type); + jsonMetaData.put("timestamp", getTimestamp()); + jsonEvent.put("metaData", jsonMetaData); + + JSONObject jsonPayloadData = new JSONObject(); + jsonPayloadData.put("battery", getBattery()); + //gps + double gpsEvents[] = getGps(); + jsonPayloadData.put("gps_lat", gpsEvents[0]); + jsonPayloadData.put("gps_long", gpsEvents[1]); + //acceleromter + float events[] = getAccelerometer(); + jsonPayloadData.put("accelerometer_x", events[0]); + jsonPayloadData.put("accelerometer_y", events[1]); + jsonPayloadData.put("accelerometer_z", events[2]); + //magnetic + events = getMagnetic(); + jsonPayloadData.put("magnetic_x", events[0]); + jsonPayloadData.put("magnetic_y", events[1]); + jsonPayloadData.put("magnetic_z", events[2]); + //gyroscope + events = getGyroscope(); + jsonPayloadData.put("gyroscope_x", events[0]); + jsonPayloadData.put("gyroscope_y", events[1]); + jsonPayloadData.put("gyroscope_z", events[2]); + + jsonPayloadData.put("light", getLight()); + jsonPayloadData.put("pressure", getPressure()); + jsonPayloadData.put("proximity", getProximity()); + //gravity + events = getGravity(); + jsonPayloadData.put("gravity_x", events[0]); + jsonPayloadData.put("gravity_y", events[1]); + jsonPayloadData.put("gravity_z", events[2]); + //rotation + events = getRotation(); + jsonPayloadData.put("rotation_x", events[0]); + jsonPayloadData.put("rotation_y", events[1]); + jsonPayloadData.put("rotation_z", events[2]); + //word + jsonPayloadData.put("word", getWord()); + jsonPayloadData.put("word_sessionId", getWordSessionId()); + jsonPayloadData.put("word_status", getWordStatus()); + + jsonEvent.put("payloadData", jsonPayloadData); + + return jsonEvent; + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/AndroidSenseMQTTHandler.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/AndroidSenseMQTTHandler.java new file mode 100644 index 000000000..5ae9b3739 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/AndroidSenseMQTTHandler.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.wso2.carbon.iot.android.sense.data.publisher.mqtt; + +import android.content.Context; +import android.util.Log; + +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.wso2.carbon.iot.android.sense.data.publisher.mqtt.transport.MQTTTransportHandler; +import org.wso2.carbon.iot.android.sense.data.publisher.mqtt.transport.TransportHandlerException; +import org.wso2.carbon.iot.android.sense.constants.SenseConstants; +import org.wso2.carbon.iot.android.sense.speech.detector.util.ProcessWords; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * This is an example for the use of the MQTT capabilities provided by the IoT-Server. This example depicts the use + * of MQTT Transport for the Android Sense device-type. This class extends the abstract class + * "MQTTTransportHandler". "MQTTTransportHandler" consists of the MQTT client specific functionality and implements + * the "TransportHandler" interface. The actual functionality related to the "TransportHandler" interface is + * implemented here, in this concrete class. Whilst the abstract class "MQTTTransportHandler" is intended to provide + * the common MQTT functionality, this class (which is its extension) provides the implementation specific to the + * MQTT communication of the Device-Type (Android Sense) in concern. + *

+ * Hence, the methods of this class are implementation of the "TransportHandler" interface which handles the device + * specific logic to connect-to, publish-to, process-incoming-messages-from and disconnect-from the MQTT broker + * listed in the configurations. + */ +public class AndroidSenseMQTTHandler extends MQTTTransportHandler { + private static final String TAG = "AndroidSenseMQTTHandler"; + private static volatile AndroidSenseMQTTHandler mInstance; + + + /** + * return a sigleton Instance + * @param context is the android context object. + * @return AndroidSenseMQTTHandler. + */ + public static AndroidSenseMQTTHandler getInstance(Context context) { + if (mInstance == null) { + Class clazz = AndroidSenseMQTTHandler.class; + synchronized (clazz) { + if (mInstance == null) { + mInstance = new AndroidSenseMQTTHandler(context); + } + } + } + return mInstance; + } + + /** + * Default constructor for the AndroidSenseMQTTHandler. + */ + private AndroidSenseMQTTHandler(Context context) { + super(context); + } + + /** + * {@inheritDoc} + * AndroidSense device-type specific implementation to connect to the MQTT broker and subscribe to a topic. + * This method is called to initiate a MQTT communication. + */ + @Override + public void connect() { + Runnable connector = new Runnable() { + public void run() { + while (!isConnected()) { + try { + connectToQueue(); + } catch (TransportHandlerException e) { + Log.e(TAG, "Connection to MQTT Broker at: " + mqttBrokerEndPoint + " failed", e); + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + Log.e(TAG, "MQTT-Connector: Thread Sleep Interrupt Exception.", ex); + } + } + + try { + subscribeToQueue(); + } catch (TransportHandlerException e) { + Log.w(TAG, "Subscription to MQTT Broker at: " + mqttBrokerEndPoint + " failed", e); + } + } + } + }; + + Thread connectorThread = new Thread(connector); + connectorThread.setDaemon(true); + connectorThread.start(); + } + + /** + * {@inheritDoc} + * AndroidSense device-type specific implementation to process incoming messages. This is the specific + * method signature of the overloaded "processIncomingMessage" method that gets called from the messageArrived() + * callback of the "MQTTTransportHandler". + */ + @Override + public void processIncomingMessage(MqttMessage mqttMessage, String... messageParams) { + if (messageParams.length != 0) { + // owner and the deviceId are extracted from the MQTT topic to which the message was received. + // = [ServerName/Owner/DeviceType/DeviceId/#] + String topic = messageParams[0]; + String[] topicParams = topic.split("/"); + String owner = topicParams[1]; + String deviceId = topicParams[3]; + + Log.d(TAG, "Received MQTT message for: [OWNER-" + owner + "] & [DEVICE.ID-" + deviceId + "]"); + + String msg; + msg = mqttMessage.toString(); + Log.d(TAG, "MQTT: Received Message [" + msg + "] topic: [" + topic + "]"); + if (topic.contains("threshold")) { + try { + ProcessWords.setThreshold(Integer.parseInt(msg)); + } catch (NumberFormatException e) { + Log.e(TAG, "Invalid threshold value " + msg); + } + } else if (topic.contains("words")) { + String words[] = msg.split(" "); + ProcessWords.addWords(Arrays.asList(words)); + } else if (topic.contains("remove")) { + String words[] = msg.split(" "); + for (String word: words) { + ProcessWords.removeWord(word); + } + } + } else { + String errorMsg = + "MQTT message [" + mqttMessage.toString() + "] was received without the topic information."; + Log.w(TAG, errorMsg); + } + } + + /** + * {@inheritDoc} + * AndroidSense device-type specific implementation to publish data to the device. This method calls the + * {@link #publishToQueue(String, MqttMessage)} method of the "MQTTTransportHandler" class. + */ + @Override + public void publishDeviceData(String... publishData) throws TransportHandlerException { + if (publishData.length != 3) { + String errorMsg = "Incorrect number of arguments received to SEND-MQTT Message. " + + "Need to be [owner, deviceId, content]"; + Log.e(TAG, errorMsg); + throw new TransportHandlerException(errorMsg); + } + + String deviceOwner = publishData[0]; + String deviceId = publishData[1]; + String resource = publishData[2]; + + MqttMessage pushMessage = new MqttMessage(); + String publishTopic = "wso2/" + SenseConstants.DEVICE_TYPE + "/" + deviceId + "/data"; + String actualMessage = resource; + pushMessage.setPayload(actualMessage.getBytes(StandardCharsets.UTF_8)); + pushMessage.setQos(DEFAULT_MQTT_QUALITY_OF_SERVICE); + pushMessage.setRetained(false); + publishToQueue(publishTopic, pushMessage); + } + + + /** + * {@inheritDoc} + * Android Sense device-type specific implementation to disconnect from the MQTT broker. + */ + @Override + public void disconnect() { + Runnable stopConnection = new Runnable() { + public void run() { + while (isConnected()) { + try { + closeConnection(); + } catch (MqttException e) { + Log.w(TAG, "Unable to 'STOP' MQTT connection at broker at: " + mqttBrokerEndPoint + + " for device-type - " + SenseConstants.DEVICE_TYPE, e); + + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException e1) { + Thread.currentThread().interrupt(); + Log.e(TAG, "MQTT-Terminator: Thread Sleep Interrupt Exception at device-type - " + + SenseConstants.DEVICE_TYPE, e1); + } + } + } + } + }; + + Thread terminatorThread = new Thread(stopConnection); + terminatorThread.setDaemon(true); + terminatorThread.start(); + } + + + /** + * {@inheritDoc} + */ + @Override + public void publishDeviceData() { + // nothing to do + } + + @Override + public void publishDeviceData(MqttMessage publishData) throws TransportHandlerException { + } + + /** + * {@inheritDoc} + */ + @Override + public void processIncomingMessage() { + // nothing to do + } + + @Override + public void processIncomingMessage(MqttMessage message) throws TransportHandlerException { + + } + +} + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/MQTTTransportHandler.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/MQTTTransportHandler.java new file mode 100644 index 000000000..689138d72 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/MQTTTransportHandler.java @@ -0,0 +1,385 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.wso2.carbon.iot.android.sense.data.publisher.mqtt.transport; +import android.content.Context; +import android.util.Log; +import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; +import org.eclipse.paho.client.mqttv3.MqttCallback; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.wso2.carbon.iot.android.sense.constants.SenseConstants; +import org.wso2.carbon.iot.android.sense.util.LocalRegistry; + +import java.nio.charset.StandardCharsets; + +/** + * This is an abstract class that implements the "TransportHandler" interface. The interface is an abstraction for + * the core functionality with regards to device-server communication regardless of the Transport protocol. This + * specific class contains the MQTT-Transport specific implementations. The class implements utility methods for the + * case of a MQTT communication. However, this "abstract class", even-though it implements the "TransportHandler" + * interface, does not contain the logic relevant to the interface methods. The specific functionality of the + * interface methods are intended to be implemented by the concrete class that extends this abstract class and + * utilizes the MQTT specific functionality (ideally a device API writer who would like to communicate to the device + * via MQTT Protocol). + *

+ * This class contains the Device-Management specific implementation for all the MQTT functionality. This includes + * connecting to a MQTT Broker & subscribing to the appropriate MQTT-topic, action plan upon losing connection or + * successfully delivering a message to the broker and upon receiving a MQTT message. Makes use of the 'Paho-MQTT' + * library provided by Eclipse Org. + */ +public abstract class MQTTTransportHandler implements MqttCallback, TransportHandler { + private static final String TAG = "MQTTTransportHandler"; + + private MqttClient client; + private String clientId; + private MqttConnectOptions options; // options to be set to the client-connection. + // topic to which a will-message is automatically published by the broker upon the device losing its connection. + private String clientWillTopic; + + protected String mqttBrokerEndPoint; + protected int timeoutInterval; // interval to use for reconnection attempts etc. + protected String subscribeTopic; + + // Quality of Service Levels for MQTT Subscription and Publishing. + public static final int QoS_0 = 0; // At-Most Once + @SuppressWarnings("unused") + public static final int QoS_1 = 1; // At-Least Once + public static final int QoS_2 = 2; // Exactly Once + + public static final int DEFAULT_MQTT_QUALITY_OF_SERVICE = QoS_0; + // Prefix to the Will-Topic to which a message is published if client loses its connection. + private static final String DISCONNECTION_WILL_TOPIC_PREFIX = "Disconnection/"; + // Will-Message of the client to be published if connection is lost. + private static final String DISCONNECTION_WILL_MSG = "Lost-Connection"; + /** + * Constructor for the MQTTTransportHandler which takes in the owner, type of the device and the MQTT Broker URL + * and the topic to subscribe. + * @param context activity context. + */ + protected MQTTTransportHandler(Context context) { + String username = LocalRegistry.getUsername(context); + String deviceId = LocalRegistry.getDeviceId(context); + this.clientId = deviceId + ":" + SenseConstants.DEVICE_TYPE; + this.subscribeTopic = "wso2/" + SenseConstants.DEVICE_TYPE + "/" + deviceId + "/command/#"; + this.clientWillTopic = DISCONNECTION_WILL_TOPIC_PREFIX + SenseConstants.DEVICE_TYPE; + this.mqttBrokerEndPoint = "tcp://" + LocalRegistry.getServerHost(context) + ":" + LocalRegistry.getMqttPort(context); + this.timeoutInterval = DEFAULT_TIMEOUT_INTERVAL; + setUsernameAndPassword(LocalRegistry.getAccessToken(context), ""); + this.initMQTTClient(); + } + + /** + * Constructor for the MQTTTransportHandler which takes in the owner, type of the device and the MQTT Broker URL + * and the topic to subscribe. Additionally this constructor takes in the reconnection-time interval between + * successive attempts to connect to the broker. + * + * @param deviceOwner the owner of the device. + * @param deviceType the CDMF Device-Type of the device. + * @param mqttBrokerEndPoint the IP/URL of the MQTT broker endpoint. + * @param subscribeTopic the MQTT topic to which the client is to be subscribed + * @param intervalInMillis the time interval in MILLI-SECONDS between attempts to connect to the broker. + */ + protected MQTTTransportHandler(String deviceOwner, String deviceType, + String mqttBrokerEndPoint, String subscribeTopic, int intervalInMillis) { + this.clientId = deviceOwner + ":" + deviceType; + this.subscribeTopic = subscribeTopic; + this.clientWillTopic = DISCONNECTION_WILL_TOPIC_PREFIX + deviceType; + this.mqttBrokerEndPoint = mqttBrokerEndPoint; + this.timeoutInterval = intervalInMillis; + this.initMQTTClient(); + } + + + /** + * Initializes the MQTT-Client. Creates a client using the given MQTT-broker endpoint and the clientId (which is + * constructed by a concatenation of [deviceOwner]:[deviceType]). Also sets the client's options parameter with + * the clientWillTopic (in-case of connection failure) and other info. Also sets the callback to this current class. + */ + private void initMQTTClient() { + try { + client = new MqttClient(this.mqttBrokerEndPoint, clientId, null); + Log.i(TAG, "MQTT client was created with ClientID : " + clientId); + } catch (MqttException ex) { + String errorMsg = "Initializing the MQTT Client failed."; + Log.e(TAG, errorMsg, ex); + } + + options = new MqttConnectOptions(); + options.setKeepAliveInterval(120); // set the keep alive interval to 120 seconds by default. + options.setCleanSession(true); // sets clean session to true by default. + setDisconnectionWillForClient(QoS_2, true); // sets default will-topic & msg with QoS 2 and retained true. + client.setCallback(this); // callback for MQTT events are set to `this` object. + } + + /** + * @param qos the Quality of Service at which the last-will-message is to be published. + * @param isRetained indicate whether to retain the last-will-message. + * @see MQTTTransportHandler#setDisconnectionWillForClient(String, String, int, boolean). Uses the default values + * for Will-Topic and Will-Message. + */ + protected void setDisconnectionWillForClient(int qos, boolean isRetained) { + this.setDisconnectionWillForClient(clientWillTopic, DISCONNECTION_WILL_MSG, qos, isRetained); + } + + /** + * Sets the [Will] option in the default options-set of the MQTT Client. A will-topic, will-message is parsed + * along with the QoS and the retained flag. When the client loses its connection to the broker, the broker + * publishes the will-message to the will-topic, to itself. + * + * @param willTopic the topic to which the last will message is to be published when client exists ungracefully. + * @param willMsg the message to be published upon client's ungraceful exit from the broker. + * @param qos the Quality of Service at which the last-will-message is to be published. + * @param isRetained indicate whether to retain the last-will-message. + */ + protected void setDisconnectionWillForClient(String willTopic, String willMsg, int qos, boolean isRetained) { + this.options.setWill(willTopic, willMsg.getBytes(StandardCharsets.UTF_8), qos, isRetained); + } + + /** + * Sets the [Clean-Session] option in the default options-set of the MQTT Client. It is set to `true` by default. + * + * @param setCleanSession `true` indicates that the session details can be cleared/cleaned upon disconnection, + * `false` indicates that the session details are to be persisted if the client disconnects. + */ + @SuppressWarnings("unused") + protected void setClientCleanSession(boolean setCleanSession) { + this.options.setCleanSession(setCleanSession); + } + + /** + * Sets the [Username] & [Password] options in the default options-set of the MQTT Client. By default these + * values are not set. + * + * @param username the username to be used by the client to connect to the broker. + * @param password the password to be used by the client to connect to the broker. + */ + @SuppressWarnings("unused") + protected void setUsernameAndPassword(String username, String password) { + this.options.setUserName(username); + this.options.setPassword(password.toCharArray()); + } + + /** + * Connects to the MQTT-Broker at the endpoint specified in the constructor to this class using default the + * MQTT-options. + * + * @throws TransportHandlerException in the event of 'Connecting to' the MQTT broker fails. + */ + protected void connectToQueue() throws TransportHandlerException { + this.connectToQueue(options); + } + + /** + * Connects to the MQTT-Broker at the endpoint specified in the constructor to this class using the MQTT-Options + * passed. + * + * @param options options to be used by the client for this connection. (username, password, clean-session, etc) + * @throws TransportHandlerException in the event of 'Connecting to' the MQTT broker fails. + */ + protected void connectToQueue(MqttConnectOptions options) throws TransportHandlerException { + try { + client.connect(options); + Log.d(TAG, "MQTT Client connected to queue at: " + this.mqttBrokerEndPoint); + } catch (MqttException ex) { + String errorMsg = "MQTT Exception occured whilst connecting to queue at [" + this.mqttBrokerEndPoint + "]"; + Log.e(TAG, errorMsg); + throw new TransportHandlerException(errorMsg, ex); + } + } + + /** + * @throws TransportHandlerException in the event of 'Subscribing to' the MQTT broker fails. + * @see MQTTTransportHandler#subscribeToQueue(int). Uses default QoS of 1. + */ + protected void subscribeToQueue() throws TransportHandlerException { + this.subscribeToQueue(QoS_0); + } + + /** + * Subscribes to the MQTT-Topic specified in the constructor to this class. + * + * @throws TransportHandlerException in the event of 'Subscribing to' the MQTT broker fails. + */ + protected void subscribeToQueue(int qos) throws TransportHandlerException { + try { + client.subscribe(subscribeTopic, qos); + Log.d(TAG, "Client [" + clientId + "] subscribed to topic: " + subscribeTopic); + } catch (MqttException ex) { + String errorMsg = "MQTT Exception occurred whilst client [" + clientId + "] tried to subscribe to " + + "topic: [" + subscribeTopic + "]"; + Log.e(TAG, errorMsg); + throw new TransportHandlerException(errorMsg, ex); + } + } + + /** + * @param topic the topic to which the message is to be published. + * @param payLoad the message (payload) of the MQTT publish action. + * @see MQTTTransportHandler#publishToQueue(String, String, int, boolean) + */ + @SuppressWarnings("unused") + protected void publishToQueue(String topic, String payLoad) throws TransportHandlerException { + publishToQueue(topic, payLoad, DEFAULT_MQTT_QUALITY_OF_SERVICE, false); + } + + /** + * @param topic the topic to which the message is to be published. + * @param message the message (payload) of the MQTT publish action as a `MQTTMessage`. + * @throws TransportHandlerException if any error occurs whilst trying to publish to the MQTT Queue. + * @see MQTTTransportHandler#publishToQueue(String, String, int, boolean) + */ + protected void publishToQueue(String topic, MqttMessage message) throws TransportHandlerException { + try { + client.publish(topic, message); + Log.d(TAG, "Message: " + message.toString() + " to MQTT topic [" + topic + "] published successfully"); + } catch (MqttException ex) { + String errorMsg = "MQTT Client Error whilst client [" + clientId + "] tried to publish to queue at " + + "[" + mqttBrokerEndPoint + "] under topic [" + topic + "]"; + Log.e(TAG, errorMsg); + throw new TransportHandlerException(errorMsg, ex); + } + } + + /** + * This method is used to publish messages to the MQTT-Endpoint to which this client is connected to. It is via + * publishing to this broker that the messages are communicated to the device. This is an overloaded method with + * different parameter combinations. This method invokes the publish method provided by the MQTT-Client library. + * + * @param topic the topic to which the message is to be published. + * @param payLoad the message (payload) of the MQTT publish action. + * @param qos the Quality-of-Service of the current publish action. + * Could be 0(At-most once), 1(At-least once) or 2(Exactly once) + * @param retained indicate whether to retain the publish-message in the event of no subscribers. + * @throws TransportHandlerException if any error occurs whilst trying to publish to the MQTT Queue. + */ + protected void publishToQueue(String topic, String payLoad, int qos, boolean retained) + throws TransportHandlerException { + try { + client.publish(topic, payLoad.getBytes(StandardCharsets.UTF_8), qos, retained); + Log.d(TAG, "Message: " + payLoad + " to MQTT topic [" + topic + "] published successfully"); + + } catch (MqttException ex) { + String errorMsg = "MQTT Client Error whilst client [" + clientId + "] tried to publish to queue at " + + "[" + mqttBrokerEndPoint + "] under topic [" + topic + "]"; + Log.e(TAG, errorMsg); + throw new TransportHandlerException(errorMsg, ex); + } + } + + /** + * Checks whether the connection to the MQTT-Broker exists. + * + * @return `true` if the client is connected to the MQTT-Broker, else `false`. + */ + @Override + public boolean isConnected() { + return client.isConnected(); + } + + /** + * Callback method which is triggered once the MQTT client losers its connection to the broker. Spawns a new + * thread that executes necessary actions to try and reconnect to the endpoint. + * + * @param throwable a Throwable Object containing the details as to why the failure occurred. + */ + @Override + public void connectionLost(Throwable throwable) { + Log.w(TAG, "Connection for client: " + this.clientId + " to " + this.mqttBrokerEndPoint + " was lost." + + "\nThis was due to - " + throwable.getMessage()); + Thread reconnectThread = new Thread() { + public void run() { + connect(); + } + }; + reconnectThread.setDaemon(true); + reconnectThread.start(); + } + + /** + * Callback method which is triggered upon receiving a MQTT Message from the broker. Spawns a new thread that + * executes any actions to be taken with the received message. + * + * @param topic the MQTT-Topic to which the received message was published to and the client subscribed to. + * @param mqttMessage the actual MQTT-Message that was received from the broker. + */ + @Override + public void messageArrived(final String topic, final MqttMessage mqttMessage) { + Log.d(TAG, "Got an MQTT message '" + mqttMessage.toString() + "' for topic '" + topic + "'."); + + Thread messageProcessorThread = new Thread() { + public void run() { + try { + processIncomingMessage(mqttMessage, topic); + } catch (TransportHandlerException e) { + Log.e(TAG, "An error occurred when trying to process received MQTT message [" + mqttMessage + "] " + + "for topic [" + topic + "].", e); + } + } + }; + messageProcessorThread.setDaemon(true); + messageProcessorThread.start(); + } + + /** + * Callback method which gets triggered upon successful completion of a message delivery to the broker. + * + * @param iMqttDeliveryToken the MQTT-DeliveryToken which includes the details about the specific message delivery. + */ + @Override + public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) { + String topic = iMqttDeliveryToken.getTopics()[0]; + String client = iMqttDeliveryToken.getClient().getClientId(); + + try { + if (iMqttDeliveryToken.isComplete()) { + if (iMqttDeliveryToken.getMessage() != null) { + String message = iMqttDeliveryToken.getMessage().toString(); + Log.d(TAG, "Message to client [" + client + "] under topic (" + topic + + ") was delivered successfully with the delivery message: '" + message + "'"); + } else { + Log.d(TAG, "Message to client [" + client + "] under topic (" + topic + + ") was delivered successfully."); + } + } else { + Log.w(TAG, "FAILED: Delivery of MQTT message to [" + client + "] under topic [" + topic + "] failed."); + } + } catch (MqttException e) { + Log.w(TAG, "Error occurred whilst trying to read the message from the MQTT delivery token."); + } + } + + /** + * Closes the connection to the MQTT Broker. + */ + public void closeConnection() throws MqttException { + if (client != null && isConnected()) { + client.disconnect(); + } + } + + /** + * Fetches the default options set for the MQTT Client + * + * @return the options that are currently set for the client. + */ + public MqttConnectOptions getOptions() { + return options; + } +} \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/TransportHandler.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/TransportHandler.java new file mode 100644 index 000000000..88efba9a4 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/TransportHandler.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.iot.android.sense.data.publisher.mqtt.transport; + +/** + * This interface consists of the core functionality related to the transport between any device and the server. The + * interface is an abstraction, regardless of the underlying protocol used for the transport. Implementation of this + * interface by any class that caters a specific protocol (ex: HTTP, XMPP, MQTT, CoAP) would ideally have methods + * specific to the protocol used for communication and other methods that implement the logic related to the devices + * using the protocol. The methods of the interface are identified as generic ones for implementing transport + * protocols for device communication. The implementation can utilize the appropriate method signatures applicable for + * intended protocol. + * + * @param an object of the message type specific to the protocol implemented. To be set to 'String' if there + * isn't anything specific. + */ +public interface TransportHandler { + // a default timeout interval to be used for the protocol specific connections + int DEFAULT_TIMEOUT_INTERVAL = 5000; // millis ~ 5 sec + + /** + * Implements the underlying connect mechanism specific to the protocol enabled by the interface. An object of a + * class that implements this interface would call this method before any communication is started via the + * intended protocol. + */ + void connect(); + + /** + * Used to check whether a connection (via the implemented protocol) to the external-endpoint exists. Ideally + * used to verify that the connection persists and to spawn a reconnection attempt if not. + * + * @return 'true' if connection is already made & exists, else 'false'. + */ + boolean isConnected(); + + /** + * @throws TransportHandlerException in the event of any exceptions that occur whilst processing the message. + * @see TransportHandler#processIncomingMessage(Object, String...) + */ + void processIncomingMessage() throws TransportHandlerException; + + /** + * @param message the message (of the type specific to the protocol) received from the device. + * @throws TransportHandlerException + * @see TransportHandler#processIncomingMessage(Object, String...) + */ + void processIncomingMessage(T message) throws TransportHandlerException; + + /** + * This is an overloaded method with three different method-signatures. This method is used to process any + * incoming messages via the implemented protocol. It would ideally be invoked at a point where a message + * received event is activated (Ex: `MessageArrived` callback in Eclipse-Paho-MQTT Client & `PacketListener`(s) + * in XMPP). + *

+ * + * @param message the message (of the type specific to the protocol) received from the device. + * @param messageParams one or more other parameters received as part-of & relevant-to the message (Ex: MQTT Topic). + * @throws TransportHandlerException in the event of any exceptions that occur whilst processing the message. + */ + void processIncomingMessage(T message, String... messageParams) throws TransportHandlerException; + + /** + * @throws TransportHandlerException in the event of any exceptions that occur whilst sending the message. + * @see TransportHandler#publishDeviceData(String...) + */ + void publishDeviceData() throws TransportHandlerException; + + /** + * @param publishData the message (of the type specific to the protocol) to be sent to the device. + * @throws TransportHandlerException in the event of any exceptions that occur whilst sending the message. + * @see TransportHandler#publishDeviceData(String...) + */ + void publishDeviceData(T publishData) throws TransportHandlerException; + + /** + * This is an overloaded method with three different method-signatures. This method is used to publish messages + * to an external-endpoint/device via the implemented protocol. It could in itself call the (communicating) + * external-endpoint or invoke any method provided by the protocol specific library. + *

+ * + * @param publishData one or more parameters specific to the message and the data to be sent. + * @throws TransportHandlerException in the event of any exceptions that occur whilst sending the message. + */ + void publishDeviceData(String... publishData) throws TransportHandlerException; + + /** + * Implements the underlying disconnect mechanism specific to the protocol enabled by the interface. An object of a + * class that implements this interface would call this method upon completion of all communication. In the case of + * the IoT-Server invoking this would only be required if the server shuts-down. + */ + void disconnect(); +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/TransportHandlerException.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/TransportHandlerException.java new file mode 100644 index 000000000..c52ca2ed0 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/data/publisher/mqtt/transport/TransportHandlerException.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.iot.android.sense.data.publisher.mqtt.transport; + +public class TransportHandlerException extends Exception { + private static final long serialVersionUID = 2736466230451105440L; + + private String errorMessage; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public TransportHandlerException(String msg, Exception nestedEx) { + super(msg, nestedEx); + setErrorMessage(msg); + } + + public TransportHandlerException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public TransportHandlerException(String msg) { + super(msg); + setErrorMessage(msg); + } + + public TransportHandlerException() { + super(); + } + + public TransportHandlerException(Throwable cause) { + super(cause); + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/SenseScheduleReceiver.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/SenseScheduleReceiver.java new file mode 100644 index 000000000..41ff9feff --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/SenseScheduleReceiver.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.event; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import java.util.Calendar; + +/** + * This is a service which triggers to collect + */ +public class SenseScheduleReceiver extends BroadcastReceiver { + private static final int ALARM_INTERVAL = 5000; + @Override + public void onReceive(Context context, Intent intent) { + + AlarmManager service = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + Intent i = new Intent(context, SenseService.class); + PendingIntent pending = PendingIntent.getService(context, 0, i, 0); + + Calendar cal = Calendar.getInstance(); + + cal.add(Calendar.SECOND, 30); + service.setRepeating(AlarmManager.RTC_WAKEUP, cal.getTimeInMillis(), ALARM_INTERVAL, pending); + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/SenseService.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/SenseService.java new file mode 100644 index 000000000..69285234f --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/SenseService.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.event; + +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.IBinder; +import org.wso2.carbon.iot.android.sense.event.streams.SenseDataCollector; +import org.wso2.carbon.iot.android.sense.event.streams.battery.BatteryDataReceiver; +import org.wso2.carbon.iot.android.sense.util.LocalRegistry; +import org.wso2.carbon.iot.android.sense.util.SenseWakeLock; + +/** + * This service caters to initiate the data collection. + */ +public class SenseService extends Service { + + public static Context context; + + @Override + public void onCreate() { + super.onCreate(); + SenseWakeLock.acquireWakeLock(this); + } + + @Override + public IBinder onBind(Intent arg0) { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + context = this; + if (!LocalRegistry.isExist(context)) return Service.START_NOT_STICKY; + //Below triggers the data collection for sensors,location and battery. + SenseDataCollector Sensor = new SenseDataCollector(this, SenseDataCollector.DataType.SENSOR); + SenseDataCollector Location = new SenseDataCollector(this, SenseDataCollector.DataType.LOCATION); + registerReceiver(new BatteryDataReceiver(), new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); + + //service will not be stopped until we manually stop the service + return Service.START_NOT_STICKY; + + } + + @Override + public void onDestroy() { + SenseWakeLock.releaseCPUWakeLock(); + super.onDestroy(); + } +} \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/DataReader.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/DataReader.java new file mode 100644 index 000000000..1d8e42204 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/DataReader.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package org.wso2.carbon.iot.android.sense.event.streams; + +/** + * this class extended by each data reader implementation, where the data store logic is implemented in thread. + */ +public abstract class DataReader implements Runnable { +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Location/LocationData.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Location/LocationData.java new file mode 100644 index 000000000..3f5de2888 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Location/LocationData.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.event.streams.Location; + +import java.util.Date; + +/** + * This defines the data structure of the location data that is been collected. + */ +public class LocationData { + private double latitude; // latitude + private double longitude; // longitude + private long timestamp; + + LocationData(double latitude, double longitude) { + this.latitude = latitude; + this.longitude = longitude; + timestamp = new Date().getTime(); + + } + + public double getLatitude() { + return latitude; + } + + public void setLatitude(double latitude) { + this.latitude = latitude; + } + + public double getLongitude() { + return longitude; + } + + public void setLongitude(double longitude) { + this.longitude = longitude; + } + + public long getTimeStamp() { + return timestamp; + } + + public void setTimeStamp(long timeStamp) { + timestamp = timeStamp; + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Location/LocationDataReader.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Location/LocationDataReader.java new file mode 100644 index 000000000..b8d3a4e58 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Location/LocationDataReader.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.event.streams.Location; + +import android.content.Context; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.os.Bundle; +import android.util.Log; +import org.wso2.carbon.iot.android.sense.event.streams.DataReader; +import org.wso2.carbon.iot.android.sense.util.SenseDataHolder; +import java.util.concurrent.TimeUnit; + +/** + * This is used to retrieve the location data using GPS and used Network connection to increase the accuracy. + */ +public class LocationDataReader extends DataReader implements LocationListener { + protected LocationManager locationManager; + private Context mContext; + private boolean canGetLocation = false; + private static final String TAG = "Location Data"; + + Location location; // location + double latitude; // latitude + double longitude; // longitude + + public LocationDataReader(Context context) { + mContext = context; + getLocation(); + } + + public Location getLocation() { + locationManager = (LocationManager) mContext.getSystemService(mContext.LOCATION_SERVICE); + + // getting GPS status + boolean isGPSEnabled = locationManager + .isProviderEnabled(LocationManager.GPS_PROVIDER); + + // getting network status + boolean isNetworkEnabled = locationManager + .isProviderEnabled(LocationManager.NETWORK_PROVIDER); + + if (!isGPSEnabled && !isNetworkEnabled) { + // no network provider is enabled + } else { + this.canGetLocation = true; + // First get location from Network Provider + if (isNetworkEnabled) { + locationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, 0, 0, this); + // MIN_TIME_BW_UPDATES, + // MIN_DISTANCE_CHANGE_FOR_UPDATES, this); + + if (locationManager != null) { + location = locationManager + .getLastKnownLocation(LocationManager.NETWORK_PROVIDER); + if (location != null) { + latitude = location.getLatitude(); + longitude = location.getLongitude(); + } + } + } + // if GPS Enabled get lat/long using GPS Services + if (isGPSEnabled) { + if (location == null) { + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, 0, 0, this); + //MIN_TIME_BW_UPDATES, + //MIN_DISTANCE_CHANGE_FOR_UPDATES, this); + + Log.d(TAG, "GPS Enabled"); + if (locationManager != null) { + location = locationManager + .getLastKnownLocation(LocationManager.GPS_PROVIDER); + if (location != null) { + latitude = location.getLatitude(); + longitude = location.getLongitude(); + } + } + } + } + } + return location; + } + + public boolean canGetLocation() { + return this.canGetLocation; + } + + public void stopUsingGPS() { + if (locationManager != null) { + locationManager.removeUpdates(LocationDataReader.this); + } + } + + public double getLatitude() { + if (location != null) { + latitude = location.getLatitude(); + } + // return latitude + return latitude; + } + + /** + * Function to get longitude + */ + public double getLongitude() { + if (location != null) { + longitude = location.getLongitude(); + } + // return longitude + return longitude; + } + + @Override + public void onLocationChanged(Location arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void onProviderDisabled(String arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void onProviderEnabled(String arg0) { + // TODO Auto-generated method stub + + } + + @Override + public void onStatusChanged(String arg0, int arg1, Bundle arg2) { + // TODO Auto-generated method stub + + } + + @Override + public void run() { + Log.d(TAG, "running -Location"); + try { + TimeUnit.MILLISECONDS.sleep(10000); + double lat = getLatitude(); + double longit = getLongitude(); + if (lat != 0 && longit != 0) { + SenseDataHolder.getLocationDataHolder().add(new LocationData(getLatitude(), getLongitude())); + } + } catch (InterruptedException e) { + // Restore the interrupted status + Thread.currentThread().interrupt(); + Log.e(TAG, " Location Data Retrieval Failed"); + } + } + + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/SenseDataCollector.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/SenseDataCollector.java new file mode 100644 index 000000000..2febb8ba2 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/SenseDataCollector.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package org.wso2.carbon.iot.android.sense.event.streams; + +import android.content.Context; +import org.wso2.carbon.iot.android.sense.event.streams.Location.LocationDataReader; +import org.wso2.carbon.iot.android.sense.event.streams.Sensor.SensorDataReader; + +/** + * This class triggered by service to collect the sensor data. + */ +public class SenseDataCollector { + public enum DataType { + SENSOR, LOCATION + } + + public SenseDataCollector(Context ctx, DataType dt) { + DataReader dr = null; + switch (dt) { + case SENSOR: + dr = new SensorDataReader(ctx); + break; + case LOCATION: + dr = new LocationDataReader(ctx); + break; + } + if (dr != null) { + Thread DataCollector = new Thread(dr); + DataCollector.start(); + } + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Sensor/SensorData.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Sensor/SensorData.java new file mode 100644 index 000000000..abdfeb155 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Sensor/SensorData.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package org.wso2.carbon.iot.android.sense.event.streams.Sensor; + +import android.hardware.SensorEvent; + +import org.wso2.carbon.iot.android.sense.realtimeviewer.sensorlisting.SupportedSensors; + +import java.util.Date; + +/** + * This defines the data structure of the sensor data that is been collected. + * look at http://developer.android.com/guide/topics/sensors/sensors_overview.html for field description. + */ +public class SensorData { + private int sensorType; + private String sensorName; + private String sensorVendor; + private float sensorValues[]; + private int accuracyStatus; + private long timestamp; + private String collectTimestamp; + private SupportedSensors supportedSensors = SupportedSensors.getInstance(); + + SensorData(SensorEvent event) { + sensorValues = event.values; + accuracyStatus = event.accuracy; + collectTimestamp = String.valueOf(event.timestamp); + timestamp = new Date().getTime(); + sensorName = supportedSensors.getType(event.sensor.getType()).toUpperCase(); + sensorVendor = event.sensor.getVendor(); + sensorType = event.sensor.getType(); + } + + public int getSensorType() { + return sensorType; + } + + public void setSensorType(int sensorType) { + this.sensorType = sensorType; + } + + public String getSensorName() { + return sensorName; + } + + public void setSensorName(String sensorName) { + this.sensorName = sensorName; + } + + public String getSensorVendor() { + return sensorVendor; + } + + public void setSensorVendor(String sensorVendor) { + this.sensorVendor = sensorVendor; + } + + public float[] getSensorValues() { + return sensorValues; + } + + public void setSensorValues(float sensorValues[]) { + this.sensorValues = sensorValues; + } + + public int getAccuracyStatus() { + return accuracyStatus; + } + + public void setAccuracyStatus(int accuracyStatus) { + this.accuracyStatus = accuracyStatus; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public String getCollectTimestamp() { + return collectTimestamp; + } + + public void setCollectTimestamp(String collectTimestamp) { + this.collectTimestamp = collectTimestamp; + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Sensor/SensorDataReader.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Sensor/SensorDataReader.java new file mode 100644 index 000000000..4f4697e50 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/Sensor/SensorDataReader.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.event.streams.Sensor; + +import android.content.Context; +import android.content.SharedPreferences; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.util.Log; + +import org.wso2.carbon.iot.android.sense.event.streams.DataReader; +import org.wso2.carbon.iot.android.sense.realtimeviewer.sensorlisting.SupportedSensors; +import org.wso2.carbon.iot.android.sense.util.SenseDataHolder; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Vector; +import java.util.concurrent.TimeUnit; + +/** + * This is used to retrieve the sensor data. + */ +public class SensorDataReader extends DataReader implements SensorEventListener { + private SensorManager mSensorManager; + private Map senseDataStruct = new HashMap<>(); + private Vector sensorVector = new Vector<>(); + Context ctx; + private List sensorList = new ArrayList<>(); + private SupportedSensors supportedSensors = SupportedSensors.getInstance(); + + public SensorDataReader(Context context) { + ctx = context; + SharedPreferences sharedPreferences = ctx.getSharedPreferences(SupportedSensors.SELECTED_SENSORS, Context + .MODE_MULTI_PROCESS); + Set selectedSet = sharedPreferences.getStringSet(SupportedSensors.SELECTED_SENSORS_BY_USER, null); + mSensorManager = (SensorManager) ctx.getSystemService(Context.SENSOR_SERVICE); + selectedSensorList(selectedSet); + for (Sensor sensor : sensorList) { + mSensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL); + } + + } + + private void collectSensorData() { + for (Sensor sensor : sensorList) { + try { + if (senseDataStruct.containsKey(sensor.getName())) { + SensorData sensorInfo = senseDataStruct.get(sensor.getName()); + sensorVector.add(sensorInfo); + Log.d(this.getClass().getName(), "Sensor Name " + sensor.getName() + ", Type " + sensor.getType() + " " + + ", sensorValue :" + sensorInfo.getSensorValues()); + } + } catch (Throwable e) { + Log.d(this.getClass().getName(), "error on sensors"); + } + + } + mSensorManager.unregisterListener(this); + } + + public Vector getSensorData() { + try { + TimeUnit.MILLISECONDS.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Log.e(SensorDataReader.class.getName(), e.getMessage()); + } + collectSensorData(); + return sensorVector; + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + + @Override + public void onSensorChanged(SensorEvent event) { + senseDataStruct.put(event.sensor.getName(), new SensorData(event)); + } + + @Override + public void run() { + Log.d(this.getClass().getName(), "running -sensorDataMap"); + Vector sensorDatas = getSensorData(); + for (SensorData data : sensorDatas) { + SenseDataHolder.getSensorDataHolder().add(data); + } + } + + public void selectedSensorList(Set set) { + if (set != null) { + String[] sensorsSet = set.toArray(new String[set.size()]); + for (String s : sensorsSet) { + sensorList.add(mSensorManager.getDefaultSensor(supportedSensors.getType(s.toLowerCase()))); + } + } + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/battery/BatteryData.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/battery/BatteryData.java new file mode 100644 index 000000000..666e02a85 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/battery/BatteryData.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ + +package org.wso2.carbon.iot.android.sense.event.streams.battery; + +import android.content.Intent; +import android.os.BatteryManager; + +import java.util.Date; + +/** + * This defines the data structure of the battery data that is been collected. + * look at http://developer.android.com/reference/android/os/BatteryManager.html for field description. + */ +public class BatteryData { + + private int health; + private int level; + private int plugged; + private int present; + private int scale; + private int status; + private int temperature; + private int voltage; + private long timestamp; + + BatteryData(Intent intent) { + timestamp = new Date().getTime(); + health = intent.getIntExtra(BatteryManager.EXTRA_HEALTH, 0); + level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0); + plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0); + present = intent.getExtras().getBoolean(BatteryManager.EXTRA_PRESENT) ? 1 : 0; + scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0); + status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, 0); + String technology = intent.getExtras().getString(BatteryManager.EXTRA_TECHNOLOGY); + temperature = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0); + voltage = intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, 0); + + } + + public int getHealth() { + return health; + } + + public void setHealth(int health) { + this.health = health; + } + + public int getLevel() { + return level; + } + + public void setLevel(int level) { + this.level = level; + } + + public int getPlugged() { + return plugged; + } + + public void setPlugged(int plugged) { + this.plugged = plugged; + } + + public int getPresent() { + return present; + } + + public void setPresent(int present) { + this.present = present; + } + + public int getScale() { + return scale; + } + + public void setScale(int scale) { + this.scale = scale; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public int getTemperature() { + return temperature; + } + + public void setTemperature(int temperature) { + this.temperature = temperature; + } + + public int getVoltage() { + return voltage; + } + + public void setVoltage(int voltage) { + this.voltage = voltage; + } + + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/battery/BatteryDataReceiver.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/battery/BatteryDataReceiver.java new file mode 100644 index 000000000..9fffa1c15 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/event/streams/battery/BatteryDataReceiver.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.event.streams.battery; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import org.wso2.carbon.iot.android.sense.util.SenseDataHolder; + +/** + * Whenever the battery level changes This receiver will be triggered. + */ +public class BatteryDataReceiver extends BroadcastReceiver { + + /** + * when the data is retreived then its added to a inmemory map. + * + * @param context of the reciever. + * @param intent of the reciver + */ + @Override + public void onReceive(Context context, Intent intent) { + SenseDataHolder.getBatteryDataHolder().add(new BatteryData(intent)); + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/ActivitySelectSensor.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/ActivitySelectSensor.java new file mode 100644 index 000000000..053b7e38c --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/ActivitySelectSensor.java @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.realtimeviewer; + +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.hardware.Sensor; +import android.hardware.SensorManager; +import android.os.Bundle; +import android.support.design.widget.FloatingActionButton; +import android.support.design.widget.NavigationView; +import android.support.design.widget.Snackbar; +import android.support.v4.view.GravityCompat; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.Toast; + +import org.wso2.carbon.iot.android.sense.RegisterActivity; +import org.wso2.carbon.iot.android.sense.data.publisher.DataPublisherReceiver; +import org.wso2.carbon.iot.android.sense.data.publisher.DataPublisherService; +import org.wso2.carbon.iot.android.sense.event.SenseScheduleReceiver; +import org.wso2.carbon.iot.android.sense.event.SenseService; +import org.wso2.carbon.iot.android.sense.realtimeviewer.datastore.TempStore; +import org.wso2.carbon.iot.android.sense.realtimeviewer.event.RealTimeSensorChangeReceiver; +import org.wso2.carbon.iot.android.sense.realtimeviewer.event.realtimesensor.RealTimeSensorReader; +import org.wso2.carbon.iot.android.sense.realtimeviewer.sensorlisting.SupportedSensors; +import org.wso2.carbon.iot.android.sense.realtimeviewer.view.adaptor.SensorViewAdaptor; +import org.wso2.carbon.iot.android.sense.realtimeviewer.view.sensor.selector.SelectSensorDialog; +import org.wso2.carbon.iot.android.sense.speech.detector.WordRecognitionActivity; +import org.wso2.carbon.iot.android.sense.util.LocalRegistry; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +import agent.sense.android.iot.carbon.wso2.org.wso2_senseagent.R; + +/** + * Activity for selecting sensors available in the device + */ + +public class ActivitySelectSensor extends AppCompatActivity + implements NavigationView.OnNavigationItemSelectedListener, SelectSensorDialog.SensorListListener { + + private SharedPreferences sharedPreferences; + private SelectSensorDialog sensorDialog = new SelectSensorDialog(); + private Set selectedSensorSet = new HashSet<>(); + private ListView listView; + private SensorManager sensorManager; + private ArrayList sensors = new ArrayList<>(); + + private RealTimeSensorReader sensorReader = null; + private RealTimeSensorChangeReceiver realTimeSensorChangeReceiver = new RealTimeSensorChangeReceiver(); + private SupportedSensors supportedSensors = SupportedSensors.getInstance(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_activity_select_sensor); + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); + + listView = (ListView) findViewById(R.id.senseListContainer); + + registerReceiver(realTimeSensorChangeReceiver, new IntentFilter("sensorDataMap")); + + //Publish data + FloatingActionButton fbtnPublishData = (FloatingActionButton) findViewById(R.id.publish); + + fbtnPublishData.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Snackbar.make(view, "Publishing data started", Snackbar.LENGTH_LONG) + .setAction("Action", null).show(); + + DataPublisherReceiver dataPublisherReceiver = new DataPublisherReceiver(); + dataPublisherReceiver.clearAbortBroadcast(); + dataPublisherReceiver.onReceive(getApplicationContext(), null); + } + }); + + FloatingActionButton fbtnAddSensors = (FloatingActionButton) findViewById(R.id.addSensors); + fbtnAddSensors.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + sensorDialog.show(getFragmentManager(), "Sensor List"); + } + }); + + FloatingActionButton fbtnSpeechRecongnizer = (FloatingActionButton) findViewById(R.id.speech); + fbtnSpeechRecongnizer.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + EditText sessionIdText = (EditText) findViewById(R.id.sessionId); + String sessionId = sessionIdText.getText().toString(); + if (!sessionId.isEmpty()) { + Intent intent = new Intent(getApplicationContext(), WordRecognitionActivity.class); + intent.putExtra("sessionId", sessionId); + startActivity(intent); + } else { + Toast.makeText(ActivitySelectSensor.this, "Please type a session id value", Toast.LENGTH_SHORT) + .show(); + } + + } + }); + + sharedPreferences = getSharedPreferences(SupportedSensors.SELECTED_SENSORS, 0); + + DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); + ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( + this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); + drawer.setDrawerListener(toggle); + toggle.syncState(); + + NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); + navigationView.setNavigationItemSelectedListener(this); + } + + @Override + public void onBackPressed() { + DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); + if (drawer.isDrawerOpen(GravityCompat.START)) { + drawer.closeDrawer(GravityCompat.START); + } else { + super.onBackPressed(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.activity_select_sensor, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + //noinspection SimplifiableIfStatement + if (id == R.id.action_deEnroll) { + + /** + * unregister the sensors and broadcast receivers. + * */ + unregisterSensors(); + unregisterReceivers(); + + if (!LocalRegistry.isExist(getApplicationContext())) { + Intent activity = new Intent(getApplicationContext(), RegisterActivity.class); + startActivity(activity); + } + + LocalRegistry.removeUsername(getApplicationContext()); + LocalRegistry.removeDeviceId(getApplicationContext()); + LocalRegistry.removeServerURL(getApplicationContext()); + LocalRegistry.setExist(false); + //Stop the current running background services. + stopService(new Intent(this, SenseService.class)); //Stop sensor reading service + stopService(new Intent(this, DataPublisherService.class)); //Stop data uploader service + + Intent registerActivity = new Intent(getApplicationContext(), RegisterActivity.class); + registerActivity.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(registerActivity); + finish(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + @SuppressWarnings("StatementWithEmptyBody") + @Override + public boolean onNavigationItemSelected(MenuItem item) { + // Handle navigation view item clicks here. + int id = item.getItemId(); + + if (id == R.id.select) { + sensorDialog.show(getFragmentManager(), "Sensor List"); + } + + DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); + drawer.closeDrawer(GravityCompat.START); + return true; + } + + @Override + public void onDialogPositiveClick(SelectSensorDialog dialog) { + + Log.d("Selected sensors", dialog.getSet().toString()); + selectedSensorSet = dialog.getSet(); + update(); + unregisterSensors(); + + SenseScheduleReceiver senseScheduleReceiver = new SenseScheduleReceiver(); + senseScheduleReceiver.clearAbortBroadcast(); + senseScheduleReceiver.onReceive(this, null); + + /** + * Get the selected sensors + * Register them + * */ + SensorViewAdaptor adaptor1 = new SensorViewAdaptor(getApplicationContext(), TempStore.sensorArrayList); + adaptor1.notifyDataSetChanged(); + + sensorReader = new RealTimeSensorReader(this, adaptor1); + getSensors(); + + for (Sensor s : sensors) { + sensorManager.registerListener(sensorReader, s, SensorManager.SENSOR_DELAY_NORMAL); + } + + realTimeSensorChangeReceiver.updateOnChange(adaptor1); + listView.setAdapter(adaptor1); + + } + + public void update() { + Log.d("Update", "Set the values to Shared Preferences"); + + TempStore.sensorArrayList.clear(); + TempStore.sensorDataMap.clear(); + + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putStringSet(SupportedSensors.SELECTED_SENSORS_BY_USER, selectedSensorSet); + editor.apply(); + } + + public void getSensors() { + sensors.clear(); + for (String sensor : selectedSensorSet.toArray(new String[selectedSensorSet.size()])) { + sensors.add(sensorManager.getDefaultSensor(supportedSensors.getType(sensor.toLowerCase()))); + } + } + + /** + * This method will unregister all the registered sensors. + */ + public void unregisterSensors() { + if (sensors.size() > 0) { + for (Sensor s : sensors) { + System.out.println(s.getName() + " Unregistered!"); + sensorManager.unregisterListener(sensorReader, s); + } + } + } + + /** + * This method unregisters the real-time broadcast receiver. + */ + public void unregisterReceivers() { + unregisterReceiver(realTimeSensorChangeReceiver); + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/datastore/TempStore.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/datastore/TempStore.java new file mode 100644 index 000000000..2e5343834 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/datastore/TempStore.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.realtimeviewer.datastore; + +import org.wso2.carbon.iot.android.sense.realtimeviewer.event.realtimesensor.RealTimeSensor; + +import java.util.ArrayList; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Class to store the real time sensor readings. + */ +public class TempStore { + + /** + * Hash map which is used to store sensor values with the sensor names. + */ + public static ConcurrentMap sensorDataMap = new ConcurrentHashMap<>(); + + /** + * Array List which is used to populate the List view. + */ + public static ArrayList sensorArrayList = new ArrayList<>(); + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/RealTimeSensorChangeReceiver.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/RealTimeSensorChangeReceiver.java new file mode 100644 index 000000000..c231ac87e --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/RealTimeSensorChangeReceiver.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.realtimeviewer.event; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import org.wso2.carbon.iot.android.sense.realtimeviewer.datastore.TempStore; +import org.wso2.carbon.iot.android.sense.realtimeviewer.view.adaptor.SensorViewAdaptor; + +/** + * This class is to detect the sensor change event and update the sensor array list. + * And update the view adaptor which is used to show the sensors list in the Android List view. + */ +public class RealTimeSensorChangeReceiver extends BroadcastReceiver { + + SensorViewAdaptor adaptor; + + public void updateOnChange(SensorViewAdaptor adaptor) { + this.adaptor = adaptor; + } + + @Override + public void onReceive(Context context, Intent intent) { + TempStore.sensorArrayList.clear(); + TempStore.sensorArrayList.addAll(TempStore.sensorDataMap.values()); + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/realtimesensor/RealTimeSensor.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/realtimesensor/RealTimeSensor.java new file mode 100644 index 000000000..6a71739dc --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/realtimesensor/RealTimeSensor.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.realtimeviewer.event.realtimesensor; + +import android.support.annotation.NonNull; + +/** + * The class to store the sensor data captured by the RealTimeSensorReader. + */ +public class RealTimeSensor implements Comparable { + + + /** + * Name of the sensor. + */ + private String name; + + /** + * The X value reading of the sensor. + */ + private String valueX; + + /** + * The Y value reading of the sensor. + */ + private String valueY; + + /** + * The Y value reading of the sensor. + */ + private String valueZ; + + public RealTimeSensor() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValueX() { + return valueX; + } + + public void setValueX(String valueX) { + this.valueX = valueX; + } + + public String getValueY() { + return valueY; + } + + public void setValueY(String valueY) { + this.valueY = valueY; + } + + public String getValueZ() { + return valueZ; + } + + public void setValueZ(String valueZ) { + this.valueZ = valueZ; + } + + @Override + public String toString() { + return this.valueX + ", " + valueY + ", " + valueZ; + } + + @Override + public int compareTo(@NonNull Object another) { + return this.toString().contains(another.toString()) ? 1 : 0; + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/realtimesensor/RealTimeSensorReader.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/realtimesensor/RealTimeSensorReader.java new file mode 100644 index 000000000..fc4b474e6 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/event/realtimesensor/RealTimeSensorReader.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.realtimeviewer.event.realtimesensor; + +import android.content.Context; +import android.content.Intent; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; + +import org.wso2.carbon.iot.android.sense.realtimeviewer.datastore.TempStore; +import org.wso2.carbon.iot.android.sense.realtimeviewer.sensorlisting.SupportedSensors; +import org.wso2.carbon.iot.android.sense.realtimeviewer.view.adaptor.SensorViewAdaptor; + +/** + * This class reads the sensor values in real time. + */ +public class RealTimeSensorReader implements SensorEventListener { + + private Context context; + private SensorViewAdaptor adaptor; + private SupportedSensors supportedSensors = SupportedSensors.getInstance(); + + public RealTimeSensorReader(Context context, SensorViewAdaptor adaptor) { + this.context = context; + this.adaptor = adaptor; + } + + @Override + public void onSensorChanged(SensorEvent event) { + RealTimeSensor realTimeSensor = new RealTimeSensor(); + realTimeSensor.setName(supportedSensors.getType(event.sensor.getType()).toUpperCase()); + + realTimeSensor.setValueX(event.values[0] + ""); + realTimeSensor.setValueY(event.values[1] + ""); + realTimeSensor.setValueZ(event.values[2] + ""); + + TempStore.sensorDataMap.put(supportedSensors.getType(event.sensor.getType()), realTimeSensor); + + Intent intent = new Intent(); + intent.setAction("sensorDataMap"); + context.sendBroadcast(intent); + + adaptor.notifyDataSetChanged(); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/sensorlisting/AvailableSensorsInDevice.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/sensorlisting/AvailableSensorsInDevice.java new file mode 100644 index 000000000..726c8cded --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/sensorlisting/AvailableSensorsInDevice.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.realtimeviewer.sensorlisting; + +import android.content.Context; +import android.content.SharedPreferences; +import android.hardware.Sensor; +import android.hardware.SensorManager; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Class to save the list of sensors that are available in the device, which are supported by the iot server. + * This list will be saved in Shared preferences so that app can use this data when needed. + */ +public class AvailableSensorsInDevice { + + private SharedPreferences sensorPreference; + + /** + * The Android sensor manager which is used to get the sensors available in device. + */ + private SensorManager mSensorManager; + + public AvailableSensorsInDevice(Context context) { + this.sensorPreference = context.getSharedPreferences(SupportedSensors.AVAILABLE_SENSORS, 0); + this.mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + } + + /** + * This method filters the pre defined sensor types from sensors available in device and sets them in Shared + * preferences. + */ + public void setContent() { + SupportedSensors supportedSensors = SupportedSensors.getInstance(); + List sensor_List = supportedSensors.getSensorList(); + Set sensorSet = new HashSet<>(); + List sensors = mSensorManager.getSensorList(Sensor.TYPE_ALL); + + for (String sen : sensor_List) { + if (sensors.contains(mSensorManager.getDefaultSensor(supportedSensors.getType(sen.toLowerCase())))) { + sensorSet.add(sen); + } + } + + SharedPreferences.Editor editor = this.sensorPreference.edit(); + editor.putStringSet(SupportedSensors.GET_AVAILABLE_SENSORS, sensorSet); + editor.apply(); + } + + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/sensorlisting/SupportedSensors.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/sensorlisting/SupportedSensors.java new file mode 100644 index 000000000..6d57e6d70 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/sensorlisting/SupportedSensors.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.realtimeviewer.sensorlisting; + +import android.hardware.Sensor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * Class to store the supported sensorDataMap types. + */ +public class SupportedSensors { + + //For set user selected sensors. Will be used by sensorDataMap reading and dialog + public static String SELECTED_SENSORS = "Selected"; + public static String SELECTED_SENSORS_BY_USER = "userSelection"; + + //For setting the available sensors in the device in dialog and AvailableSensorsInDevice + public static String AVAILABLE_SENSORS = "Sensors"; + public static String GET_AVAILABLE_SENSORS = "getAvailableSensors"; + + public static final int SUPPORTED_SENSOR_COUNT = 10; + private static List sensorList = new ArrayList<>(); + private static HashMap sensorTypeMap = new HashMap<>(); + private static HashMap typeSensorMap = new HashMap<>(); + private static SupportedSensors supportedSensors = new SupportedSensors(); + + private SupportedSensors() { + this.setList(); + this.setSensorTypeMap(); + this.setTypeSensorMap(); + } + + public static SupportedSensors getInstance() { + return supportedSensors; + } + + /** + * Set the supported sensor types by the IOT server. + */ + private void setList() { + sensorList.add("Accelerometer"); + sensorList.add("Magnetometer"); + sensorList.add("Gravity"); + sensorList.add("Rotation Vector"); + sensorList.add("Pressure"); + sensorList.add("Light"); + sensorList.add("Gyroscope"); + sensorList.add("Proximity"); + } + + /** + * Populate the hash map which has Sensor name as the key and the sensor type as the value. + */ + private void setSensorTypeMap() { + sensorTypeMap.put("accelerometer", Sensor.TYPE_ACCELEROMETER); + sensorTypeMap.put("magnetometer", Sensor.TYPE_MAGNETIC_FIELD); + sensorTypeMap.put("gravity", Sensor.TYPE_GRAVITY); + sensorTypeMap.put("rotation vector", Sensor.TYPE_GAME_ROTATION_VECTOR); + sensorTypeMap.put("pressure", Sensor.TYPE_PRESSURE); + sensorTypeMap.put("gyroscope", Sensor.TYPE_GYROSCOPE); + sensorTypeMap.put("light", Sensor.TYPE_LIGHT); + sensorTypeMap.put("proximity", Sensor.TYPE_PROXIMITY); + } + + /** + * Populates the hash map which has Sensor type as the key and sensor name as the value. + */ + private void setTypeSensorMap() { + typeSensorMap.put(Sensor.TYPE_ACCELEROMETER, "accelerometer"); + typeSensorMap.put(Sensor.TYPE_MAGNETIC_FIELD, "magnetometer"); + typeSensorMap.put(Sensor.TYPE_GRAVITY, "gravity"); + typeSensorMap.put(Sensor.TYPE_GAME_ROTATION_VECTOR, "rotation vector"); + typeSensorMap.put(Sensor.TYPE_PRESSURE, "pressure"); + typeSensorMap.put(Sensor.TYPE_GYROSCOPE, "gyroscope"); + typeSensorMap.put(Sensor.TYPE_LIGHT, "light"); + typeSensorMap.put(Sensor.TYPE_PROXIMITY, "proximity"); + } + + /** + * Method to get the supported sensor list. + * + * @return the list of sensors supported by the iot server. + */ + public List getSensorList() { + return sensorList; + } + + + /** + * @param sensor The name of the sensor. + * @return The integer representing the type of the sensor, + */ + public int getType(String sensor) { + return sensorTypeMap.get(sensor); + } + + + /** + * @param type The type of the sensor. + * @return The sensor name related to the given sensor type. + */ + public String getType(int type) { + return typeSensorMap.get(type); + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/view/adaptor/SensorViewAdaptor.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/view/adaptor/SensorViewAdaptor.java new file mode 100644 index 000000000..d2d852226 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/view/adaptor/SensorViewAdaptor.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.realtimeviewer.view.adaptor; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; + +import org.wso2.carbon.iot.android.sense.realtimeviewer.event.realtimesensor.RealTimeSensor; + +import java.util.List; + +import agent.sense.android.iot.carbon.wso2.org.wso2_senseagent.R; + +/** + * Adaptor for populate the ListView. + * Takes list of Sensor readings + */ + +//TODO : Add the location and battery data sections. +public class SensorViewAdaptor extends BaseAdapter { + + private Context context; + private List data; + + public SensorViewAdaptor(Context context, List data) { + this.context = context; + this.data = data; + } + + @Override + public int getCount() { + return data.size(); + } + + @Override + public Object getItem(int position) { + return data.get(position); + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder holder; + + LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View view; + + if (convertView == null) { + view = inflater.inflate(R.layout.display_sensor_values, parent, false); + holder = new ViewHolder(); + holder.name = (TextView) view.findViewById(R.id.name); + holder.valuesX = (TextView) view.findViewById(R.id.X); + holder.valuesY = (TextView) view.findViewById(R.id.Y); + holder.valuesZ = (TextView) view.findViewById(R.id.Z); + view.setTag(holder); + } else { + view = convertView; + holder = (ViewHolder) view.getTag(); + } + + RealTimeSensor data = this.data.get(position); + + holder.name.setText(data.getName()); + holder.valuesX.setText(data.getValueX()); + holder.valuesY.setText(data.getValueY()); + holder.valuesZ.setText(data.getValueZ()); + + return view; + + } + + private class ViewHolder { + public TextView name; + public TextView valuesX; + public TextView valuesY; + public TextView valuesZ; + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/view/sensor/selector/SelectSensorDialog.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/view/sensor/selector/SelectSensorDialog.java new file mode 100644 index 000000000..5c163c802 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/realtimeviewer/view/sensor/selector/SelectSensorDialog.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.realtimeviewer.view.sensor.selector; + +import android.app.Activity; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.support.v7.app.AlertDialog; +import android.util.Log; + +import org.wso2.carbon.iot.android.sense.realtimeviewer.sensorlisting.SupportedSensors; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * Functionality + *

+ * Show the list of available sensors in a list + * Get the user selections + * Put them in to shared preferences + */ + +public class SelectSensorDialog extends DialogFragment { + + protected boolean[] selections = new boolean[SupportedSensors.SUPPORTED_SENSOR_COUNT]; + Activity activity; + SensorListListener sensorListListener; + private Set selectedSensorSet = new HashSet<>(); + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle("Select Sensors"); + activity = getActivity(); + + SharedPreferences preferences = getActivity().getSharedPreferences(SupportedSensors.AVAILABLE_SENSORS, Context. + MODE_MULTI_PROCESS); + + Set set = preferences.getStringSet(SupportedSensors.GET_AVAILABLE_SENSORS, null); + final CharSequence[] sequence = getSequence(set); + + final boolean[] pos = new boolean[selections.length]; + final boolean[] neg = new boolean[selections.length]; + + builder.setMultiChoiceItems(sequence, selections, new DialogInterface.OnMultiChoiceClickListener() { + @Override + public void onClick(DialogInterface dialog, int which, boolean isChecked) { + if (isChecked) { + selectedSensorSet.add(sequence[which].toString()); + + pos[which] = true; + } else { + selectedSensorSet.remove(sequence[which].toString()); + neg[which] = true; + } + } + }); + + builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Log.d("Click", "Ok"); + //call sensorDataMap reading class + sensorListListener.onDialogPositiveClick(SelectSensorDialog.this); + + } + }); + + builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Log.d("Click", "Cancel"); + for (int i = 0; i < SupportedSensors.SUPPORTED_SENSOR_COUNT; i++) { + + if (pos[i]) + selections[i] = false; + if (neg[i]) + selections[i] = true; + } + } + }); + + return builder.create(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public void onDetach() { + super.onDetach(); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + try { + sensorListListener = (SensorListListener) getActivity(); + } catch (ClassCastException ex) { + throw new ClassCastException(activity.toString() + " must implement the SensorListener"); + } + } + + @Override + public void onStart() { + super.onStart(); + } + + /** + * Interface to be implemented by the parent + */ + public CharSequence[] getSequence(Set sensorset) { + CharSequence[] seq; + String[] seq2 = sensorset.toArray(new String[sensorset.size()]); + seq = Arrays.copyOf(seq2, seq2.length); + return seq; + } + + public Set getSet() { + return this.selectedSensorSet; + } + + public interface SensorListListener { + void onDialogPositiveClick(SelectSensorDialog dialog); + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/IVoiceControl.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/IVoiceControl.java new file mode 100644 index 000000000..83b702d45 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/IVoiceControl.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.speech.detector; + +/** + * This interface is used to retrieve the voice commands and restart the listenting service + */ +public interface IVoiceControl { + /** + * This will be executed when a voice command was found + * @param voiceCommands + */ + public abstract void processVoiceCommands(String... voiceCommands); + + /** + * This will be executed after a voice command was processed to keep the recognition service activated + */ + public void restartListeningService(); + + public void finish(); +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/WordRecognitionActivity.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/WordRecognitionActivity.java new file mode 100644 index 000000000..903b6c3a8 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/WordRecognitionActivity.java @@ -0,0 +1,132 @@ +package org.wso2.carbon.iot.android.sense.speech.detector; + +import android.content.Intent; +import android.os.Bundle; +import android.support.design.widget.FloatingActionButton; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import org.wso2.carbon.iot.android.sense.constants.SenseConstants; +import org.wso2.carbon.iot.android.sense.realtimeviewer.ActivitySelectSensor; +import org.wso2.carbon.iot.android.sense.speech.detector.util.ListeningActivity; +import org.wso2.carbon.iot.android.sense.speech.detector.util.ProcessWords; +import org.wso2.carbon.iot.android.sense.speech.detector.util.VoiceRecognitionListener; +import org.wso2.carbon.iot.android.sense.speech.detector.util.WordData; +import org.wso2.carbon.iot.android.sense.util.SenseDataHolder; + +import agent.sense.android.iot.carbon.wso2.org.wso2_senseagent.R; + +public class WordRecognitionActivity extends ListeningActivity { + Button setThreasholdButton; + Button addWordButton; + Button removeWordButton; + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_speech_sense_main); + context = getApplicationContext(); // Needs to be set + + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + VoiceRecognitionListener.getInstance().setListener(this); // Here we set the current listener + addListenerOnSetThreasholdButton(); + addListenerOnAddWordButton(); + addListenerOnRemoveWordButton(); + String sessionId = getIntent().getStringExtra("sessionId"); + ProcessWords.setSessionId(sessionId); + FloatingActionButton fbtnSpeechRecongnizer = (FloatingActionButton) findViewById(R.id.sensorChange); + fbtnSpeechRecongnizer.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + WordData wordData = new WordData(ProcessWords.getSessionId(), SenseConstants.EVENT_LISTENER_FINISHED, 1); + SenseDataHolder.getWordDataHolder().add(wordData); + stopListening(); + Intent intent = new Intent(getApplicationContext(), ActivitySelectSensor.class); + startActivity(intent); + } + }); + Long tsLong = System.currentTimeMillis() / 1000; + WordData wordData = new WordData(sessionId, SenseConstants.EVENT_LISTENER_STARTED, 1); + SenseDataHolder.getWordDataHolder().add(wordData); + startListening(); // starts listening + } + + @Override + public void onBackPressed() { + } + + @Override + public void processVoiceCommands(String... voiceCommands) { + if(voiceCommands==null || voiceCommands.length==0){ + return; + } + ProcessWords processWords = new ProcessWords(this); + processWords.execute(voiceCommands); + restartListeningService(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + // getMenuInflater().inflate(R.menu.menu_main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + // Handle action bar item clicks here. The action bar will + // automatically handle clicks on the Home/Up button, so long + // as you specify a parent activity in AndroidManifest.xml. + int id = item.getItemId(); + + //noinspection SimplifiableIfStatement + if (id == R.id.action_deEnroll) { + return true; + } + return super.onOptionsItemSelected(item); + } + + public void addListenerOnSetThreasholdButton() { + setThreasholdButton = (Button) findViewById(R.id.setThreshold); + setThreasholdButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View arg0) { + String thresholdString = ((EditText) findViewById(R.id.editThreashold)).getText().toString(); + try{ + ProcessWords.setThreshold(Integer.parseInt(thresholdString)); + } catch (NumberFormatException e) { + Toast.makeText(WordRecognitionActivity.this, "Invalid Threshold - " + thresholdString, Toast.LENGTH_SHORT).show(); + } + } + }); + } + + public void addListenerOnAddWordButton() { + addWordButton = (Button) findViewById(R.id.addWord); + addWordButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View arg0) { + String word = ((EditText) findViewById(R.id.wordText)).getText().toString(); + ProcessWords.addWord(word); + Toast.makeText(WordRecognitionActivity.this, word + " is added to the list", Toast.LENGTH_SHORT).show(); + } + }); + } + + public void addListenerOnRemoveWordButton() { + removeWordButton = (Button) findViewById(R.id.removeWord); + removeWordButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View arg0) { + String word = ((EditText) findViewById(R.id.wordText)).getText().toString(); + Toast.makeText(WordRecognitionActivity.this, word + " is removed from the list", Toast.LENGTH_SHORT).show(); + ProcessWords.removeWord(word); + } + + }); + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/ListeningActivity.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/ListeningActivity.java new file mode 100644 index 000000000..db37583b5 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/ListeningActivity.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.speech.detector.util; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.speech.RecognizerIntent; +import android.speech.SpeechRecognizer; +import android.widget.Toast; + +import org.wso2.carbon.iot.android.sense.speech.detector.IVoiceControl; + +/** + * This Activity Contols the Speech Recognizer Activity. + */ +public abstract class ListeningActivity extends Activity implements IVoiceControl { + + protected SpeechRecognizer sr; + protected Context context; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + /** + * Starts the listening service. + */ + protected void startListening() { + initSpeech(); + Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); + intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); + sr.startListening(intent); + } + + /** + * Stop the listening service. + */ + protected void stopListening() { + if (sr != null) { + sr.stopListening(); + sr.cancel(); + sr.destroy(); + } + sr = null; + } + + /** + * Initialize the speech. + */ + protected void initSpeech() { + if (sr == null) { + sr = SpeechRecognizer.createSpeechRecognizer(this); + if (!SpeechRecognizer.isRecognitionAvailable(context)) { + Toast.makeText(context, "Speech Recognition is not available", + Toast.LENGTH_LONG).show(); + finish(); + } + sr.setRecognitionListener(VoiceRecognitionListener.getInstance()); + } + } + + @Override + public void finish() { + stopListening(); + super.finish(); + } + + @Override + protected void onStop() { + stopListening(); + super.onStop(); + } + + @Override + protected void onDestroy() { + if (sr != null) { + sr.stopListening(); + sr.cancel(); + sr.destroy(); + } + super.onDestroy(); + } + + @Override + protected void onPause() { + if(sr!=null){ + sr.stopListening(); + sr.cancel(); + sr.destroy(); + + } + sr = null; + + super.onPause(); + } + + /** + * Is abstract so the inheriting classes need to implement it. Here you put your code which should be executed once + * a command was found + */ + @Override + public abstract void processVoiceCommands(String ... voiceCommands); + + @Override + public void restartListeningService() { + stopListening(); + startListening(); + } +} + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/ProcessWords.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/ProcessWords.java new file mode 100644 index 000000000..9933904a5 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/ProcessWords.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.speech.detector.util; + +import android.app.Activity; +import android.os.AsyncTask; +import android.widget.EditText; + +import org.apache.commons.codec.language.Soundex; +import org.wso2.carbon.iot.android.sense.event.streams.Location.LocationData; +import org.wso2.carbon.iot.android.sense.util.SenseDataHolder; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.tartarus.snowball.ext.englishStemmer; + +import agent.sense.android.iot.carbon.wso2.org.wso2_senseagent.R; + +/** + * This class process the words form required words with the recongnized words to check whether it matches with the + * certain threshold. + */ +public class ProcessWords extends AsyncTask { + private static volatile double threshold = 80; + private static volatile Map wordDataMap = new ConcurrentHashMap<>(); + private static String sessionId = "default"; + private static Soundex soundex = new Soundex(); + Activity activity; + + public ProcessWords(Activity activity) { + this.activity = activity; + } + + /** + * Add the list of words which are used for reference. + * @param wordlist that needs to be looked upon in the speech + */ + public static void addWords(List wordlist) { + for (String word : wordlist) { + if (!wordDataMap.keySet().contains(word) && !word.isEmpty()) { + wordDataMap.put(word, new WordData(sessionId, word, 0)); + } + } + } + + /** + * Process the recognized word list. + * @param voiceCommands word lists. + */ + private void processTexts(String... voiceCommands) { + for (String requiredWord : wordDataMap.keySet()) { + int maxOccurunce = 0; + for (String command : voiceCommands) { + int occurence = 0; + for (String word : command.split(" ")) { + if (StringSimilarity.similarity(requiredWord, word) > threshold) { + occurence++; + continue; + } + if (soundex.encode(requiredWord).equals(soundex.encode(word))) { + occurence++; + continue; + } + if (StringSimilarity.similarity(requiredWord, stem(word)) > threshold) { + occurence++; + continue; + } + } + if (maxOccurunce < occurence) { + maxOccurunce = occurence; + } + } + if (maxOccurunce > 0) { + WordData wordData = wordDataMap.get(requiredWord); + wordData.addOccurences(maxOccurunce); + wordDataMap.put(requiredWord, wordData); + } + } + } + + /** + * Check for distance between the referenced and recognized words. + * @param params + * @return + */ + @Override + protected String doInBackground(String... params) { + processTexts(params); + publishProgress(); + return ""; + } + + /** + * update it in the UI. + * @param values words list. + */ + @Override + protected void onProgressUpdate(Void... values) { + super.onProgressUpdate(values); + EditText content = (EditText) activity.findViewById(R.id.command); + String text = ""; + for (String key : ProcessWords.wordDataMap.keySet()) { + text = text + key + " - " + ProcessWords.wordDataMap.get(key).getOccurences() + "\n"; + } + content.setText(text); + EditText thresholdText = (EditText) activity.findViewById(R.id.editThreashold); + thresholdText.setText("" + threshold); + } + + /** + * set the threshold to determine the distance. + * @param threshold to determine the distance. + */ + public static synchronized void setThreshold(int threshold) { + ProcessWords.threshold = threshold; + } + + /** + * + * @param sessionId for each listening session. + */ + public static synchronized void setSessionId(String sessionId) { + ProcessWords.sessionId = sessionId; + } + + /** + * + * retrieve sessionId for each listening session. + */ + public static synchronized String getSessionId() { + return ProcessWords.sessionId; + } + + /** + * @param word that is used for refrerence. + */ + public static synchronized void addWord(String word) { + if (!wordDataMap.keySet().contains(word) && !word.isEmpty()) { + wordDataMap.put(word, new WordData(sessionId, word, 0)); + } + } + + /** + * + * @param word that needs to be removed from the reference list. + */ + public static synchronized void removeWord(String word) { + cleanAndPushToWordMap(); + wordDataMap.remove(word); + } + + /** + * clean in memory content and pubish it to the server. + */ + public static synchronized void cleanAndPushToWordMap() { + for (String word : wordDataMap.keySet()) { + WordData wordData = wordDataMap.get(word); + SenseDataHolder.getWordDataHolder().add(wordData); + wordDataMap.put(word, new WordData(sessionId, word, 0)); + } + } + + /** + * apply porter stem algorithm + * @param word to be stemmed. + * @return + */ + private static String stem(String word) + { + englishStemmer stemmer = new englishStemmer(); + stemmer.setCurrent(word); + boolean result = stemmer.stem(); + if (!result) + { + return word; + } + return stemmer.getCurrent(); + } + + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/StringSimilarity.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/StringSimilarity.java new file mode 100644 index 000000000..a44e304f4 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/StringSimilarity.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.speech.detector.util; + +/** + * Calculates the similarity of strings. + */ +public class StringSimilarity { + + /** + * Calculates the similarity (a number within 0 and 1) between two strings. + */ + public static double similarity(String s1, String s2) { + String longer = s1, shorter = s2; + if (s1.length() < s2.length()) { // longer should always have greater length + longer = s2; + shorter = s1; + } + int longerLength = longer.length(); + if (longerLength == 0) { + return 1.0; /* both strings are zero length */ + } + return ((longerLength - editDistance(longer, shorter)) / (double) longerLength) * 100; + } + + // Example implementation of the Levenshtein Edit Distance + // See http://rosettacode.org/wiki/Levenshtein_distance#Java + private static int editDistance(String s1, String s2) { + s1 = s1.toLowerCase(); + s2 = s2.toLowerCase(); + + int[] costs = new int[s2.length() + 1]; + for (int i = 0; i <= s1.length(); i++) { + int lastValue = i; + for (int j = 0; j <= s2.length(); j++) { + if (i == 0) + costs[j] = j; + else { + if (j > 0) { + int newValue = costs[j - 1]; + if (s1.charAt(i - 1) != s2.charAt(j - 1)) + newValue = Math.min(Math.min(newValue, lastValue), + costs[j]) + 1; + costs[j - 1] = lastValue; + lastValue = newValue; + } + } + } + if (i > 0) + costs[s2.length()] = lastValue; + } + return costs[s2.length()]; + } + + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/VoiceRecognitionListener.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/VoiceRecognitionListener.java new file mode 100644 index 000000000..52ce8d52f --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/VoiceRecognitionListener.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.speech.detector.util; + +import android.os.Bundle; +import android.speech.RecognitionListener; +import android.speech.SpeechRecognizer; +import org.wso2.carbon.iot.android.sense.speech.detector.IVoiceControl; +import java.util.ArrayList; + +/** + * This triggers android voice recognition listener. + */ +public class VoiceRecognitionListener implements RecognitionListener { + private static VoiceRecognitionListener instance = null; + + IVoiceControl listener; // This is your running activity (we will initialize it later) + + public static VoiceRecognitionListener getInstance() { + if (instance == null) { + instance = new VoiceRecognitionListener(); + } + return instance; + } + + private VoiceRecognitionListener() { } + + public void setListener(IVoiceControl listener) { + this.listener = listener; + } + + public void processVoiceCommands(String... voiceCommands) { + listener.processVoiceCommands(voiceCommands); + } + + // This method will be executed when voice commands were found + public void onResults(Bundle data) { + ArrayList matches = data.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); + String[] commands = new String[matches.size()]; + commands = matches.toArray(commands); + processVoiceCommands(commands); + } + + // User starts speaking + public void onBeginningOfSpeech() { + System.out.println("Starting to listen"); + } + + public void onBufferReceived(byte[] buffer) { } + + // User finished speaking + public void onEndOfSpeech() { + System.out.println("Waiting for result..."); + } + + // If the user said nothing the service will be restarted + public void onError(int error) { + if (listener != null) { + listener.restartListeningService(); + } + } + public void onEvent(int eventType, Bundle params) { } + + public void onPartialResults(Bundle partialResults) { } + + public void onReadyForSpeech(Bundle params) { } + + public void onRmsChanged(float rmsdB) { } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/WordData.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/WordData.java new file mode 100644 index 000000000..fb76eab24 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/speech/detector/util/WordData.java @@ -0,0 +1,47 @@ +package org.wso2.carbon.iot.android.sense.speech.detector.util; + +/** + * This defines the data structure of the word data. + */ +public class WordData { + /** + * timestamp for all the occurences + */ + private long timestamp; + private int occurences; + private String word; + private String sessionId; + + public WordData(String sessionId, String word, int occurences) { + this.timestamp = System.currentTimeMillis() / 1000; + this.occurences = occurences; + this.word = word; + this.sessionId = sessionId; + } + + public long getTimestamp() { + return timestamp; + } + + public int getOccurences() { + return occurences; + } + + public String getWord() { + return word; + } + + public String getSessionId() { + return sessionId; + } + + /** + * @param occurences for the word and then add the timestamp for each occurences. + */ + public void addOccurences(int occurences) { + this.occurences = this.occurences + occurences; + this.timestamp = System.currentTimeMillis() / 1000; + } + + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/LocalRegistry.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/LocalRegistry.java new file mode 100644 index 000000000..e9d3d430f --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/LocalRegistry.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import org.wso2.carbon.iot.android.sense.constants.SenseConstants; +import org.wso2.carbon.iot.android.sense.data.publisher.mqtt.transport.MQTTTransportHandler; + +import java.net.MalformedURLException; +import java.net.URL; + +/** + * This is used to store the values in either in memory or in shared preferences. + */ +public class LocalRegistry { + + private static final String SENSE_SHARED_PREFERENCES = "senseSharedPreferences"; + private static final String USERNAME_KEY = "usernameKey"; + private static final String DEVICE_ID_KEY = "deviceIdKey"; + private static final String SERVER_HOST_KEY = "serverHostKey"; + private static final String ACCESS_TOKEN_KEY = "accessTokenKey"; + private static final String REFRESH_TOKEN_KEY = "refreshTokenKey"; + private static final String MQTT_PORT_KEY = "mqttPort"; + private static boolean exists = false; + private static String username; + private static String deviceId; + private static String serverURL; + private static MQTTTransportHandler mqttTransportHandler; + private static String accessToken; + private static String refreshToken; + private static int mqttPort; + + public static boolean isExist(Context context) { + if (!exists) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + String username = sharedpreferences.getString(USERNAME_KEY, ""); + String deviceId = sharedpreferences.getString(DEVICE_ID_KEY, ""); + exists = (username != null && !username.isEmpty() && deviceId != null && !deviceId.isEmpty()); + } + return exists; + } + + public static void setExist(boolean status) { + exists = status; + } + + + public static void addUsername(Context context, String username) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(USERNAME_KEY, username); + editor.commit(); + LocalRegistry.username = username; + } + + public static String getUsername(Context context) { + if (LocalRegistry.username == null || username.isEmpty()) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + LocalRegistry.username = sharedpreferences.getString(USERNAME_KEY, ""); + } + return LocalRegistry.username; + } + + public static void removeUsername(Context context) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.clear(); + editor.remove(USERNAME_KEY); + editor.commit(); + LocalRegistry.username = null; + } + + public static void addDeviceId(Context context, String deviceId) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(DEVICE_ID_KEY, deviceId); + editor.commit(); + LocalRegistry.deviceId = deviceId; + } + + public static void removeDeviceId(Context context) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.remove(DEVICE_ID_KEY); + editor.clear(); + editor.commit(); + LocalRegistry.deviceId = null; + } + + public static String getDeviceId(Context context) { + if (LocalRegistry.deviceId == null || LocalRegistry.deviceId.isEmpty()) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + LocalRegistry.deviceId = sharedpreferences.getString(DEVICE_ID_KEY, ""); + } + return LocalRegistry.deviceId; + } + + public static void addServerURL(Context context, String host) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(SERVER_HOST_KEY, host); + editor.commit(); + LocalRegistry.serverURL = host; + } + + public static void removeServerURL(Context context) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.remove(SERVER_HOST_KEY); + editor.clear(); + editor.commit(); + LocalRegistry.serverURL = null; + } + + public static String getServerURL(Context context) { + if (LocalRegistry.serverURL == null || LocalRegistry.serverURL.isEmpty()) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + LocalRegistry.serverURL = sharedpreferences.getString(SERVER_HOST_KEY, ""); + } + return LocalRegistry.serverURL; + } + + public static void addAccessToken(Context context, String accessToken) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(ACCESS_TOKEN_KEY, accessToken); + editor.commit(); + LocalRegistry.accessToken = accessToken; + } + + public static void removeAccessToken(Context context) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.remove(ACCESS_TOKEN_KEY); + editor.clear(); + editor.commit(); + LocalRegistry.accessToken = null; + } + + public static String getAccessToken(Context context) { + if (LocalRegistry.accessToken == null || LocalRegistry.accessToken.isEmpty()) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + LocalRegistry.accessToken = sharedpreferences.getString(ACCESS_TOKEN_KEY, ""); + } + return LocalRegistry.accessToken; + } + + public static void addRefreshToken(Context context, String refreshToken) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putString(REFRESH_TOKEN_KEY, refreshToken); + editor.commit(); + LocalRegistry.refreshToken = refreshToken; + } + + public static void removeRefreshToken(Context context) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.remove(REFRESH_TOKEN_KEY); + editor.clear(); + editor.commit(); + LocalRegistry.refreshToken = null; + } + + public static String getRefreshToken(Context context) { + if (LocalRegistry.refreshToken == null || LocalRegistry.refreshToken.isEmpty()) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + LocalRegistry.refreshToken = sharedpreferences.getString(REFRESH_TOKEN_KEY, ""); + } + return LocalRegistry.refreshToken; + } + + public static void addMqttPort(Context context, int port) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.putInt(MQTT_PORT_KEY, port); + editor.commit(); + LocalRegistry.mqttPort = port; + } + + public static void removeMqttPort(Context context) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedpreferences.edit(); + editor.remove(MQTT_PORT_KEY); + editor.clear(); + editor.commit(); + LocalRegistry.mqttPort = 0; + } + + public static int getMqttPort(Context context) { + if (LocalRegistry.mqttPort == 0) { + SharedPreferences sharedpreferences = context.getSharedPreferences(SENSE_SHARED_PREFERENCES, Context.MODE_PRIVATE); + LocalRegistry.mqttPort = sharedpreferences.getInt(MQTT_PORT_KEY, SenseConstants.MQTT_BROKER_PORT); + } + return LocalRegistry.mqttPort; + } + + public static String getServerHost(Context context) { + + URL url = null; + String urlString = getServerURL(context); + try { + url = new URL(urlString); + return url.getHost(); + } catch (MalformedURLException e) { + Log.e("Host ", "Invalid urlString :" + urlString); + return null; + } + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseClient.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseClient.java new file mode 100644 index 000000000..173525b82 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseClient.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.util; + + +import android.content.Context; +import android.os.Build; +import android.util.Log; +import android.widget.Toast; + +import org.wso2.carbon.iot.android.sense.constants.SenseConstants; + +import java.util.Map; +import java.util.concurrent.ExecutionException; + +/** + * This Client is used for http communication with the server. + */ +public class SenseClient { + private final static String TAG = "SenseService Client"; + + private Context context; + + public SenseClient(Context context) { + this.context = context; + } + + /** + * Enroll the device. + * + * @param username + * @param password + * @param deviceId + * @return + */ + public boolean register(String username, String password, String deviceId) { + Map response = registerWithTimeWait(username, password, deviceId); + String responseStatus = response.get("status"); + if (responseStatus.trim().contains(SenseConstants.Request.REQUEST_SUCCESSFUL)) { + Toast.makeText(context, "Device Registered", Toast.LENGTH_LONG).show(); + return true; + } else if (responseStatus.trim().contains(SenseConstants.Request.REQUEST_CONFLICT)) { + Toast.makeText(context, "Login Successful", Toast.LENGTH_LONG).show(); + return true; + } else { + Toast.makeText(context, "Authentication failed, please check your credentials and try again.", Toast + .LENGTH_LONG).show(); + + return false; + } + } + + public Map registerWithTimeWait(String username, String password, String deviceId) { + for (int i = 1; i <= SenseConstants.Request.MAX_ATTEMPTS; i++) { + Log.d(TAG, "Attempt #" + i + " to register"); + try { + SenseClientAsyncExecutor senseClientAsyncExecutor = new SenseClientAsyncExecutor(context); + String endpoint = LocalRegistry.getServerURL(context); + senseClientAsyncExecutor.execute(username, password, deviceId, endpoint); + Map response = senseClientAsyncExecutor.get(); + if (response != null) { + return response; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Log.e("Send Sensor Data", "Thread Interruption for endpoint " + LocalRegistry.getServerURL(context)); + } catch (ExecutionException e) { + Log.e("Send Sensor Data", "Failed to push data to the endpoint " + LocalRegistry.getServerURL(context)); + } + } + return null; + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseClientAsyncExecutor.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseClientAsyncExecutor.java new file mode 100644 index 000000000..56842f6b2 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseClientAsyncExecutor.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.util; + + +import android.content.Context; +import android.os.AsyncTask; +import android.os.Build; +import android.util.Log; + +import org.wso2.carbon.iot.android.sense.constants.SenseConstants; +import org.wso2.carbon.iot.android.sense.util.dto.AccessTokenInfo; +import org.wso2.carbon.iot.android.sense.util.dto.AndroidSenseManagerService; +import org.wso2.carbon.iot.android.sense.util.dto.ApiApplicationRegistrationService; +import org.wso2.carbon.iot.android.sense.util.dto.ApiRegistrationProfile; +import org.wso2.carbon.iot.android.sense.util.dto.DynamicClientRegistrationService; +import org.wso2.carbon.iot.android.sense.util.dto.OAuthApplicationInfo; +import org.wso2.carbon.iot.android.sense.util.dto.OAuthRequestInterceptor; +import org.wso2.carbon.iot.android.sense.util.dto.RegistrationProfile; +import org.wso2.carbon.iot.android.sense.util.dto.TokenIssuerService; + +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import feign.Client; +import feign.Feign; +import feign.FeignException; +import feign.auth.BasicAuthRequestInterceptor; +import feign.jackson.JacksonDecoder; +import feign.jackson.JacksonEncoder; +import feign.jaxrs.JAXRSContract; + +public class SenseClientAsyncExecutor extends AsyncTask> { + + private final static String TAG = "SenseService Client"; + private static final String STATUS = "status"; + private final static String DEVICE_NAME = Build.MANUFACTURER + " " + Build.MODEL; + private Context context; + + public SenseClientAsyncExecutor(Context context) { + this.context = context; + + } + + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + return null; + } + public void checkClientTrusted( + java.security.cert.X509Certificate[] certs, String authType) { + } + public void checkServerTrusted( + java.security.cert.X509Certificate[] certs, String authType) { + } + } + }; + + Client disableHostnameVerification = new Client.Default(getTrustedSSLSocketFactory(), new HostnameVerifier() { + @Override + public boolean verify(String s, SSLSession sslSession) { + return true; + } + }); + + @Override + protected Map doInBackground(String... parameters) { + if (android.os.Debug.isDebuggerConnected()) + android.os.Debug.waitForDebugger(); + String response; + Map response_params = new HashMap<>(); + String username = parameters[0]; + String password = parameters[1]; + String deviceId = parameters[2]; + String endpoint = parameters[3]; + Map responseMap = new HashMap<>(); + responseMap.put(STATUS, "200"); + try { + //DynamicClientRegistraiton. + DynamicClientRegistrationService dynamicClientRegistrationService = Feign.builder() + .client(disableHostnameVerification).contract(new + JAXRSContract()).encoder(new JacksonEncoder()).decoder(new JacksonDecoder()) + .target(DynamicClientRegistrationService.class, endpoint + SenseConstants.DCR_CONTEXT); + RegistrationProfile registrationProfile = new RegistrationProfile(); + String applicationName = "android-sense:" + deviceId; + registrationProfile.setOwner(username); + registrationProfile.setClientName(applicationName); + registrationProfile.setCallbackUrl(""); + registrationProfile.setGrantType("password refresh_token client_credentials"); + registrationProfile.setApplicationType("device"); + registrationProfile.setTokenScope("production"); + OAuthApplicationInfo oAuthApplicationInfo = dynamicClientRegistrationService.register(registrationProfile); + + //PasswordGrantType + TokenIssuerService tokenIssuerService = Feign.builder().client(disableHostnameVerification).requestInterceptor( + new BasicAuthRequestInterceptor(oAuthApplicationInfo.getClient_id(), oAuthApplicationInfo.getClient_secret())) + .contract(new JAXRSContract()).encoder(new JacksonEncoder()).decoder(new JacksonDecoder()) + .target(TokenIssuerService.class, endpoint + SenseConstants.TOKEN_ISSUER_CONTEXT); + AccessTokenInfo accessTokenInfo = tokenIssuerService.getToken("password", username, password); + + //ApiApplicationRegistration + ApiApplicationRegistrationService apiApplicationRegistrationService = Feign.builder().client(disableHostnameVerification) + .requestInterceptor(new OAuthRequestInterceptor(accessTokenInfo.getAccess_token())) + .contract(new JAXRSContract()).encoder(new JacksonEncoder()).decoder(new JacksonDecoder()) + .target(ApiApplicationRegistrationService.class, endpoint + SenseConstants.API_APPLICATION_REGISTRATION_CONTEXT); + ApiRegistrationProfile apiRegistrationProfile = new ApiRegistrationProfile(); + apiRegistrationProfile.setApplicationName(applicationName); + apiRegistrationProfile.setConsumerKey(oAuthApplicationInfo.getClient_id()); + apiRegistrationProfile.setConsumerSecret(oAuthApplicationInfo.getClient_secret()); + apiRegistrationProfile.setIsAllowedToAllDomains(false); + apiRegistrationProfile.setIsMappingAnExistingOAuthApp(true); + apiRegistrationProfile.setTags(new String[]{SenseConstants.DEVICE_TYPE}); + String replyMsg = apiApplicationRegistrationService.register(apiRegistrationProfile); + + //DeviceRegister + AndroidSenseManagerService androidSenseManagerService = Feign.builder().client(disableHostnameVerification) + .requestInterceptor(new OAuthRequestInterceptor(accessTokenInfo.getAccess_token())) + .contract(new JAXRSContract()).encoder(new JacksonEncoder()).decoder(new JacksonDecoder()) + .target(AndroidSenseManagerService.class, "https://192.168.56.1:8243" + SenseConstants.REGISTER_CONTEXT); + boolean registered = androidSenseManagerService.register(deviceId, DEVICE_NAME); + if (registered) { + LocalRegistry.addAccessToken(context, accessTokenInfo.getAccess_token()); + LocalRegistry.addRefreshToken(context, accessTokenInfo.getRefresh_token()); + } + return responseMap; + } catch (FeignException e) { + responseMap.put(STATUS, "" + e.status()); + return responseMap; + } + } + + private SSLSocketFactory getTrustedSSLSocketFactory() { + try { + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, trustAllCerts, new java.security.SecureRandom()); + return sc.getSocketFactory(); + } catch (KeyManagementException | NoSuchAlgorithmException e) { + Log.e(SenseClientAsyncExecutor.class.getName(), "Invalid Certificate"); + return null; + } + + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseDataHolder.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseDataHolder.java new file mode 100644 index 000000000..463cf8ab9 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseDataHolder.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.util; + +import org.wso2.carbon.iot.android.sense.event.streams.Location.LocationData; +import org.wso2.carbon.iot.android.sense.event.streams.Sensor.SensorData; +import org.wso2.carbon.iot.android.sense.event.streams.battery.BatteryData; +import org.wso2.carbon.iot.android.sense.speech.detector.util.WordData; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * This holds the sensor,battery and location data inmemory. + */ +public class SenseDataHolder { + + private static List sensorDataHolder; + private static List batteryDataHolder; + private static List locationDataHolder; + private static List wordDataHolder; + + public static List getSensorDataHolder(){ + if(sensorDataHolder == null){ + sensorDataHolder = new CopyOnWriteArrayList<>(); + } + return sensorDataHolder; + } + + public static List getBatteryDataHolder(){ + if(batteryDataHolder == null){ + batteryDataHolder = new CopyOnWriteArrayList<>(); + } + return batteryDataHolder; + } + + public static List getLocationDataHolder(){ + if(locationDataHolder == null){ + locationDataHolder = new CopyOnWriteArrayList<>(); + } + return locationDataHolder; + } + + public static List getWordDataHolder(){ + if(wordDataHolder == null){ + wordDataHolder = new CopyOnWriteArrayList<>(); + } + return wordDataHolder; + } + + public static void resetSensorDataHolder(){ + sensorDataHolder = null; + } + + public static void resetBatteryDataHolder(){ + batteryDataHolder = null; + } + + public static void resetLocationDataHolder(){ + locationDataHolder = null; + } + + public static void resetWordDataHolder() { + wordDataHolder = null; + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseUtils.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseUtils.java new file mode 100644 index 000000000..956e780de --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseUtils.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.util; + + +import android.content.ContentResolver; +import android.content.Context; +import android.telephony.TelephonyManager; + +import java.util.UUID; + +public class SenseUtils { + + /** + * this generate the device Id + * + * @param baseContext + * @param contentResolver + * @return + */ + //http://stackoverflow.com/questions/2785485/is-there-a-unique-android-device-id + public static String generateDeviceId(Context baseContext, ContentResolver contentResolver) { + + final TelephonyManager tm = (TelephonyManager) baseContext.getSystemService(Context.TELEPHONY_SERVICE); + + final String tmDevice, tmSerial, androidId; + tmDevice = String.valueOf(tm.getDeviceId()); + tmSerial = String.valueOf(tm.getSimSerialNumber()); + androidId = String.valueOf(android.provider.Settings.Secure.getString(contentResolver, android.provider.Settings.Secure.ANDROID_ID)); + + UUID deviceUuid = new UUID(androidId.hashCode(), ((long) tmDevice.hashCode() << 32) | tmSerial.hashCode()); + return deviceUuid.toString(); + + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseWakeLock.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseWakeLock.java new file mode 100644 index 000000000..1a6a36bbb --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/SenseWakeLock.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and limitations under the License. + * + */ +package org.wso2.carbon.iot.android.sense.util; + + +import android.content.Context; +import android.os.PowerManager; +import android.util.Log; + +/** + * It doesnt let the phone goto sleep. + */ +public class SenseWakeLock { + + private static PowerManager.WakeLock wakeLock; + private static String TAG = "Wake Lock"; + + public static void acquireWakeLock(Context context) { + Log.i(SenseWakeLock.class.getSimpleName(), "Acquire CPU wakeup lock start"); + if (wakeLock == null) { + Log.i(TAG, "CPU wakeUp log is not null"); + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "SenseWakeLock"); + } + wakeLock.acquire(); + } + + public static void releaseCPUWakeLock() { + if (wakeLock != null) { + wakeLock.release(); + wakeLock = null; + } + Log.i(TAG, "Release wakeup"); + } + +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/AccessTokenInfo.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/AccessTokenInfo.java new file mode 100644 index 000000000..65170970b --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/AccessTokenInfo.java @@ -0,0 +1,40 @@ +package org.wso2.carbon.iot.android.sense.util.dto; + +public class AccessTokenInfo { + public String token_type; + public String expires_in; + public String refresh_token; + public String access_token; + + public String getToken_type() { + return token_type; + } + + public void setToken_type(String token_type) { + this.token_type = token_type; + } + + public String getExpires_in() { + return expires_in; + } + + public void setExpires_in(String expires_in) { + this.expires_in = expires_in; + } + + public String getRefresh_token() { + return refresh_token; + } + + public void setRefresh_token(String refresh_token) { + this.refresh_token = refresh_token; + } + + public String getAccess_token() { + return access_token; + } + + public void setAccess_token(String access_token) { + this.access_token = access_token; + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/AndroidSenseManagerService.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/AndroidSenseManagerService.java new file mode 100644 index 000000000..0cbf26bd2 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/AndroidSenseManagerService.java @@ -0,0 +1,13 @@ +package org.wso2.carbon.iot.android.sense.util.dto; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; + +public interface AndroidSenseManagerService { + + @Path("devices/{device_id}") + @POST + boolean register(@PathParam("device_id") String deviceId, @QueryParam("deviceName") String deviceName); +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/ApiApplicationRegistrationService.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/ApiApplicationRegistrationService.java new file mode 100644 index 000000000..e8cd26148 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/ApiApplicationRegistrationService.java @@ -0,0 +1,25 @@ +package org.wso2.carbon.iot.android.sense.util.dto; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +/** + * This is the application registration service that exposed for apimApplicationRegistration + */ + +@Path("/register") +public interface ApiApplicationRegistrationService { + + /** + * This method is used to register api application + * + * @param registrationProfile contains the necessary attributes that are needed in order to register an app. + */ + @POST + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + String register(ApiRegistrationProfile registrationProfile); +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/ApiRegistrationProfile.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/ApiRegistrationProfile.java new file mode 100644 index 000000000..433ef707b --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/ApiRegistrationProfile.java @@ -0,0 +1,64 @@ +package org.wso2.carbon.iot.android.sense.util.dto; + + +/** + * This class represents the data that are required to register + * the oauth application. + */ +public class ApiRegistrationProfile { + + public String applicationName; + public String tags[]; + public boolean isAllowedToAllDomains; + public String consumerKey; + public String consumerSecret; + public boolean isMappingAnExistingOAuthApp; + + public String getApplicationName() { + return applicationName; + } + + public void setApplicationName(String applicationName) { + this.applicationName = applicationName; + } + + public String[] getTags() { + return tags; + } + + public void setTags(String[] tags) { + this.tags = tags; + } + + public boolean isAllowedToAllDomains() { + return isAllowedToAllDomains; + } + + public void setIsAllowedToAllDomains(boolean isAllowedToAllDomains) { + this.isAllowedToAllDomains = isAllowedToAllDomains; + } + + public boolean isMappingAnExistingOAuthApp() { + return isMappingAnExistingOAuthApp; + } + + public void setIsMappingAnExistingOAuthApp(boolean isMappingAnExistingOAuthApp) { + this.isMappingAnExistingOAuthApp = isMappingAnExistingOAuthApp; + } + + public String getConsumerKey() { + return consumerKey; + } + + public void setConsumerKey(String consumerKey) { + this.consumerKey = consumerKey; + } + + public String getConsumerSecret() { + return consumerSecret; + } + + public void setConsumerSecret(String consumerSecret) { + this.consumerSecret = consumerSecret; + } +} \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/DynamicClientRegistrationService.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/DynamicClientRegistrationService.java new file mode 100644 index 000000000..aa409673a --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/DynamicClientRegistrationService.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * you may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.wso2.carbon.iot.android.sense.util.dto; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/register") +public interface DynamicClientRegistrationService { + + /** + * This method is used to register an Oauth application. + * + * @param profile contains the necessary attributes that are + * needed in order to register an app. + * @return Status 200 if success including consumerKey and consumerSecret. + */ + @POST + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + OAuthApplicationInfo register(RegistrationProfile profile); +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/OAuthApplicationInfo.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/OAuthApplicationInfo.java new file mode 100644 index 000000000..4bf5bf354 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/OAuthApplicationInfo.java @@ -0,0 +1,44 @@ +package org.wso2.carbon.iot.android.sense.util.dto; + +/** + * This class represents an OAuth application populated with necessary data. + */ +public class OAuthApplicationInfo { + + public String client_id; + public String client_name; + public String callback_url; + public String client_secret; + + public String getClient_id() { + return client_id; + } + + public void setClient_id(String client_id) { + this.client_id = client_id; + } + + public String getClient_name() { + return client_name; + } + + public void setClient_name(String client_name) { + this.client_name = client_name; + } + + public String getCallback_url() { + return callback_url; + } + + public void setCallback_url(String callback_url) { + this.callback_url = callback_url; + } + + public String getClient_secret() { + return client_secret; + } + + public void setClient_secret(String client_secret) { + this.client_secret = client_secret; + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/OAuthRequestInterceptor.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/OAuthRequestInterceptor.java new file mode 100644 index 000000000..32aac77c9 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/OAuthRequestInterceptor.java @@ -0,0 +1,26 @@ +package org.wso2.carbon.iot.android.sense.util.dto; + + +import feign.RequestInterceptor; +import feign.RequestTemplate; + +import static feign.Util.checkNotNull; + +public class OAuthRequestInterceptor implements RequestInterceptor { + + private final String headerValue; + + /** + * Creates an interceptor that authenticates all requests with the specified OAUTH token + * + * @param token the access token to use for authentication + */ + public OAuthRequestInterceptor(String token) { + checkNotNull(token, "access_token"); + headerValue = "Bearer " + token; + } + @Override + public void apply(RequestTemplate template) { + template.header("Authorization", headerValue); + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/RegistrationProfile.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/RegistrationProfile.java new file mode 100644 index 000000000..046c112a3 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/RegistrationProfile.java @@ -0,0 +1,67 @@ +package org.wso2.carbon.iot.android.sense.util.dto; + + +/** + * This class represents the data that are required to register + * the oauth application. + */ +public class RegistrationProfile { + + public String callbackUrl; + public String clientName; + public String tokenScope; + public String owner; + public String grantType; + public String applicationType; + + private static final String TAG = RegistrationProfile.class.getSimpleName(); + + public String getCallbackUrl() { + return callbackUrl; + } + + public void setCallbackUrl(String callBackUrl) { + this.callbackUrl = callBackUrl; + } + + public String getClientName() { + return clientName; + } + + public void setClientName(String clientName) { + this.clientName = clientName; + } + + public String getTokenScope() { + return tokenScope; + } + + public void setTokenScope(String tokenScope) { + this.tokenScope = tokenScope; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + + public String getGrantType() { + return grantType; + } + + public void setGrantType(String grantType) { + this.grantType = grantType; + } + + public String getApplicationType() { + return applicationType; + } + + public void setApplicationType(String applicationType) { + this.applicationType = applicationType; + } + +} \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/TokenIssuerService.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/TokenIssuerService.java new file mode 100644 index 000000000..570aeda7f --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/java/org/wso2/carbon/iot/android/sense/util/dto/TokenIssuerService.java @@ -0,0 +1,16 @@ +package org.wso2.carbon.iot.android.sense.util.dto; + +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; + +@Path("/token") +public interface TokenIssuerService { + + @POST + @Produces(MediaType.APPLICATION_JSON) + AccessTokenInfo getToken(@QueryParam("grant_type") String grant, @QueryParam("username") String username, + @QueryParam("password") String password); +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/drawable/mic.png b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/drawable/mic.png new file mode 100644 index 0000000000000000000000000000000000000000..c7996102950c849c7cae6f05111fd6b6eddf7ea8 GIT binary patch literal 26775 zcmX6kWmuG5)AugDbax{l(nv|e3QH)plyoB@DILqBbhm(nbV`aKwMt2sN-2$WgEV|M z@Ardixz5g+J~MM>&PHiHS0RMa!2kdtR7Jpb000X93k7ho!H*N)AD7?EP(tpp7#E6ea#ZqeX5=6tMaaLIi z;@=cTMh{#q1r$2{YCHQoeEVricD?F2r(qNajZ2V@5YZMz5tJ)3kuJXJ>+W4z<(ChI zakv2_xD7U}o;U0e;5=Ab`Vku%rvrfaPU7PN=x5nzL57Gs;w{BY6A0=p1nre1p^l59 z0ObAS6-xklWe6(g9j7jkj|o@|T3f9ET6};7SIFN<0F`r|82|x{Q&}k?`AGl^xospI zaF7D3M)jkg0)~PBg`Il0Brwef2&x*`A%NP?0D6Q3zaGFP00gxo!yW>d0f5DKc6MLj zZ6-jWd}JVf_~;|aIuEF+%zEh-4#}sX#<+a$I0gp%Oxz=?)Q>2{ETNWJ^8DZYGHHcF z9}_Pg4*@`a5+x|@@r~~Y(Z`VyvG^t;bH1%k>^n9qtCice(MmUY09f)09>3)kXrMz$ zVWV7b3m)uYI+)?-zdnnxt0$KK1mthd8XUXb>*nc)#Fm+vwbj*WwQfaY%K?L+Tl;V3 zXoDN)i(u*Nv(v?nWsXo`(@+)6^TlsNCmO{Jqse%0t$zPaP`Un$e|OC|$=0oE)ndp) zv7$}p9uLooJr*uzi&9K}*z;iG&SrUm_Xa!jxx{H;ppp?L%3qSCz5~MlU5B@+2&IPBYs>r~_@vZQCQkQOxd4C6#!DzGfhs?H z%%hP`CRXgj`dFm{@oa*uQlB7tg9b;W+gXsmw@a~3f)_8vda#bIilDNWu2#Paqc!T( zdbG|5lPvTp8cz(*xMmwU6{PF87$>BFts_Mnn1!rZ3W-n>L#UhsK`PhoBTO(WR+Wq@7=9zS7pqf^SXR;LFSEL|7UAI#jfqvu=1M(3_|A z-3?=Fj7&xO2T3InH4%dbo#RwHIh>Vu?&t5lp5v`^mu4IXN6%C2KU{uDU-DR^Mab5~ zsUkfi-LMi-sa>hE-7fZVw=}1u{b{SwRVa}{pBk45mx_JD2v*Lz?u_oAQdlX~Gnpr+ zANebsis?VJ!hJelmF~UE?Jz^-YPgop`?arMG6V_SAa7hQOHWk@46z#sII!mkMrfpa zMBQDb83p?04z-m`yX2LMEz>MBmfj|{j7t~(RE|?7j(yo{vcj?A((BS&$cfF#%BlWr z>)BqG&a;-1hb2-PQW|~3L&F8bzcNL$WrU`L*R$8Nr?MA5TfFdn!P`{U)NHF-zxtxY zXs@a5RoDwjqhdq77p4u$)j8D~dn;=D&pjWz_EOqYg1!XB-N@h&M$8aZGQMC8_H6W^c%uJgK5OLto?KARN&2RM zz2PXy=(Na66Zzo7?w2{b*^?^v)b=f&Z;m5lqSXHz zG$*?u``d2WT+N=?-pjp+-+4oi^B(3JiFl56eOFr4UK^Lg&r1_@TBr)vRMHH!|Fq67 zU@eejP(4-qx!9-T%yOSv_Mfar-iqvswD`R6ypj2kIQsMV&x4v>uDRx63rZpEcQ$wa zcjG`Q>H)+aBPvuO)EIho?jOk7887!_&h}sZ%&tOgev1O7Lc)L5|E+zkW_9?!lUU7q zw*GD;Y2+nQm{#z{uQ43Uzm_pw!311k5n%@5a`A4FhIHdX8KOSpzm+@};Fs`p_W16no{|v2eY69I>3UjK2~< z{Z8P!shi)2k#8qqrBvnoWV}z(jY zTFHcm>G=_Y=b3-9{Ho|r)a>iPe_uaAoVM}xU~6GP+rOuFO*IV#j21pwUWVt; z%Th)~f25eB&Y%pL=Kk%wMjzoi(3$x=wXwXiyTxYIZo+Q!?>t|wF;RL&#@`I0SA;XO zjf4yOjk~A+p3Vtm40F%5zG+fwDeRPPZnG`z9~qXCLlz-Vke5iYy@@}cET_(2jqlM1 zwEcU$^K;I)?d(Rjzhh1hqWA2l-A~&q)vJaxxsOp5o9F3QE~eGbUEFv3FeWf=aM+2O zrdoddWSo#e>LmPnw5>wa$J}w7v+u)4C`PCk_r79P2G3zKxIa@dD%Gk>Ii{_B*_^+*b|7(>wX5#mcrH%a5j*0QjaRGZX@$x*Y{QT^N+xk0l*R?QQ0 zmo2X?>D!b^&A!(6tyv#iOq^Qw{M$bT*qv?C=ZcT~IcfJhq`Ld{GgngPPy4aI>sim) z=>pZCU=OaFk-^p(AFjRXHqAEk6Zu_*8KplFKjn;Xe^2lK4sk^LAueOqul_>r1QAo%>}_SDGuyYUYh zM;R%dE`h~2Tj*`tnz`O@y-Xebz9K?qC;C^rUwwZ2Fzy#F5NoamvV?HoIb778>W-CO zrO>9N++kw8JHECS;hO{NO%`i}&NBe;e*gfeHvn*P2Yzk>z-vJO*s=hCC+`4&+BMm{ zM;QQ2zp26%kiN4!e|+AXs#yLzTo`(I_G&?IzbARk%uQhNKbKf{$YcO+P9h0Q5unP= ziiJxe$k8URs0d{(dGpl*jiTrGNNIJhtZJN4n>BNlvY(e5G4*p%pKYz=`z&o9!e1X& zWICMb`8QKicZjj(MvzUXX+mCR48{w*K>77((vKmpFG`k6Kb~*v_GmI%gyx?w6i1@Q zmpc=nEz1d+%l5VBn^>38d1m?a8v?h}_yp8)&okWZ-X9KYmXX*D1X6~6f3HeO|F5TK z)WZU+_RIPHadcdOQ=N8AWn$-)p#=?hcqmgo`81IiAQgSr-bzCej--TB0Pu>qhhlq= zVhFGNEeLBro-g2s{8^M-}cub?C23I8ealm3SS!&iAcS`dw5UFmvtZHAQE-w zGxEaD0uePGhPm`8^}d^H!2=s10%s@_H}+fa%cm7z$L!w{EE7tTGCLpA#q#D zx#tbKV|kruQEZVZ*#Or+hy_Hg=v1e2C!KsY)|WEWmND>4=ei1K1bbe+Uf5E^P0H=j z!QArq|Dh$sRps1}A#To}J{K^k`|Q* z_G?0p!Q-MPQH)W(U|E74$R1`e!F9Mz{fB!N!z80u8E^4b!%-P!2pecj=%biC?d1Nq z;uLac`7(HLhS%g+%#+L>cSnEzWURO#X~s$qQ&>GK zR6LnC^OC7QAXxO4t$({lGigWcTqya55A^Gzi#7uwj<|9s(?2<1a)Rp(*^L&j$XP~4|pCQ#eWJwQOn`xlI zL?)r>Jp+#?qzp5Ojzi@DGh+fEG#YO6BF;td1haqh?H#ssnN?Dk<(}_ugdR*-@8#k! z8fSbqC}}1Y(m@ayzUs!7q&oQskbED0_dI#&p5=xR;*JcPjISYMGTbmgIGgaUGlkcj zEUBddWrO)#L7oDz=`~RhuG1v!MEK!^o>6yw0d>ca-hg5TRw}Z~y>i9ggUY_;gz*4~XGV%1v(v<(DS8ZSqA!^a&Nymr zMzq|y0mN(L=k-AwoXL~DaQFy^(eCl1MQO?iCnR)C$~pQe+BG`o zZ7~lSXskf46oSJWdjC-YHuBGS`z)Ev2R#4V^zkzl{oS!11)DJ5V5lpGIfvw)F=%>4 z2ApexbDpu1Ws;j-)fNBAaTD+99jALuzgz|BGOE372pmhkbIXhA7HWF6y@Lw z4We_%;kh#aiF2~04qX@&{GWg~a5x}`gW5N>4TQ1K(x5sH=< zPd@-GBI{PkKCYv>+K~uaBaM2UMGX0}We|L`f$H3>?$Ep(6hN3Fu}bj^ezRf^xFKTS zF}Yt%04p|H2|n;UisKl1t6c~IeMgkF|EOcX@QozAsGerA24$WYYY`1arIv--Fu&s4 zCr?uIh8!F(m!Y_U@0js8!7+9}q7kpBf!_qzq0x$<{r5hhpP^s!z(~v4hS4F=JQd@# zN5J>VDoC)J$8`8}5QF0>lLfn5+C6l#=#4l;KnG--a2mKKIoG8OgUa)@M%tuz{`wb# z;1fc30wL9)KFXf%V+UjNYJre(mr}_Fd0Eu?fEIb+B0idWoRgM~tp+O?tI7a04zgj5 zL(1kRDq0b4(TmoqQA->)pdI>UY?Oc@sW9U_I0!;vj1c*ocgZJ}?17l_fmPDYwp z_UzJ_h48vKT8SM<+e<*$T~;1alRg|a2k!8HRB}@QJU_Wm0w0p7NR#^gMcrhc5IlqA ze|w9b3MH=OhQmvCq!*{qLOd`ml-d~V8t_d4IasY_)FptF-tQim;YpUc`BYsK`TfL5 ztv=|0@*dQ~i?`p7YQ5|p!5`t|@t@-VgYC}*qZ~Wvk8C2nIJb2wxj)o#k^-=6JiI%z3L_^?Itq}jG}Q&P z*2qZ5Jzr1&$Z|N`anr9_X_^8cnj8Fi9utW)!9C}?=VPgv?7V(J6<)M_O?z<06aHqB zr2nfac3{S;gE=W_ximAB4s}++{TQlMfsfiR0~K5D8v+Yd>;bJ9J*nz_?gW}P5*bE} z5mIiY1xTb?qnIHspcf@o9BHDZkOVLQr@jD8p?ddV#UWT9b%eNq*eRF^K0`SuLHU;0 z{v$BZz_0)ma8Bjc1WI4D#fKe?`@RfdF=sV8;$ma_WC`iF*{6g7pSGD1-mzq)Nwf7I zX`oq@fa7Q@ig=3&#A8&7afjgxf~>$6>3#;0CT00NIPk+l%^r{~W`D{$hr?Hm<_QCJ zAo*YTpdD(2oyVl4Nf?*-iym00tyPQ#mdu-nY-~pw3qZU9P$rv)!LN4%dd!=5IM?Qf zDp)9mk{;qW4g3|)*AXr&AwonVu2r*&!shjT7r zaP;9C@O4Jty{Zg#EmRHB=`z`2@otuQe)~t#P3xLj5ZikAHe}&$q1Qs=a2@VA#f7To zSnmN(LgF0`ukKCS?8{w7{^%%tp>CoTUZdh5NZis93x$2!DO18RelsRA1OHmdbZRh_j9M@2Q2P|+iDkn2EgW)M zNj>I$`Q&fTm-tIj{I8etnA)*1%b9+Lh8P`v)4!JjM}ap9-eaC;RMTM^SDl=c^9)(R)Se?n@D_-`wbc0lt6FMg#6n<>U@&nYqZji#cys< zO`~m|@s{(FeTn;f_6LX0!o0)LL z>&Mp@xETo!hOLPYGIG9s!;kO5w49g}fUCYS;U^{={V!uk==8rtKppg!?6suVh9S`@>#+1O{dB;T}i@L2MWMGY4cApcX;pFM4Wxws{ zSJzi>6`82}D9?Oo@u4k2?K_u;%1=Vv?`|cbYw@pgJFU-6Y_(ZQlf=MFkQ_gtbT zSpXe|UO4_yV3!n4ZhJ&yZRq{8yeeR-xQOc0qDPC(REHypR3zJ$fq>oD78TNU7=g^V z>idM+j#3y)T1A+R)uI3;V7Cx%wcq*lREy`+6tn$NV~#(jO4~6RmAQxTFOg=WcjI#6 zs$AbH@n1^0Fb?!Sb;9eg;G|8=p@C3r0xuN z9;MkgTvM)FNN1m1R^IHJMxrABt{6;uop^Yg1<*4tbYhVE(p08Fb&@)8oiX=n8eQ9T z_M3@Qc)Li-zxOg*+%TICL>uHqxD#azPU+LGdeC4zG7pe_WFGXYezgwvR|@e=4a1z> z4=gqWyGF2Oi=J?$&0&Opa&3>qqJEDWT<7r!zw^Rk%^0n_UR$r-GtIE7VjWSb;LwSB zjv-qB2sUhm)+Dw;Ny?jh7ktxCLp*3>_+@D`2K7hboe!y_yVg8;zie=bwe7>&yyV{t zl+`01fWfT=28gT|q7N(VbzUx;Sca^~-Tk&s={1jL1x)zWw>Rk}wnl5dxoVKxr=*dx zQFde8R!?i|0q^W;Ah#N?HF)X{HTj&EH;CI){p^{md+#hHSu%<*-ik6@O$qcF{~8!O z5d7s}m5)qa2_1b1Xw;CLG%M_ZlDbV$~j~Y9wZw;VL-$tH;D}3J6Lv9n{EYZzUj+rJ^a;{cELT-hYL%4&_W)SuzTP|G-9q`zU>doVU02 zaTC>!h|zU0pg7=>+sCaqByuJ1(U@DeRl0c)C*1M6R1|YngxCKziXr51b5*&%k@$og z6H$!7$kPM3hlLmSk@iLPFUoAv7b*<$Vx|&*1>9wFo3BXxJ*A$Q{iP^okROzvF|KIF z3f_Z?(N2^A=RZ=54_h(FWbPFEdD`ulGA@k#HS>2vu}}&`&5Q4(rObpa&IKwmyF!uW!spjBP?ej%#GJ5BuWp>MFHgU zV+HEGR^q8S%PYNjNBKCf7FO!HW6$MD$<}tk=iGcQ=^Mu3KI6^~0S)ta9`}7QaFFT) z=_@xw^oT0gS&ttl^qt?{rUAnASj=_H$tjuQooU?B6wsn%U$k6O8(G-}P)l>3xb+rk zvfoLf{W*}yrIaaY=S-ks>UJH!NT1(kn6^)z-+wE#goGt+DR5B_1{^3f?r8GVkq%J4 z^SShI>%}%Hn!htKJiR7=iL2YaqHGSLR}6e_lailB2-rjSwPX^bovy7T7838{WM~YM z#q&dA>Dt?$yi$&o?zfv$i{&I^>LprQxQQNKHgy;#DG)S9hr|>f!iHzrB(%3MLp!9~ z%0waM?k~^V%rB3f1SFzZg8xBdIWQ@vWyhZBv&s)l;)Ier3IT1{63?d0c|;)Hjk#^u zv8BZ!o>@?~zjJc^xGw+Ljda0MD!*D;&${r+puLdvTa!wQ&=Le_>y{^I@SnS!pjx%G;DnNhU$JcYc!gSYCL_ z;BAw&K8f!<>zNfOx$r+Obn**aAVb>@hb>Y=e^Ogr57&0`R^@TExuWTI=sE;E6JfE? zz^YjcdDwZ?$4|&t)+udwt`b&4&4z2*Lj;cIDY*ix!I@q#<*2I&yJAFP6UNLNkACz z5JP}thO^TViZ=dnm9gbuPhe?WIPl>*nYR4m8D@vvC!D2blEj!sm)fBVZnTPO(_qax z0N#G8Kb9kVa$XV!t25Hw33LJ^O^-=1Z&PdHM8lI5bN_{!#bF{OW1%Kygpftz%r}HY za63m#bftPZ!+;>*y9vnQx4A*A&T|4 z=HMOWTek!2X#7_*n0poY{4jwt!DDi8W^c3G8Hq@X!=wrKxc?#-L%Nd;idIMIeKBLx z*=1$9U#%|S01nt@CEKiQ$p^8vEZMxINN`sz zU2`)76uqV~d4)@wrXjgA1vD{q;E^V6G2nscj@I*GziOx_74QqXzWpRJ z@}5;kOtU8$kVQg=qrAxPZ&~x@qy|7ET2&JzZXRNS!;Q_BVrp-x{CYYLfNNSzR znBRAS5=87$EVg)(0wXxT%C4Wn)fn+=Pi$2-X|`}Epf)es?D(PRM|yi~Y$?om8~w2$ zC9jtMY$R{1uuxQsPkDSulgeZ7Rn=$&GZD;&EMB0k(pn=UK|8b<4BG*|Zw8Fvs|QN* zu6A#8l!CguqXA((0UH?o(X$6Y6@Rc0DXFAJ6Ajdxn9w4&7h{ zr41CN(0UgYIRtYe_`}}iMdb7vmAb>rd6*R}46%@LD;O|<+oUT)tBM1Vtnz9#!f1Ru zHb9!0@Gd5D=n{eMNMZ$pf?ogJi6$ptQsO}g8a}b!8~eelpXQE$1J;OkOEhRIgpm_u zBxeLTkhb_Rpa-|vQ$p{3fPi*VYH@smf&R&(Nt)z=2YIu@NZU>K6p+lP2I3}3fp5#W zsL04v$molIaRhZfHVD@HcB0#W7ba>2#g5EnV_WN@QCdF$t|*|ZX#V%{fE$yp)MEr3 zZpQS>^FDi_2^a{sj~OZdA4K zHEuSYTULuH5T!DXC>e|O$0zRrx7-0uc#*K7^|X{UVrrvS5v9cdIVhn6ax0I>Nz30S z0kM`-TAhk0z!}ijd;stQ&$TY1B9Z#If-8zBeViYS6#X+m3K75s93z9-*wW~sUsNIv zWA#0zYTxAIFKVudM}fyL0z`9m0MNY4c!icM{jXexOVbu}F)%GV5)l>w zVg4vtllK>fpxI~C4dHMeX-q@w4F;wnuEMBLQ3oL$$>B2#NIo9p-6J-gr|gjU_z%JU z!t5}}r(z}vchIJUg63 zha)o5@(Gef2F5o*CpL|m@^KQ7vPTa9U-S%*0BI5rA*6Q^mGdU3M;B9+OlwUas**tl zP(fA7+TidCKMoc=Xn+d#=Ah$q`LqTG3%12jC|aAN5&*jz14<~*cyK3p<|v^RbN$7b z!0w#@o6aLvG))+4R%-$m9mT^1He>K1jBjoTR_n9KwQ|G&!U!0k%npEU8(OFpb%S+n zM#oN>J`OVPf%J;QqCGb1g2VG3J*N_?FeqQ86jz^_Sz=QQV!=07;sMGsP6E(i1cc+z ze7Ai}rezv0hXkhckHBWsDM?~1<_^t8ZW&Xbv?s!%&00@ZhtI_1a%R2vUFpd4 zw5=NYk)`(-ece-kAisW?us4T1N6QOTX3aUkbm1fAN64mAj_jMKI50q^GO*~ybpFW- zugQ))@#n0hBaFZhk5l%PQEewto9k}0-ZPiS;V;>!E1`NHDbdmv3nCaO0}ULuv_Gve zR@c2x#I9%7&f{L1cy07G;RSjJUr~Jt>7>nufwd-oG+R2~i=jWcTYpnVZycP)7 zf^OrOoe~=VDON?iaN&TX>9+Jo`X2xI3YU37zARqC!oL=z@VdU2q~+c2jSR1=qpjH& z1$GGKr2~iG&(6b4h{c0hs9^=-eZoNj*!JP_?V$c~UKz|#5N!C9I5{eGL;{R%Brg)e z1&fnV1754Q7UFwc28A!R*w{QGNl02(U!&b*OMgPrUj7M%qUOP9^8JI7y!&%B7{8Ta zFbJi??sVrl&ZTOS8pS$0*ob*5BzBTHl)5Wnz(Uv0cu6f8fkB9q@o=4C+#%lgal|2! zY?mBA9G-byw5=kYRY*cxOC@kp*W~Tf-_Vp(}^*z3ypYo(Gz7%9?3@a-iH8e{a6vVJm$p%%EO+Kyo4f+q8NhJ zWw_c`o-!D7b6h=PX#CP$H1SenBh63_#`|R7kl#==@AnvY4zWw=%2&SFSi1wLIjaSW z5#tz*!*lsCLtI{HQ>K*Jl_iSAH_<%>1J3hzCU3am$kx7Y$JXAm3$^@ky*cpXs#z{x zP;y%BgX?fbvqE5PhRyQ2_Qwy2o=Z#AyW6WKk7K!8fq>S4gTT&`m_L#@y|o49X-&u+ zWhhS<7J9$e@s+URx^>iA>AO$2VC14=wctSd&a zH?&MwCkaM3y~i(uMrKzL=Id@36B(zp3yM)vtu2A6ZT^WzPVe84sr<*FTgj}gQPLih zv(S+?{Mp&6yVr4=6BlBgjaB_BS7pQR1)Cw$Rw$NiBIgW_e36w3q~7$6^X<;-$5m#i z1(O7G>iU1X)Js#PoWC3U&ShIeJg8;pKQ9(vD~Z#gv&3^X{|sEbxYc`380Xr*vuQs! z6%rG(tmHjlx^u9toAQx4=*+pNC%&UPXJ>P(WZ`A9vFcf=kPXbQ>HY$rI&KU3d(p#@ z53r<}W@8XD5V-EMjZoqmZ@Gt^m(RS22v!toYmb?4{k%BEY{ezH9vMn0AccQY@nOgR z&Ye^9?q)9Xi1Z)H6c>MYN)>bZhutgRMR6bUVws~}Qc8L;ZQL>o{2Qy4h5Y)&0awgq z7Ks0Y;9_eG&*7^_Mo)9k;d|sFGO~6%Ll(ytITSXm3Yq#ER-?moV7j z8qWMsnW zlB*g3|FI>7)?@9`NW>_%L&5Y|j|(xrYNh@8@V^E+AWM&kjZJupm4#Wwi{PJjh8Fa@5%i(-9y1ygl#-^Brm zI+Br*h?a0vVAX^F=pZ1SEEuG#g~9mhOTR|{TK%r1`(2mq38&J=RKfAX%tXz zq7%}WQHz#1063SkB{{d5}KribHK?Q_*(op>Wemg~ZSMOA?=wP73(*i(lW?_&_(xy`% z-LWlHG8Cl-7H1WO+9Kf}0@FdLb|P+8fC5Rt#-<~~4oUxN!ifjopBZ?7*a{kun^|Qj zSiFEm6vI97NaPX%^?^%dXm1H3NQX0aUmX6>!mag+BWegJVl$5x{N8Qt1J*x5W(B{`MlEt4zHIA|Ky_iHhEr z+8XO_A=L?(t9*iGJgi{NAwp)cK}TES>6@RfBtMUXjbsG*`=ty8D^o~q(FfZ1T|XCb z19@`%-{sm{^s|xHFKAE&HYi;sYrcS#8mQMFzm^pI{-@ZN)|D9l2#775xG-(_ z#wrH|Kq|6iwXQaiLF`JO$e`G~V}#u3LB-1DuJ_k@xEeurD&YA=nz8%!pArCW}w-VHgD> z$?a`OpC7_CWmoe3Ne%rMpMy14tNKmUtf1-o_~F(O1~H1R1YbJ6&MZ!+e@*WH;MyOC8|+GqQ!u>L93P)*0~96wRN?E6aI3ibWs-I)fC3C zI#gjux&INw{;smBQZHfZ@$jgsm`g;%)vicq9n(v2Z?oAr16iR)p-VH8dLF~5;K|sV z?#A91A9z;=m8ZoEn1~%j^JH*do?i@f*Yepimj4!p?J%yV1Ih>u)PsuT{o%&CSQnr~eH6i)Z^|@cy zV51);C=u>_#?lhVREWufQ~f|y=C_pa_xYu-9wCflD(?Gvhnlt3 zUt)Dc#fv_zUMWY4Tto$g5!Eu4hYgcIXA}8F5@!dFHL(f-Eq+UZ0qSJ!%qXTdexoDW z#vf^7=73U(6G~AbKo0RxZ@k{sD{_9vK{5hmN-=szdxsgIn4ww01<)Mbu*2DBk|S0p z)aTUZ5S$(aE;Da>-wv*oJN2ItB<(Y29(;<8vHErq=Nq>Cd!uLtJ@bHxwyc8$VwrwQE^X!E7{#k!2o zv}yXEUBg(S%Z-19AHE^e4+OMBmt=zAy_=M%R2nG3h@^MAE3R)~hf~}+fT4!X4LB<+vq^@W;bb!Q%;eZ9$k_u49 zu3ICAGCk1(zJHhkbe>6f9W=Es7LZN+har%Ex++RugUQlQ(SGLqGZ^p;jtdGG+vjX8 zC0=2E0(7@P37x34KU+aQ7O4iMFZ~N5AdW*GQ;%TYcya&BJ6=ralV_!WT+*$%*cy)(_|>8*}QL zibz?l32i)2Waky;CE|eZk7F?blESDJhKD+YN}6 zF*VMN?@MBhxArDD5q>wgJUlGRmJolJU>}U5S)deJl=bS#!i3Mhz6rna^2-pc+2=C< zxy^UCLgU-9X=dB-$sH&xezhX`?H2Z#9_~&_^<8_4aioZ;&))SFp3PUEoD!Cu2}~Ne z#Ibq{Nnr=jevcb@itD?f1<5a@fx;{PBMc1xykon#l(VS$8_}u=Z!U|&T4$76aso6w zkUJmqoHe}Dbfts>h$mR`?>byg7CH$9C>buZ{<3cw@LAj%(X(VVpSf0jjkDWmzOYm_ zRe;Fbv0zHf;`ndUF=h=+^s$3W45f3$mt{2j(_>Iq%GZ{CocwT##S*AAcm z!@Q1wCgJl9A`OzVHv8(c{f+CMpzbh-Z;O=c_`JS*>|jpP*bHFFh`93LdX-3P9L7ho1ZF}GL=_k4>!a%`lK~6fbCGw8Nu-KtXms7!Hv!} zm(Bk#w`cxtu}f;B)s^1N|G)VV3i3v=+Qk+XE-CT!bKiNNtCf=khl{)+XI#PFF-d8( z#VdV%9A}6>7YsPto|9dx6ZlAyWY&bbSZ8?pemLVXY|QVb-R8+ZOT{o0 zt-lU5-c)V|VG_x;cS8*;N^cyi^=G9}W4_(@%g?#p;a2XG8RV@Sg+WAg&eULlkwqbY z6!Iq_)g>5yl}7i)-fWR^o2-;+voBY47T-tf z>(=*HZMq{REn42zlxQ`G*i- zk;s&&yo0F7a8K%f9Ixxe=izz3!|*=w zx@F}?FJA04{}WU8_Oj&i?^#Y32OqvaAtVnkKE3Vxb$jPeUI@>+5%8pgy442wqLvh2 zec>#a_e9$-vrc?Nfy9iJmSLnh;?DjRmYHr8EWNHScAIOZBL0Ra|$b z$;$|m{tttX5#yd$);kw#H|MX(6RO}d($!p5CA%l5&4E$vA#(Zqdwe_o&aL5hwtL%c zs!fEim^W-~{1wqTDR_Vk+RPNY{EIfqOMXGdm^RqTXOsSn;s~57A3k6td;Cunn`uV% z>hmkesDYD*1o5{quDO}vrkXoWRcCT(+#Jtov~|zms7AWYmkIwn3YgdRwV8iIq2aUJ z6k$Bb1$S0Fi59SztpR+8TzTOY(YaP|rxYHqpJ_V|?kv+7XV~>T8l`oz<=oTy73Ouj z!}RaQj+_%wzF6u}9he}IVn3J>qUybXe#>#P2@rnY}W{QGkpMbGUAJ>V?Y`np@v5@zV#+qLVRhr#NN2<-8jvJ~= zColEiwPywuDj8PJccz--a>GgIE?YN88*jiqged|-c!OEx5^9{&R;R=RHxFlv7la+AI%mCIdz*!SQOhJO2= z-}x|M@$QBB&du5H6Wm?mSL3ZA4_8#uS);AJiT;geFy`za&~`-iZ(SPKXp9<(n z!V+N{A^ADEtxN)b{HQ^dBC1jAe*8S$iF$d&KW3WU9qVKJ;ACgYDiXoNE`G%LX)Df5 zz{;C*!s2$>C}BS;nLErEnnn?WX-!GxhrHv9f^)%vTtV9U`?tkA_w8XY@}>kax{Zxo ze{j4eS`?yeMLG#gN^V?V+>odg``o~=?nnR6RQR|?QmLSAUg(2#8R>uJ%nva&yo|n4 z$aIFhJkj}%y7r9vtYIa}JLi&%61?ikjwzKil8|70 z20Qogh|4&(w2^Qy0PowK_m51dK$dB;iHwf_;@7=a=vNvxGyT5!_s>0{q-{DaT;G2w zE+H#--J(+~F!z3S?m~^+VuSZ;{xg9l9gTb;ar;TzJC=i*dp78`*Bjsq4BYQeN$1qT zwl>G$B^M(=BhzTA{@B<8pa$1o{;vH`ujWaHgfaI^J@h%r!G>I5 z-Z-Oa7L9~Aqckx&=FZLOjr3vIw!5~;QF-(@BECuYSN-*ti2A}IG1`CUa@^F*%ZtqT z^tSlh6M-I*G1+N#F&+jUOlM4GaBT4kP7hV3Jf48a(C67qY3_Z)EEfOHpJ&~lR$8~b zW=|HE9_(9UKn)g)JzdOKL^o$HU*B#fzfsW>yA4&bBaC&<$9nmFNR>s7c$(R?S;8TUIC%z1a)PB25pQ08`%^)42#qnFq#fH@wRCXUmY36UI2A>7*tD_MJHPV7fG9D0f@j3fj4< zTvI`JC&)rKQg@LDE}F}s2-}h)ZcJs_oU85sFt?L*7Bd+{s$CagFTZnH_>vj)N5+tG zmV=TBdgR_6A3Bc_hGKA=T*3rU3+3;ME9lw4nss38R!&^uag+hXJ+>~KeLffuEN{~q z)faE+1rFS`htg|qqWSJFJz-rlvwjb>0vF`!T?7&Wms?NdrsEgh~tI|H2uD0xj;LPhLlH>fDQEcud?-FrR_w%1k)bPkj2kDR^{GirijsAKG2LM z{p~PrIHpS+!{NUea8CX7T{p{@4?_ScpY z%m&9+a(p*cF`3sRJ+buLm;4%Y{3!S{a(+yW>~1a{y8eM11XFyv(Q#pi?7nGVEx4)q z^olccD8vx5_$s;K?bC`YPFUbl*p@Y&HkJ z(lKXVpT5oOEN%~?PU6e`!m0C=l|}b^Lr}gwFXr`ZVwIchbYH+6-uknXbvx8?C?Tj0 zyIwk&l96z0fRWqCxT&afj^+SE7>KGYl-->=JFQb zI3+K^^l3&po?@*1jAvK7wBPV3?Q9!;&8s7J#;^|r!+lj2H?8LHuIHOiMDI8?CQoAm z{1#`kH&`P|pM@0BW+|ftK1J=9#Y#Ch z8z^4#xrp2|W-7S}c~x+1f3^(17IHi)qz4b`g*|isrCbL=XB`psv*j;uS#Nw?{@x1Q z#Qy2RpmF+nmcc^=u@L-BxqKeebs^0jai02QUI5xIJuVjk_@8AVzQ_fSMzCYTL>)OS?5l{_qI!ZeYLlm{9Wf}{nUQ2<5*XpPEGIxEwG(L&!K(7C3qw~(|*gmFdjS&^6+u};F&w|o7a#3<;)Zoa{tUM~4`-Jw-cu)GB z4~ELkMNEvywK_3`Kg0Ix1(y{@!tmXZ?z?Vm@lj`!b}wLL+;b}BWWGJ@@+W?#&tLz) zIUVtrY4~XTuas_Kz8>Tx|JTEn2SWY4|2# zR48{+uH09SUCE-uk}Jd__tCC%9n1d4_mAJ-J3I5tGxI#p%)DQ(7e_-s<+8kXV{Cu+ z#Mb0C9XQmvxUJa5EB8-w76JjeyXTqK+GR1F$2F@gIMsl$1*Hm4eWFReHAS{VTOoh#&383>@M z%-H15CU~*SpT%saB|Lf3stc}5+2GLep4RR^V*{0&1BEWl`M;c)bygRR4vYeV=ol*U zmma~AnJE0(JLF~d$RxhkBixu|Yn&>=-+7bXgcg!2`0vyLa~b;YURfykQ1l|Lj+VCP z-@zS4@t%&5vq^8EP_`!R-Nm44_u6QW2pKDar)Wx;(XL=e{{hMu7}>ex+Q_MqTNL*YK!-|a&2FXuHgS>gEl4f9Y{l$y(2DZzPk$WTq6Jg~pWo8}KQGFa-Z`J-9HjDk|#(;2!7-@d)N`qg@h&p1eGrB`E+=fi_1tpZ6-R z<8&~AY95hP_!BLSHrG8QL}1KZoc&LJStTBhTaahdz4HS8p7vb4MSGxwKl_#olmiWe ztRy^|MTLwRRnO?|?72vLjk2iRzs1|v_<)}KN!r*rtr(9KtqGIk2-{%P8tu==+q;wa zo{4QyMNU(oQcDyg|Ds;`%3Jy}%0W>44|Yp-5*B|H#fMLC!I@FRQE{)r?)Tt;fIRg9i?Ng$ecl9>p6o#(AFk$?zzl( zPljazc2LmchqlL$?3l4V?-EUDV#8@Q<4&16epo@jV5>lG;e)lSzrOQcG0mWgaBIr< zf$TS*o4s0nd8{qT?QG1p%uw*XTUjzw1+l|v!vKyg=dk|%gM&<(-3(<5kL;KyQ2&cj z)&3g0Kpxz^nQ;9zEhwp|jP|TYZ?TvTgPvM*6UGcaVd|b;Y~4Ju{JQQQHdq>e>j7IE zynqQSroSS+;-=|*eCAJCJBY0biodQ>UA!A(vKp%tWFt6G`+(2mh9Yj?M|CM zl9si&^x#G4i0E6|o`F_yR}|~0A()~kongpPUE9K;<2_2>SqrGnMkLv#RyuibU&bO9 zXZ5;mk=wjw=A!4Kag(Ix@LPIBAh@D4jxDtvJ_Z1N$xV){*qLkeGogUs0KnQ0HP@I< zXZdo=bX9yQ{3896OeFBfi}X_{<)B!u_W$YmxCQci{=Wd|&7n_cz9t-+x^&eh>E3 zXRqM@+PeVt#HlJjX}zUdC|tn0*R|}u*Iej)uHV^B!sQpAd05iKlCD0up+u#mH`Ptu z*T4p7CZRV~0RrUx6rb|Zy^gSO0|#nIxdP?3$~}O=fusgOIPyCp^ioX$CVX;fMQuh% zPBdfWsb#{SqqhS!Q+u2SfKt%YM%31$75OWM9Qf?H(Js=Ktm2S$AChkJAI{u}tgMRw z;jTz3JM&Q6pWZGktoqjDBeJpLr3l8}%C!sF*(wp@ZPy2Mt{H<>-^BrXlRs+EzNlr& zBCqm+ILv;Jc9KgAP1I^pXIfp}lo%{ZA z#}nqLGpv`Z-hx%(3oVT5PZVa>%{+`~5A>gR+0yjsWFYEusZ|zZwr0%vlWeYTo+x$G zcA+osEbpn1$MAr+8td8o^+i-%>-YXo`+tQ<*0*U&*(DQUhVthlpDFz&uW^92 zdIpS(EklYUKS!92V9g;mybIT%X}MX%lKMXK*`Gp=TJkp3P$GCaFEz zMi6-52c#tMAstowF5QMAy4S`9s7eJX7w<|WeK$A=**FleLg5-L3633*3RzsKF<0$i zn%$qP?swN~@}B$HovD{KW93rdE|7jS>AU!v=nx4>|JlEHXdN^Y((tQ)yd(Cx@_@Zl zR^R3)KUwuZt}1XHNpkj#Atp9{r7qY{tTn*t6?j@;x96-bv4g>`dIPtK zPdI(nRh+YHfH;*ThwWozoxq|oLLbF!?4ZOwd6vzi|hm5e53A9=hT7&&8_VliGtt6&fPe$0zHa1S&|8Q3}m^AvOAnvxq z^j_D=TIHyl{=KF9j#B@;r#6E2NugOn#HNJAnEHMb#5oT^e8Dt2<-~tE6qyC7#*Pfr zr)u9JUCr!`HtBkWPf^x$MW-98!Gm!Pxdrvnx(-54{prixzjX_7G3}a&=(0$W_usUq zNBd9>kum&aznG!~KJQkT-i^<>@|p9d%ZR8^f3@&Ly={R2TF^`;>dPJc>Cm%=iQ$3pg-VTBRE}rLw*EUZ-+D{-i^~%WO^V z(FqYtKP-K8t_8>D`M>6=c7K?M84H{y!fc`+;xY>R8-%B%wm6iifsz25@_uDfX7v%N zP);fJ(tb{Rh7(7)@}YRz@}Yq=fi|+VTT^raGJDF@#`emCMz8#G$@Ummzn z28&EwCw-D-Ffc$!hiQL84Tcb!hiQ^Nwc+%Pm?tW$kkULx*xZ7#NjDN(}yTFef-)=G?gyh?1(3%pdpAC|(m zNQLKb|Et0O5xh0fdU>K|2h09+@!sPY@NFW& zsBE03AzyY=0-$Uz|FR>SEp#`}Gzkx^XFi8i3hhS7rpLy1PwyPHE7hxRwa{{UC$S1H zR+7*}%DE}OJc~$ifDbway=O<35e(s2n8m^~iy=UT~~_S7&LATXZCE8KN!C0u)~ zg|bH#!FAYo`N*=iw*dg>WH3z)w(^Kwi}Wc|YiT;~Grg5uFF>Bc?E9TTYjsPK2pEi|oRZ6~`q$oDMRjSfi2cV8~`bA)>=f;JWm38>HYZxHWtirYP{)eOaePA5aPJpk3DfzPpV2qU+AsBCn zYRP%98ZuRQ7yd}{A%jUq4Jn`X+qAAu;WB{z)craz4mq+2MTtCCi=W2i0*+4rgjRkG z{uv_fMtQGJ1|z8ljcWY_p;;3a@JZ;5Tbu*HM@2B*;r%aH9>1EsLMpU@>1dLrLHXw@ zo-87AS<%{Sc|)D+*{Q|U@YA)^X^)t`P#~ilyXbFSOl1GmTajQHN=hj9 zy#Lvg%PDW4EFPv?Lm-X3jknBzn@c{dGZKF1)Gp9aO_z@55D>@_gA5BdKw-atfE@W+ zxu_wlt@H^iqya+9XFdZg?mM6)OxP|khA(_u4XJUqatUW!y*mVU(~S|%LT$H%32QH+ zMH2(26(<6(DoF&`Di;zlWrs?N&Y{@PZT-XkfkSWs5pt!>0GfVI<*04j|59{uL;4EE z?RyI=E!rtf6m-Vy=tl0~Kp0hA7$8;tf$MOPwaw0g2^LO49&rS|DtYx|KS8jwN78?e)_R)x>nJrIouDR?BJ%TOr?AUu;X5%SP$dLmW=|N8 zFmuU#k4N6$Bu>SU{2Hs@02RI-WuByqsc>77Xi|mUN zRs!a%E_Zyr0EpL%KfB5SL#()3gM$O79II)+Gd`G>zd(Go(Nb~QZ_s-$QFx!n zv_70I3vQF=E}=9O#b9~J$XmI!VbX3D%hK3=<)sA_`ib+df`A{tymT&jDf~&TN&35Z zfiOeKrLvDV{KFYb({*pv;GF~_2Q`b6&#k_?Bs`A^fJT+LC?3{*pDxHy_HIjlZ;^B2 zF~FmMcPf|Y0cZ0ZgluBkN`}Du>5n{Gq`%${LnY4KPv~DI8TtD84jxPs_wbV@(#G*O zLd15Y$XCpiGrjIg5DFGV2s6A|jOhgPuZ76WS-3{}xjIKNS87k(!|U&#qt=up21a&2 zXwhB=BX=bsO1A)+TWdTWvtl;qH0CuaCE@1buy>O{$6MTG|8qnmH-E17jQVn9n%D=W zE~hvA_fRncQG=QvE2CD=``uV=#(0@&X0kv2gljQL6s>DyOSgD;XU!GyTt{ci_awhH1_eTvIWUp;sileVgsN+AVLgLooR9FFX@tA_r)e1{dTw2=hEef`9!U3?zsK-Bc_G7g87X*1D6 zi-M2%p&40NdN1=K5XqSy)ON=wx327S$P|T3gwXh*Qntc@XDaWTnF2aXBybPhmQ>Y2e{4@rlb{E>qO19oI(i*p0LC6D-5b#cvRAZ<8l2+(SIZR|-P z3zWF^1}T4v+Vh^`H@gpN0TnM2y7CBQAyppA;@i4#z?GhtizqMo3;_CStlV!R2L&#p zD7~vMk+)!3vhQ#qrhcHq62}F@12D6diIaa_4#A|0o7n^9#?Aszr$i5w?aJ;ZUUb{; z$Cme|Jp=Hz#hx-%tB;PFT~Z_X%X8B2p74m(Y+eHPOzlzY&j;XqKQryvS^OFM!5%)c zaSHbsS87&1m-pyPWBnEPDaRF!?_$A6IaLSz9fPIfs>8b$lGtmXmc}t>B23I$=N>+a zP;Qh0?~4~{)Ge8m+X00>k~qip%^nynV()H&##!VH#Tiiy2Z$Puq?!c#bvzZs9E z*JcmZ8?wa{cjE-!FliNrzn3f6*6mO#=J@2jB{I3C6lLku_@lTlytPt5JcFd1_o9|7WX@OqdlgZ~ zF*1{F_Z{j$7pP8-^`Z}&H`LKhcD#>bk)n$=JqN!;H*F2`2)8Z`2rRFN7=^cRFAlDW z{_X7TfBnf-A9YjlWS%-y+-&e5;L@`t%1F?3gZp0Zu(&0VW>cuN))ynNL)pqay*emb zw^+H0+3)Hm@89{hiL_XG7s3pAt{Kq)q33y~wRU~l(dx@3k2eck=fSZ41fu$K6q$t$ zy)0q|6rcava8KsO$Ow!|7==Q)^yaVdsXu`2yUQ#(6x;xryVLZIm{$U*a1m_toZx+}K{g z5%W8yXGO&v%zOo;2^LJ8(%{|%b(;Dxfx=JkUJ zU|Z%kRd;1#6j3CroZ&x_%?vTww!np#AZB&MM7OleRS71bvv)aI*OuO3WZ;=zwo)KE z#%>Vdlmz&<>TbtS_!kvJP;uwJ-M76O$iNhZd4*T8FkG$H0uOBVj+p2gW10J}yI;h8 z$9KnjYeibRajoZpG8teF)^YYBiY$i)qXF+%kvD<%sc>DDvi#2klF`FS<^v02V#*3F zeY=n{ybMXX?*Z&QY!hRWGbh+X9aOI37%|~@?si99)d}OQE^^lPdxMZt>^P3*NfOBA z=dKwvUx555;DF~^D0J7HEbyi490JTlaix)C|9uoQY-Rpa`A?A^GrD6pMKWW3Qps6a zecuMMTevpSMdX2DoYP(pkgk7 zQ=hY%>ah-%lqymx8WVP&Imm^n4>Tw)r9^Rz928SDyGt(tX{Moa+&Ns@d1gN2QaCu= z&jDRTMB&lJEo%5vS@V1;$jjD)hy{4SM=7vqI>$}^7g_tFp|)U?mul)m^HOW!=`iHI zyWL8gT)|G~)Kv9-4RhDbxZ^D=SD|Kb~`;KYuu2 zt_TQ3l0yk&D`w+BH0e4WRh_K*KSPek$X_=OI%4bWK_?Zzz5p34F@|`b9aYuQ1Al%Y zfQB$Xl%pmEBexYD?$lXB^M(&qecaa#xBx4(WOe%huRRJv>Gr{qHAd{!3mE3?XFzc# z!K=@+_b)?=i$Lh^k0}2)?Hh(dFPNtDKxQ#V^nuXIoBCo_LZYuG$;T4D<72LZ+d<%F%F!k#0tC$k->6w!l-B`ki;j!Q{^8Ppf7ajZv^xBL5M#+%>piiQARFS${}Pz|6Su7LRDVW?L#%?0v!C$$)C z*jjRDc_IMA0~im)4T(^2)xqNB{i0o08%(1>BgL>pCoi0_8FwAeu@8!V?S|n0d9vQ~ zF5Wz}fFGFpf;QEUU_b{&N6}cv9jtt}6F=JH2e?St4

gz%YCiQxXTmn5NU9w;Ao2 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

8wQEx_ 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 + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_activity_select_sensor.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_activity_select_sensor.xml new file mode 100644 index 000000000..0650eddb5 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_activity_select_sensor.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_register.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_register.xml new file mode 100644 index 000000000..a02e11ade --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.agent/app/src/main/res/layout/activity_register.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + +