From 39d2ce12dc1eba6893ad42b3dd307435bb73144c Mon Sep 17 00:00:00 2001 From: prabathabey Date: Sun, 27 Mar 2016 03:02:20 +0530 Subject: [PATCH] merging resturctured iot and mobile plugins --- .../build.xml | 78 + .../pom.xml | 90 + .../src/assembly/src.xml | 36 + .../EventReceiver_accelerometer.xml | 26 + .../artifact.xml | 22 + .../artifact.xml | 22 + .../org_wso2_iot_devices_accelerometer.xml | 62 + .../artifact.xml | 23 + ....wso2.iot.devices.accelerometer_1.0.0.json | 20 + .../Accelerometer_Sensor_Script.xml | 31 + .../Sparkscripts_1.0.0/artifact.xml | 23 + .../carbonapps/Accelerometer/artifacts.xml | 29 + .../EventReceiver_battery.xml | 26 + .../Eventreceiver_battery_1.0.0/artifact.xml | 22 + .../Eventstore_battery_1.0.0/artifact.xml | 22 + .../org_wso2_iot_devices_battery.xml | 62 + .../Eventstream_battery_1.0.0/artifact.xml | 23 + .../org.wso2.iot.devices.battery_1.0.0.json | 20 + .../Battery_Sensor_Script.xml | 31 + .../Battery/Sparkscripts_1.0.0/artifact.xml | 22 + .../carbonapps/Battery/artifacts.xml | 29 + .../EventReceiver_gps.xml | 26 + .../GPS/Eventreceiver_gps_1.0.0/artifact.xml | 22 + .../GPS/Eventstore_gps_1.0.0/artifact.xml | 22 + .../org_wso2_iot_devices_gps.xml | 69 + .../GPS/Eventstream_gps_1.0.0/artifact.xml | 23 + .../org.wso2.iot.devices.gps_1.0.0.json | 23 + .../Sparkscripts_1.0.0/GPS_Sensor_Script.xml | 31 + .../GPS/Sparkscripts_1.0.0/artifact.xml | 22 + .../resources/carbonapps/GPS/artifacts.xml | 29 + .../EventReceiver_gravity.xml | 26 + .../Eventreceiver_gravaity_1.0.0/artifact.xml | 22 + .../Eventstore_gravity_1.0.0/artifact.xml | 22 + .../org_wso2_iot_devices_gravity.xml | 62 + .../Eventstream_gravity_1.0.0/artifact.xml | 23 + .../org.wso2.iot.devices.gravity_1.0.0.json | 20 + .../Gravity_Sensor_Script.xml | 31 + .../Gravity/Sparkscripts_1.0.0/artifact.xml | 22 + .../carbonapps/Gravity/artifacts.xml | 29 + .../EventReceiver_gyroscope.xml | 26 + .../artifact.xml | 22 + .../Eventstore_gyroscope_1.0.0/artifact.xml | 22 + .../org_wso2_iot_devices_gyroscope.xml | 62 + .../Eventstream_gyroscope_1.0.0/artifact.xml | 23 + .../org.wso2.iot.devices.gyroscope_1.0.0.json | 20 + .../Gyroscope_Sensor_Script.xml | 31 + .../Gyroscope/Sparkscripts_1.0.0/artifact.xml | 22 + .../carbonapps/Gyroscope/artifacts.xml | 29 + .../EventReceiver_light.xml | 26 + .../Eventreceiver_light_1.0.0/artifact.xml | 22 + .../Light/Eventstore_light_1.0.0/artifact.xml | 22 + .../org_wso2_iot_devices_light.xml | 62 + .../Eventstream_light_1.0.0/artifact.xml | 23 + .../org.wso2.iot.devices.light_1.0.0.json | 20 + .../Light_Sensor_Script.xml | 31 + .../Light/Sparkscripts_1.0.0/artifact.xml | 22 + .../resources/carbonapps/Light/artifacts.xml | 29 + .../EventReceiver_magnetic.xml | 26 + .../Eventreceiver_magnetic_1.0.0/artifact.xml | 22 + .../Eventstore_magnetic_1.0.0/artifact.xml | 22 + .../org_wso2_iot_devices_magnetic.xml | 62 + .../Eventstream_magnetic_1.0.0/artifact.xml | 23 + .../org.wso2.iot.devices.magnetic_1.0.0.json | 20 + .../Magnetic_Sensor_Script.xml | 30 + .../Magnetic/Sparkscripts_1.0.0/artifact.xml | 22 + .../carbonapps/Magnetic/artifacts.xml | 29 + .../EventReceiver_pressure.xml | 26 + .../Eventreceiver_pressure_1.0.0/artifact.xml | 22 + .../Eventstore_pressure_1.0.0/artifact.xml | 22 + .../org_wso2_iot_devices_pressure.xml | 62 + .../Eventstream_pressure_1.0.0/artifact.xml | 23 + .../org.wso2.iot.devices.pressure_1.0.0.json | 20 + .../Pressure_Sensor_Script.xml | 31 + .../Pressure/Sparkscripts_1.0.0/artifact.xml | 22 + .../carbonapps/Pressure/artifacts.xml | 29 + .../EventReceiver_proximity.xml | 26 + .../artifact.xml | 22 + .../Eventstore_proximity_1.0.0/artifact.xml | 22 + .../org_wso2_iot_devices_proximity.xml | 62 + .../Eventstream_proximity_1.0.0/artifact.xml | 23 + .../org.wso2.iot.devices.proximity_1.0.0.json | 20 + .../Proximity_Sensor_Script.xml | 31 + .../Proximity/Sparkscripts_1.0.0/artifact.xml | 22 + .../carbonapps/Proximity/artifacts.xml | 29 + .../EventReceiver_rotation.xml | 26 + .../Eventreceiver_rotation_1.0.0/artifact.xml | 22 + .../Eventstore_rotation_1.0.0/artifact.xml | 22 + .../org_wso2_iot_devices_rotation.xml | 62 + .../Eventstream_rotation_1.0.0/artifact.xml | 23 + .../org.wso2.iot.devices.rotation_1.0.0.json | 20 + .../Rotation_Sensor_Script.xml | 31 + .../Rotation/Sparkscripts_1.0.0/artifact.xml | 22 + .../carbonapps/Rotation/artifacts.xml | 29 + .../EventReceiver_wordcount.xml | 27 + .../artifact.xml | 22 + .../Eventstore_wordcount_1.0.0/artifact.xml | 22 + .../org_wso2_iot_devices_wordcount.xml | 76 + .../Eventstream_wordcount_1.0.0/artifact.xml | 23 + .../org.wso2.iot.devices.wordcount_1.0.0.json | 17 + .../Sparkscripts_1.0.0/Wordcount_Script.xml | 34 + .../WordCount/Sparkscripts_1.0.0/artifact.xml | 22 + .../carbonapps/WordCount/artifacts.xml | 29 + .../pom.xml | 205 + .../impl/AndroidSenseControllerService.java | 667 ++ .../transport/AndroidSenseMQTTConnector.java | 339 + .../service/impl/util/DeviceData.java | 15 + .../service/impl/util/SensorData.java | 44 + .../src/main/webapp/META-INF/permissions.xml | 143 + .../webapp/META-INF/webapp-classloading.xml | 33 + .../src/main/webapp/WEB-INF/cxf-servlet.xml | 42 + .../src/main/webapp/WEB-INF/web.xml | 57 + .../pom.xml | 205 + .../impl/AndroidSenseManagerService.java | 166 + .../manager/service/impl/util/APIUtil.java | 36 + .../src/main/webapp/META-INF/permissions.xml | 66 + .../webapp/META-INF/webapp-classloading.xml | 33 + .../src/main/webapp/WEB-INF/cxf-servlet.xml | 41 + .../src/main/webapp/WEB-INF/web.xml | 49 + .../pom.xml | 115 + .../constants/AndroidSenseConstants.java | 53 + .../AndroidSenseDeviceMgtPluginException.java | 56 + .../plugin/impl/AndroidSenseManager.java | 256 + .../impl/AndroidSenseManagerService.java | 101 + .../plugin/impl/dao/AndroidSenseDAO.java | 123 + .../impl/dao/impl/AndroidSenseDAOImpl.java | 198 + .../impl/dao/util/AndroidSenseUtils.java | 109 + .../dao/util/DeviceSchemaInitializer.java | 50 + .../feature/AndroidSenseFeatureManager.java | 59 + ...ndroidSenseManagementServiceComponent.java | 84 + .../pom.xml | 61 + .../src/assembly/src.xml | 36 + .../device-view.hbs | 84 + .../device-view.js | 35 + .../device-view.json | 3 + .../public/images/android-sense-icon.png | Bin 0 -> 8464 bytes .../public/images/thumb.png | Bin 0 -> 7888 bytes .../public/images/android-sense-icon.png | Bin 0 -> 8464 bytes .../public/images/myDevices_analytics.png | Bin 0 -> 286419 bytes .../public/images/publishDataView.png | Bin 0 -> 36622 bytes .../public/images/registerView.png | Bin 0 -> 17868 bytes .../public/images/selectSensorView.png | Bin 0 -> 38015 bytes .../public/images/thumb.png | Bin 0 -> 7888 bytes .../public/js/download.js | 180 + .../public/js/jquery.validate.js | 1227 +++ .../type-view.hbs | 337 + .../type-view.js | 26 + .../type-view.json | 3 + .../iot-plugins/androidsense-plugin/pom.xml | 62 + .../build.xml | 38 + .../pom.xml | 91 + .../src/assembly/src.xml | 36 + .../EventReceiver_temperature.xml | 26 + .../artifact.xml | 22 + .../Eventstore_temperature_1.0.0/artifact.xml | 22 + .../org_wso2_iot_devices_temperature.xml | 62 + .../artifact.xml | 23 + ...rg.wso2.iot.devices.temperature_1.0.0.json | 20 + .../Temperature_Sensor_Script.xml | 31 + .../Sparkscripts_1.0.0/artifact.xml | 22 + .../Temperature_Sensor/artifacts.xml | 29 + .../pom.xml | 170 + .../impl/ArduinoControllerService.java | 300 + .../service/impl/dto/DeviceData.java | 35 + .../service/impl/dto/SensorData.java | 44 + .../impl/exception/ArduinoException.java | 31 + .../impl/transport/ArduinoMQTTConnector.java | 193 + .../impl/util/ArduinoServiceUtils.java | 217 + .../src/main/webapp/META-INF/permissions.xml | 87 + .../webapp/META-INF/webapp-classloading.xml | 33 + .../src/main/webapp/WEB-INF/cxf-servlet.xml | 48 + .../src/main/webapp/WEB-INF/web.xml | 62 + .../pom.xml | 180 + .../service/impl/ArduinoManagerService.java | 274 + .../manager/service/impl/util/APIUtil.java | 55 + .../src/main/webapp/META-INF/permissions.xml | 59 + .../webapp/META-INF/webapp-classloading.xml | 33 + .../src/main/webapp/WEB-INF/cxf-servlet.xml | 40 + .../src/main/webapp/WEB-INF/web.xml | 51 + .../pom.xml | 124 + .../plugin/constants/ArduinoConstants.java | 39 + .../ArduinoDeviceMgtPluginException.java | 56 + .../arduino/plugin/impl/ArduinoManager.java | 259 + .../plugin/impl/ArduinoManagerService.java | 108 + .../arduino/plugin/impl/dao/ArduinoDAO.java | 125 + .../impl/dao/impl/ArduinoDeviceDAOImpl.java | 196 + .../impl/feature/ArduinoFeatureManager.java | 59 + .../plugin/impl/util/ArduinoUtils.java | 114 + .../impl/util/DeviceSchemaInitializer.java | 50 + .../ArduinoManagementServiceComponent.java | 86 + .../pom.xml | 62 + .../src/assembly/src.xml | 36 + .../device-view.hbs | 68 + .../device-view.js | 44 + .../device-view.json | 3 + .../public/images/ardunio-icon.png | Bin 0 -> 21675 bytes .../public/images/thumb.png | Bin 0 -> 11936 bytes .../public/images/ardunio-icon.png | Bin 0 -> 21675 bytes .../public/images/myDevices_analytics.png | Bin 0 -> 147394 bytes .../public/images/schematicsGuide.png | Bin 0 -> 94571 bytes .../public/images/thumb.png | Bin 0 -> 11936 bytes .../public/js/download.js | 184 + .../public/js/jquery.validate.js | 1227 +++ .../type-view.hbs | 336 + .../type-view.json | 3 + components/iot-plugins/arduino-plugin/pom.xml | 63 + .../pom.xml | 261 + .../pom.xml | 261 + .../pom.xml | 123 + .../plugin/constants/CameraConstants.java | 27 + .../iot/camera/plugin/impl/CameraManager.java | 281 + .../plugin/impl/CameraManagerService.java | 112 + .../iot/camera/plugin/impl/dao/CameraDAO.java | 121 + .../impl/dao/impl/CameraDeviceDAOImpl.java | 237 + .../impl/util/VirtualFireAlarmUtils.java | 45 + .../CameraManagementServiceComponent.java | 98 + .../pom.xml | 62 + components/iot-plugins/camera-plugin/pom.xml | 63 + .../pom.xml | 243 + .../api/DigitalDisplayControllerService.java | 432 + .../controller/api/dto/DeviceJSON.java | 33 + .../exception/DigitalDisplayException.java | 44 + .../controller/api/model/ScreenShotModel.java | 33 + .../api/util/DigitalDisplayMQTTConnector.java | 185 + ...DigitalDisplayWebSocketServerEndPoint.java | 64 + .../webapp/META-INF/webapp-classloading.xml | 33 + .../src/main/webapp/WEB-INF/cxf-servlet.xml | 44 + .../src/main/webapp/WEB-INF/web.xml | 77 + .../pom.xml | 246 + .../api/DigitalDisplayManagerService.java | 243 + .../manager/api/util/APIUtil.java | 54 + .../src/main/webapp/META-INF/permissions.xml | 66 + .../webapp/META-INF/webapp-classloading.xml | 33 + .../src/main/webapp/WEB-INF/cxf-servlet.xml | 38 + .../src/main/webapp/WEB-INF/web.xml | 68 + .../pom.xml | 106 + .../constants/DigitalDisplayConstants.java | 37 + ...igitalDisplayDeviceMgtPluginException.java | 56 + .../plugin/impl/DigitalDisplayManager.java | 251 + .../impl/DigitalDisplayManagerService.java | 101 + .../plugin/impl/dao/DigitalDisplayDAO.java | 126 + .../dao/impl/DigitalDisplayDeviceDAOImpl.java | 194 + .../feature/DigitalDisplayFeatureManager.java | 59 + .../impl/util/DeviceSchemaInitializer.java | 50 + .../plugin/impl/util/DigitalDisplayUtils.java | 112 + ...italDisplayManagementServiceComponent.java | 85 + .../pom.xml | 62 + .../src/assembly/src.xml | 36 + .../device-view.hbs | 98 + .../device-view.js | 40 + .../device-view.json | 3 + .../public/images/default-screen.png | Bin 0 -> 55235 bytes .../public/images/display-icon.png | Bin 0 -> 4963 bytes .../public/images/thumb.png | Bin 0 -> 6246 bytes .../public/js/websocket.js | 81 + .../public/images/display-icon.png | Bin 0 -> 4963 bytes .../public/images/myDevices_analytics.png | Bin 0 -> 195921 bytes .../public/images/operations/add-resource.png | Bin 0 -> 4108 bytes .../images/operations/edit-sequence.png | Bin 0 -> 8099 bytes .../images/operations/get-content-list.png | Bin 0 -> 7366 bytes .../images/operations/get-device-status.png | Bin 0 -> 3589 bytes .../images/operations/remove-resource.png | Bin 0 -> 3929 bytes .../images/operations/restart-browser.png | Bin 0 -> 3305 bytes .../images/operations/restart-display.png | Bin 0 -> 5834 bytes .../images/operations/restart-server.png | Bin 0 -> 3293 bytes .../public/images/operations/screenshot.png | Bin 0 -> 8786 bytes .../images/operations/terminate-display.png | Bin 0 -> 4346 bytes .../images/operations/upload-content.png | Bin 0 -> 4551 bytes .../public/images/schematicsGuide.png | Bin 0 -> 393114 bytes .../public/images/thumb.png | Bin 0 -> 6246 bytes .../public/js/download.js | 191 + .../public/js/jquery.validate.js | 1227 +++ .../type-view.hbs | 292 + .../type-view.json | 3 + .../digital-display-plugin/pom.xml | 61 + .../pom.xml | 254 + .../api/impl/DroneControllerService.java | 82 + .../api/impl/DroneRealTimeService.java | 124 + .../exception/DroneAnalyzerException.java | 31 + .../transport/DroneAnalyzerXMPPConnector.java | 160 + .../impl/trasformer/MessageTransformer.java | 134 + .../impl/util/DroneAnalyzerServiceUtils.java | 96 + .../webapp/META-INF/webapp-classloading.xml | 33 + .../src/main/webapp/WEB-INF/cxf-servlet.xml | 40 + .../src/main/webapp/WEB-INF/web.xml | 49 + .../pom.xml | 261 + .../manager/api/impl/DroneManagerService.java | 319 + .../manager/api/impl/util/APIUtil.java | 55 + .../webapp/META-INF/webapp-classloading.xml | 33 + .../src/main/webapp/WEB-INF/cxf-servlet.xml | 40 + .../src/main/webapp/WEB-INF/web.xml | 62 + .../pom.xml | 108 + .../plugin/constants/DroneConstants.java | 41 + .../plugin/constants/MessageConfig.java | 40 + .../plugin/controller/DroneController.java | 32 + .../controller/impl/DroneControllerImpl.java | 73 + ...DroneAnalyzerDeviceMgtPluginException.java | 56 + .../plugin/impl/DroneAnalyzerManager.java | 250 + .../impl/DroneAnalyzerManagerService.java | 106 + .../plugin/impl/dao/DroneAnalyzerDAO.java | 126 + .../dao/impl/DroneAnalyzerDeviceDAOImpl.java | 194 + .../feature/DroneAnalyzerFeatureManager.java | 58 + .../impl/util/DeviceSchemaInitializer.java | 50 + .../plugin/impl/util/DroneAnalyzerUtils.java | 108 + ...oneAnalyzerManagementServiceComponent.java | 89 + .../pom.xml | 62 + .../src/assembly/src.xml | 36 + .../device-view.hbs | 52 + .../device-view.js | 34 + .../device-view.json | 3 + .../public/images/drone-icon.png | Bin 0 -> 46940 bytes .../public/images/thumb.png | Bin 0 -> 46940 bytes .../public/images/devices_analytics.png | Bin 0 -> 114202 bytes .../public/images/drone-icon.png | Bin 0 -> 46940 bytes .../public/images/schematicsGuide.png | Bin 0 -> 151933 bytes .../public/images/thumb.png | Bin 0 -> 48636 bytes .../public/js/download.js | 184 + .../public/js/jquery.validate.js | 1227 +++ .../type-view.hbs | 315 + .../type-view.json | 3 + .../public/css/main-app.css | 220 + .../background_drone.png | Bin 0 -> 26787 bytes .../direction_drone.png | Bin 0 -> 11617 bytes .../drone_position_controller/pitch_drone.png | Bin 0 -> 20046 bytes .../public/images/no_video_preview.gif | Bin 0 -> 2111 bytes .../3dobject_controller/3dObjectControler.js | 206 + .../js/3dobject_controller/Coordinates.js | 166 + .../OrbitAndPanControls.js | 549 + .../js/3dobject_controller/three.min.js | 724 ++ .../public/js/common/ajax_handler.js | 50 + .../public/js/common/general_handler | 101 + .../public/js/common/websocket_api | 60 + .../public/js/config/config.js | 37 + .../public/js/d3.min.js | 23 + .../public/js/download.js | 174 + .../public/js/initJs | 21 + .../public/js/jQueryRotate.js | 357 + .../public/js/jquery.validate.js | 1227 +++ .../public/js/mainHandler | 153 + .../public/js/modules/controller.js | 29 + .../public/js/modules/flight_dynamics.js | 77 + .../public/js/modules/realtime_plotting | 148 + .../statistics.hbs | 250 + .../statistics.js | 25 + .../statistics.json | 3 + .../iot-plugins/drone-analyzer-plugin/pom.xml | 61 + .../pom.xml | 139 + .../src/main/java/log4j.properties | 8 + .../device/mgt/iot/api/DeviceUsageDTO.java | 42 + .../mgt/iot/api/DevicesManagerService.java | 641 ++ .../mgt/iot/api/GroupManagerService.java | 584 + .../mgt/iot/api/PolicyManagementService.java | 507 + .../mgt/iot/api/StatsManagerService.java | 103 + .../device/mgt/iot/util/ResponsePayload.java | 108 + .../src/main/webapp/META-INF/permissions.xml | 113 + .../webapp/META-INF/webapp-classloading.xml | 33 + .../src/main/webapp/WEB-INF/cxf-servlet.xml | 79 + .../src/main/webapp/WEB-INF/web.xml | 27 + .../org.wso2.carbon.device.mgt.iot.ui/pom.xml | 62 + .../src/assembly/src.xml | 36 + .../devicemgt/app/conf/app-conf.json | 35 + .../public/js/validate-register.js | 163 + .../app/pages/iot.page.register/register.hbs | 75 + .../app/pages/iot.page.register/register.js | 38 + .../app/pages/iot.page.register/register.json | 6 + .../app/pages/iot.page.sign-in/sign-in.hbs | 5 + .../app/pages/iot.page.sign-in/sign-in.json | 4 + .../operation-bar.hbs | 75 + .../operation-bar.js | 31 + .../operation-bar.json | 3 + .../public/js/operation-bar.js | 56 + .../public/css/graph.css | 471 + .../public/images/map-marker-1.png | Bin 0 -> 2824 bytes .../public/images/map-marker-2.png | Bin 0 -> 2877 bytes .../iot.unit.device.stats/public/js/d3.min.js | 9488 +++++++++++++++++ .../public/js/device-stats.js | 247 + .../public/js/moment.min.js | 25 + .../public/js/rickshaw.min.js | 21 + .../app/units/iot.unit.device.stats/stats.hbs | 29 + .../app/units/iot.unit.device.stats/stats.js | 32 + .../units/iot.unit.device.stats/stats.json | 3 + .../configuration.hbs | 492 + .../configuration.json | 4 + .../public/js/platform-configuration.js | 856 ++ .../app/units/iot.unit.policy.edit/edit.hbs | 246 + .../app/units/iot.unit.policy.edit/edit.js | 25 + .../app/units/iot.unit.policy.edit/edit.json | 3 + .../public/css/codemirror.css | 342 + .../public/js/codemirror.js | 8720 +++++++++++++++ .../public/js/policy-edit.js | 729 ++ .../iot.unit.policy.edit/public/js/sql.js | 310 + .../public/css/codemirror.css | 342 + .../public/js/codemirror.js | 8720 +++++++++++++++ .../public/js/policy-view.js | 128 + .../iot.unit.policy.view/public/js/sql.js | 310 + .../app/units/iot.unit.policy.view/view.hbs | 95 + .../app/units/iot.unit.policy.view/view.js | 25 + .../app/units/iot.unit.policy.view/view.json | 3 + .../public/css/codemirror.css | 342 + .../public/js/codemirror.js | 8720 +++++++++++++++ .../public/js/policy-create.js | 670 ++ .../iot.unit.policy.wizard/public/js/sql.js | 310 + .../units/iot.unit.policy.wizard/wizard.hbs | 255 + .../units/iot.unit.policy.wizard/wizard.js | 47 + .../units/iot.unit.policy.wizard/wizard.json | 3 + .../units/iot.unit.ui.header.logo/logo.hbs | 1 + .../units/iot.unit.ui.header.logo/logo.json | 5 + .../org.wso2.carbon.device.mgt.iot/pom.xml | 213 + .../DeviceManagementConfigurationManager.java | 118 + .../server/datasource/ControlQueue.java | 229 + .../datasource/ControlQueuesConfig.java | 82 + .../DeviceManagementConfiguration.java | 74 + .../server/datasource/ObjectFactory.java | 69 + .../mgt/iot/controlqueue/mqtt/MqttConfig.java | 81 + .../iot/controlqueue/xmpp/XmppAccount.java | 60 + .../mgt/iot/controlqueue/xmpp/XmppConfig.java | 93 + .../controlqueue/xmpp/XmppServerClient.java | 352 + .../exception/DeviceControllerException.java | 42 + .../mgt/iot/exception/IoTException.java | 43 + .../exception/NotImplementedException.java | 43 + .../iot/exception/UnauthorizedException.java | 39 + .../mgt/iot/internal/IoTCommonDataHolder.java | 54 + .../IotDeviceManagementServiceComponent.java | 100 + .../device/mgt/iot/mqtt/PolicyPush.java | 66 + .../mgt/iot/sensormgt/DeviceRecord.java | 43 + .../mgt/iot/sensormgt/SensorDataManager.java | 137 + .../mgt/iot/sensormgt/SensorRecord.java | 46 + .../iot/service/IoTServerStartupListener.java | 41 + .../mgt/iot/transport/CommunicationUtils.java | 219 + .../mgt/iot/transport/TransportHandler.java | 108 + .../transport/TransportHandlerException.java | 56 + .../mgt/iot/transport/TransportUtils.java | 296 + .../transport/mqtt/MQTTTransportHandler.java | 402 + .../transport/xmpp/XMPPTransportHandler.java | 376 + .../carbon/device/mgt/iot/util/IoTUtil.java | 127 + .../mgt/iot/util/IotDeviceManagementUtil.java | 346 + .../device/mgt/iot/util/ZipArchive.java | 49 + .../carbon/device/mgt/iot/util/ZipUtil.java | 100 + .../iot-plugins/iot-base-plugin/pom.xml | 60 + components/iot-plugins/pom.xml | 67 + .../build.xml | 38 + .../pom.xml | 92 + .../src/assembly/src.xml | 36 + .../EventReceiver_temperature.xml | 26 + .../artifact.xml | 22 + .../Eventstore_temperature_1.0.0/artifact.xml | 22 + .../org_wso2_iot_devices_temperature.xml | 62 + .../artifact.xml | 23 + ...rg.wso2.iot.devices.temperature_1.0.0.json | 20 + .../Temperature_Sensor_Script.xml | 31 + .../Sparkscripts_1.0.0/artifact.xml | 22 + .../Temperature_Sensor/artifacts.xml | 29 + .../pom.xml | 158 + .../impl/RaspberryPiControllerService.java | 295 + .../service/impl/dto/DeviceData.java | 36 + .../service/impl/dto/SensorData.java | 44 + .../impl/exception/RaspberrypiException.java | 31 + .../transport/RaspberryPiMQTTConnector.java | 195 + .../impl/util/RaspberrypiServiceUtils.java | 228 + .../webapp/META-INF/webapp-classloading.xml | 33 + .../src/main/webapp/WEB-INF/cxf-servlet.xml | 45 + .../src/main/webapp/WEB-INF/web.xml | 54 + .../pom.xml | 166 + .../impl/RaspberryPiManagerService.java | 323 + .../manager/service/impl/util/APIUtil.java | 55 + .../webapp/META-INF/webapp-classloading.xml | 33 + .../src/main/webapp/WEB-INF/cxf-servlet.xml | 43 + .../src/main/webapp/WEB-INF/web.xml | 50 + .../pom.xml | 124 + .../constants/RaspberrypiConstants.java | 39 + .../RaspberrypiDeviceMgtPluginException.java | 56 + .../plugin/impl/RaspberrypiManager.java | 252 + .../impl/RaspberrypiManagerService.java | 108 + .../plugin/impl/dao/RaspberrypiDAO.java | 124 + .../dao/impl/RaspberrypiDeviceDAOImpl.java | 191 + .../feature/RaspberrypiFeatureManager.java | 58 + .../impl/util/DeviceSchemaInitializer.java | 50 + .../plugin/impl/util/RaspberrypiUtils.java | 111 + ...RaspberrypiManagementServiceComponent.java | 85 + .../pom.xml | 62 + .../src/assembly/src.xml | 36 + .../device-view.hbs | 68 + .../device-view.js | 35 + .../device-view.json | 3 + .../public/images/respberry-icon.png | Bin 0 -> 40558 bytes .../public/images/thumb.png | Bin 0 -> 44816 bytes .../public/images/myDevices_analytics.png | Bin 0 -> 220158 bytes .../public/images/respberry-icon.png | Bin 0 -> 40558 bytes .../public/images/schematicsGuide.png | Bin 0 -> 142048 bytes .../public/images/thumb.png | Bin 0 -> 44816 bytes .../public/js/download.js | 183 + .../public/js/jquery.validate.js | 1227 +++ .../type-view.hbs | 324 + .../type-view.json | 3 + .../iot-plugins/raspberrypi-plugin/pom.xml | 62 + .../pom.xml | 255 + .../agent/advanced/Bootstrap.java | 36 + .../http/FireAlarmHTTPCommunicator.java | 495 + .../mqtt/FireAlarmMQTTCommunicator.java | 294 + .../xmpp/FireAlarmXMPPCommunicator.java | 265 + .../advanced/core/AgentConfiguration.java | 155 + .../agent/advanced/core/AgentConstants.java | 135 + .../agent/advanced/core/AgentManager.java | 377 + .../advanced/core/AgentUtilOperations.java | 371 + .../enrollment/EnrollmentManager.java | 440 + .../AgentCoreOperationException.java | 57 + .../agent/advanced/sidhdhi/SidhdhiQuery.java | 205 + .../transport/CommunicationUtils.java | 226 + .../advanced/transport/TransportHandler.java | 47 + .../transport/TransportHandlerException.java | 56 + .../advanced/transport/TransportUtils.java | 302 + .../transport/http/HTTPTransportHandler.java | 91 + .../transport/mqtt/MQTTTransportHandler.java | 360 + .../transport/xmpp/XMPPTransportHandler.java | 366 + .../virtual/VirtualHardwareManager.java | 218 + .../agent/advanced/virtual/ui/AgentUI.java | 1085 ++ .../src/main/resources/alarm-off.gif | Bin 0 -> 4265 bytes .../src/main/resources/alarm-on.gif | Bin 0 -> 12067 bytes .../src/main/resources/cep_query.txt | 11 + .../main/resources/deviceConfig.properties | 33 + .../src/main/resources/fireAlarmSound.mid | Bin 0 -> 6719 bytes .../src/main/ui/build.xml | 73 + .../src/main/ui/manifest.mf | 3 + .../src/main/ui/nbproject/build-impl.xml | 1413 +++ .../src/main/ui/nbproject/genfiles.properties | 8 + .../ui/nbproject/private/private.properties | 2 + .../src/main/ui/nbproject/private/private.xml | 9 + .../src/main/ui/nbproject/project.properties | 73 + .../src/main/ui/nbproject/project.xml | 15 + .../src/main/ui/src/bulb-on.jpg | Bin 0 -> 6942 bytes .../mgt/iot/agent/virtual/VirtualAgentUI.java | 37 + .../mgt/iot/agent/virtual/ui/AgentUI.form | 830 ++ .../mgt/iot/agent/virtual/ui/AgentUI.java | 758 ++ .../pom.xml | 232 + .../iot/virtualfirealarm/agent/Bootstrap.java | 36 + .../http/FireAlarmHTTPCommunicator.java | 495 + .../mqtt/FireAlarmMQTTCommunicator.java | 265 + .../xmpp/FireAlarmXMPPCommunicator.java | 265 + .../agent/core/AgentConfiguration.java | 155 + .../agent/core/AgentConstants.java | 109 + .../agent/core/AgentManager.java | 347 + .../agent/core/AgentUtilOperations.java | 280 + .../agent/enrollment/EnrollmentManager.java | 436 + .../AgentCoreOperationException.java | 57 + .../agent/transport/CommunicationUtils.java | 225 + .../agent/transport/TransportHandler.java | 47 + .../transport/TransportHandlerException.java | 56 + .../agent/transport/TransportUtils.java | 302 + .../transport/http/HTTPTransportHandler.java | 91 + .../transport/mqtt/MQTTTransportHandler.java | 360 + .../transport/xmpp/XMPPTransportHandler.java | 366 + .../agent/virtual/VirtualHardwareManager.java | 213 + .../agent/virtual/ui/AgentUI.java | 954 ++ .../src/main/resources/alarm-off.gif | Bin 0 -> 4265 bytes .../src/main/resources/alarm-on.gif | Bin 0 -> 12067 bytes .../main/resources/deviceConfig.properties | 33 + .../src/main/resources/fireAlarmSound.mid | Bin 0 -> 6719 bytes .../src/main/ui/build.xml | 73 + .../src/main/ui/manifest.mf | 3 + .../src/main/ui/nbproject/build-impl.xml | 1413 +++ .../src/main/ui/nbproject/genfiles.properties | 8 + .../ui/nbproject/private/private.properties | 2 + .../src/main/ui/nbproject/private/private.xml | 9 + .../src/main/ui/nbproject/project.properties | 73 + .../src/main/ui/nbproject/project.xml | 15 + .../src/main/ui/src/bulb-on.jpg | Bin 0 -> 6942 bytes .../mgt/iot/agent/virtual/VirtualAgentUI.java | 37 + .../mgt/iot/agent/virtual/ui/AgentUI.form | 803 ++ .../mgt/iot/agent/virtual/ui/AgentUI.java | 744 ++ .../build.xml | 38 + .../pom.xml | 92 + .../src/assembly/src.xml | 36 + .../EventReceiver_temperature.xml | 26 + .../artifact.xml | 22 + .../Eventstore_temperature_1.0.0/artifact.xml | 22 + .../org_wso2_iot_devices_temperature.xml | 62 + .../artifact.xml | 23 + ...rg.wso2.iot.devices.temperature_1.0.0.json | 20 + .../Temperature_Sensor_Script.xml | 31 + .../Sparkscripts_1.0.0/artifact.xml | 22 + .../Temperature_Sensor/artifacts.xml | 29 + .../pom.xml | 262 + .../VirtualFireAlarmControllerService.java | 614 ++ .../service/impl/dto/DeviceData.java | 36 + .../service/impl/dto/SensorData.java | 44 + .../exception/VirtualFireAlarmException.java | 31 + .../VirtualFireAlarmMQTTConnector.java | 309 + .../VirtualFireAlarmXMPPConnector.java | 293 + .../service/impl/util/SecurityManager.java | 254 + .../util/VirtualFireAlarmServiceUtils.java | 366 + .../service/impl/util/scep/ContentType.java | 26 + .../service/impl/util/scep/SCEPOperation.java | 39 + .../webapp/META-INF/webapp-classloading.xml | 33 + .../src/main/webapp/WEB-INF/cxf-servlet.xml | 52 + .../src/main/webapp/WEB-INF/web.xml | 65 + .../pom.xml | 271 + .../impl/VirtualFireAlarmManagerService.java | 312 + .../manager/service/impl/util/APIUtil.java | 55 + .../webapp/META-INF/webapp-classloading.xml | 33 + .../src/main/webapp/WEB-INF/cxf-servlet.xml | 39 + .../src/main/webapp/WEB-INF/web.xml | 65 + .../pom.xml | 124 + .../constants/VirtualFireAlarmConstants.java | 40 + ...tualFirealarmDeviceMgtPluginException.java | 56 + .../plugin/impl/VirtualFireAlarmManager.java | 256 + .../impl/VirtualFireAlarmManagerService.java | 107 + .../plugin/impl/dao/VirtualFireAlarmDAO.java | 125 + .../impl/VirtualFireAlarmDeviceDAOImpl.java | 196 + .../VirtualFirealarmFeatureManager.java | 58 + .../impl/util/DeviceSchemaInitializer.java | 50 + .../impl/util/VirtualFireAlarmUtils.java | 111 + ...alFirealarmManagementServiceComponent.java | 84 + .../pom.xml | 62 + .../src/assembly/src.xml | 36 + .../device-view.hbs | 105 + .../device-view.js | 35 + .../device-view.json | 3 + .../public/images/firealarm-icon.png | Bin 0 -> 28584 bytes .../public/images/thumb.png | Bin 0 -> 31021 bytes .../policy-edit.hbs | 1 + .../policy-edit.json | 3 + .../policy-view.hbs | 1 + .../policy-view.json | 3 + .../policy-wizard.hbs | 1 + .../policy-wizard.json | 3 + .../public/images/firealarm-icon.png | Bin 0 -> 28584 bytes .../public/images/myDevices_analytics.png | Bin 0 -> 759452 bytes .../public/images/schematicsGuide.png | Bin 0 -> 52343 bytes .../public/images/thumb.png | Bin 0 -> 31021 bytes .../public/js/download.js | 184 + .../public/js/jquery.validate.js | 1227 +++ .../type-view.hbs | 297 + .../type-view.json | 3 + .../virtual-fire-alarm-plugin/pom.xml | 64 + .../pom.xml | 83 - .../key/mgt/handler/valve/HandlerUtil.java | 141 - .../valve/OAuthTokenValidatorValve.java | 144 - .../pom.xml | 180 + .../android/ConfigurationMgtService.java | 161 + .../android/DeviceManagementService.java | 183 + .../services/android/EnrollmentService.java | 153 + .../services/android/OperationMgtService.java | 931 ++ .../services/android/PolicyMgtService.java | 92 + .../android/bean/AndroidOperation.java | 36 + .../android/bean/ApplicationInstallation.java | 55 + .../bean/ApplicationUninstallation.java | 64 + .../android/bean/BlacklistApplications.java | 39 + .../mdm/services/android/bean/Camera.java | 37 + .../android/bean/DeviceEncryption.java | 37 + .../services/android/bean/Disenrollment.java | 35 + .../mdm/services/android/bean/LockCode.java | 37 + .../services/android/bean/Notification.java | 37 + .../services/android/bean/PasscodePolicy.java | 91 + .../mdm/services/android/bean/WebClip.java | 55 + .../mdm/services/android/bean/Wifi.java | 46 + .../mdm/services/android/bean/WipeData.java | 37 + .../ApplicationInstallationBeanWrapper.java | 47 + .../ApplicationUninstallationBeanWrapper.java | 47 + .../BlacklistApplicationsBeanWrapper.java | 47 + .../bean/wrapper/CameraBeanWrapper.java | 47 + .../wrapper/DisenrollmentBeanWrapper.java | 44 + .../bean/wrapper/EncryptionBeanWrapper.java | 47 + .../bean/wrapper/LockCodeBeanWrapper.java | 47 + .../bean/wrapper/NotificationBeanWrapper.java | 47 + .../wrapper/PasswordPolicyBeanWrapper.java | 47 + .../bean/wrapper/WebClipBeanWrapper.java | 47 + .../android/bean/wrapper/WifiBeanWrapper.java | 47 + .../bean/wrapper/WipeDataBeanWrapper.java | 47 + .../services/android/common/ErrorHandler.java | 35 + .../services/android/common/ErrorMessage.java | 41 + .../common/GsonMessageBodyHandler.java | 97 + .../exception/AndroidAgentException.java | 63 + .../exception/AndroidOperationException.java | 37 + .../exception/BadRequestException.java | 36 + .../OperationConfigurationException.java | 37 + .../android/util/AndroidAPIUtils.java | 215 + .../android/util/AndroidConstants.java | 114 + .../android/util/AndroidDeviceUtils.java | 98 + .../services/android/util/DeviceIDHolder.java | 48 + .../mdm/services/android/util/Message.java | 88 + .../src/main/webapp/META-INF/permissions.xml | 315 + .../webapp/META-INF/webapp-classloading.xml | 35 + .../src/main/webapp/WEB-INF/cxf-servlet.xml | 80 + .../src/main/webapp/WEB-INF/web.xml | 77 + .../src/main/webapp/servicelist.css | 117 + .../pom.xml | 176 + .../impl/AndroidDeviceManagementService.java | 109 + .../android/impl/AndroidDeviceManager.java | 356 + .../android/impl/AndroidFeatureManager.java | 244 + .../impl/AndroidPolicyMonitoringService.java | 100 + .../android/impl/dao/AndroidDAOFactory.java | 120 + .../AndroidFeatureManagementDAOException.java | 79 + .../impl/dao/impl/AndroidDeviceDAOImpl.java | 265 + .../impl/dao/impl/AndroidFeatureDAOImpl.java | 285 + .../mobile/android/impl/gcm/GCMResult.java | 53 + .../mobile/android/impl/gcm/GCMService.java | 64 + .../mgt/mobile/android/impl/gcm/GCMUtil.java | 204 + .../impl/util/AndroidPluginConstants.java | 47 + .../android/impl/util/AndroidPluginUtils.java | 58 + .../android/impl/util/AndroidUtils.java | 31 + .../AndroidDeviceManagementDataHolder.java | 67 + ...droidDeviceManagementServiceComponent.java | 137 + .../mobile-plugins/android-plugin/pom.xml | 59 + .../pom.xml | 205 + .../wso2/carbon/mdm/api/Authentication.java | 36 + .../wso2/carbon/mdm/api/Configuration.java | 98 + .../carbon/mdm/api/DeviceNotification.java | 108 + .../java/org/wso2/carbon/mdm/api/Feature.java | 62 + .../java/org/wso2/carbon/mdm/api/License.java | 99 + .../org/wso2/carbon/mdm/api/MobileDevice.java | 218 + .../org/wso2/carbon/mdm/api/Operation.java | 225 + .../java/org/wso2/carbon/mdm/api/Policy.java | 475 + .../java/org/wso2/carbon/mdm/api/Profile.java | 89 + .../java/org/wso2/carbon/mdm/api/Role.java | 422 + .../java/org/wso2/carbon/mdm/api/User.java | 786 ++ .../carbon/mdm/api/common/ErrorHandler.java | 32 + .../carbon/mdm/api/common/ErrorMessage.java} | 23 +- .../mdm/api/common/MDMAPIException.java | 59 + .../api/context/DeviceOperationContext.java | 54 + .../wso2/carbon/mdm/api/util/MDMAPIUtils.java | 255 + .../mdm/api/util/MDMAndroidOperationUtil.java | 116 + .../carbon/mdm/api/util/MDMAppConstants.java | 55 + .../mdm/api/util/MDMIOSOperationUtil.java | 106 + .../carbon/mdm/api/util/ResponsePayload.java | 106 + .../carbon/mdm/beans/ApplicationWrapper.java | 63 + .../org/wso2/carbon/mdm/beans/MobileApp.java | 130 + .../carbon/mdm/beans/MobileAppTypes.java} | 12 +- .../wso2/carbon/mdm/beans/PolicyWrapper.java | 126 + .../beans/PriorityUpdatedPolicyWrapper.java | 41 + .../org/wso2/carbon/mdm/beans/Profile.java | 107 + .../wso2/carbon/mdm/beans/ProfileFeature.java | 85 + .../wso2/carbon/mdm/beans/RoleWrapper.java | 59 + .../mdm/beans/UserCredentialWrapper.java | 53 + .../wso2/carbon/mdm/beans/UserWrapper.java | 87 + .../beans/android/AppStoreApplication.java | 52 + .../beans/android/EnterpriseApplication.java | 61 + .../mdm/beans/android/WebApplication.java | 61 + .../mdm/beans/ios/AppStoreApplication.java | 86 + .../mdm/beans/ios/EnterpriseApplication.java | 83 + .../mdm/beans/ios/RemoveApplication.java | 24 + .../wso2/carbon/mdm/beans/ios/WebClip.java | 60 + .../org/wso2/carbon/mdm/util/Constants.java | 30 + .../org/wso2/carbon/mdm/util/MDMUtil.java | 60 + .../mdm/util/SetReferenceTransformer.java | 60 + .../java/org/wso2/mdm/common/Application.java | 119 + .../mdm/common/GsonMessageBodyHandler.java | 94 + .../src/main/webapp/META-INF/permissions.xml | 565 + .../webapp/META-INF/webapp-classloading.xml | 35 + .../src/main/webapp/WEB-INF/cxf-servlet.xml | 144 + .../src/main/webapp/WEB-INF/web.xml | 74 + .../pom.xml | 62 + .../src/assembly/src.xml | 36 + .../configuration.hbs | 5 + .../configuration.json | 5 + .../operation-bar.hbs | 7 + .../operation-bar.json | 4 + .../public/js/operation-bar.js | 195 + .../templates/hidden-operations-android.hbs | 304 + .../templates/hidden-operations-ios.hbs | 366 + .../public/templates/operations.hbs | 156 + .../operation-mod.hbs | 3 + .../operation-mod.json | 4 + .../public/js/operation-mod.js | 1228 +++ .../configuration.hbs | 484 + .../configuration.js | 9 + .../configuration.json | 4 + .../public/js/platform-configuration.js | 878 ++ .../app/units/mdm.unit.policy.edit/edit.hbs | 223 + .../app/units/mdm.unit.policy.edit/edit.js | 26 + .../app/units/mdm.unit.policy.edit/edit.json | 3 + .../public/js/policy-create.js | 2321 ++++ .../templates/hidden-operations-android.hbs | 457 + .../templates/hidden-operations-ios.hbs | 2923 +++++ .../templates/hidden-operations-windows.hbs | 460 + .../public/js/policy-view.js | 2256 ++++ .../templates/hidden-operations-android.hbs | 439 + .../templates/hidden-operations-ios.hbs | 2923 +++++ .../templates/hidden-operations-windows.hbs | 460 + .../app/units/mdm.unit.policy.view/view.hbs | 87 + .../app/units/mdm.unit.policy.view/view.js | 25 + .../app/units/mdm.unit.policy.view/view.json | 3 + .../public/js/policy-create.js | 2355 ++++ .../templates/hidden-operations-android.hbs | 458 + .../templates/hidden-operations-ios.hbs | 2923 +++++ .../templates/hidden-operations-windows.hbs | 460 + .../units/mdm.unit.policy.wizard/wizard.hbs | 278 + .../units/mdm.unit.policy.wizard/wizard.js | 37 + .../units/mdm.unit.policy.wizard/wizard.json | 3 + .../units/mdm.unit.ui.header.logo/logo.hbs | 1 + .../units/mdm.unit.ui.header.logo/logo.json | 5 + .../pom.xml | 81 + .../url/printer/URLPrinterStartupHandler.java | 64 + .../internal/URLPrinterDataHolder.java | 42 + ...PrinterStartupHandlerServiceComponent.java | 73 + .../org.wso2.carbon.device.mgt.mobile/pom.xml | 172 + .../AbstractMobileOperationManager.java | 41 + .../device/mgt/mobile/DataSourceListener.java | 25 + .../DataSourceNotAvailableException.java | 52 + .../MobileDeviceMgtPluginException.java | 57 + .../mobile/common/MobilePluginConstants.java | 29 + .../MobileDeviceConfigurationManager.java | 79 + .../config/MobileDeviceManagementConfig.java | 42 + .../MobileDeviceManagementRepository.java | 57 + .../datasource/DataSourceConfigAdapter.java | 51 + .../datasource/JNDILookupDefinition.java | 79 + .../datasource/MobileDataSourceConfig.java | 51 + .../MobileDataSourceConfigurations.java | 41 + ...tractMobileDeviceManagementDAOFactory.java | 102 + .../mgt/mobile/dao/MobileDeviceDAO.java | 74 + .../MobileDeviceManagementDAOException.java | 80 + .../dao/MobileDeviceManagementDAOFactory.java | 27 + .../mgt/mobile/dao/MobileFeatureDAO.java | 110 + .../mobile/dao/impl/MobileFeatureDAOImpl.java | 336 + .../util/MobileDeviceManagementDAOUtil.java | 139 + .../device/mgt/mobile/dto/MobileDevice.java | 125 + .../dto/MobileDeviceOperationMapping.java | 86 + .../device/mgt/mobile/dto/MobileFeature.java | 73 + .../mgt/mobile/dto/MobileFeatureProperty.java | 45 + .../mgt/mobile/dto/MobileOperation.java | 64 + .../mobile/dto/MobileOperationProperty.java | 54 + .../MobileDeviceManagementDataHolder.java | 48 + ...obileDeviceManagementServiceComponent.java | 137 + ...bileDeviceManagementSchemaInitializer.java | 62 + .../util/MobileDeviceManagementUtil.java | 320 + .../MobileDeviceManagementConfigTests.java | 141 + .../mgt/mobile/impl/common/DBTypes.java | 29 + .../impl/common/TestDBConfiguration.java | 90 + .../impl/common/TestDBConfigurations.java | 39 + .../impl/dao/util/MobileDatabaseUtils.java | 111 + .../malformed-mobile-config-no-api-config.xml | 41 + ...-mobile-config-no-api-publisher-config.xml | 42 + ...malformed-mobile-config-no-apis-config.xml | 42 + .../malformed-mobile-config-no-ds-config.xml | 42 + ...malformed-mobile-config-no-jndi-config.xml | 42 + .../malformed-mobile-config-no-mgt-repo.xml | 42 + .../MobileDeviceManagementConfigSchema.xsd | 69 + .../src/test/resources/log4j.properties | 33 + .../src/test/resources/sql/h2.sql | 54 + .../src/test/resources/testdbconfig.xml | 24 + .../src/test/resources/testng.xml | 27 + .../mobile-plugins/mobile-base-plugin/pom.xml | 62 + .../{key-mgt => mobile-plugins}/pom.xml | 17 +- .../pom.xml | 309 + .../windows/common/PluginConstants.java | 293 + .../windows/common/SyncmlCommandType.java | 34 + .../windows/common/beans/CacheEntry.java | 52 + .../windows/common/beans/Token.java | 35 + .../common/beans/WindowsPluginProperties.java | 92 + .../exceptions/AuthenticationException.java | 58 + .../exceptions/BadRequestException.java | 36 + .../CertificateGenerationException.java | 58 + .../exceptions/FileOperationException.java | 58 + .../KeyStoreGenerationException.java | 58 + .../common/exceptions/MDMAPIException.java | 60 + .../SyncmlMessageFormatException.java | 57 + .../exceptions/SyncmlOperationException.java | 58 + .../exceptions/WAPProvisioningException.java | 58 + .../WindowsConfigurationException.java | 61 + .../WindowsDeviceEnrolmentException.java | 59 + .../WindowsOperationsException.java | 37 + .../common/util/AuthenticationInfo.java | 62 + .../windows/common/util/BSTValidator.java | 162 + .../ConfigInitializerContextListener.java | 138 + .../windows/common/util/DeviceIDHolder.java | 48 + .../windows/common/util/DeviceUtil.java | 81 + .../windows/common/util/ErrorHandler.java | 38 + .../windows/common/util/ErrorMessage.java | 39 + .../common/util/GsonMessageBodyHandler.java | 98 + .../windows/common/util/Message.java | 49 + .../common/util/SOAPSecurityHandler.java | 63 + .../common/util/UsernameTokenValidator.java | 134 + .../windows/common/util/WindowsAPIUtils.java | 210 + .../common/util/WindowsDeviceUtils.java | 85 + .../windows/operations/AddTag.java | 68 + .../windows/operations/Alert.java | 63 + .../windows/operations/AtomicTag.java | 85 + .../windows/operations/ChallengeTag.java | 46 + .../windows/operations/Credential.java | 60 + .../windows/operations/DeleteTag.java | 68 + .../windows/operations/ExecuteTag.java | 69 + .../windows/operations/Get.java | 71 + .../windows/operations/Item.java | 90 + .../windows/operations/MetaTag.java | 88 + .../windows/operations/Replace.java | 70 + .../windows/operations/Results.java | 96 + .../windows/operations/SequenceTag.java | 117 + .../windows/operations/Source.java | 63 + .../windows/operations/Status.java | 144 + .../windows/operations/SyncmlBody.java | 144 + .../windows/operations/SyncmlDocument.java | 55 + .../windows/operations/SyncmlHeader.java | 114 + .../windows/operations/Target.java | 63 + .../operations/WindowsOperationException.java | 58 + .../windows/operations/util/Constants.java | 98 + .../windows/operations/util/DeviceInfo.java | 92 + .../operations/util/OperationCode.java | 112 + .../operations/util/OperationReply.java | 772 ++ .../operations/util/OperationUtils.java | 505 + .../operations/util/SyncmlCredentials.java | 70 + .../operations/util/SyncmlGenerator.java | 100 + .../windows/operations/util/SyncmlParser.java | 458 + .../services/adminoperations/Operations.java | 57 + .../adminoperations/beans/Device.java | 36 + .../adminoperations/beans/Disenrollment.java | 42 + .../beans/OperationRequest.java | 49 + .../beans/OperationResponse.java | 48 + .../beans/StorageEncryption.java | 36 + .../wrapper/DisenrollmentBeanWrapper.java | 50 + .../beans/wrapper/EncryptBeanWrapper.java | 48 + .../adminoperations/impl/OperationsImpl.java | 242 + .../adminoperations/util/OperationStore.java | 112 + .../windows/services/authbst/BSTProvider.java | 40 + .../services/authbst/beans/Credentials.java | 87 + .../authbst/impl/BSTProviderImpl.java | 131 + .../ConfigurationMgtService.java | 181 + .../DeviceManagementService.java | 158 + .../services/discovery/DiscoveryService.java | 62 + .../discovery/beans/DiscoveryRequest.java | 61 + .../discovery/beans/DiscoveryResponse.java | 73 + .../discovery/beans/package-info.java | 22 + .../discovery/impl/DiscoveryServiceImpl.java | 112 + .../policymgtservice/PolicyMgtService.java | 71 + .../services/syncml/SyncmlService.java | 48 + .../services/syncml/beans/BasicOperation.java | 35 + .../services/syncml/beans/PasscodePolicy.java | 107 + .../services/syncml/beans/Profile.java | 62 + .../windows/services/syncml/beans/Wifi.java | 107 + .../syncml/impl/SyncmlServiceImpl.java | 462 + .../services/syncml/util/PolicyManager.java | 54 + .../services/syncml/util/SyncmlUtils.java | 49 + .../wstep/CertificateEnrollmentService.java | 62 + .../wstep/beans/AdditionalContext.java | 48 + .../wstep/beans/BinarySecurityToken.java | 67 + .../services/wstep/beans/ContextItem.java | 44 + .../wstep/beans/RequestSecurityToken.java | 42 + .../beans/RequestSecurityTokenResponse.java | 68 + .../wstep/beans/RequestedSecurityToken.java | 42 + .../services/wstep/beans/package-info.java | 27 + .../CertificateEnrollmentServiceImpl.java | 381 + .../wstep/util/CertificateSigningService.java | 143 + .../wstep/util/KeyStoreGenerator.java | 93 + .../services/wstep/util/MessageHandler.java | 207 + .../CertificateEnrollmentPolicyService.java | 70 + .../services/xcep/beans/Attributes.java | 451 + .../windows/services/xcep/beans/CA.java | 166 + .../services/xcep/beans/CACollection.java | 83 + .../xcep/beans/CAReferenceCollection.java | 83 + .../windows/services/xcep/beans/CAURI.java | 168 + .../services/xcep/beans/CAURICollection.java | 83 + .../beans/CertificateEnrollmentPolicy.java | 149 + .../xcep/beans/CertificateValidity.java | 99 + .../windows/services/xcep/beans/Client.java | 136 + .../services/xcep/beans/CryptoProviders.java | 87 + .../xcep/beans/EnrollmentPermission.java | 82 + .../services/xcep/beans/Extension.java | 108 + .../xcep/beans/ExtensionCollection.java | 87 + .../xcep/beans/FilterOIDCollection.java | 87 + .../services/xcep/beans/GetPolicies.java | 101 + .../xcep/beans/GetPoliciesResponse.java | 126 + .../xcep/beans/KeyArchivalAttributes.java | 86 + .../windows/services/xcep/beans/OID.java | 166 + .../services/xcep/beans/OIDCollection.java | 83 + .../xcep/beans/OIDReferenceCollection.java | 83 + .../services/xcep/beans/ObjectFactory.java | 253 + .../services/xcep/beans/PolicyCollection.java | 84 + .../xcep/beans/PrivateKeyAttributes.java | 193 + .../services/xcep/beans/RARequirements.java | 119 + .../services/xcep/beans/RequestFilter.java | 107 + .../windows/services/xcep/beans/Response.java | 204 + .../windows/services/xcep/beans/Revision.java | 85 + .../xcep/beans/SupersededPolicies.java | 83 + .../services/xcep/beans/package-info.java | 23 + ...ertificateEnrollmentPolicyServiceImpl.java | 101 + .../src/main/resources/properties.xml | 26 + .../src/main/resources/wap-provisioning.xml | 87 + .../src/main/resources/wso2mdm.jks | Bin 0 -> 9485 bytes .../src/main/webapp/META-INF/permissions.xml | 142 + .../webapp/META-INF/webapp-classloading.xml | 35 + .../src/main/webapp/WEB-INF/cxf-servlet.xml | 207 + .../src/main/webapp/WEB-INF/web.xml | 63 + .../src/main/webapp/servicelist.css | 125 + .../pom.xml | 176 + .../impl/WindowsDeviceManagementService.java | 108 + .../windows/impl/WindowsDeviceManager.java | 308 + .../windows/impl/WindowsFeatureManager.java | 196 + .../impl/WindowsPolicyMonitoringService.java | 73 + .../windows/impl/dao/WindowsDAOFactory.java | 131 + .../WindowsFeatureManagementDAOException.java | 80 + .../impl/dao/impl/WindowsDeviceDAOImpl.java | 255 + .../impl/dao/impl/WindowsFeatureDAOImpl.java | 271 + .../impl/util/WindowsPluginConstants.java | 48 + .../windows/impl/util/WindowsPluginUtils.java | 58 + .../windows/impl/util/WindowsUtils.java | 35 + .../WindowsDeviceManagementDataHolder.java | 47 + ...ndowsDeviceManagementServiceComponent.java | 121 + .../mobile-plugins/windows-plugin/pom.xml | 61 + pom.xml | 566 +- 996 files changed, 166704 insertions(+), 407 deletions(-) create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/build.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/pom.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/assembly/src.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventreceiver_accelerometer_1.0.0/EventReceiver_accelerometer.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventreceiver_accelerometer_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventstore_accelerometer_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventstore_accelerometer_1.0.0/org_wso2_iot_devices_accelerometer.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventstream_accelerometer_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventstream_accelerometer_1.0.0/org.wso2.iot.devices.accelerometer_1.0.0.json create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Sparkscripts_1.0.0/Accelerometer_Sensor_Script.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Sparkscripts_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/artifacts.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventreceiver_battery_1.0.0/EventReceiver_battery.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventreceiver_battery_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventstore_battery_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventstore_battery_1.0.0/org_wso2_iot_devices_battery.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventstream_battery_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventstream_battery_1.0.0/org.wso2.iot.devices.battery_1.0.0.json create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Sparkscripts_1.0.0/Battery_Sensor_Script.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Sparkscripts_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/artifacts.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventreceiver_gps_1.0.0/EventReceiver_gps.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventreceiver_gps_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventstore_gps_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventstore_gps_1.0.0/org_wso2_iot_devices_gps.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventstream_gps_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventstream_gps_1.0.0/org.wso2.iot.devices.gps_1.0.0.json create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Sparkscripts_1.0.0/GPS_Sensor_Script.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Sparkscripts_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/artifacts.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventreceiver_gravaity_1.0.0/EventReceiver_gravity.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventreceiver_gravaity_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventstore_gravity_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventstore_gravity_1.0.0/org_wso2_iot_devices_gravity.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventstream_gravity_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventstream_gravity_1.0.0/org.wso2.iot.devices.gravity_1.0.0.json create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Sparkscripts_1.0.0/Gravity_Sensor_Script.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Sparkscripts_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/artifacts.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventreceiver_gyroscope_1.0.0/EventReceiver_gyroscope.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventreceiver_gyroscope_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventstore_gyroscope_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventstore_gyroscope_1.0.0/org_wso2_iot_devices_gyroscope.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventstream_gyroscope_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventstream_gyroscope_1.0.0/org.wso2.iot.devices.gyroscope_1.0.0.json create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Sparkscripts_1.0.0/Gyroscope_Sensor_Script.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Sparkscripts_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/artifacts.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventreceiver_light_1.0.0/EventReceiver_light.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventreceiver_light_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventstore_light_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventstore_light_1.0.0/org_wso2_iot_devices_light.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventstream_light_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventstream_light_1.0.0/org.wso2.iot.devices.light_1.0.0.json create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Sparkscripts_1.0.0/Light_Sensor_Script.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Sparkscripts_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/artifacts.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventreceiver_magnetic_1.0.0/EventReceiver_magnetic.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventreceiver_magnetic_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventstore_magnetic_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventstore_magnetic_1.0.0/org_wso2_iot_devices_magnetic.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventstream_magnetic_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventstream_magnetic_1.0.0/org.wso2.iot.devices.magnetic_1.0.0.json create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Sparkscripts_1.0.0/Magnetic_Sensor_Script.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Sparkscripts_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/artifacts.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventreceiver_pressure_1.0.0/EventReceiver_pressure.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventreceiver_pressure_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventstore_pressure_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventstore_pressure_1.0.0/org_wso2_iot_devices_pressure.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventstream_pressure_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventstream_pressure_1.0.0/org.wso2.iot.devices.pressure_1.0.0.json create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Sparkscripts_1.0.0/Pressure_Sensor_Script.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Sparkscripts_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/artifacts.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventreceiver_proximity_1.0.0/EventReceiver_proximity.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventreceiver_proximity_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventstore_proximity_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventstore_proximity_1.0.0/org_wso2_iot_devices_proximity.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventstream_proximity_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventstream_proximity_1.0.0/org.wso2.iot.devices.proximity_1.0.0.json create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Sparkscripts_1.0.0/Proximity_Sensor_Script.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Sparkscripts_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/artifacts.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventreceiver_rotation_1.0.0/EventReceiver_rotation.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventreceiver_rotation_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventstore_rotation_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventstore_rotation_1.0.0/org_wso2_iot_devices_rotation.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventstream_rotation_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventstream_rotation_1.0.0/org.wso2.iot.devices.rotation_1.0.0.json create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Sparkscripts_1.0.0/Rotation_Sensor_Script.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Sparkscripts_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/artifacts.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventreceiver_wordcount_1.0.0/EventReceiver_wordcount.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventreceiver_wordcount_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventstore_wordcount_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventstore_wordcount_1.0.0/org_wso2_iot_devices_wordcount.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventstream_wordcount_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventstream_wordcount_1.0.0/org.wso2.iot.devices.wordcount_1.0.0.json create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Sparkscripts_1.0.0/Wordcount_Script.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Sparkscripts_1.0.0/artifact.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/artifacts.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/pom.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/controller/service/impl/AndroidSenseControllerService.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/controller/service/impl/transport/AndroidSenseMQTTConnector.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/controller/service/impl/util/DeviceData.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/controller/service/impl/util/SensorData.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/webapp/META-INF/permissions.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/webapp/META-INF/webapp-classloading.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/webapp/WEB-INF/cxf-servlet.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/webapp/WEB-INF/web.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/pom.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/manager/service/impl/AndroidSenseManagerService.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/manager/service/impl/util/APIUtil.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/webapp/META-INF/permissions.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/webapp/META-INF/webapp-classloading.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/webapp/WEB-INF/cxf-servlet.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/webapp/WEB-INF/web.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.plugin/pom.xml create mode 100644 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 create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/plugin/exception/AndroidSenseDeviceMgtPluginException.java create mode 100644 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/AndroidSenseManager.java create mode 100644 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 create mode 100644 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/dao/AndroidSenseDAO.java create mode 100644 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/dao/impl/AndroidSenseDAOImpl.java create mode 100644 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/dao/util/AndroidSenseUtils.java create mode 100644 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/dao/util/DeviceSchemaInitializer.java create mode 100644 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/feature/AndroidSenseFeatureManager.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/plugin/internal/AndroidSenseManagementServiceComponent.java create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/pom.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/assembly/src.xml create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.device-view/device-view.hbs create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.device-view/device-view.js create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.device-view/device-view.json create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.device-view/public/images/android-sense-icon.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.device-view/public/images/thumb.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/images/android-sense-icon.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/images/myDevices_analytics.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/images/publishDataView.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/images/registerView.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/images/selectSensorView.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/images/thumb.png create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/js/download.js create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/js/jquery.validate.js create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/type-view.hbs create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/type-view.js create mode 100644 components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/type-view.json create mode 100644 components/iot-plugins/androidsense-plugin/pom.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/build.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/pom.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/assembly/src.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/EventReceiver_temperature.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/artifact.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/artifact.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/org_wso2_iot_devices_temperature.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/artifact.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/org.wso2.iot.devices.temperature_1.0.0.json create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/Temperature_Sensor_Script.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/artifact.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/artifacts.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/pom.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/ArduinoControllerService.java create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/dto/DeviceData.java create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/dto/SensorData.java create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/exception/ArduinoException.java create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/transport/ArduinoMQTTConnector.java create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/util/ArduinoServiceUtils.java create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/webapp/META-INF/permissions.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/webapp/META-INF/webapp-classloading.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/webapp/WEB-INF/web.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/pom.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/manager/service/impl/ArduinoManagerService.java create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/manager/service/impl/util/APIUtil.java create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/webapp/META-INF/permissions.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/webapp/META-INF/webapp-classloading.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/webapp/WEB-INF/web.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/pom.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/constants/ArduinoConstants.java create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/exception/ArduinoDeviceMgtPluginException.java create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/ArduinoManager.java create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/ArduinoManagerService.java create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/dao/ArduinoDAO.java create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/dao/impl/ArduinoDeviceDAOImpl.java create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/feature/ArduinoFeatureManager.java create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/util/ArduinoUtils.java create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/util/DeviceSchemaInitializer.java create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/internal/ArduinoManagementServiceComponent.java create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/pom.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/assembly/src.xml create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.device-view/device-view.hbs create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.device-view/device-view.js create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.device-view/device-view.json create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.device-view/public/images/ardunio-icon.png create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.device-view/public/images/thumb.png create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/public/images/ardunio-icon.png create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/public/images/myDevices_analytics.png create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/public/images/schematicsGuide.png create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/public/images/thumb.png create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/public/js/download.js create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/public/js/jquery.validate.js create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/type-view.hbs create mode 100644 components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/type-view.json create mode 100644 components/iot-plugins/arduino-plugin/pom.xml create mode 100644 components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.controller.service.impl/pom.xml create mode 100644 components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.manager.service.impl/pom.xml create mode 100644 components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/pom.xml create mode 100644 components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/constants/CameraConstants.java create mode 100644 components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/CameraManager.java create mode 100644 components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/CameraManagerService.java create mode 100644 components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/dao/CameraDAO.java create mode 100644 components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/dao/impl/CameraDeviceDAOImpl.java create mode 100644 components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/util/VirtualFireAlarmUtils.java create mode 100644 components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/internal/CameraManagementServiceComponent.java create mode 100644 components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.ui/pom.xml create mode 100644 components/iot-plugins/camera-plugin/pom.xml create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/pom.xml create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/DigitalDisplayControllerService.java create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/dto/DeviceJSON.java create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/exception/DigitalDisplayException.java create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/model/ScreenShotModel.java create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/util/DigitalDisplayMQTTConnector.java create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/websocket/DigitalDisplayWebSocketServerEndPoint.java create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/webapp/META-INF/webapp-classloading.xml create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/webapp/WEB-INF/cxf-servlet.xml create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/webapp/WEB-INF/web.xml create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/pom.xml create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/manager/api/DigitalDisplayManagerService.java create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/manager/api/util/APIUtil.java create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/webapp/META-INF/permissions.xml create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/webapp/META-INF/webapp-classloading.xml create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/webapp/WEB-INF/cxf-servlet.xml create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/webapp/WEB-INF/web.xml create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin/pom.xml create mode 100644 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 create mode 100644 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/exception/DigitalDisplayDeviceMgtPluginException.java create mode 100644 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/DigitalDisplayManager.java create mode 100644 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 create mode 100644 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/dao/DigitalDisplayDAO.java create mode 100644 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/dao/impl/DigitalDisplayDeviceDAOImpl.java create mode 100644 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/feature/DigitalDisplayFeatureManager.java create mode 100644 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/util/DeviceSchemaInitializer.java create mode 100644 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/util/DigitalDisplayUtils.java create mode 100644 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/internal/DigitalDisplayManagementServiceComponent.java create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/pom.xml create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/assembly/src.xml create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/device-view.hbs create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/device-view.js create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/device-view.json create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/public/images/default-screen.png create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/public/images/display-icon.png create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/public/images/thumb.png create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/public/js/websocket.js create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/display-icon.png create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/myDevices_analytics.png create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/add-resource.png create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/edit-sequence.png create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/get-content-list.png create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/get-device-status.png create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/remove-resource.png create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/restart-browser.png create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/restart-display.png create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/restart-server.png create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/screenshot.png create mode 100755 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/terminate-display.png create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/upload-content.png create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/schematicsGuide.png create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/thumb.png create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/js/download.js create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/js/jquery.validate.js create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/type-view.hbs create mode 100644 components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/type-view.json create mode 100644 components/iot-plugins/digital-display-plugin/pom.xml create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/pom.xml create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/DroneControllerService.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/DroneRealTimeService.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/exception/DroneAnalyzerException.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/transport/DroneAnalyzerXMPPConnector.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/trasformer/MessageTransformer.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/util/DroneAnalyzerServiceUtils.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/webapp/META-INF/webapp-classloading.xml create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/webapp/WEB-INF/cxf-servlet.xml create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/webapp/WEB-INF/web.xml create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/pom.xml create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/manager/api/impl/DroneManagerService.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/manager/api/impl/util/APIUtil.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/webapp/META-INF/webapp-classloading.xml create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/webapp/WEB-INF/cxf-servlet.xml create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/webapp/WEB-INF/web.xml create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/pom.xml create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/constants/DroneConstants.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/constants/MessageConfig.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/controller/DroneController.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/controller/impl/DroneControllerImpl.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/exception/DroneAnalyzerDeviceMgtPluginException.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/DroneAnalyzerManager.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/DroneAnalyzerManagerService.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/dao/DroneAnalyzerDAO.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/dao/impl/DroneAnalyzerDeviceDAOImpl.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/feature/DroneAnalyzerFeatureManager.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/util/DeviceSchemaInitializer.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/util/DroneAnalyzerUtils.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/internal/DroneAnalyzerManagementServiceComponent.java create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/pom.xml create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/assembly/src.xml create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.device-view/device-view.hbs create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.device-view/device-view.js create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.device-view/device-view.json create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.device-view/public/images/drone-icon.png create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.device-view/public/images/thumb.png create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/public/images/devices_analytics.png create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/public/images/drone-icon.png create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/public/images/schematicsGuide.png create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/public/images/thumb.png create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/public/js/download.js create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/public/js/jquery.validate.js create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/type-view.hbs create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/type-view.json create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/css/main-app.css create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/images/drone_position_controller/background_drone.png create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/images/drone_position_controller/direction_drone.png create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/images/drone_position_controller/pitch_drone.png create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/images/no_video_preview.gif create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/3dobject_controller/3dObjectControler.js create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/3dobject_controller/Coordinates.js create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/3dobject_controller/OrbitAndPanControls.js create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/3dobject_controller/three.min.js create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/common/ajax_handler.js create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/common/general_handler create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/common/websocket_api create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/config/config.js create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/d3.min.js create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/download.js create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/initJs create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/jQueryRotate.js create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/jquery.validate.js create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/mainHandler create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/modules/controller.js create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/modules/flight_dynamics.js create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/modules/realtime_plotting create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/statistics.hbs create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/statistics.js create mode 100644 components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/statistics.json create mode 100644 components/iot-plugins/drone-analyzer-plugin/pom.xml create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/pom.xml create mode 100755 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/log4j.properties create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/DeviceUsageDTO.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/DevicesManagerService.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/GroupManagerService.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/PolicyManagementService.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/StatsManagerService.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/util/ResponsePayload.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/webapp/META-INF/permissions.xml create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/webapp/META-INF/webapp-classloading.xml create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/webapp/WEB-INF/cxf-servlet.xml create mode 100755 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/webapp/WEB-INF/web.xml create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/pom.xml create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/assembly/src.xml create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/conf/app-conf.json create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.register/public/js/validate-register.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.register/register.hbs create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.register/register.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.register/register.json create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.sign-in/sign-in.hbs create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.sign-in/sign-in.json create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.operation-bar/operation-bar.hbs create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.operation-bar/operation-bar.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.operation-bar/operation-bar.json create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.operation-bar/public/js/operation-bar.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/css/graph.css create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/images/map-marker-1.png create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/images/map-marker-2.png create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/js/d3.min.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/js/device-stats.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/js/moment.min.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/js/rickshaw.min.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/stats.hbs create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/stats.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/stats.json create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.platform.configuration/configuration.hbs create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.platform.configuration/configuration.json create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.platform.configuration/public/js/platform-configuration.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/edit.hbs create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/edit.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/edit.json create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/public/css/codemirror.css create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/public/js/codemirror.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/public/js/policy-edit.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/public/js/sql.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/public/css/codemirror.css create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/public/js/codemirror.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/public/js/policy-view.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/public/js/sql.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/view.hbs create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/view.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/view.json create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/public/css/codemirror.css create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/public/js/codemirror.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/public/js/policy-create.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/public/js/sql.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/wizard.hbs create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/wizard.js create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/wizard.json create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.ui.header.logo/logo.hbs create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.ui.header.logo/logo.json create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/pom.xml create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/DeviceManagementConfigurationManager.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/datasource/ControlQueue.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/datasource/ControlQueuesConfig.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/datasource/DeviceManagementConfiguration.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/datasource/ObjectFactory.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/controlqueue/mqtt/MqttConfig.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/controlqueue/xmpp/XmppAccount.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/controlqueue/xmpp/XmppConfig.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/controlqueue/xmpp/XmppServerClient.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/exception/DeviceControllerException.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/exception/IoTException.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/exception/NotImplementedException.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/exception/UnauthorizedException.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/internal/IoTCommonDataHolder.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/internal/IotDeviceManagementServiceComponent.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/mqtt/PolicyPush.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/sensormgt/DeviceRecord.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/sensormgt/SensorDataManager.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/sensormgt/SensorRecord.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/service/IoTServerStartupListener.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/CommunicationUtils.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/TransportHandler.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/TransportHandlerException.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/TransportUtils.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/mqtt/MQTTTransportHandler.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/xmpp/XMPPTransportHandler.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/util/IoTUtil.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/util/IotDeviceManagementUtil.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/util/ZipArchive.java create mode 100644 components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/util/ZipUtil.java create mode 100644 components/iot-plugins/iot-base-plugin/pom.xml create mode 100644 components/iot-plugins/pom.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/build.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/pom.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/assembly/src.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/EventReceiver_temperature.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/artifact.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/artifact.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/org_wso2_iot_devices_temperature.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/artifact.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/org.wso2.iot.devices.temperature_1.0.0.json create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/Temperature_Sensor_Script.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/artifact.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/artifacts.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/pom.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/RaspberryPiControllerService.java create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/dto/DeviceData.java create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/dto/SensorData.java create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/exception/RaspberrypiException.java create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/transport/RaspberryPiMQTTConnector.java create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/util/RaspberrypiServiceUtils.java create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/webapp/META-INF/webapp-classloading.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/webapp/WEB-INF/web.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/pom.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/manager/service/impl/RaspberryPiManagerService.java create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/manager/service/impl/util/APIUtil.java create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/webapp/META-INF/webapp-classloading.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/webapp/WEB-INF/web.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/pom.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/constants/RaspberrypiConstants.java create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/exception/RaspberrypiDeviceMgtPluginException.java create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/RaspberrypiManager.java create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/RaspberrypiManagerService.java create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/dao/RaspberrypiDAO.java create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/dao/impl/RaspberrypiDeviceDAOImpl.java create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/feature/RaspberrypiFeatureManager.java create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/util/DeviceSchemaInitializer.java create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/util/RaspberrypiUtils.java create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/internal/RaspberrypiManagementServiceComponent.java create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/pom.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/assembly/src.xml create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.device-view/device-view.hbs create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.device-view/device-view.js create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.device-view/device-view.json create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.device-view/public/images/respberry-icon.png create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.device-view/public/images/thumb.png create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/public/images/myDevices_analytics.png create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/public/images/respberry-icon.png create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/public/images/schematicsGuide.png create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/public/images/thumb.png create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/public/js/download.js create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/public/js/jquery.validate.js create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/type-view.hbs create mode 100644 components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/type-view.json create mode 100644 components/iot-plugins/raspberrypi-plugin/pom.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/pom.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/Bootstrap.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/communication/http/FireAlarmHTTPCommunicator.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/communication/mqtt/FireAlarmMQTTCommunicator.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/communication/xmpp/FireAlarmXMPPCommunicator.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/core/AgentConfiguration.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/core/AgentConstants.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/core/AgentManager.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/core/AgentUtilOperations.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/enrollment/EnrollmentManager.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/exception/AgentCoreOperationException.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/sidhdhi/SidhdhiQuery.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/CommunicationUtils.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/TransportHandler.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/TransportHandlerException.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/TransportUtils.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/http/HTTPTransportHandler.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/mqtt/MQTTTransportHandler.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/xmpp/XMPPTransportHandler.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/virtual/VirtualHardwareManager.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/virtual/ui/AgentUI.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/resources/alarm-off.gif create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/resources/alarm-on.gif create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/resources/cep_query.txt create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/resources/deviceConfig.properties create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/resources/fireAlarmSound.mid create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/build.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/manifest.mf create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/build-impl.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/genfiles.properties create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/private/private.properties create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/private/private.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/project.properties create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/project.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/src/bulb-on.jpg create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/VirtualAgentUI.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/ui/AgentUI.form create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/ui/AgentUI.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/pom.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/Bootstrap.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/communication/http/FireAlarmHTTPCommunicator.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/communication/mqtt/FireAlarmMQTTCommunicator.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/communication/xmpp/FireAlarmXMPPCommunicator.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/core/AgentConfiguration.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/core/AgentConstants.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/core/AgentManager.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/core/AgentUtilOperations.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/enrollment/EnrollmentManager.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/exception/AgentCoreOperationException.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/CommunicationUtils.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/TransportHandler.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/TransportHandlerException.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/TransportUtils.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/http/HTTPTransportHandler.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/mqtt/MQTTTransportHandler.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/xmpp/XMPPTransportHandler.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/virtual/VirtualHardwareManager.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/virtual/ui/AgentUI.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/resources/alarm-off.gif create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/resources/alarm-on.gif create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/resources/deviceConfig.properties create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/resources/fireAlarmSound.mid create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/build.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/manifest.mf create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/build-impl.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/genfiles.properties create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/private/private.properties create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/private/private.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/project.properties create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/project.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/src/bulb-on.jpg create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/VirtualAgentUI.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/ui/AgentUI.form create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/ui/AgentUI.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/build.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/pom.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/assembly/src.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/EventReceiver_temperature.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/artifact.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/artifact.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/org_wso2_iot_devices_temperature.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/artifact.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/org.wso2.iot.devices.temperature_1.0.0.json create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/Temperature_Sensor_Script.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/artifact.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/artifacts.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/pom.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/VirtualFireAlarmControllerService.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/dto/DeviceData.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/dto/SensorData.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/exception/VirtualFireAlarmException.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/transport/VirtualFireAlarmMQTTConnector.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/transport/VirtualFireAlarmXMPPConnector.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/util/SecurityManager.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/util/VirtualFireAlarmServiceUtils.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/util/scep/ContentType.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/util/scep/SCEPOperation.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/webapp/META-INF/webapp-classloading.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/webapp/WEB-INF/web.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/pom.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/manager/service/impl/VirtualFireAlarmManagerService.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/manager/service/impl/util/APIUtil.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/webapp/META-INF/webapp-classloading.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/webapp/WEB-INF/web.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/pom.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/constants/VirtualFireAlarmConstants.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/exception/VirtualFirealarmDeviceMgtPluginException.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/VirtualFireAlarmManager.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/VirtualFireAlarmManagerService.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/dao/VirtualFireAlarmDAO.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/dao/impl/VirtualFireAlarmDeviceDAOImpl.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/feature/VirtualFirealarmFeatureManager.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/util/DeviceSchemaInitializer.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/util/VirtualFireAlarmUtils.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/internal/VirtualFirealarmManagementServiceComponent.java create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/pom.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/assembly/src.xml create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.device-view/device-view.hbs create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.device-view/device-view.js create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.device-view/device-view.json create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.device-view/public/images/firealarm-icon.png create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.device-view/public/images/thumb.png create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.policy-edit/policy-edit.hbs create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.policy-edit/policy-edit.json create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.policy-view/policy-view.hbs create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.policy-view/policy-view.json create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.policy-wizard/policy-wizard.hbs create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.policy-wizard/policy-wizard.json create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/public/images/firealarm-icon.png create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/public/images/myDevices_analytics.png create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/public/images/schematicsGuide.png create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/public/images/thumb.png create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/public/js/download.js create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/public/js/jquery.validate.js create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/type-view.hbs create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/type-view.json create mode 100644 components/iot-plugins/virtual-fire-alarm-plugin/pom.xml delete mode 100644 components/key-mgt/org.wso2.carbon.key.mgt.handler.valve/pom.xml delete mode 100644 components/key-mgt/org.wso2.carbon.key.mgt.handler.valve/src/main/java/org/wso2/carbon/key/mgt/handler/valve/HandlerUtil.java delete mode 100644 components/key-mgt/org.wso2.carbon.key.mgt.handler.valve/src/main/java/org/wso2/carbon/key/mgt/handler/valve/OAuthTokenValidatorValve.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/pom.xml create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/ConfigurationMgtService.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/DeviceManagementService.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/EnrollmentService.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/OperationMgtService.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/PolicyMgtService.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/AndroidOperation.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/ApplicationInstallation.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/ApplicationUninstallation.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/BlacklistApplications.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/Camera.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/DeviceEncryption.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/Disenrollment.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/LockCode.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/Notification.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/PasscodePolicy.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/WebClip.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/Wifi.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/WipeData.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/ApplicationInstallationBeanWrapper.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/ApplicationUninstallationBeanWrapper.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/BlacklistApplicationsBeanWrapper.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/CameraBeanWrapper.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/DisenrollmentBeanWrapper.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/EncryptionBeanWrapper.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/LockCodeBeanWrapper.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/NotificationBeanWrapper.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/PasswordPolicyBeanWrapper.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/WebClipBeanWrapper.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/WifiBeanWrapper.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/WipeDataBeanWrapper.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/common/ErrorHandler.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/common/ErrorMessage.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/common/GsonMessageBodyHandler.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/exception/AndroidAgentException.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/exception/AndroidOperationException.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/exception/BadRequestException.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/exception/OperationConfigurationException.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/AndroidAPIUtils.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/AndroidConstants.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/AndroidDeviceUtils.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/DeviceIDHolder.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/Message.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/META-INF/permissions.xml create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/META-INF/webapp-classloading.xml create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/WEB-INF/cxf-servlet.xml create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/WEB-INF/web.xml create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/servicelist.css create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/pom.xml create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/AndroidDeviceManagementService.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/AndroidDeviceManager.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/AndroidFeatureManager.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/AndroidPolicyMonitoringService.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/dao/AndroidDAOFactory.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/dao/AndroidFeatureManagementDAOException.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/dao/impl/AndroidDeviceDAOImpl.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/dao/impl/AndroidFeatureDAOImpl.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/gcm/GCMResult.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/gcm/GCMService.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/gcm/GCMUtil.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/util/AndroidPluginConstants.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/util/AndroidPluginUtils.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/util/AndroidUtils.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/internal/AndroidDeviceManagementDataHolder.java create mode 100644 components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/internal/AndroidDeviceManagementServiceComponent.java create mode 100644 components/mobile-plugins/android-plugin/pom.xml create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/pom.xml create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Authentication.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Configuration.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/DeviceNotification.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Feature.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/License.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/MobileDevice.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Operation.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Policy.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Profile.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Role.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/User.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/common/ErrorHandler.java rename components/{key-mgt/org.wso2.carbon.key.mgt.handler.valve/src/main/java/org/wso2/carbon/key/mgt/handler/valve/APIFaultException.java => mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/common/ErrorMessage.java} (63%) create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/common/MDMAPIException.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/context/DeviceOperationContext.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/MDMAPIUtils.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/MDMAndroidOperationUtil.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/MDMAppConstants.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/MDMIOSOperationUtil.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/ResponsePayload.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ApplicationWrapper.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/MobileApp.java rename components/{key-mgt/org.wso2.carbon.key.mgt.handler.valve/src/main/java/org/wso2/carbon/key/mgt/handler/valve/HandlerConstants.java => mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/MobileAppTypes.java} (69%) create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/PolicyWrapper.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/PriorityUpdatedPolicyWrapper.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/Profile.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ProfileFeature.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/RoleWrapper.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/UserCredentialWrapper.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/UserWrapper.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/android/AppStoreApplication.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/android/EnterpriseApplication.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/android/WebApplication.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ios/AppStoreApplication.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ios/EnterpriseApplication.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ios/RemoveApplication.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ios/WebClip.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/util/Constants.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/util/MDMUtil.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/util/SetReferenceTransformer.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/mdm/common/Application.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/mdm/common/GsonMessageBodyHandler.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/webapp/META-INF/permissions.xml create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/webapp/META-INF/webapp-classloading.xml create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/webapp/WEB-INF/cxf-servlet.xml create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/webapp/WEB-INF/web.xml create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/pom.xml create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/assembly/src.xml create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/cdmf.page.platform.configuration/configuration.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/cdmf.page.platform.configuration/configuration.json create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/operation-bar.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/operation-bar.json create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/public/js/operation-bar.js create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/public/templates/hidden-operations-android.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/public/templates/hidden-operations-ios.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/public/templates/operations.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-mod/operation-mod.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-mod/operation-mod.json create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-mod/public/js/operation-mod.js create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.platform.configuration/configuration.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.platform.configuration/configuration.js create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.platform.configuration/configuration.json create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.platform.configuration/public/js/platform-configuration.js create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/edit.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/edit.js create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/edit.json create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/public/js/policy-create.js create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/public/templates/hidden-operations-android.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/public/templates/hidden-operations-ios.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/public/templates/hidden-operations-windows.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/public/js/policy-view.js create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/public/templates/hidden-operations-android.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/public/templates/hidden-operations-ios.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/public/templates/hidden-operations-windows.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/view.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/view.js create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/view.json create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/public/js/policy-create.js create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/public/templates/hidden-operations-android.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/public/templates/hidden-operations-ios.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/public/templates/hidden-operations-windows.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/wizard.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/wizard.js create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/wizard.json create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.ui.header.logo/logo.hbs create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.ui.header.logo/logo.json create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.url.printer/pom.xml create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.url.printer/src/main/java/org/wso2/carbon/device/mgt/mobile/url/printer/URLPrinterStartupHandler.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.url.printer/src/main/java/org/wso2/carbon/device/mgt/mobile/url/printer/internal/URLPrinterDataHolder.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.url.printer/src/main/java/org/wso2/carbon/device/mgt/mobile/url/printer/internal/URLPrinterStartupHandlerServiceComponent.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/pom.xml create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/AbstractMobileOperationManager.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/DataSourceListener.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/DataSourceNotAvailableException.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/common/MobileDeviceMgtPluginException.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/common/MobilePluginConstants.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/MobileDeviceConfigurationManager.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/MobileDeviceManagementConfig.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/MobileDeviceManagementRepository.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/datasource/DataSourceConfigAdapter.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/datasource/JNDILookupDefinition.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/datasource/MobileDataSourceConfig.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/datasource/MobileDataSourceConfigurations.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/AbstractMobileDeviceManagementDAOFactory.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/MobileDeviceDAO.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/MobileDeviceManagementDAOException.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/MobileDeviceManagementDAOFactory.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/MobileFeatureDAO.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/impl/MobileFeatureDAOImpl.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/util/MobileDeviceManagementDAOUtil.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileDevice.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileDeviceOperationMapping.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileFeature.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileFeatureProperty.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileOperation.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileOperationProperty.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/internal/MobileDeviceManagementDataHolder.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/internal/MobileDeviceManagementServiceComponent.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/util/MobileDeviceManagementSchemaInitializer.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/util/MobileDeviceManagementUtil.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/MobileDeviceManagementConfigTests.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/common/DBTypes.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/common/TestDBConfiguration.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/common/TestDBConfigurations.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/dao/util/MobileDatabaseUtils.java create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-api-config.xml create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-api-publisher-config.xml create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-apis-config.xml create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-ds-config.xml create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-jndi-config.xml create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-mgt-repo.xml create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/schema/MobileDeviceManagementConfigSchema.xsd create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/log4j.properties create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/sql/h2.sql create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/testdbconfig.xml create mode 100644 components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/testng.xml create mode 100644 components/mobile-plugins/mobile-base-plugin/pom.xml rename components/{key-mgt => mobile-plugins}/pom.xml (79%) create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/pom.xml create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/PluginConstants.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/SyncmlCommandType.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/beans/CacheEntry.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/beans/Token.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/beans/WindowsPluginProperties.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/AuthenticationException.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/BadRequestException.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/CertificateGenerationException.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/FileOperationException.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/KeyStoreGenerationException.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/MDMAPIException.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/SyncmlMessageFormatException.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/SyncmlOperationException.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/WAPProvisioningException.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/WindowsConfigurationException.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/WindowsDeviceEnrolmentException.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/WindowsOperationsException.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/AuthenticationInfo.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/BSTValidator.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/ConfigInitializerContextListener.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/DeviceIDHolder.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/DeviceUtil.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/ErrorHandler.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/ErrorMessage.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/GsonMessageBodyHandler.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/Message.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/SOAPSecurityHandler.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/UsernameTokenValidator.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/WindowsAPIUtils.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/WindowsDeviceUtils.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/AddTag.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Alert.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/AtomicTag.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/ChallengeTag.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Credential.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/DeleteTag.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/ExecuteTag.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Get.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Item.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/MetaTag.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Replace.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Results.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/SequenceTag.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Source.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Status.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/SyncmlBody.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/SyncmlDocument.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/SyncmlHeader.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Target.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/WindowsOperationException.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/Constants.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/DeviceInfo.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/OperationCode.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/OperationReply.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/OperationUtils.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/SyncmlCredentials.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/SyncmlGenerator.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/SyncmlParser.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/Operations.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/Device.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/Disenrollment.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/OperationRequest.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/OperationResponse.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/StorageEncryption.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/wrapper/DisenrollmentBeanWrapper.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/wrapper/EncryptBeanWrapper.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/impl/OperationsImpl.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/util/OperationStore.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/authbst/BSTProvider.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/authbst/beans/Credentials.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/authbst/impl/BSTProviderImpl.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/configurationmgtservice/ConfigurationMgtService.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/devicemgtservice/DeviceManagementService.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/DiscoveryService.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/beans/DiscoveryRequest.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/beans/DiscoveryResponse.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/beans/package-info.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/impl/DiscoveryServiceImpl.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/policymgtservice/PolicyMgtService.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/SyncmlService.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/beans/BasicOperation.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/beans/PasscodePolicy.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/beans/Profile.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/beans/Wifi.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/impl/SyncmlServiceImpl.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/util/PolicyManager.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/util/SyncmlUtils.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/CertificateEnrollmentService.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/AdditionalContext.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/BinarySecurityToken.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/ContextItem.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/RequestSecurityToken.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/RequestSecurityTokenResponse.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/RequestedSecurityToken.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/package-info.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/impl/CertificateEnrollmentServiceImpl.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/util/CertificateSigningService.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/util/KeyStoreGenerator.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/util/MessageHandler.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/CertificateEnrollmentPolicyService.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Attributes.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CA.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CACollection.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CAReferenceCollection.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CAURI.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CAURICollection.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CertificateEnrollmentPolicy.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CertificateValidity.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Client.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CryptoProviders.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/EnrollmentPermission.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Extension.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/ExtensionCollection.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/FilterOIDCollection.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/GetPolicies.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/GetPoliciesResponse.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/KeyArchivalAttributes.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/OID.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/OIDCollection.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/OIDReferenceCollection.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/ObjectFactory.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/PolicyCollection.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/PrivateKeyAttributes.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/RARequirements.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/RequestFilter.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Response.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Revision.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/SupersededPolicies.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/package-info.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/impl/CertificateEnrollmentPolicyServiceImpl.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/resources/properties.xml create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/resources/wap-provisioning.xml create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/resources/wso2mdm.jks create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/META-INF/permissions.xml create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/META-INF/webapp-classloading.xml create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/WEB-INF/cxf-servlet.xml create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/WEB-INF/web.xml create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/servicelist.css create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/pom.xml create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/WindowsDeviceManagementService.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/WindowsDeviceManager.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/WindowsFeatureManager.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/WindowsPolicyMonitoringService.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/dao/WindowsDAOFactory.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/dao/WindowsFeatureManagementDAOException.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/dao/impl/WindowsDeviceDAOImpl.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/dao/impl/WindowsFeatureDAOImpl.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/util/WindowsPluginConstants.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/util/WindowsPluginUtils.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/util/WindowsUtils.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/internal/WindowsDeviceManagementDataHolder.java create mode 100644 components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/internal/WindowsDeviceManagementServiceComponent.java create mode 100644 components/mobile-plugins/windows-plugin/pom.xml diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/build.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/build.xml new file mode 100644 index 000000000..b0b52fc6b --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/build.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/pom.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/pom.xml new file mode 100644 index 000000000..a3a622fc4 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/pom.xml @@ -0,0 +1,90 @@ + + + + + + androidsense-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.androidsense.analytics + WSO2 Carbon - IoT Server Android Sense Analytics capp + pom + + + + + maven-clean-plugin + 2.4.1 + + + auto-clean + initialize + + clean + + + + + + maven-antrun-plugin + 1.7 + + + process-resources + + + + + + + run + + + + + + maven-assembly-plugin + 2.5.5 + + ${project.artifactId}-${carbon.device.mgt.version} + false + + src/assembly/src.xml + + + + + create-archive + package + + single + + + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/assembly/src.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/assembly/src.xml new file mode 100644 index 000000000..a5a375010 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/assembly/src.xml @@ -0,0 +1,36 @@ + + + + src + + zip + + false + ${basedir}/src + + + ${basedir}/target/carbonapps + / + true + + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventreceiver_accelerometer_1.0.0/EventReceiver_accelerometer.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventreceiver_accelerometer_1.0.0/EventReceiver_accelerometer.xml new file mode 100644 index 000000000..cf95025b6 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventreceiver_accelerometer_1.0.0/EventReceiver_accelerometer.xml @@ -0,0 +1,26 @@ + + + + + + false + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventreceiver_accelerometer_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventreceiver_accelerometer_1.0.0/artifact.xml new file mode 100644 index 000000000..aeead5417 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventreceiver_accelerometer_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + EventReceiver_accelerometer.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventstore_accelerometer_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventstore_accelerometer_1.0.0/artifact.xml new file mode 100644 index 000000000..cc7c49907 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventstore_accelerometer_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + org_wso2_iot_devices_accelerometer.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventstore_accelerometer_1.0.0/org_wso2_iot_devices_accelerometer.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventstore_accelerometer_1.0.0/org_wso2_iot_devices_accelerometer.xml new file mode 100644 index 000000000..5e2d427c4 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventstore_accelerometer_1.0.0/org_wso2_iot_devices_accelerometer.xml @@ -0,0 +1,62 @@ + + + + + + org.wso2.iot.devices.accelerometer:1.0.0 + + EVENT_STORE + + + meta_owner + true + true + false + STRING + + + meta_deviceType + true + true + false + STRING + + + meta_deviceId + true + true + false + STRING + + + meta_time + true + true + false + LONG + + + accelerometer + false + false + false + FLOAT + + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventstream_accelerometer_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventstream_accelerometer_1.0.0/artifact.xml new file mode 100644 index 000000000..4a9d840f9 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventstream_accelerometer_1.0.0/artifact.xml @@ -0,0 +1,23 @@ + + + + + org.wso2.iot.devices.accelerometer_1.0.0.json + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventstream_accelerometer_1.0.0/org.wso2.iot.devices.accelerometer_1.0.0.json b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventstream_accelerometer_1.0.0/org.wso2.iot.devices.accelerometer_1.0.0.json new file mode 100644 index 000000000..db6af8eec --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Eventstream_accelerometer_1.0.0/org.wso2.iot.devices.accelerometer_1.0.0.json @@ -0,0 +1,20 @@ +{ + "name": "org.wso2.iot.devices.accelerometer", + "version": "1.0.0", + "nickName": "accelerometer Data", + "description": "accelerometer data received from the Device", + "metaData": [ + {"name":"owner","type":"STRING"}, + {"name":"deviceType","type":"STRING"}, + {"name":"deviceId","type":"STRING"}, + {"name":"time","type":"LONG"} + ], + "payloadData": [ + { + "name": "accelerometer","type": "FLOAT" + } + ] +} + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Sparkscripts_1.0.0/Accelerometer_Sensor_Script.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Sparkscripts_1.0.0/Accelerometer_Sensor_Script.xml new file mode 100644 index 000000000..e1533c75c --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Sparkscripts_1.0.0/Accelerometer_Sensor_Script.xml @@ -0,0 +1,31 @@ + + + + + Accelerometer_Sensor_Script + + 0 * * * * ? + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Sparkscripts_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Sparkscripts_1.0.0/artifact.xml new file mode 100644 index 000000000..3cdac3edd --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/Sparkscripts_1.0.0/artifact.xml @@ -0,0 +1,23 @@ + + + + + Accelerometer_Sensor_Script.xml + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/artifacts.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/artifacts.xml new file mode 100644 index 000000000..bc58f6a36 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Accelerometer/artifacts.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventreceiver_battery_1.0.0/EventReceiver_battery.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventreceiver_battery_1.0.0/EventReceiver_battery.xml new file mode 100644 index 000000000..af55acea8 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventreceiver_battery_1.0.0/EventReceiver_battery.xml @@ -0,0 +1,26 @@ + + + + + + false + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventreceiver_battery_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventreceiver_battery_1.0.0/artifact.xml new file mode 100644 index 000000000..3b963dafc --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventreceiver_battery_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + EventReceiver_battery.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventstore_battery_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventstore_battery_1.0.0/artifact.xml new file mode 100644 index 000000000..6bc9aadd8 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventstore_battery_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + org_wso2_iot_devices_battery.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventstore_battery_1.0.0/org_wso2_iot_devices_battery.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventstore_battery_1.0.0/org_wso2_iot_devices_battery.xml new file mode 100644 index 000000000..ce70882da --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventstore_battery_1.0.0/org_wso2_iot_devices_battery.xml @@ -0,0 +1,62 @@ + + + + + + org.wso2.iot.devices.battery:1.0.0 + + EVENT_STORE + + + meta_owner + true + true + false + STRING + + + meta_deviceType + true + true + false + STRING + + + meta_deviceId + true + true + false + STRING + + + meta_time + true + true + false + LONG + + + battery + false + false + false + FLOAT + + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventstream_battery_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventstream_battery_1.0.0/artifact.xml new file mode 100644 index 000000000..a03e58d40 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventstream_battery_1.0.0/artifact.xml @@ -0,0 +1,23 @@ + + + + + org.wso2.iot.devices.battery_1.0.0.json + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventstream_battery_1.0.0/org.wso2.iot.devices.battery_1.0.0.json b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventstream_battery_1.0.0/org.wso2.iot.devices.battery_1.0.0.json new file mode 100644 index 000000000..c0a2106f0 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Eventstream_battery_1.0.0/org.wso2.iot.devices.battery_1.0.0.json @@ -0,0 +1,20 @@ +{ + "name": "org.wso2.iot.devices.battery", + "version": "1.0.0", + "nickName": "battery Data", + "description": "battery data received from the Device", + "metaData": [ + {"name":"owner","type":"STRING"}, + {"name":"deviceType","type":"STRING"}, + {"name":"deviceId","type":"STRING"}, + {"name":"time","type":"LONG"} + ], + "payloadData": [ + { + "name": "battery","type": "FLOAT" + } + ] +} + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Sparkscripts_1.0.0/Battery_Sensor_Script.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Sparkscripts_1.0.0/Battery_Sensor_Script.xml new file mode 100644 index 000000000..07559e342 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Sparkscripts_1.0.0/Battery_Sensor_Script.xml @@ -0,0 +1,31 @@ + + + + + Battery_Sensor_Script + + 0 * * * * ? + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Sparkscripts_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Sparkscripts_1.0.0/artifact.xml new file mode 100644 index 000000000..b573355bb --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/Sparkscripts_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + Battery_Sensor_Script.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/artifacts.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/artifacts.xml new file mode 100644 index 000000000..2b72bb5b9 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Battery/artifacts.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventreceiver_gps_1.0.0/EventReceiver_gps.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventreceiver_gps_1.0.0/EventReceiver_gps.xml new file mode 100644 index 000000000..dd51b2817 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventreceiver_gps_1.0.0/EventReceiver_gps.xml @@ -0,0 +1,26 @@ + + + + + + false + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventreceiver_gps_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventreceiver_gps_1.0.0/artifact.xml new file mode 100644 index 000000000..4ae981667 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventreceiver_gps_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + EventReceiver_gps.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventstore_gps_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventstore_gps_1.0.0/artifact.xml new file mode 100644 index 000000000..1ad7609e6 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventstore_gps_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + org_wso2_iot_devices_gps.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventstore_gps_1.0.0/org_wso2_iot_devices_gps.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventstore_gps_1.0.0/org_wso2_iot_devices_gps.xml new file mode 100644 index 000000000..781c37ab0 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventstore_gps_1.0.0/org_wso2_iot_devices_gps.xml @@ -0,0 +1,69 @@ + + + + + + org.wso2.iot.devices.gps:1.0.0 + + EVENT_STORE + + + meta_owner + true + true + false + STRING + + + meta_deviceType + true + true + false + STRING + + + meta_deviceId + true + true + false + STRING + + + meta_time + true + true + false + LONG + + + latitude + false + false + false + FLOAT + + + longitude + false + false + false + FLOAT + + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventstream_gps_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventstream_gps_1.0.0/artifact.xml new file mode 100644 index 000000000..2028042e0 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventstream_gps_1.0.0/artifact.xml @@ -0,0 +1,23 @@ + + + + + org.wso2.iot.devices.gps_1.0.0.json + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventstream_gps_1.0.0/org.wso2.iot.devices.gps_1.0.0.json b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventstream_gps_1.0.0/org.wso2.iot.devices.gps_1.0.0.json new file mode 100644 index 000000000..04d2b1069 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Eventstream_gps_1.0.0/org.wso2.iot.devices.gps_1.0.0.json @@ -0,0 +1,23 @@ +{ + "name": "org.wso2.iot.devices.gps", + "version": "1.0.0", + "nickName": "GPS Data", + "description": "GPS data received from the Device", + "metaData": [ + {"name":"owner","type":"STRING"}, + {"name":"deviceType","type":"STRING"}, + {"name":"deviceId","type":"STRING"}, + {"name":"time","type":"LONG"} + ], + "payloadData": [ + { + "name": "latitude","type": "FLOAT" + }, + { + "name": "longitude","type": "FLOAT" + } + ] +} + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Sparkscripts_1.0.0/GPS_Sensor_Script.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Sparkscripts_1.0.0/GPS_Sensor_Script.xml new file mode 100644 index 000000000..e9f0b83a7 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Sparkscripts_1.0.0/GPS_Sensor_Script.xml @@ -0,0 +1,31 @@ + + + + + IoTServer_Sensor_Script + + 0 * * * * ? + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Sparkscripts_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Sparkscripts_1.0.0/artifact.xml new file mode 100644 index 000000000..87dcbba09 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/Sparkscripts_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + GPS_Sensor_Script.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/artifacts.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/artifacts.xml new file mode 100644 index 000000000..750796e82 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/GPS/artifacts.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventreceiver_gravaity_1.0.0/EventReceiver_gravity.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventreceiver_gravaity_1.0.0/EventReceiver_gravity.xml new file mode 100644 index 000000000..a1d23503a --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventreceiver_gravaity_1.0.0/EventReceiver_gravity.xml @@ -0,0 +1,26 @@ + + + + + + false + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventreceiver_gravaity_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventreceiver_gravaity_1.0.0/artifact.xml new file mode 100644 index 000000000..00108bb4e --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventreceiver_gravaity_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + EventReceiver_gravity.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventstore_gravity_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventstore_gravity_1.0.0/artifact.xml new file mode 100644 index 000000000..0abc61558 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventstore_gravity_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + org_wso2_iot_devices_gravity.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventstore_gravity_1.0.0/org_wso2_iot_devices_gravity.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventstore_gravity_1.0.0/org_wso2_iot_devices_gravity.xml new file mode 100644 index 000000000..2c68c0d43 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventstore_gravity_1.0.0/org_wso2_iot_devices_gravity.xml @@ -0,0 +1,62 @@ + + + + + + org.wso2.iot.devices.gravity:1.0.0 + + EVENT_STORE + + + meta_owner + true + true + false + STRING + + + meta_deviceType + true + true + false + STRING + + + meta_deviceId + true + true + false + STRING + + + meta_time + true + true + false + LONG + + + gravity + false + false + false + FLOAT + + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventstream_gravity_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventstream_gravity_1.0.0/artifact.xml new file mode 100644 index 000000000..7d6e0d5f6 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventstream_gravity_1.0.0/artifact.xml @@ -0,0 +1,23 @@ + + + + + org.wso2.iot.devices.gravity_1.0.0.json + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventstream_gravity_1.0.0/org.wso2.iot.devices.gravity_1.0.0.json b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventstream_gravity_1.0.0/org.wso2.iot.devices.gravity_1.0.0.json new file mode 100644 index 000000000..f18bd87be --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Eventstream_gravity_1.0.0/org.wso2.iot.devices.gravity_1.0.0.json @@ -0,0 +1,20 @@ +{ + "name": "org.wso2.iot.devices.gravity", + "version": "1.0.0", + "nickName": "Gravity Data", + "description": "Gravity data received from the Device", + "metaData": [ + {"name":"owner","type":"STRING"}, + {"name":"deviceType","type":"STRING"}, + {"name":"deviceId","type":"STRING"}, + {"name":"time","type":"LONG"} + ], + "payloadData": [ + { + "name": "gravity","type": "FLOAT" + } + ] +} + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Sparkscripts_1.0.0/Gravity_Sensor_Script.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Sparkscripts_1.0.0/Gravity_Sensor_Script.xml new file mode 100644 index 000000000..13fd7222d --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Sparkscripts_1.0.0/Gravity_Sensor_Script.xml @@ -0,0 +1,31 @@ + + + + + IoTServer_Sensor_Script + + 0 * * * * ? + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Sparkscripts_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Sparkscripts_1.0.0/artifact.xml new file mode 100644 index 000000000..4459a44fe --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/Sparkscripts_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + Gravity_Sensor_Script.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/artifacts.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/artifacts.xml new file mode 100644 index 000000000..7fc9acb07 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gravity/artifacts.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventreceiver_gyroscope_1.0.0/EventReceiver_gyroscope.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventreceiver_gyroscope_1.0.0/EventReceiver_gyroscope.xml new file mode 100644 index 000000000..00c7092ba --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventreceiver_gyroscope_1.0.0/EventReceiver_gyroscope.xml @@ -0,0 +1,26 @@ + + + + + + false + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventreceiver_gyroscope_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventreceiver_gyroscope_1.0.0/artifact.xml new file mode 100644 index 000000000..1ce90879c --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventreceiver_gyroscope_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + EventReceiver_gyroscope.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventstore_gyroscope_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventstore_gyroscope_1.0.0/artifact.xml new file mode 100644 index 000000000..6a2662c04 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventstore_gyroscope_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + org_wso2_iot_devices_gyroscope.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventstore_gyroscope_1.0.0/org_wso2_iot_devices_gyroscope.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventstore_gyroscope_1.0.0/org_wso2_iot_devices_gyroscope.xml new file mode 100644 index 000000000..899e32c85 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventstore_gyroscope_1.0.0/org_wso2_iot_devices_gyroscope.xml @@ -0,0 +1,62 @@ + + + + + + org.wso2.iot.devices.gyroscope:1.0.0 + + EVENT_STORE + + + meta_owner + true + true + false + STRING + + + meta_deviceType + true + true + false + STRING + + + meta_deviceId + true + true + false + STRING + + + meta_time + true + true + false + LONG + + + gyroscope + false + false + false + FLOAT + + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventstream_gyroscope_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventstream_gyroscope_1.0.0/artifact.xml new file mode 100644 index 000000000..f221d409c --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventstream_gyroscope_1.0.0/artifact.xml @@ -0,0 +1,23 @@ + + + + + org.wso2.iot.devices.gyroscope_1.0.0.json + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventstream_gyroscope_1.0.0/org.wso2.iot.devices.gyroscope_1.0.0.json b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventstream_gyroscope_1.0.0/org.wso2.iot.devices.gyroscope_1.0.0.json new file mode 100644 index 000000000..0583da295 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Eventstream_gyroscope_1.0.0/org.wso2.iot.devices.gyroscope_1.0.0.json @@ -0,0 +1,20 @@ +{ + "name": "org.wso2.iot.devices.gyroscope", + "version": "1.0.0", + "nickName": "Gyroscope Data", + "description": "Gyroscope data received from the Device", + "metaData": [ + {"name":"owner","type":"STRING"}, + {"name":"deviceType","type":"STRING"}, + {"name":"deviceId","type":"STRING"}, + {"name":"time","type":"LONG"} + ], + "payloadData": [ + { + "name": "gyroscope","type": "FLOAT" + } + ] +} + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Sparkscripts_1.0.0/Gyroscope_Sensor_Script.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Sparkscripts_1.0.0/Gyroscope_Sensor_Script.xml new file mode 100644 index 000000000..88e20226b --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Sparkscripts_1.0.0/Gyroscope_Sensor_Script.xml @@ -0,0 +1,31 @@ + + + + + IoTServer_Sensor_Script + + 0 * * * * ? + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Sparkscripts_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Sparkscripts_1.0.0/artifact.xml new file mode 100644 index 000000000..de967666d --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/Sparkscripts_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + Gyroscope_Sensor_Script.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/artifacts.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/artifacts.xml new file mode 100644 index 000000000..928f9398e --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Gyroscope/artifacts.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventreceiver_light_1.0.0/EventReceiver_light.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventreceiver_light_1.0.0/EventReceiver_light.xml new file mode 100644 index 000000000..09ced9de4 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventreceiver_light_1.0.0/EventReceiver_light.xml @@ -0,0 +1,26 @@ + + + + + + false + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventreceiver_light_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventreceiver_light_1.0.0/artifact.xml new file mode 100644 index 000000000..604e9384e --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventreceiver_light_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + EventReceiver_light.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventstore_light_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventstore_light_1.0.0/artifact.xml new file mode 100644 index 000000000..1e06aaa29 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventstore_light_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + org_wso2_iot_devices_light.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventstore_light_1.0.0/org_wso2_iot_devices_light.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventstore_light_1.0.0/org_wso2_iot_devices_light.xml new file mode 100644 index 000000000..d7051c715 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventstore_light_1.0.0/org_wso2_iot_devices_light.xml @@ -0,0 +1,62 @@ + + + + + + org.wso2.iot.devices.light:1.0.0 + + EVENT_STORE + + + meta_owner + true + true + false + STRING + + + meta_deviceType + true + true + false + STRING + + + meta_deviceId + true + true + false + STRING + + + meta_time + true + true + false + LONG + + + light + false + false + false + FLOAT + + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventstream_light_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventstream_light_1.0.0/artifact.xml new file mode 100644 index 000000000..6a6db53ad --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventstream_light_1.0.0/artifact.xml @@ -0,0 +1,23 @@ + + + + + org.wso2.iot.devices.light_1.0.0.json + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventstream_light_1.0.0/org.wso2.iot.devices.light_1.0.0.json b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventstream_light_1.0.0/org.wso2.iot.devices.light_1.0.0.json new file mode 100644 index 000000000..253f0b0c0 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Eventstream_light_1.0.0/org.wso2.iot.devices.light_1.0.0.json @@ -0,0 +1,20 @@ +{ + "name": "org.wso2.iot.devices.light", + "version": "1.0.0", + "nickName": "light Data", + "description": "light data received from the Device", + "metaData": [ + {"name":"owner","type":"STRING"}, + {"name":"deviceType","type":"STRING"}, + {"name":"deviceId","type":"STRING"}, + {"name":"time","type":"LONG"} + ], + "payloadData": [ + { + "name": "light","type": "FLOAT" + } + ] +} + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Sparkscripts_1.0.0/Light_Sensor_Script.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Sparkscripts_1.0.0/Light_Sensor_Script.xml new file mode 100644 index 000000000..53b53d544 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Sparkscripts_1.0.0/Light_Sensor_Script.xml @@ -0,0 +1,31 @@ + + + + + Light_Sensor_Script + + 0 * * * * ? + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Sparkscripts_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Sparkscripts_1.0.0/artifact.xml new file mode 100644 index 000000000..817faf53c --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/Sparkscripts_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + Light_Sensor_Script.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/artifacts.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/artifacts.xml new file mode 100644 index 000000000..1138a4c60 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Light/artifacts.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventreceiver_magnetic_1.0.0/EventReceiver_magnetic.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventreceiver_magnetic_1.0.0/EventReceiver_magnetic.xml new file mode 100644 index 000000000..33b0ee1ac --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventreceiver_magnetic_1.0.0/EventReceiver_magnetic.xml @@ -0,0 +1,26 @@ + + + + + + false + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventreceiver_magnetic_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventreceiver_magnetic_1.0.0/artifact.xml new file mode 100644 index 000000000..4a9272db8 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventreceiver_magnetic_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + EventReceiver_magnetic.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventstore_magnetic_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventstore_magnetic_1.0.0/artifact.xml new file mode 100644 index 000000000..67a726ea6 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventstore_magnetic_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + org_wso2_iot_devices_magnetic.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventstore_magnetic_1.0.0/org_wso2_iot_devices_magnetic.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventstore_magnetic_1.0.0/org_wso2_iot_devices_magnetic.xml new file mode 100644 index 000000000..725e2103f --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventstore_magnetic_1.0.0/org_wso2_iot_devices_magnetic.xml @@ -0,0 +1,62 @@ + + + + + + org.wso2.iot.devices.magnetic:1.0.0 + + EVENT_STORE + + + meta_owner + true + true + false + STRING + + + meta_deviceType + true + true + false + STRING + + + meta_deviceId + true + true + false + STRING + + + meta_time + true + true + false + LONG + + + magnetic + false + false + false + FLOAT + + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventstream_magnetic_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventstream_magnetic_1.0.0/artifact.xml new file mode 100644 index 000000000..24b52cb62 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventstream_magnetic_1.0.0/artifact.xml @@ -0,0 +1,23 @@ + + + + + org.wso2.iot.devices.magnetic_1.0.0.json + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventstream_magnetic_1.0.0/org.wso2.iot.devices.magnetic_1.0.0.json b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventstream_magnetic_1.0.0/org.wso2.iot.devices.magnetic_1.0.0.json new file mode 100644 index 000000000..5cbe22ba1 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Eventstream_magnetic_1.0.0/org.wso2.iot.devices.magnetic_1.0.0.json @@ -0,0 +1,20 @@ +{ + "name": "org.wso2.iot.devices.magnetic", + "version": "1.0.0", + "nickName": "magnetic Data", + "description": "magnetic data received from the Device", + "metaData": [ + {"name":"owner","type":"STRING"}, + {"name":"deviceType","type":"STRING"}, + {"name":"deviceId","type":"STRING"}, + {"name":"time","type":"LONG"} + ], + "payloadData": [ + { + "name": "magnetic","type": "FLOAT" + } + ] +} + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Sparkscripts_1.0.0/Magnetic_Sensor_Script.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Sparkscripts_1.0.0/Magnetic_Sensor_Script.xml new file mode 100644 index 000000000..3ee9b5797 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Sparkscripts_1.0.0/Magnetic_Sensor_Script.xml @@ -0,0 +1,30 @@ + + + + + IoTServer_Sensor_Script + + 0 * * * * ? + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Sparkscripts_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Sparkscripts_1.0.0/artifact.xml new file mode 100644 index 000000000..0c88076bf --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/Sparkscripts_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + Magnetic_Sensor_Script.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/artifacts.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/artifacts.xml new file mode 100644 index 000000000..b98adc8cf --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Magnetic/artifacts.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventreceiver_pressure_1.0.0/EventReceiver_pressure.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventreceiver_pressure_1.0.0/EventReceiver_pressure.xml new file mode 100644 index 000000000..e23884d78 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventreceiver_pressure_1.0.0/EventReceiver_pressure.xml @@ -0,0 +1,26 @@ + + + + + + false + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventreceiver_pressure_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventreceiver_pressure_1.0.0/artifact.xml new file mode 100644 index 000000000..3f6e571d3 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventreceiver_pressure_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + EventReceiver_pressure.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventstore_pressure_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventstore_pressure_1.0.0/artifact.xml new file mode 100644 index 000000000..139505cfc --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventstore_pressure_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + org_wso2_iot_devices_pressure.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventstore_pressure_1.0.0/org_wso2_iot_devices_pressure.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventstore_pressure_1.0.0/org_wso2_iot_devices_pressure.xml new file mode 100644 index 000000000..5e2f91f34 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventstore_pressure_1.0.0/org_wso2_iot_devices_pressure.xml @@ -0,0 +1,62 @@ + + + + + + org.wso2.iot.devices.pressure:1.0.0 + + EVENT_STORE + + + meta_owner + true + true + false + STRING + + + meta_deviceType + true + true + false + STRING + + + meta_deviceId + true + true + false + STRING + + + meta_time + true + true + false + LONG + + + pressure + false + false + false + FLOAT + + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventstream_pressure_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventstream_pressure_1.0.0/artifact.xml new file mode 100644 index 000000000..9c5d908c2 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventstream_pressure_1.0.0/artifact.xml @@ -0,0 +1,23 @@ + + + + + org.wso2.iot.devices.pressure_1.0.0.json + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventstream_pressure_1.0.0/org.wso2.iot.devices.pressure_1.0.0.json b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventstream_pressure_1.0.0/org.wso2.iot.devices.pressure_1.0.0.json new file mode 100644 index 000000000..f58059d28 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Eventstream_pressure_1.0.0/org.wso2.iot.devices.pressure_1.0.0.json @@ -0,0 +1,20 @@ +{ + "name": "org.wso2.iot.devices.pressure", + "version": "1.0.0", + "nickName": "Pressure Data", + "description": "Pressure data received from the Device", + "metaData": [ + {"name":"owner","type":"STRING"}, + {"name":"deviceType","type":"STRING"}, + {"name":"deviceId","type":"STRING"}, + {"name":"time","type":"LONG"} + ], + "payloadData": [ + { + "name": "pressure","type": "FLOAT" + } + ] +} + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Sparkscripts_1.0.0/Pressure_Sensor_Script.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Sparkscripts_1.0.0/Pressure_Sensor_Script.xml new file mode 100644 index 000000000..8d731ca31 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Sparkscripts_1.0.0/Pressure_Sensor_Script.xml @@ -0,0 +1,31 @@ + + + + + IoTServer_Sensor_Script + + 0 * * * * ? + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Sparkscripts_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Sparkscripts_1.0.0/artifact.xml new file mode 100644 index 000000000..4ff8b5331 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/Sparkscripts_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + Pressure_Sensor_Script.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/artifacts.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/artifacts.xml new file mode 100644 index 000000000..7403d3a69 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Pressure/artifacts.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventreceiver_proximity_1.0.0/EventReceiver_proximity.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventreceiver_proximity_1.0.0/EventReceiver_proximity.xml new file mode 100644 index 000000000..a5024f7d7 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventreceiver_proximity_1.0.0/EventReceiver_proximity.xml @@ -0,0 +1,26 @@ + + + + + + false + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventreceiver_proximity_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventreceiver_proximity_1.0.0/artifact.xml new file mode 100644 index 000000000..a6c67ff6c --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventreceiver_proximity_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + EventReceiver_proximity.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventstore_proximity_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventstore_proximity_1.0.0/artifact.xml new file mode 100644 index 000000000..0d21663c7 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventstore_proximity_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + org_wso2_iot_devices_proximity.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventstore_proximity_1.0.0/org_wso2_iot_devices_proximity.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventstore_proximity_1.0.0/org_wso2_iot_devices_proximity.xml new file mode 100644 index 000000000..ef4819322 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventstore_proximity_1.0.0/org_wso2_iot_devices_proximity.xml @@ -0,0 +1,62 @@ + + + + + + org.wso2.iot.devices.proximity:1.0.0 + + EVENT_STORE + + + meta_owner + true + true + false + STRING + + + meta_deviceType + true + true + false + STRING + + + meta_deviceId + true + true + false + STRING + + + meta_time + true + true + false + LONG + + + proximity + false + false + false + FLOAT + + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventstream_proximity_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventstream_proximity_1.0.0/artifact.xml new file mode 100644 index 000000000..4458b50a9 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventstream_proximity_1.0.0/artifact.xml @@ -0,0 +1,23 @@ + + + + + org.wso2.iot.devices.proximity_1.0.0.json + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventstream_proximity_1.0.0/org.wso2.iot.devices.proximity_1.0.0.json b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventstream_proximity_1.0.0/org.wso2.iot.devices.proximity_1.0.0.json new file mode 100644 index 000000000..88fb90089 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Eventstream_proximity_1.0.0/org.wso2.iot.devices.proximity_1.0.0.json @@ -0,0 +1,20 @@ +{ + "name": "org.wso2.iot.devices.proximity", + "version": "1.0.0", + "nickName": "Proximity Data", + "description": "Proximity data received from the Device", + "metaData": [ + {"name":"owner","type":"STRING"}, + {"name":"deviceType","type":"STRING"}, + {"name":"deviceId","type":"STRING"}, + {"name":"time","type":"LONG"} + ], + "payloadData": [ + { + "name": "proximity","type": "FLOAT" + } + ] +} + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Sparkscripts_1.0.0/Proximity_Sensor_Script.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Sparkscripts_1.0.0/Proximity_Sensor_Script.xml new file mode 100644 index 000000000..4bdbcc212 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Sparkscripts_1.0.0/Proximity_Sensor_Script.xml @@ -0,0 +1,31 @@ + + + + + IoTServer_Sensor_Script + + 0 * * * * ? + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Sparkscripts_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Sparkscripts_1.0.0/artifact.xml new file mode 100644 index 000000000..8486e84ee --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/Sparkscripts_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + Proximity_Sensor_Script.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/artifacts.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/artifacts.xml new file mode 100644 index 000000000..8b24e448e --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Proximity/artifacts.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventreceiver_rotation_1.0.0/EventReceiver_rotation.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventreceiver_rotation_1.0.0/EventReceiver_rotation.xml new file mode 100644 index 000000000..a2bb72edd --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventreceiver_rotation_1.0.0/EventReceiver_rotation.xml @@ -0,0 +1,26 @@ + + + + + + false + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventreceiver_rotation_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventreceiver_rotation_1.0.0/artifact.xml new file mode 100644 index 000000000..d49ec80d5 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventreceiver_rotation_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + EventReceiver_rotation.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventstore_rotation_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventstore_rotation_1.0.0/artifact.xml new file mode 100644 index 000000000..816c8f58c --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventstore_rotation_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + org_wso2_iot_devices_rotation.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventstore_rotation_1.0.0/org_wso2_iot_devices_rotation.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventstore_rotation_1.0.0/org_wso2_iot_devices_rotation.xml new file mode 100644 index 000000000..941f62198 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventstore_rotation_1.0.0/org_wso2_iot_devices_rotation.xml @@ -0,0 +1,62 @@ + + + + + + org.wso2.iot.devices.rotation:1.0.0 + + EVENT_STORE + + + meta_owner + true + true + false + STRING + + + meta_deviceType + true + true + false + STRING + + + meta_deviceId + true + true + false + STRING + + + meta_time + true + true + false + LONG + + + rotation + false + false + false + FLOAT + + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventstream_rotation_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventstream_rotation_1.0.0/artifact.xml new file mode 100644 index 000000000..585d53afb --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventstream_rotation_1.0.0/artifact.xml @@ -0,0 +1,23 @@ + + + + + org.wso2.iot.devices.rotation_1.0.0.json + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventstream_rotation_1.0.0/org.wso2.iot.devices.rotation_1.0.0.json b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventstream_rotation_1.0.0/org.wso2.iot.devices.rotation_1.0.0.json new file mode 100644 index 000000000..03881bcf3 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Eventstream_rotation_1.0.0/org.wso2.iot.devices.rotation_1.0.0.json @@ -0,0 +1,20 @@ +{ + "name": "org.wso2.iot.devices.rotation", + "version": "1.0.0", + "nickName": "Rotation Data", + "description": "Rotation data received from the Device", + "metaData": [ + {"name":"owner","type":"STRING"}, + {"name":"deviceType","type":"STRING"}, + {"name":"deviceId","type":"STRING"}, + {"name":"time","type":"LONG"} + ], + "payloadData": [ + { + "name": "rotation","type": "FLOAT" + } + ] +} + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Sparkscripts_1.0.0/Rotation_Sensor_Script.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Sparkscripts_1.0.0/Rotation_Sensor_Script.xml new file mode 100644 index 000000000..e03e7fab5 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Sparkscripts_1.0.0/Rotation_Sensor_Script.xml @@ -0,0 +1,31 @@ + + + + + IoTServer_Sensor_Script + + 0 * * * * ? + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Sparkscripts_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Sparkscripts_1.0.0/artifact.xml new file mode 100644 index 000000000..9a959892d --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/Sparkscripts_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + Rotation_Sensor_Script.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/artifacts.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/artifacts.xml new file mode 100644 index 000000000..7bbe4ac50 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/Rotation/artifacts.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventreceiver_wordcount_1.0.0/EventReceiver_wordcount.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventreceiver_wordcount_1.0.0/EventReceiver_wordcount.xml new file mode 100644 index 000000000..dac705e76 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventreceiver_wordcount_1.0.0/EventReceiver_wordcount.xml @@ -0,0 +1,27 @@ + + + + + + false + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventreceiver_wordcount_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventreceiver_wordcount_1.0.0/artifact.xml new file mode 100644 index 000000000..b9ee00adb --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventreceiver_wordcount_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + EventReceiver_wordcount.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventstore_wordcount_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventstore_wordcount_1.0.0/artifact.xml new file mode 100644 index 000000000..f1ff8a5b8 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventstore_wordcount_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + org_wso2_iot_devices_wordcount.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventstore_wordcount_1.0.0/org_wso2_iot_devices_wordcount.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventstore_wordcount_1.0.0/org_wso2_iot_devices_wordcount.xml new file mode 100644 index 000000000..8e9649dd1 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventstore_wordcount_1.0.0/org_wso2_iot_devices_wordcount.xml @@ -0,0 +1,76 @@ + + + + + + org.wso2.iot.devices.wordcount:1.0.0 + + EVENT_STORE + + + meta_owner + true + true + false + STRING + + + meta_deviceType + true + true + false + STRING + + + meta_deviceId + true + true + false + STRING + + + meta_time + true + true + false + LONG + + + sessionId + false + false + false + STRING + + + word + false + false + false + STRING + + + status + false + false + false + STRING + + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventstream_wordcount_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventstream_wordcount_1.0.0/artifact.xml new file mode 100644 index 000000000..c3cc85f26 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventstream_wordcount_1.0.0/artifact.xml @@ -0,0 +1,23 @@ + + + + + org.wso2.iot.devices.wordcount_1.0.0.json + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventstream_wordcount_1.0.0/org.wso2.iot.devices.wordcount_1.0.0.json b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventstream_wordcount_1.0.0/org.wso2.iot.devices.wordcount_1.0.0.json new file mode 100644 index 000000000..c5e6e7748 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Eventstream_wordcount_1.0.0/org.wso2.iot.devices.wordcount_1.0.0.json @@ -0,0 +1,17 @@ +{ + "name": "org.wso2.iot.devices.wordcount", + "version": "1.0.0", + "nickName": "Word Count Data", + "description": "Word Count received from the Device", + "metaData": [ + {"name":"owner","type":"STRING"}, + {"name":"deviceType","type":"STRING"}, + {"name":"deviceId","type":"STRING"}, + {"name":"time","type":"LONG"} + ], + "payloadData": [ + {"name" : "sessionId", "type" : "STRING"}, + {"name" : "word", "type" : "STRING"}, + {"name" : "status", "type" : "STRING"} + ] +} \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Sparkscripts_1.0.0/Wordcount_Script.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Sparkscripts_1.0.0/Wordcount_Script.xml new file mode 100644 index 000000000..9050af8ba --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Sparkscripts_1.0.0/Wordcount_Script.xml @@ -0,0 +1,34 @@ + + + + + IoTServer_Sensor_Script + + 0 * * * * ? + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Sparkscripts_1.0.0/artifact.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Sparkscripts_1.0.0/artifact.xml new file mode 100644 index 000000000..4f9088a72 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/Sparkscripts_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + Wordcount_Script.xml + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/artifacts.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/artifacts.xml new file mode 100644 index 000000000..737b58626 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.analytics/src/main/resources/carbonapps/WordCount/artifacts.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/pom.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/pom.xml new file mode 100644 index 000000000..4b2b5e682 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/pom.xml @@ -0,0 +1,205 @@ + + + + androidsense-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.androidsense.controller.api + war + WSO2 Carbon - Android Sense Controller API + WSO2 Carbon - Android Sense Service Controller API Implementation + http://wso2.org + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.core + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + provided + + + org.apache.cxf + 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 + provided + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.androidsense.plugin + provided + + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-jaxrs + + + javax + javaee-web-api + provided + + + javax.ws.rs + jsr311-api + provided + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.analytics + provided + + + + commons-httpclient.wso2 + commons-httpclient + provided + + + + org.wso2.carbon + org.wso2.carbon.utils + provided + + + org.bouncycastle.wso2 + bcprov-jdk15on + + + org.wso2.carbon + org.wso2.carbon.user.api + + + org.wso2.carbon + org.wso2.carbon.queuing + + + org.wso2.carbon + org.wso2.carbon.base + + + org.apache.axis2.wso2 + axis2 + + + org.igniterealtime.smack.wso2 + smack + + + org.igniterealtime.smack.wso2 + smackx + + + jaxen + jaxen + + + commons-fileupload.wso2 + commons-fileupload + + + org.apache.ant.wso2 + ant + + + org.apache.ant.wso2 + ant + + + commons-httpclient.wso2 + commons-httpclient + + + org.eclipse.equinox + javax.servlet + + + org.wso2.carbon + org.wso2.carbon.registry.api + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.annotations + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.extensions + provided + + + + + + + maven-compiler-plugin + + UTF-8 + ${wso2.maven.compiler.source} + ${wso2.maven.compiler.target} + + + + maven-war-plugin + + android_sense + + + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/controller/service/impl/AndroidSenseControllerService.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/controller/service/impl/AndroidSenseControllerService.java new file mode 100644 index 000000000..5d740dc33 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/controller/service/impl/AndroidSenseControllerService.java @@ -0,0 +1,667 @@ +/* + * 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.device.mgt.iot.androidsense.controller.service.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.apimgt.annotations.api.API; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.device.mgt.analytics.common.AnalyticsDataRecord; +import org.wso2.carbon.device.mgt.analytics.exception.DataPublisherConfigurationException; +import org.wso2.carbon.device.mgt.analytics.exception.DeviceManagementAnalyticsException; +import org.wso2.carbon.device.mgt.analytics.service.DeviceAnalyticsService; +import org.wso2.carbon.device.mgt.extensions.feature.mgt.annotations.DeviceType; +import org.wso2.carbon.device.mgt.extensions.feature.mgt.annotations.Feature; +import org.wso2.carbon.device.mgt.iot.androidsense.controller.service.impl.transport.AndroidSenseMQTTConnector; +import org.wso2.carbon.device.mgt.iot.androidsense.controller.service.impl.util.DeviceData; +import org.wso2.carbon.device.mgt.iot.androidsense.controller.service.impl.util.SensorData; +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.servlet.http.HttpServletResponse; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * The api for + */ +@DeviceType(value = "android_sense") +@API(name = "android_sense", version = "1.0.0", context = "/android_sense", tags = {"android_sense"}) +public class AndroidSenseControllerService { + @Context //injected response proxy supporting multiple thread + private HttpServletResponse response; + private static Log log = LogFactory.getLog(AndroidSenseControllerService.class); + private static AndroidSenseMQTTConnector androidSenseMQTTConnector; + + /** + * Fetches the `AndroidSenseMQTTConnector` specific to this Android Sense controller service. + * + * @return the 'AndroidSenseMQTTConnector' instance bound to the 'AndroidSenseMQTTConnector' variable of + * this service. + */ + @SuppressWarnings("Unused") + public AndroidSenseMQTTConnector getAndroidSenseMQTTConnector() { + return androidSenseMQTTConnector; + } + + /** + * Sets the `AndroidSenseMQTTConnector` variable of this Android Sense controller service. + * + * @param androidSenseMQTTConnector a 'AndroidSenseMQTTConnector' object that handles all MQTT related + * communications of any connected Android Sense device-type + */ + @SuppressWarnings("Unused") + public void setAndroidSenseMQTTConnector(final AndroidSenseMQTTConnector androidSenseMQTTConnector) { + Runnable connector = new Runnable() { + public void run() { + if (waitForServerStartup()) { + return; + } + AndroidSenseControllerService.androidSenseMQTTConnector = androidSenseMQTTConnector; + if (MqttConfig.getInstance().isEnabled()) { + androidSenseMQTTConnector.connect(); + } else { + log.warn("MQTT disabled in 'devicemgt-config.xml'. Hence, VirtualFireAlarmMQTTConnector not started."); + } + } + }; + Thread connectorThread = new Thread(connector); + connectorThread.setDaemon(true); + connectorThread.start(); + } + + private boolean waitForServerStartup() { + while (!IoTServerStartupListener.isServerReady()) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + return true; + } + } + return false; + } + + /** + * Service to push all the sensor data collected by the Android. Called by the Android device + * + * @param dataMsg The json string containing sensor readings + * @param response the HTTP servlet response object received by default as part of the HTTP + * call to this API + */ + @Path("controller/sensors") + @POST + @Consumes(MediaType.APPLICATION_JSON) + public void addSensorData(DeviceData dataMsg, @Context HttpServletResponse response) { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + DeviceAnalyticsService deviceAnalyticsService = (DeviceAnalyticsService) ctx + .getOSGiService(DeviceAnalyticsService.class, null); + SensorData[] sensorData = dataMsg.values; + String streamDef = null; + Object payloadData[] = null; + String sensorName = null; + for (SensorData sensor : sensorData) { + switch (sensor.key) { + case "battery" : + streamDef = AndroidSenseConstants.BATTERY_STREAM_DEFINITION; + payloadData = new Float[]{Float.parseFloat(sensor.value)}; + sensorName = AndroidSenseConstants.SENSOR_BATTERY; + break; + case "GPS" : + streamDef = AndroidSenseConstants.GPS_STREAM_DEFINITION; + String gpsValue = sensor.value; + String gpsValuesString[] = gpsValue.split(","); + Float gpsValues[] = new Float[2]; + gpsValues[0] = Float.parseFloat(gpsValuesString[0]); + gpsValues[1] = Float.parseFloat(gpsValuesString[0]); + payloadData = gpsValues; + sensorName = AndroidSenseConstants.SENSOR_GPS; + break; + default : + try { + int androidSensorId = Integer.parseInt(sensor.key); + String value = sensor.value; + String sensorValueString[] = value.split(","); + Float sensorValues[] = new Float[1]; + switch (androidSensorId) { + case 1: + streamDef = AndroidSenseConstants.ACCELEROMETER_STREAM_DEFINITION; + sensorValues[0] = Float.parseFloat(sensorValueString[0]) * + Float.parseFloat(sensorValueString[0]) * Float.parseFloat(sensorValueString[0]); + payloadData = sensorValues; + sensorName = AndroidSenseConstants.SENSOR_ACCELEROMETER; + break; + case 2: + streamDef = AndroidSenseConstants.MAGNETIC_STREAM_DEFINITION; + sensorValues[0] = Float.parseFloat(sensorValueString[0]) * + Float.parseFloat(sensorValueString[0]) * Float.parseFloat(sensorValueString[0]); + payloadData = sensorValues; + sensorName = AndroidSenseConstants.SENSOR_MAGNETIC; + break; + case 4: + streamDef = AndroidSenseConstants.GYROSCOPE_STREAM_DEFINITION; + sensorValues[0] = Float.parseFloat(sensorValueString[0]) * + Float.parseFloat(sensorValueString[0]) * Float.parseFloat(sensorValueString[0]); + payloadData = sensorValues; + sensorName = AndroidSenseConstants.SENSOR_GYROSCOPE; + break; + case 5: + streamDef = AndroidSenseConstants.LIGHT_STREAM_DEFINITION; + sensorName = AndroidSenseConstants.SENSOR_LIGHT; + payloadData = new Float[]{Float.parseFloat(sensorValueString[0])}; + break; + case 6: + streamDef = AndroidSenseConstants.PRESSURE_STREAM_DEFINITION; + sensorName = AndroidSenseConstants.SENSOR_PRESSURE; + payloadData = new Float[]{Float.parseFloat(sensorValueString[0])}; + break; + case 8: + streamDef = AndroidSenseConstants.PROXIMITY_STREAM_DEFINITION; + sensorName = AndroidSenseConstants.SENSOR_PROXIMITY; + payloadData = new Float[]{Float.parseFloat(sensorValueString[0])}; + break; + case 9: + streamDef = AndroidSenseConstants.GRAVITY_STREAM_DEFINITION; + sensorValues[0] = Float.parseFloat(sensorValueString[0]) * + Float.parseFloat(sensorValueString[0]) * Float.parseFloat(sensorValueString[0]); + payloadData = sensorValues; + sensorName = AndroidSenseConstants.SENSOR_GRAVITY; + break; + case 11: + streamDef = AndroidSenseConstants.ROTATION_STREAM_DEFINITION; + sensorValues[0] = Float.parseFloat(sensorValueString[0]) * + Float.parseFloat(sensorValueString[0]) * Float.parseFloat(sensorValueString[0]); + payloadData = sensorValues; + sensorName = AndroidSenseConstants.SENSOR_ROTATION; + break; + } + } catch (NumberFormatException e) { + log.error("Invalid sensor value is sent from the device"); + continue; + } + } + Object metaData[] = {dataMsg.owner, AndroidSenseConstants.DEVICE_TYPE, dataMsg.deviceId, sensor.time}; + if (streamDef != null && payloadData != null && payloadData.length > 0) { + try { + SensorDataManager.getInstance() + .setSensorRecord(dataMsg.deviceId, sensorName, sensor.value, sensor.time); + deviceAnalyticsService.publishEvent(streamDef, "1.0.0", metaData, new Object[0], payloadData); + } catch (DataPublisherConfigurationException e) { + response.setStatus(Response.Status.UNSUPPORTED_MEDIA_TYPE.getStatusCode()); + } + } + } + } + + + /** + * End point which is called by Front-end js to get Light sensor readings from the server. + * + * @param deviceId The registered device id + * @param response the HTTP servlet response object received by default as part of the HTTP + * call to this API. + * @return This method returns a SensorRecord object. + */ + @Path("controller/device/{deviceId}/sensors/light") + @GET + @Consumes("application/json") + @Produces("application/json") + @Feature(code = "light", name = "Light", description = "Read Light data from the device", type = "monitor") + public SensorRecord getLightData(@PathParam("deviceId") String deviceId, @Context HttpServletResponse response) { + SensorRecord sensorRecord = null; + try { + sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, AndroidSenseConstants.SENSOR_LIGHT); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceControllerException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } + return sensorRecord; + } + + /** + * End point which is called by Front-end js to get Battery data from the server. + * @param deviceId The registered device id + * @param response the HTTP servlet response object received by default as part of the HTTP + * call to this API. + * @return This method returns a SensorRecord object. + */ + @Path("controller/device/{deviceId}/sensors/battery") + @GET + @Consumes("application/json") + @Produces("application/json") + @Feature(code = "battery", name = "Battery", description = "Read Battery data from the device", type = "monitor") + public SensorRecord getBattery(@PathParam("deviceId") String deviceId, @Context HttpServletResponse response) { + SensorRecord sensorRecord = null; + try { + sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, AndroidSenseConstants.SENSOR_BATTERY); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceControllerException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } + return sensorRecord; + } + + /** + * End point which is called by Front-end js to get GPS data from the server. + * @param deviceId The registered device id + * @param response the HTTP servlet response object received by default as part of the HTTP + * call to this API. + * @return This method returns a SensorRecord object. + */ + @Path("controller/device/{deviceId}/sensors/gps") + @GET + @Consumes("application/json") + @Produces("application/json") + @Feature(code = "gps", name = "gps", description = "Read GPS data from the device", type = "monitor") + public SensorRecord getGPS(@PathParam("deviceId") String deviceId, @Context HttpServletResponse response) { + SensorRecord sensorRecord = null; + try { + sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, AndroidSenseConstants.SENSOR_GPS); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceControllerException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } + return sensorRecord; + } + + /** + * End point which is called by Front-end js to get Magnetic data readings from the server. + * @param deviceId The registered device id + * @param response the HTTP servlet response object received by default as part of the HTTP + * call to this API. + * @return This method returns a SensorRecord object. + */ + @Path("controller/device/{deviceId}/sensors/magnetic") + @GET + @Consumes("application/json") + @Produces("application/json") + @Feature(code = "magnetic", name = "Magnetic", description = "Read Magnetic data from the device", type = "monitor") + public SensorRecord readMagnetic(@PathParam("deviceId") String deviceId, @Context HttpServletResponse response) { + SensorRecord sensorRecord = null; + try { + sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, AndroidSenseConstants.SENSOR_MAGNETIC); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceControllerException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } + return sensorRecord; + } + + /** + * End point which is called by Front-end js to get Accelerometer data from the server. + * @param deviceId The registered device id + * @param response the HTTP servlet response object received by default as part of the HTTP call to this API. + * @return This method returns a SensorRecord object. + */ + @Path("controller/device/{deviceId}/sensors/accelerometer") + @GET + @Consumes("application/json") + @Produces("application/json") + @Feature(code = "accelerometer", name = "Accelerometer", description = "Read Accelerometer data from the device", + type = "monitor") + public SensorRecord readAccelerometer(@PathParam("deviceId") String deviceId, @Context HttpServletResponse response) { + SensorRecord sensorRecord = null; + try { + sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, + AndroidSenseConstants.SENSOR_ACCELEROMETER); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceControllerException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } + return sensorRecord; + } + + /** + * End point which is called by Front-end js to get Rotation data from the server. + * @param deviceId The registered device id + * @param response the HTTP servlet response object received by default as part of the HTTP + * call to this API. + * @return This method returns a SensorRecord object. + */ + @Path("controller/device/{deviceId}/sensors/rotation") + @GET + @Consumes("application/json") + @Produces("application/json") + @Feature(code = "rotation", name = "Rotation", description = "Read Rotational Vector data from the device", + type = "monitor") + public SensorRecord readRotation(@PathParam("deviceId") String deviceId, @Context HttpServletResponse response) { + SensorRecord sensorRecord = null; + try { + sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, + AndroidSenseConstants.SENSOR_ROTATION); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceControllerException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } + return sensorRecord; + } + + /** + * End point which is called by Front-end js to get Proximity data from the server. + * @param deviceId The registered device id + * @param response the HTTP servlet response object received by default as part of the HTTP + * call to this API. + * @return This method returns a SensorRecord object. + */ + @Path("controller/device/{deviceId}/sensors/proximity") + @GET + @Consumes("application/json") + @Produces("application/json") + @Feature(code = "proximity", name = "Proximity", description = "Read Proximity data from the device", + type = "monitor") + public SensorRecord readProximity(@PathParam("deviceId") String deviceId, @Context HttpServletResponse response) { + SensorRecord sensorRecord = null; + try { + sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, + AndroidSenseConstants.SENSOR_PROXIMITY); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceControllerException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } + return sensorRecord; + } + + /** + * End point which is called by Front-end js to get Gyroscope data from the server. + * @param deviceId The registered device id + * @param response the HTTP servlet response object received by default as part of the HTTP + * call to this API. + * @return This method returns a SensorRecord object. + */ + @Path("controller/device/{deviceId}/sensors/gyroscope") + @GET + @Consumes("application/json") + @Produces("application/json") + @Feature(code = "gyroscope", name = "Gyroscope", description = "Read Gyroscope data from the device", + type = "monitor") + public SensorRecord readGyroscope(@PathParam("deviceId") String deviceId, @Context HttpServletResponse response) { + SensorRecord sensorRecord = null; + try { + sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, + AndroidSenseConstants.SENSOR_GYROSCOPE); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceControllerException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } + return sensorRecord; + } + + /** + * End point which is called by Front-end js to get Pressure data from the server. + * @param deviceId The registered device id + * @param response the HTTP servlet response object received by default as part of the HTTP + * call to this API. + * @return This method returns a SensorRecord object. + */ + @Path("controller/device/{deviceId}/sensors/pressure") + @GET + @Consumes("application/json") + @Produces("application/json") + @Feature(code = "pressure", name = "Pressure", description = "Read Pressure data from the device", type = "monitor") + public SensorRecord readPressure(@PathParam("deviceId") String deviceId, @Context HttpServletResponse response) { + SensorRecord sensorRecord = null; + try { + sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, + AndroidSenseConstants.SENSOR_PRESSURE); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceControllerException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } + return sensorRecord; + } + + /** + * End point which is called by Front-end js to get Gravity data from the server. + * + * @param deviceId The registered device id + * @param response the HTTP servlet response object received by default as part of the HTTP + * call to this API. + * @return This method returns a SensorRecord object. + */ + @Path("controller/device/{deviceId}/sensors/gravity") + @GET + @Consumes("application/json") + @Produces("application/json") + @Feature(code = "gravity", name = "Gravity", + description = "Read Gravity data from the device", type = "monitor") + public SensorRecord readGravity(@PathParam("deviceId") String deviceId, @Context HttpServletResponse response) { + SensorRecord sensorRecord = null; + try { + sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, + AndroidSenseConstants.SENSOR_GRAVITY); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceControllerException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } + return sensorRecord; + } + + @Path("controller/device/{deviceId}/sensors/words") + @GET + @Consumes("application/json") + @Produces("application/json") + @Feature(code = "words", name = "Words", description = "Get the key words and occurrences", + type = "monitor") + public SensorRecord getWords(@PathParam("deviceId") String deviceId, @QueryParam("sessionId") String sessionId, + @Context HttpServletResponse response) { + SensorRecord sensorRecord = null; + try { + sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, + AndroidSenseConstants.SENSOR_WORDCOUNT); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceControllerException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } + return sensorRecord; + } + + /** + * End point to send the key words to the device + + * @param deviceId The registered device Id. + * @param keywords The key words to be sent. (Comma separated values) + * @param response the HTTP servlet response object received by default as part of the HTTP + * call to this API + */ + @Path("controller/device/{deviceId}/sensors/words") + @POST + @Feature(code = "keywords", name = "Add Keywords", description = "Send keywords to the device", + type = "operation") + public void sendKeyWords(@PathParam("deviceId") String deviceId, @FormParam("keywords") String keywords, + @Context HttpServletResponse response) { + try { + String username = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUsername(); + androidSenseMQTTConnector.publishDeviceData(username, deviceId, "add", keywords); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (TransportHandlerException e) { + log.error(e); + response.setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); + } + } + + /** + * End point to send the key words to the device + * + * @param deviceId The registered device Id. + * @param threshold The key words to be sent. (Comma separated values) + * @param response the HTTP servlet response object received by default as part of the HTTP + * call to this API + */ + @Path("controller/device/{deviceId}/sensors/words/threshold") + @POST + @Feature(code = "threshold", name = "Add a Threshold", description = "Set a threshold for word in the device", + type = "operation") + public void sendThreshold(@PathParam("deviceId") String deviceId, @FormParam("threshold") String threshold, + @Context HttpServletResponse response) { + try { + String username = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUsername(); + androidSenseMQTTConnector.publishDeviceData(username, deviceId, "threshold", threshold); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (TransportHandlerException e) { + log.error(e); + response.setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); + } + } + + @Path("controller/device/{deviceId}/sensors/words") + @DELETE + @Feature(code = "remove", name = "Remove Keywords", description = "Remove the keywords", + type = "operation") + public void removeKeyWords(@PathParam("deviceId") String deviceId, @QueryParam("words") String words, + @Context HttpServletResponse response) { + try { + String username = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUsername(); + androidSenseMQTTConnector.publishDeviceData(username, deviceId, "remove", words); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (TransportHandlerException e) { + log.error(e); + response.setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); + } + } + + /** + * Retrieve Sensor data for the device type + * + */ + @Path("controller/stats/device/{deviceId}/sensors/{sensorName}") + @GET + @Consumes("application/json") + @Produces("application/json") + public SensorData[] getAndroidSenseDeviceStats(@PathParam("deviceId") String deviceId, + @PathParam("sensorName") String sensor, + @QueryParam("from") long from, + @QueryParam("to") long to) { + 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); + 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); + } + } 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); + } + } + return sensorDatas.toArray(new SensorData[sensorDatas.size()]); + } catch (DeviceManagementAnalyticsException e) { + String errorMsg = "Error on retrieving stats on table " + sensorTableName + " with query " + query; + log.error(errorMsg); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return sensorDatas.toArray(new SensorData[sensorDatas.size()]); + } + } + + /** + * get the event table from the sensor name. + */ + private String getSensorEventTableName(String sensorName) { + String sensorEventTableName; + switch (sensorName) { + case AndroidSenseConstants.SENSOR_ACCELEROMETER: + sensorEventTableName = "DEVICE_ACCELEROMETER_SUMMARY"; + break; + case AndroidSenseConstants.SENSOR_BATTERY: + sensorEventTableName = "DEVICE_BATTERY_SUMMARY"; + break; + case AndroidSenseConstants.SENSOR_GPS: + sensorEventTableName = "DEVICE_GPS_SUMMARY"; + break; + case AndroidSenseConstants.SENSOR_GRAVITY: + sensorEventTableName = "DEVICE_GRAVITY_SUMMARY"; + break; + case AndroidSenseConstants.SENSOR_GYROSCOPE: + sensorEventTableName = "DEVICE_GYROSCOPE_SUMMARY"; + break; + case AndroidSenseConstants.SENSOR_LIGHT: + sensorEventTableName = "DEVICE_LIGHT_SUMMARY"; + break; + case AndroidSenseConstants.SENSOR_MAGNETIC: + sensorEventTableName = "DEVICE_MAGNETIC_SUMMARY"; + break; + case AndroidSenseConstants.SENSOR_PRESSURE: + sensorEventTableName = "DEVICE_PRESSURE_SUMMARY"; + break; + case AndroidSenseConstants.SENSOR_PROXIMITY: + sensorEventTableName = "DevicePROXIMITYSummaryData"; + break; + case AndroidSenseConstants.SENSOR_ROTATION: + sensorEventTableName = "DEVICE_ROTATION_SUMMARY"; + break; + case AndroidSenseConstants.SENSOR_WORDCOUNT: + sensorEventTableName = "WORD_COUNT_SUMMARY"; + break; + default: + sensorEventTableName = ""; + } + return sensorEventTableName; + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/controller/service/impl/transport/AndroidSenseMQTTConnector.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/controller/service/impl/transport/AndroidSenseMQTTConnector.java new file mode 100644 index 000000000..52ace2bf5 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/controller/service/impl/transport/AndroidSenseMQTTConnector.java @@ -0,0 +1,339 @@ +/* + * 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.device.mgt.iot.androidsense.controller.service.impl.transport; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +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.context.PrivilegedCarbonContext; +import org.wso2.carbon.device.mgt.analytics.exception.DataPublisherConfigurationException; +import org.wso2.carbon.device.mgt.analytics.service.DeviceAnalyticsService; +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.controller.service.impl.util.DeviceData; +import org.wso2.carbon.device.mgt.iot.androidsense.controller.service.impl.util.SensorData; +import org.wso2.carbon.device.mgt.iot.androidsense.plugin.constants.AndroidSenseConstants; +import org.wso2.carbon.device.mgt.iot.config.server.DeviceManagementConfigurationManager; +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.utils.multitenancy.MultitenantUtils; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +@SuppressWarnings("no JAX-WS annotation") +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 AndroidSenseMQTTConnector() { + super(iotServerSubscriber, AndroidSenseConstants.DEVICE_TYPE, + MqttConfig.getInstance().getMqttQueueEndpoint(), subscribeTopic); + } + + @Override + public void connect() { + Runnable connector = new Runnable() { + public void run() { + while (!isConnected()) { + try { + connectToQueue(); + subscribeToQueue(); + } catch (TransportHandlerException e) { + log.warn("Connection/Subscription to MQTT Broker at: " + mqttBrokerEndPoint + " failed"); + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException ex) { + log.error("MQTT-Subscriber: Thread Sleep Interrupt Exception.", ex); + } + } + } + } + }; + Thread connectorThread = new Thread(connector); + connectorThread.setDaemon(true); + connectorThread.start(); + } + + /** + * @throws TransportHandlerException in the event of any exceptions that occur whilst processing the message. + */ + @Override + public void processIncomingMessage() throws TransportHandlerException { + } + + /** + * @param message the message (of the type specific to the protocol) received from the device. + * @throws TransportHandlerException + */ + @Override + public void processIncomingMessage(MqttMessage message) throws TransportHandlerException { + } + + + @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.error(errorMsg); + throw new TransportHandlerException(errorMsg); + } + String deviceId = publishData[0]; + String operation = publishData[1]; + String resource = publishData[2]; + MqttMessage pushMessage = new MqttMessage(); + String publishTopic = "wso2/" + AndroidSenseConstants.DEVICE_TYPE + "/" + deviceId + "/command"; + if (operation.equals("add")) { + publishTopic = publishTopic + "/words"; + } else if (operation.equals("remove")) { + publishTopic = publishTopic + "/remove"; + } else if (operation.equals("threshold")) { + publishTopic = publishTopic + "/threshold"; + } else { + return; + } + String actualMessage = resource; + pushMessage.setPayload(actualMessage.getBytes(StandardCharsets.UTF_8)); + pushMessage.setQos(DEFAULT_MQTT_QUALITY_OF_SERVICE); + pushMessage.setRetained(false); + publishToQueue(publishTopic, pushMessage); + } + + @Override + public void processIncomingMessage(MqttMessage mqttMessage, String... strings) throws TransportHandlerException { + String[] topic = strings[0].split("/"); + String deviceId = topic[3]; + if (log.isDebugEnabled()) { + log.debug("Received MQTT message for: & [DEVICE.ID-" + deviceId + "]"); + } + try { + Gson gson = new Gson(); + String actualMessage = mqttMessage.toString(); + DeviceData deviceData = gson.fromJson(actualMessage, DeviceData.class); + + SensorData[] sensorData = deviceData.values; + String streamDef = null; + Object payloadData[] = null; + String sensorName = null; + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + ctx.setTenantDomain("carbon.super", true); + for (SensorData sensor : sensorData) { + if (sensor.key.equals("battery")) { + streamDef = AndroidSenseConstants.BATTERY_STREAM_DEFINITION; + payloadData = new Float[]{Float.parseFloat(sensor.value)}; + sensorName = AndroidSenseConstants.SENSOR_BATTERY; + } else if (sensor.key.equals("GPS")) { + streamDef = AndroidSenseConstants.GPS_STREAM_DEFINITION; + String gpsValue = sensor.value; + String gpsValuesString[] = gpsValue.split(","); + Float gpsValues[] = new Float[2]; + gpsValues[0] = Float.parseFloat(gpsValuesString[0]); + gpsValues[1] = Float.parseFloat(gpsValuesString[0]); + payloadData = gpsValues; + sensorName = AndroidSenseConstants.SENSOR_GPS; + } else if (sensor.key.equals("word")) { + try { + streamDef = AndroidSenseConstants.WORD_COUNT_STREAM_DEFINITION; + String[] values = sensor.value.split(","); + String sessionId = values[0]; + String keyword = values[1]; + int occurrence = Integer.parseInt(values[2]); + String status = values[3]; + sensorName = AndroidSenseConstants.SENSOR_WORDCOUNT; + + if (occurrence > 0) { + payloadData = new Object[]{sessionId, keyword, status}; + for (int i = 0; i < occurrence; i++) { + Long timestamp = Long.parseLong(values[3 + occurrence]); + publishDataToDAS(deviceId, timestamp, sensorName, streamDef, + sensor.value, payloadData); + + } + continue; + } + } catch (ArrayIndexOutOfBoundsException e) { + log.error( + "Timestamp does not match the occurence sensor values are sent from the device."); + } + continue; + } else { + try { + int androidSensorId = Integer.parseInt(sensor.key); + String sensorValue = sensor.value; + String sensorValuesString[] = sensorValue.split(","); + Float sensorValues[] = new Float[1]; + + switch (androidSensorId) { + case 1: + streamDef = AndroidSenseConstants.ACCELEROMETER_STREAM_DEFINITION; + sensorValues[0] = Float.parseFloat(sensorValuesString[0]) * Float.parseFloat( + sensorValuesString[0]) * Float. + parseFloat(sensorValuesString[0]); + payloadData = sensorValues; + sensorName = AndroidSenseConstants.SENSOR_ACCELEROMETER; + break; + case 2: + streamDef = AndroidSenseConstants.MAGNETIC_STREAM_DEFINITION; + sensorValues[0] = Float.parseFloat(sensorValuesString[0]) * Float.parseFloat( + sensorValuesString[0]) * Float. + parseFloat(sensorValuesString[0]); + payloadData = sensorValues; + sensorName = AndroidSenseConstants.SENSOR_MAGNETIC; + break; + case 4: + streamDef = AndroidSenseConstants.GYROSCOPE_STREAM_DEFINITION; + sensorValues[0] = Float.parseFloat(sensorValuesString[0]) * Float.parseFloat( + sensorValuesString[0]) * Float. + parseFloat(sensorValuesString[0]); + payloadData = sensorValues; + sensorName = AndroidSenseConstants.SENSOR_GYROSCOPE; + break; + case 5: + streamDef = AndroidSenseConstants.LIGHT_STREAM_DEFINITION; + sensorName = AndroidSenseConstants.SENSOR_LIGHT; + payloadData = new Float[]{Float.parseFloat(sensorValuesString[0])}; + break; + case 6: + streamDef = AndroidSenseConstants.PRESSURE_STREAM_DEFINITION; + sensorName = AndroidSenseConstants.SENSOR_PRESSURE; + payloadData = new Float[]{Float.parseFloat(sensorValuesString[0])}; + break; + case 8: + streamDef = AndroidSenseConstants.PROXIMITY_STREAM_DEFINITION; + sensorName = AndroidSenseConstants.SENSOR_PROXIMITY; + payloadData = new Float[]{Float.parseFloat(sensorValuesString[0])}; + break; + case 9: + streamDef = AndroidSenseConstants.GRAVITY_STREAM_DEFINITION; + sensorValues[0] = Float.parseFloat(sensorValuesString[0]) * Float.parseFloat( + sensorValuesString[0]) * Float. + parseFloat(sensorValuesString[0]); + payloadData = sensorValues; + sensorName = AndroidSenseConstants.SENSOR_GRAVITY; + break; + case 11: + streamDef = AndroidSenseConstants.ROTATION_STREAM_DEFINITION; + sensorValues[0] = Float.parseFloat(sensorValuesString[0]) * Float.parseFloat( + sensorValuesString[0]) * Float. + parseFloat(sensorValuesString[0]); + payloadData = sensorValues; + sensorName = AndroidSenseConstants.SENSOR_ROTATION; + break; + } + } catch (NumberFormatException e) { + log.error("Invalid sensor values are sent from the device."); + continue; + } + } + publishDataToDAS(deviceId, sensor.time, sensorName, streamDef, sensor.value, payloadData); + } + } catch (JsonSyntaxException e) { + throw new TransportHandlerException("Invalid message format " + mqttMessage.toString()); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + private void publishDataToDAS(String deviceId, Long time, String sensorName, + String streamDefinition, String sensorValue, Object payloadData[]) { + try { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + DeviceManagementProviderService deviceManagementProviderService = (DeviceManagementProviderService) ctx + .getOSGiService(DeviceManagementProviderService.class, null); + if (deviceManagementProviderService != null) { + DeviceIdentifier identifier = new DeviceIdentifier(deviceId, AndroidSenseConstants.DEVICE_TYPE); + Device device = deviceManagementProviderService.getDevice(identifier); + if (device != null) { + String owner = device.getEnrolmentInfo().getOwner(); + ctx.setTenantDomain(MultitenantUtils.getTenantDomain(owner), true); + DeviceAnalyticsService deviceAnalyticsService = (DeviceAnalyticsService) ctx + .getOSGiService(DeviceAnalyticsService.class, null); + if (deviceAnalyticsService != null) { + Object metaData[] = {owner, AndroidSenseConstants.DEVICE_TYPE, deviceId, time}; + if (streamDefinition != null && payloadData != null && payloadData.length > 0) { + try { + SensorDataManager.getInstance().setSensorRecord(deviceId, sensorName, sensorValue, time); + deviceAnalyticsService.publishEvent(streamDefinition, "1.0.0", metaData, + new Object[0], payloadData); + } catch (DataPublisherConfigurationException e) { + log.error("Data publisher configuration failed - " + e); + } + } + } + } + } + } catch (DeviceManagementException e) { + log.error("Failed to load device management service.", e); + } + } + + /** + * @throws TransportHandlerException in the event of any exceptions that occur whilst sending the message. + */ + @Override + public 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. + */ + @Override + public void publishDeviceData(MqttMessage publishData) throws TransportHandlerException { + + } + + @Override + public void disconnect () { + Runnable stopConnection = new Runnable() { + public void run() { + while (isConnected()) { + try { + closeConnection(); + } catch (MqttException e) { + if (log.isDebugEnabled()) { + log.warn("Unable to 'STOP' MQTT connection at broker at: " + mqttBrokerEndPoint + + " for device-type - " + AndroidSenseConstants.DEVICE_TYPE, e); + } + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException e1) { + log.error("MQTT-Terminator: Thread Sleep Interrupt Exception at device-type - " + + AndroidSenseConstants.DEVICE_TYPE, e1); + } + } + } + } + }; + Thread terminatorThread = new Thread(stopConnection); + terminatorThread.setDaemon(true); + terminatorThread.start(); + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/controller/service/impl/util/DeviceData.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/controller/service/impl/util/DeviceData.java new file mode 100644 index 000000000..2577a21f8 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/controller/service/impl/util/DeviceData.java @@ -0,0 +1,15 @@ +package org.wso2.carbon.device.mgt.iot.androidsense.controller.service.impl.util; + +import org.codehaus.jackson.annotate.JsonIgnoreProperties; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement + +@JsonIgnoreProperties(ignoreUnknown = true) +public class DeviceData { + @XmlElement(required = true) public String owner; + @XmlElement(required = true) public String deviceId; + @XmlElement public SensorData[] values; +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/controller/service/impl/util/SensorData.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/controller/service/impl/util/SensorData.java new file mode 100644 index 000000000..a2f9cebf9 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/controller/service/impl/util/SensorData.java @@ -0,0 +1,44 @@ +package org.wso2.carbon.device.mgt.iot.androidsense.controller.service.impl.util; + +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 android sense. + */ +@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/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/webapp/META-INF/permissions.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/webapp/META-INF/permissions.xml new file mode 100644 index 000000000..d30a5096f --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/webapp/META-INF/permissions.xml @@ -0,0 +1,143 @@ + + + + + + + + + + add sensor information + /login + /controller/sensors + POST + + + + Get light information + /login + /controller/device/{deviceId}/sensors/light + GET + + + + Get battery information + /login + /controller/device/{deviceId}/sensors/battery + GET + + + + Get gps information + /login + /controller/device/{deviceId}/sensors/gps + GET + + + + Get magnetic information + /login + /controller/device/{deviceId}/sensors/magnetic + GET + + + + Get accelerometer information + /login + /controller/device/{deviceId}/sensors/accelerometer + GET + + + + Get rotation information + /login + /controller/device/{deviceId}/sensors/rotation + GET + + + + Get proximity information + /login + /controller/device/{deviceId}/sensors/proximity + GET + + + + Get gyroscope information + /login + /controller/device/{deviceId}/sensors/gyroscope + GET + + + + Get pressure information + /login + /controller/device/{deviceId}/sensors/pressure + GET + + + + Get gravity information + /login + /controller/device/{deviceId}/sensors/gravity + GET + + + + Get words information + /login + /controller/device/{deviceId}/sensors/words + GET + + + + Get words information + /login + /controller/device/{deviceId}/sensors/words + POST + + + + set word threshold information + /login + /controller/device/{deviceId}/sensors/words/threshold + POST + + + + delete words + /login + /controller/device/{deviceId}/sensors/words + DELETE + + + + get device stats + /login + /controller/stats/device/{deviceId}/sensors/{sensorName} + GET + + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/webapp/META-INF/webapp-classloading.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/webapp/META-INF/webapp-classloading.xml new file mode 100644 index 000000000..fa4461919 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/webapp/META-INF/webapp-classloading.xml @@ -0,0 +1,33 @@ + + + + + + + + + false + + + CXF,Carbon + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/webapp/WEB-INF/cxf-servlet.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/webapp/WEB-INF/cxf-servlet.xml new file mode 100644 index 000000000..476cbcc61 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/webapp/WEB-INF/cxf-servlet.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/webapp/WEB-INF/web.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..138f002d0 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.controller.api/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,57 @@ + + + Android Sense + Android Sense + + + CXFServlet + org.apache.cxf.transport.servlet.CXFServlet + 1 + + + CXFServlet + /* + + + isAdminService + false + + + doAuthentication + false + + + isSharedWithAllTenants + true + + + providerTenantDomain + carbon.super + + + + + managed-api-enabled + true + + + managed-api-owner + admin + + + managed-api-context-template + /android_sense/{version} + + + managed-api-application + android_sense + + + managed-api-isSecured + true + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/pom.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/pom.xml new file mode 100644 index 000000000..91f33ca0c --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/pom.xml @@ -0,0 +1,205 @@ + + + + androidsense-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.androidsense.manager.api + war + WSO2 Carbon - Android Sense Management API + WSO2 Carbon - Android Sense Management Service-API Implementation + http://wso2.org + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.core + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + provided + + + org.apache.cxf + cxf-rt-frontend-jaxrs + provided + + + org.apache.cxf + cxf-rt-transports-http + provided + + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + + + + + org.apache.httpcomponents + httpasyncclient + 4.1 + provided + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot + provided + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.androidsense.plugin + provided + + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-jaxrs + + + javax + javaee-web-api + provided + + + javax.ws.rs + jsr311-api + provided + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.analytics + provided + + + + commons-httpclient.wso2 + commons-httpclient + provided + + + + org.wso2.carbon + org.wso2.carbon.utils + provided + + + org.bouncycastle.wso2 + bcprov-jdk15on + + + org.wso2.carbon + org.wso2.carbon.user.api + + + org.wso2.carbon + org.wso2.carbon.queuing + + + org.wso2.carbon + org.wso2.carbon.base + + + org.apache.axis2.wso2 + axis2 + + + org.igniterealtime.smack.wso2 + smack + + + org.igniterealtime.smack.wso2 + smackx + + + jaxen + jaxen + + + commons-fileupload.wso2 + commons-fileupload + + + org.apache.ant.wso2 + ant + + + org.apache.ant.wso2 + ant + + + commons-httpclient.wso2 + commons-httpclient + + + org.eclipse.equinox + javax.servlet + + + org.wso2.carbon + org.wso2.carbon.registry.api + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.annotations + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.webapp.publisher + provided + + + + + + + + maven-compiler-plugin + + UTF-8 + ${wso2.maven.compiler.source} + ${wso2.maven.compiler.target} + + + + maven-war-plugin + + android_sense_mgt + + + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/manager/service/impl/AndroidSenseManagerService.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/manager/service/impl/AndroidSenseManagerService.java new file mode 100644 index 000000000..a9f942fde --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/manager/service/impl/AndroidSenseManagerService.java @@ -0,0 +1,166 @@ +/* + * 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.device.mgt.iot.androidsense.manager.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.common.Device; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.EnrolmentInfo; +import org.wso2.carbon.device.mgt.iot.androidsense.manager.service.impl.util.APIUtil; +import org.wso2.carbon.device.mgt.iot.androidsense.plugin.constants.AndroidSenseConstants; +import org.wso2.carbon.utils.CarbonUtils; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import java.io.File; +import java.util.Date; + +public class AndroidSenseManagerService { + + private static Log log = LogFactory.getLog(AndroidSenseManagerService.class); + + @Context //injected response proxy supporting multiple thread + private HttpServletResponse response; + + @Path("manager/device") + @POST + public boolean register(@QueryParam("deviceId") String deviceId, @QueryParam("deviceName") String deviceName) { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(AndroidSenseConstants.DEVICE_TYPE); + try { + if (APIUtil.getDeviceManagementService().isEnrolled(deviceIdentifier)) { + response.setStatus(Response.Status.CONFLICT.getStatusCode()); + return true; + } + + Device device = new Device(); + device.setDeviceIdentifier(deviceId); + EnrolmentInfo enrolmentInfo = new EnrolmentInfo(); + enrolmentInfo.setDateOfEnrolment(new Date().getTime()); + enrolmentInfo.setDateOfLastUpdate(new Date().getTime()); + enrolmentInfo.setStatus(EnrolmentInfo.Status.ACTIVE); + device.setName(deviceName); + device.setType(AndroidSenseConstants.DEVICE_TYPE); + enrolmentInfo.setOwner(APIUtil.getAuthenticatedUser()); + device.setEnrolmentInfo(enrolmentInfo); + boolean added = APIUtil.getDeviceManagementService().enrollDevice(device); + if (added) { + response.setStatus(Response.Status.OK.getStatusCode()); + } else { + response.setStatus(Response.Status.NOT_ACCEPTABLE.getStatusCode()); + } + return added; + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } + } + + @Path("manager/device/{device_id}") + @DELETE + public void removeDevice(@PathParam("device_id") String deviceId, @Context HttpServletResponse response) { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(AndroidSenseConstants.DEVICE_TYPE); + try { + boolean removed = APIUtil.getDeviceManagementService().disenrollDevice(deviceIdentifier); + if (removed) { + response.setStatus(Response.Status.OK.getStatusCode()); + + } else { + response.setStatus(Response.Status.NOT_ACCEPTABLE.getStatusCode()); + + } + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } + } + + @Path("manager/device/{device_id}") + @PUT + public boolean updateDevice(@PathParam("device_id") String deviceId, @QueryParam("name") String name, + @Context HttpServletResponse response) { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(AndroidSenseConstants.DEVICE_TYPE); + try { + Device device = APIUtil.getDeviceManagementService().getDevice(deviceIdentifier); + device.setDeviceIdentifier(deviceId); + device.getEnrolmentInfo().setDateOfLastUpdate(new Date().getTime()); + device.setName(name); + device.setType(AndroidSenseConstants.DEVICE_TYPE); + boolean updated = APIUtil.getDeviceManagementService().modifyEnrollment(device); + if (updated) { + response.setStatus(Response.Status.OK.getStatusCode()); + } else { + response.setStatus(Response.Status.NOT_ACCEPTABLE.getStatusCode()); + } + return updated; + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } + } + + @Path("manager/device/{device_id}") + @GET + @Consumes("application/json") + @Produces("application/json") + public Device getDevice(@PathParam("device_id") String deviceId) { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(AndroidSenseConstants.DEVICE_TYPE); + try { + return APIUtil.getDeviceManagementService().getDevice(deviceIdentifier); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } + } + + @Path("manager/device/{sketch_type}/download") + @GET + @Produces("application/octet-stream") + public Response downloadSketch(@PathParam("sketch_type") String sketchType) { + try { + String sep = File.separator; + String sketchFolder = "repository" + sep + "resources" + sep + "sketches" + sep + "android_sense" + sep; + String archivesPath = CarbonUtils.getCarbonHome() + sep + sketchFolder; + Response.ResponseBuilder rb = Response.ok(new File(archivesPath+sep+"androidsense.apk")); + rb.header("Content-Disposition", "attachment; filename=\"" + "androidsense.apk" + "\""); + return rb.build(); + } catch (IllegalArgumentException ex) { + return Response.status(400).entity(ex.getMessage()).build();//bad request + } + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/manager/service/impl/util/APIUtil.java b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/manager/service/impl/util/APIUtil.java new file mode 100644 index 000000000..ec55710dd --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/androidsense/manager/service/impl/util/APIUtil.java @@ -0,0 +1,36 @@ +package org.wso2.carbon.device.mgt.iot.androidsense.manager.service.impl.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; + +/** + * This class provides utility functions used by REST-API. + */ +public class APIUtil { + + private static Log log = LogFactory.getLog(APIUtil.class); + + public static String getAuthenticatedUser() { + PrivilegedCarbonContext threadLocalCarbonContext = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + String username = threadLocalCarbonContext.getUsername(); + String tenantDomain = threadLocalCarbonContext.getTenantDomain(); + if (username.endsWith(tenantDomain)) { + return username.substring(0, username.lastIndexOf("@")); + } + return username; + } + + public static DeviceManagementProviderService getDeviceManagementService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + DeviceManagementProviderService deviceManagementProviderService = + (DeviceManagementProviderService) ctx.getOSGiService(DeviceManagementProviderService.class, null); + if (deviceManagementProviderService == null) { + String msg = "Device Management service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return deviceManagementProviderService; + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/webapp/META-INF/permissions.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/webapp/META-INF/permissions.xml new file mode 100644 index 000000000..44c1677f0 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/webapp/META-INF/permissions.xml @@ -0,0 +1,66 @@ + + + + + + + + + + Get device + /device-mgt/user/devices/list + /manager/device/* + GET + + + + Add device + /device-mgt/user/devices/add + /manager/device + POST + + + + Remove device + /device-mgt/user/devices/remove + /manager/device/* + DELETE + + + + Download device + /device-mgt/user/devices/add + /manager/device/*/download + GET + + + + Update device + /device-mgt/user/devices/update + /manager/device/* + POST + + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/webapp/META-INF/webapp-classloading.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/webapp/META-INF/webapp-classloading.xml new file mode 100644 index 000000000..fa4461919 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/webapp/META-INF/webapp-classloading.xml @@ -0,0 +1,33 @@ + + + + + + + + + false + + + CXF,Carbon + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/webapp/WEB-INF/cxf-servlet.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/webapp/WEB-INF/cxf-servlet.xml new file mode 100644 index 000000000..e732f7427 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/webapp/WEB-INF/cxf-servlet.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/webapp/WEB-INF/web.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..0c0ec3ef1 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.manager.api/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,49 @@ + + + Android Sense + Android Sense + + + CXFServlet + org.apache.cxf.transport.servlet.CXFServlet + 1 + + + CXFServlet + /* + + + + isAdminService + false + + + doAuthentication + true + + + + managed-api-enabled + false + + + managed-api-owner + admin + + + managed-api-context-template + /android_sense_mgt/{version} + + + managed-api-application + android_sense + + + managed-api-isSecured + false + + diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.plugin/pom.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.plugin/pom.xml new file mode 100644 index 000000000..fc0d2f4c9 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.plugin/pom.xml @@ -0,0 +1,115 @@ + + + + + + + androidsense-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.androidsense.plugin + bundle + WSO2 Carbon - AndroidSense DeviceType Plugin + WSO2 Carbon - AndroidSense DeviceType Plugin Implementation + http://wso2.org + + + + + org.apache.felix + maven-scr-plugin + + + maven-compiler-plugin + + 1.7 + 1.7 + + 2.3.2 + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + ${project.artifactId} + ${project.artifactId} + ${carbon.devicemgt.plugins.version} + IoT Server Impl Bundle + org.wso2.carbon.device.mgt.iot.androidsense.plugin.internal + + org.osgi.framework, + org.osgi.service.component, + org.apache.commons.logging, + javax.xml.bind.*;resolution:=optional, + javax.naming;resolution:=optional, + javax.sql;resolution:=optional, + javax.xml.bind.annotation.*;resolution:=optional, + javax.xml.parsers;resolution:=optional, + javax.net;resolution:=optional, + javax.net.ssl;resolution:=optional, + org.w3c.dom;resolution:=optional, + org.wso2.carbon.device.mgt.common.*, + org.wso2.carbon.device.mgt.common, + org.wso2.carbon.context.*, + org.wso2.carbon.ndatasource.core, + org.wso2.carbon.device.mgt.iot.*, + org.wso2.carbon.device.mgt.extensions.feature.mgt.*, + org.wso2.carbon.utils.* + + + !org.wso2.carbon.device.mgt.iot.androidsense.plugin.internal, + org.wso2.carbon.device.mgt.iot.androidsense.plugin.* + + + + + + + + + + org.eclipse.osgi + org.eclipse.osgi + + + org.eclipse.osgi + org.eclipse.osgi.services + + + org.wso2.carbon + org.wso2.carbon.logging + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.extensions + + + org.wso2.carbon + org.wso2.carbon.ndatasource.core + + + org.wso2.carbon + org.wso2.carbon.utils + + + \ No newline at end of file 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 new file mode 100644 index 000000000..9666dcc82 --- /dev/null +++ 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 @@ -0,0 +1,53 @@ +/* + * 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.device.mgt.iot.androidsense.plugin.constants; + +public class AndroidSenseConstants { + + public final static String DEVICE_TYPE = "android_sense"; + public final static String DEVICE_PLUGIN_DEVICE_NAME = "DEVICE_NAME"; + public final static String DEVICE_PLUGIN_DEVICE_ID = "ANDROID_DEVICE_ID"; + public final static String DEVICE_TYPE_PROVIDER_DOMAIN = "carbon.super"; + //Android Sense Stream definitions. + public static final String ACCELEROMETER_STREAM_DEFINITION = "org.wso2.iot.devices.accelerometer"; + public static final String BATTERY_STREAM_DEFINITION = "org.wso2.iot.devices.battery"; + public static final String GPS_STREAM_DEFINITION = "org.wso2.iot.devices.gps"; + public static final String GRAVITY_STREAM_DEFINITION = "org.wso2.iot.devices.gravity"; + public static final String GYROSCOPE_STREAM_DEFINITION = "org.wso2.iot.devices.gyroscope"; + public static final String LIGHT_STREAM_DEFINITION = "org.wso2.iot.devices.light"; + public static final String MAGNETIC_STREAM_DEFINITION = "org.wso2.iot.devices.magnetic"; + public static final String PRESSURE_STREAM_DEFINITION = "org.wso2.iot.devices.pressure"; + public static final String PROXIMITY_STREAM_DEFINITION = "org.wso2.iot.devices.proximity"; + public static final String ROTATION_STREAM_DEFINITION = "org.wso2.iot.devices.rotation"; + public static final String WORD_COUNT_STREAM_DEFINITION = "org.wso2.iot.devices.wordcount"; + + //Android Sensor names + public static final String SENSOR_ACCELEROMETER = "accelerometer"; + public static final String SENSOR_BATTERY = "battery"; + public static final String SENSOR_GPS = "gps"; + public static final String SENSOR_GRAVITY = "gravity"; + public static final String SENSOR_GYROSCOPE = "gyroscope"; + public static final String SENSOR_LIGHT = "light"; + public static final String SENSOR_MAGNETIC = "magnetic"; + public static final String SENSOR_PRESSURE = "pressure"; + public static final String SENSOR_PROXIMITY = "proximity"; + public static final String SENSOR_ROTATION = "rotation"; + public static final String SENSOR_WORDCOUNT = "wordcounter"; + //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"; +} 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/exception/AndroidSenseDeviceMgtPluginException.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/exception/AndroidSenseDeviceMgtPluginException.java new file mode 100644 index 000000000..88557cd9f --- /dev/null +++ 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/exception/AndroidSenseDeviceMgtPluginException.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.device.mgt.iot.androidsense.plugin.exception; + + +public class AndroidSenseDeviceMgtPluginException extends Exception{ + + private String errorMessage; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public AndroidSenseDeviceMgtPluginException(String msg, Exception nestedEx) { + super(msg, nestedEx); + setErrorMessage(msg); + } + + public AndroidSenseDeviceMgtPluginException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public AndroidSenseDeviceMgtPluginException(String msg) { + super(msg); + setErrorMessage(msg); + } + + public AndroidSenseDeviceMgtPluginException() { + super(); + } + + public AndroidSenseDeviceMgtPluginException(Throwable cause) { + super(cause); + } + +} 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/AndroidSenseManager.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/AndroidSenseManager.java new file mode 100644 index 000000000..7cab751f6 --- /dev/null +++ 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/AndroidSenseManager.java @@ -0,0 +1,256 @@ +/* + * 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.device.mgt.iot.androidsense.plugin.impl; + + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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.common.DeviceManager; +import org.wso2.carbon.device.mgt.common.EnrolmentInfo; +import org.wso2.carbon.device.mgt.common.FeatureManager; +import org.wso2.carbon.device.mgt.common.configuration.mgt.TenantConfiguration; +import org.wso2.carbon.device.mgt.common.license.mgt.License; +import org.wso2.carbon.device.mgt.common.license.mgt.LicenseManagementException; +import org.wso2.carbon.device.mgt.iot.androidsense.plugin.exception.AndroidSenseDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.androidsense.plugin.impl.feature.AndroidSenseFeatureManager; +import org.wso2.carbon.device.mgt.iot.androidsense.plugin.impl.dao.AndroidSenseDAO; +import java.util.List; + + +/** + * This represents the Android implementation of DeviceManagerService. + */ +public class AndroidSenseManager implements DeviceManager { + + private static final AndroidSenseDAO androidSenseDAO = new AndroidSenseDAO(); + private static final Log log = LogFactory.getLog(AndroidSenseManager.class); + private FeatureManager androidSenseFeatureManager = new AndroidSenseFeatureManager(); + + @Override + public FeatureManager getFeatureManager() { + return androidSenseFeatureManager; + } + + @Override + public boolean saveConfiguration(TenantConfiguration tenantConfiguration) + throws DeviceManagementException { + //TODO implement this + return false; + } + + @Override + public TenantConfiguration getConfiguration() throws DeviceManagementException { + //TODO implement this + return null; + } + + @Override + public boolean enrollDevice(Device device) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Enrolling a new Android device : " + device.getDeviceIdentifier()); + } + AndroidSenseDAO.beginTransaction(); + status = androidSenseDAO.getDeviceDAO().addDevice(device); + AndroidSenseDAO.commitTransaction(); + } catch (AndroidSenseDeviceMgtPluginException e) { + try { + AndroidSenseDAO.rollbackTransaction(); + } catch (AndroidSenseDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the device enrol transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while enrolling the Android device : " + device.getDeviceIdentifier(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean modifyEnrollment(Device device) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Modifying the Android device enrollment data"); + } + AndroidSenseDAO.beginTransaction(); + status = androidSenseDAO.getDeviceDAO().updateDevice(device); + AndroidSenseDAO.commitTransaction(); + } catch (AndroidSenseDeviceMgtPluginException e) { + try { + AndroidSenseDAO.rollbackTransaction(); + } catch (AndroidSenseDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the update device transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while updating the enrollment of the Android device : " + + device.getDeviceIdentifier(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean disenrollDevice(DeviceIdentifier deviceId) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Dis-enrolling Android device : " + deviceId); + } + AndroidSenseDAO.beginTransaction(); + status = androidSenseDAO.getDeviceDAO().deleteDevice(deviceId.getId()); + AndroidSenseDAO.commitTransaction(); + } catch (AndroidSenseDeviceMgtPluginException e) { + try { + AndroidSenseDAO.rollbackTransaction(); + } catch (AndroidSenseDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the device dis enrol transaction :" + deviceId.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while removing the Android device : " + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean isEnrolled(DeviceIdentifier deviceId) throws DeviceManagementException { + boolean isEnrolled = false; + try { + if (log.isDebugEnabled()) { + log.debug("Checking the enrollment of Android device : " + deviceId.getId()); + } + Device iotDevice = androidSenseDAO.getDeviceDAO().getDevice(deviceId.getId()); + if (iotDevice != null) { + isEnrolled = true; + } + } catch (AndroidSenseDeviceMgtPluginException e) { + String msg = "Error while checking the enrollment status of Android device : " + + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return isEnrolled; + } + + @Override + public boolean isActive(DeviceIdentifier deviceId) throws DeviceManagementException { + return true; + } + + @Override + public boolean setActive(DeviceIdentifier deviceId, boolean status) + throws DeviceManagementException { + return true; + } + + @Override + public Device getDevice(DeviceIdentifier deviceId) throws DeviceManagementException { + Device device; + try { + if (log.isDebugEnabled()) { + log.debug("Getting the details of Android device : " + deviceId.getId()); + } + device = androidSenseDAO.getDeviceDAO().getDevice(deviceId.getId()); + } catch (AndroidSenseDeviceMgtPluginException e) { + String msg = "Error while fetching the Android device : " + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return device; + } + + @Override + public boolean setOwnership(DeviceIdentifier deviceId, String ownershipType) + throws DeviceManagementException { + return true; + } + + public boolean isClaimable(DeviceIdentifier deviceIdentifier) throws DeviceManagementException { + return false; + } + + @Override + public boolean setStatus(DeviceIdentifier deviceId, String currentOwner, + EnrolmentInfo.Status status) throws DeviceManagementException { + return false; + } + + @Override + public License getLicense(String s) throws LicenseManagementException { + return null; + } + + @Override + public void addLicense(License license) throws LicenseManagementException { + + } + + @Override public boolean requireDeviceAuthorization() { + return false; + } + + @Override + public boolean updateDeviceInfo(DeviceIdentifier deviceIdentifier, Device device) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug( + "updating the details of Android device : " + deviceIdentifier); + } + AndroidSenseDAO.beginTransaction(); + status = androidSenseDAO.getDeviceDAO().updateDevice(device); + AndroidSenseDAO.commitTransaction(); + } catch (AndroidSenseDeviceMgtPluginException e) { + try { + AndroidSenseDAO.rollbackTransaction(); + } catch (AndroidSenseDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the update device info transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = + "Error while updating the Android device : " + deviceIdentifier; + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public List getAllDevices() throws DeviceManagementException { + List devices = null; + try { + if (log.isDebugEnabled()) { + log.debug("Fetching the details of all Android devices"); + } + devices = androidSenseDAO.getDeviceDAO().getAllDevices(); + } catch (AndroidSenseDeviceMgtPluginException e) { + String msg = "Error while fetching all Android devices."; + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return devices; + } + +} \ No newline at end of file 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 new file mode 100644 index 000000000..b31b86531 --- /dev/null +++ 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 @@ -0,0 +1,101 @@ +/* + * 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.device.mgt.iot.androidsense.plugin.impl; + +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.DeviceManager; +import org.wso2.carbon.device.mgt.common.app.mgt.Application; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManagementException; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManager; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; +import org.wso2.carbon.device.mgt.iot.androidsense.plugin.constants.AndroidSenseConstants; + +import java.util.List; + +public class AndroidSenseManagerService implements DeviceManagementService { + private DeviceManager deviceManager; + @Override + public String getType() { + return AndroidSenseConstants.DEVICE_TYPE; + } + + @Override + public String getProviderTenantDomain() { + return "carbon.super"; + } + + @Override + public boolean isSharedWithAllTenants() { + return true; + } + + @Override + public void init() throws DeviceManagementException { + this.deviceManager=new AndroidSenseManager(); + } + + @Override + public DeviceManager getDeviceManager() { + return deviceManager; + } + + @Override + public ApplicationManager getApplicationManager() { + return null; + } + + @Override public void notifyOperationToDevices(Operation operation, List list) + throws DeviceManagementException { + + } + + @Override + public Application[] getApplications(String domain, int pageNumber, int size) + throws ApplicationManagementException { + return new Application[0]; + } + + @Override + public void updateApplicationStatus(DeviceIdentifier deviceId, Application application, + String status) throws ApplicationManagementException { + + } + + @Override + public String getApplicationStatus(DeviceIdentifier deviceId, Application application) + throws ApplicationManagementException { + return null; + } + + @Override public void installApplicationForDevices(Operation operation, List list) + throws ApplicationManagementException { + + } + + @Override public void installApplicationForUsers(Operation operation, List list) + throws ApplicationManagementException { + + } + + @Override public void installApplicationForUserRoles(Operation operation, List list) + throws ApplicationManagementException { + + } + +} 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/dao/AndroidSenseDAO.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/dao/AndroidSenseDAO.java new file mode 100644 index 000000000..ab12ac61d --- /dev/null +++ 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/dao/AndroidSenseDAO.java @@ -0,0 +1,123 @@ +/* + * 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.device.mgt.iot.androidsense.plugin.impl.dao; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.iot.androidsense.plugin.exception.AndroidSenseDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.androidsense.plugin.constants.AndroidSenseConstants; +import org.wso2.carbon.device.mgt.iot.androidsense.plugin.impl.dao.impl.AndroidSenseDAOImpl; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +public class AndroidSenseDAO { + + private static final Log log = LogFactory.getLog(AndroidSenseDAO.class); + static DataSource dataSource; + private static ThreadLocal currentConnection = new ThreadLocal(); + + public AndroidSenseDAO() { + initAndroidDAO(); + } + + public static void initAndroidDAO() { + try { + Context ctx = new InitialContext(); + dataSource = (DataSource) ctx.lookup(AndroidSenseConstants.DATA_SOURCE_NAME); + } catch (NamingException e) { + log.error("Error while looking up the data source: " + AndroidSenseConstants.DATA_SOURCE_NAME); + } + } + + public AndroidSenseDAOImpl getDeviceDAO() { + return new AndroidSenseDAOImpl(); + } + + public static void beginTransaction() throws AndroidSenseDeviceMgtPluginException { + try { + Connection conn = dataSource.getConnection(); + conn.setAutoCommit(false); + currentConnection.set(conn); + } catch (SQLException e) { + throw new AndroidSenseDeviceMgtPluginException("Error occurred while retrieving datasource connection", e); + } + } + + public static Connection getConnection() throws AndroidSenseDeviceMgtPluginException { + if (currentConnection.get() == null) { + try { + currentConnection.set(dataSource.getConnection()); + } catch (SQLException e) { + throw new AndroidSenseDeviceMgtPluginException("Error occurred while retrieving data source connection", e); + } + } + return currentConnection.get(); + } + + public static void commitTransaction() throws AndroidSenseDeviceMgtPluginException { + try { + Connection conn = currentConnection.get(); + if (conn != null) { + conn.commit(); + } else { + if (log.isDebugEnabled()) { + log.debug("Datasource connection associated with the current thread is null, hence commit " + + "has not been attempted"); + } + } + } catch (SQLException e) { + throw new AndroidSenseDeviceMgtPluginException("Error occurred while committing the transaction", e); + } finally { + closeConnection(); + } + } + + public static void closeConnection() throws AndroidSenseDeviceMgtPluginException { + + Connection con = currentConnection.get(); + if (con != null) { + try { + con.close(); + } catch (SQLException e) { + log.error("Error occurred while close the connection"); + } + } + currentConnection.remove(); + } + + public static void rollbackTransaction() throws AndroidSenseDeviceMgtPluginException { + try { + Connection conn = currentConnection.get(); + if (conn != null) { + conn.rollback(); + } else { + if (log.isDebugEnabled()) { + log.debug("Datasource connection associated with the current thread is null, hence rollback " + + "has not been attempted"); + } + } + } catch (SQLException e) { + throw new AndroidSenseDeviceMgtPluginException("Error occurred while rollback the transaction", e); + } finally { + closeConnection(); + } + } +} \ No newline at end of file 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/dao/impl/AndroidSenseDAOImpl.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/dao/impl/AndroidSenseDAOImpl.java new file mode 100644 index 000000000..30afa96f1 --- /dev/null +++ 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/dao/impl/AndroidSenseDAOImpl.java @@ -0,0 +1,198 @@ +/* + * 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.device.mgt.iot.androidsense.plugin.impl.dao.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.iot.androidsense.plugin.constants.AndroidSenseConstants; +import org.wso2.carbon.device.mgt.iot.androidsense.plugin.exception.AndroidSenseDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.androidsense.plugin.impl.dao.AndroidSenseDAO; +import org.wso2.carbon.device.mgt.iot.androidsense.plugin.impl.dao.util.AndroidSenseUtils; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * Implements dao impl for android Devices. + */ +public class AndroidSenseDAOImpl { + + + private static final Log log = LogFactory.getLog(AndroidSenseDAOImpl.class); + + public Device getDevice(String deviceId) throws AndroidSenseDeviceMgtPluginException { + Connection conn = null; + PreparedStatement stmt = null; + Device device = null; + ResultSet resultSet = null; + try { + conn = AndroidSenseDAO.getConnection(); + String selectDBQuery = + "SELECT ANDROID_DEVICE_ID, DEVICE_NAME" + + " FROM ANDROID_SENSE_DEVICE WHERE ANDROID_DEVICE_ID = ?"; + stmt = conn.prepareStatement(selectDBQuery); + stmt.setString(1, deviceId); + resultSet = stmt.executeQuery(); + + if (resultSet.next()) { + device = new Device(); + device.setName(resultSet.getString(AndroidSenseConstants.DEVICE_PLUGIN_DEVICE_NAME)); + if (log.isDebugEnabled()) { + log.debug("Android device " + deviceId + " data has been fetched from " + + "Android database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while fetching Android device : '" + deviceId + "'"; + log.error(msg, e); + throw new AndroidSenseDeviceMgtPluginException(msg, e); + } finally { + AndroidSenseUtils.cleanupResources(stmt, resultSet); + AndroidSenseDAO.closeConnection(); + } + return device; + } + + public boolean addDevice(Device device)throws AndroidSenseDeviceMgtPluginException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = AndroidSenseDAO.getConnection(); + String createDBQuery = + "INSERT INTO ANDROID_SENSE_DEVICE(ANDROID_DEVICE_ID, DEVICE_NAME) VALUES (?, ?)"; + + stmt = conn.prepareStatement(createDBQuery); + stmt.setString(1, device.getDeviceIdentifier()); + stmt.setString(2, device.getName()); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Android device " + device.getDeviceIdentifier() + " data has been" + + " added to the Android database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while adding the Android device '" + device.getDeviceIdentifier() + + "' to the Android db."; + log.error(msg, e); + throw new AndroidSenseDeviceMgtPluginException(msg, e); + } finally { + AndroidSenseUtils.cleanupResources(stmt, null); + } + return status; + } + + public boolean updateDevice(Device device) throws AndroidSenseDeviceMgtPluginException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = AndroidSenseDAO.getConnection(); + String updateDBQuery = + "UPDATE ANDROID_SENSE_DEVICE SET DEVICE_NAME = ? WHERE ANDROID_DEVICE_ID = ?"; + + stmt = conn.prepareStatement(updateDBQuery); + stmt.setString(1, device.getName()); + stmt.setString(2, device.getDeviceIdentifier()); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Android device " + device.getDeviceIdentifier() + " data has been modified."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while modifying the Android device '" + + device.getDeviceIdentifier() + "' data."; + log.error(msg, e); + throw new AndroidSenseDeviceMgtPluginException(msg, e); + } finally { + AndroidSenseUtils.cleanupResources(stmt, null); + } + return status; + } + + public boolean deleteDevice(String deviceId) throws AndroidSenseDeviceMgtPluginException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = AndroidSenseDAO.getConnection(); + String deleteDBQuery = + "DELETE FROM ANDROID_SENSE_DEVICE WHERE ANDROID_DEVICE_ID = ?"; + stmt = conn.prepareStatement(deleteDBQuery); + stmt.setString(1, deviceId); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Android device " + deviceId + " data has deleted from the Android database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while deleting Android device " + deviceId; + log.error(msg, e); + throw new AndroidSenseDeviceMgtPluginException(msg, e); + } finally { + AndroidSenseUtils.cleanupResources(stmt, null); + } + return status; + } + + public List getAllDevices() throws AndroidSenseDeviceMgtPluginException { + + Connection conn = null; + PreparedStatement stmt = null; + ResultSet resultSet = null; + Device device; + List iotDevices = new ArrayList<>(); + + try { + conn = AndroidSenseDAO.getConnection(); + String selectDBQuery = + "SELECT ANDROID_DEVICE_ID, DEVICE_NAME FROM ANDROID_SENSE_DEVICE"; + stmt = conn.prepareStatement(selectDBQuery); + resultSet = stmt.executeQuery(); + while (resultSet.next()) { + device = new Device(); + device.setDeviceIdentifier(resultSet.getString(AndroidSenseConstants.DEVICE_PLUGIN_DEVICE_ID)); + device.setName(resultSet.getString(AndroidSenseConstants.DEVICE_PLUGIN_DEVICE_NAME)); + iotDevices.add(device); + } + if (log.isDebugEnabled()) { + log.debug("All Android device details have fetched from Android database."); + } + return iotDevices; + } catch (SQLException e) { + String msg = "Error occurred while fetching all Android device data'"; + log.error(msg, e); + throw new AndroidSenseDeviceMgtPluginException(msg, e); + } finally { + AndroidSenseUtils.cleanupResources(stmt, resultSet); + AndroidSenseDAO.closeConnection(); + } + + } + + } \ No newline at end of file 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/dao/util/AndroidSenseUtils.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/dao/util/AndroidSenseUtils.java new file mode 100644 index 000000000..187649aa6 --- /dev/null +++ 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/dao/util/AndroidSenseUtils.java @@ -0,0 +1,109 @@ +/* + * 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.device.mgt.iot.androidsense.plugin.impl.dao.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.iot.androidsense.plugin.constants.AndroidSenseConstants; +import org.wso2.carbon.device.mgt.iot.androidsense.plugin.exception.AndroidSenseDeviceMgtPluginException; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +/** + * Contains utility methods used by plugin. + */ +public class AndroidSenseUtils { + + private static Log log = LogFactory.getLog(AndroidSenseUtils.class); + + public static String getDeviceProperty(List deviceProperties, String propertyKey) { + String deviceProperty = ""; + for(Device.Property property :deviceProperties){ + if(propertyKey.equals(property.getName())){ + deviceProperty = property.getValue(); + } + } + return deviceProperty; + } + + public static Device.Property getProperty(String property, String value) { + if (property != null) { + Device.Property prop = new Device.Property(); + prop.setName(property); + prop.setValue(value); + return prop; + } + return null; + } + + public static void cleanupResources(Connection conn, PreparedStatement stmt, ResultSet rs) { + if (rs != null) { + try { + rs.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing result set", e); + } + } + if (stmt != null) { + try { + stmt.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing prepared statement", e); + } + } + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing database connection", e); + } + } + } + + public static void cleanupResources(PreparedStatement stmt, ResultSet rs) { + cleanupResources(null, stmt, rs); + } + + /** + * Creates the device management schema. + */ + public static void setupDeviceManagementSchema() throws AndroidSenseDeviceMgtPluginException { + try { + Context ctx = new InitialContext(); + DataSource dataSource = (DataSource) ctx.lookup(AndroidSenseConstants.DATA_SOURCE_NAME); + DeviceSchemaInitializer initializer = + new DeviceSchemaInitializer(dataSource); + log.info("Initializing device management repository database schema"); + initializer.createRegistryDatabase(); + + } catch (NamingException e) { + log.error("Error while looking up the data source: " + AndroidSenseConstants.DATA_SOURCE_NAME); + } catch (Exception e) { + throw new AndroidSenseDeviceMgtPluginException("Error occurred while initializing Iot Device " + + "Management database schema", e); + } + } +} 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/dao/util/DeviceSchemaInitializer.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/dao/util/DeviceSchemaInitializer.java new file mode 100644 index 000000000..1b4935010 --- /dev/null +++ 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/dao/util/DeviceSchemaInitializer.java @@ -0,0 +1,50 @@ +/* + * 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.device.mgt.iot.androidsense.plugin.impl.dao.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.utils.CarbonUtils; +import org.wso2.carbon.utils.dbcreator.DatabaseCreator; + +import javax.sql.DataSource; +import java.io.File; + +/** + * Provides methods for initializing the database script. + */ +public class DeviceSchemaInitializer extends DatabaseCreator{ + + private static final Log log = LogFactory.getLog(DeviceSchemaInitializer.class); + private static final String setupSQLScriptBaseLocation = CarbonUtils.getCarbonHome() + File.separator + "dbscripts" + + File.separator + "cdm" + File.separator + "plugins" + File.separator; + + public DeviceSchemaInitializer(DataSource dataSource) { + super(dataSource); + } + + @Override + protected String getDbScriptLocation(String databaseType) { + String scriptName = databaseType + ".sql"; + if (log.isDebugEnabled()) { + log.debug("Loading database script from :" + scriptName); + } + return setupSQLScriptBaseLocation.replaceFirst("DBTYPE", databaseType) + scriptName; + } +} 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/feature/AndroidSenseFeatureManager.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/feature/AndroidSenseFeatureManager.java new file mode 100644 index 000000000..1fb947ae6 --- /dev/null +++ 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/feature/AndroidSenseFeatureManager.java @@ -0,0 +1,59 @@ +/* + * 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.device.mgt.iot.androidsense.plugin.impl.feature; + +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.Feature; +import org.wso2.carbon.device.mgt.common.FeatureManager; +import org.wso2.carbon.device.mgt.extensions.feature.mgt.GenericFeatureManager; +import org.wso2.carbon.device.mgt.iot.androidsense.plugin.constants.AndroidSenseConstants; + +import java.util.List; + +public class AndroidSenseFeatureManager implements FeatureManager { + @Override + public boolean addFeature(Feature feature) throws DeviceManagementException { + return false; + } + + @Override + public boolean addFeatures(List features) throws DeviceManagementException { + return false; + } + + @Override + public Feature getFeature(String name) throws DeviceManagementException { + GenericFeatureManager genericFeatureManager = GenericFeatureManager.getInstance(); + return genericFeatureManager.getFeature(AndroidSenseConstants.DEVICE_TYPE, name); + } + + @Override + public List getFeatures() throws DeviceManagementException { + GenericFeatureManager genericFeatureManager = GenericFeatureManager.getInstance(); + return genericFeatureManager.getFeatures(AndroidSenseConstants.DEVICE_TYPE); + } + + @Override + public boolean removeFeature(String name) throws DeviceManagementException { + return false; + } + + @Override + public boolean addSupportedFeaturesToDB() throws DeviceManagementException { + return false; + } +} 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/internal/AndroidSenseManagementServiceComponent.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/internal/AndroidSenseManagementServiceComponent.java new file mode 100644 index 000000000..4896302ea --- /dev/null +++ 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/internal/AndroidSenseManagementServiceComponent.java @@ -0,0 +1,84 @@ +/* + * 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.device.mgt.iot.androidsense.plugin.internal; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.ComponentContext; +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; +import org.wso2.carbon.device.mgt.iot.androidsense.plugin.exception.AndroidSenseDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.androidsense.plugin.impl.AndroidSenseManagerService; +import org.wso2.carbon.device.mgt.iot.androidsense.plugin.impl.dao.util.AndroidSenseUtils; + +/** + * @scr.component name="org.wso2.carbon.device.mgt.iot.android.internal.AndroidSenseManagementServiceComponent" + * immediate="true" + */ +public class AndroidSenseManagementServiceComponent { + + + private ServiceRegistration androidServiceRegRef; + private static final Log log = LogFactory.getLog(AndroidSenseManagementServiceComponent.class); + protected void activate(ComponentContext ctx) { + if (log.isDebugEnabled()) { + log.debug("Activating Android Device Management Service Component"); + } + try { + BundleContext bundleContext = ctx.getBundleContext(); + androidServiceRegRef = + bundleContext.registerService(DeviceManagementService.class.getName(), new AndroidSenseManagerService(), null); + String setupOption = System.getProperty("setup"); + if (setupOption != null) { + if (log.isDebugEnabled()) { + log.debug( + "-Dsetup is enabled. Iot Device management repository schema initialization is about " + + "to begin"); + } + try { + AndroidSenseUtils.setupDeviceManagementSchema(); + } catch (AndroidSenseDeviceMgtPluginException e) { + log.error("Exception occurred while initializing device management database schema", e); + } + } + if (log.isDebugEnabled()) { + log.debug("Android Device Management Service Component has been successfully activated"); + } + } catch (Throwable e) { + log.error("Error occurred while activating Android Device Management Service Component", e); + } + } + + protected void deactivate(ComponentContext ctx) { + if (log.isDebugEnabled()) { + log.debug("De-activating Android Device Management Service Component"); + } + try { + if (androidServiceRegRef != null) { + androidServiceRegRef.unregister(); + } + + if (log.isDebugEnabled()) { + log.debug( + "Android Device Management Service Component has been successfully de-activated"); + } + } catch (Throwable e) { + log.error("Error occurred while de-activating Android Device Management bundle", e); + } + } +} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/pom.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/pom.xml new file mode 100644 index 000000000..032743a6c --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/pom.xml @@ -0,0 +1,61 @@ + + + + + + + androidsense-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.androidsense.ui + WSO2 Carbon - IoT Server Android Sense UI + pom + + + + + maven-assembly-plugin + 2.5.5 + + ${project.artifactId}-${carbon.device.mgt.version} + false + + src/assembly/src.xml + + + + + create-archive + package + + single + + + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/assembly/src.xml b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/assembly/src.xml new file mode 100644 index 000000000..2797034e0 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/assembly/src.xml @@ -0,0 +1,36 @@ + + + + src + + zip + + false + ${basedir}/src + + + ${basedir}/src/main/resources/jaggeryapps/devicemgt + / + true + + + \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.device-view/device-view.hbs b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.device-view/device-view.hbs new file mode 100644 index 000000000..f5fb9d840 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.device-view/device-view.hbs @@ -0,0 +1,84 @@ +{{#zone "topCss"}} + +{{/zone}} +{{#zone "device-thumbnail"}} + +{{/zone}} + +{{#zone "device-opetations"}} +
+ Operations +
+
+ {{unit "iot.unit.device.operation-bar" device=device}} +
+{{/zone}} + +{{#zone "device-detail-properties"}} +
+ +
+
+ +
+
Device Statistics
+ {{unit "iot.unit.device.stats" device=device}} +
+
+
Device Location
+
+
+
+
+
+
+
+
Operations Log
+
+ +
+
+ Not available yet +
+
+
+
+
+
+
+
+{{/zone}} + +{{#zone "bottomJs"}} + +{{/zone}} \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.device-view/device-view.js b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.device-view/device-view.js new file mode 100644 index 000000000..21035b3d8 --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.device-view/device-view.js @@ -0,0 +1,35 @@ +/* + * 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. + */ + +function onRequest(context) { + var log = new Log("device-view.js"); + var deviceType = context.uriParams.deviceType; + var deviceId = request.getParameter("id"); + + if (deviceType != null && deviceType != undefined && deviceId != null && deviceId != undefined) { + var deviceModule = require("/app/modules/device.js").deviceModule; + var device = deviceModule.viewDevice(deviceType, deviceId); + + if (device && device.status != "error") { + return {"device": device}; + } else { + response.sendError(404, "Device Id " + deviceId + "of type " + deviceType + " cannot be found!"); + exit(); + } + } +} \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.device-view/device-view.json b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.device-view/device-view.json new file mode 100644 index 000000000..9eecd8f5b --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.device-view/device-view.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0" +} \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.device-view/public/images/android-sense-icon.png b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.device-view/public/images/android-sense-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4baf188bb1e7c0ddc75bd63ce3d3a0f235f0cb0c GIT binary patch literal 8464 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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.device-view/public/images/thumb.png b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.device-view/public/images/thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..1524c56fe77e4f59ccb87cffb7684dd1c5498795 GIT binary patch literal 7888 zcmbVx2UJt();3Lw6cN!#M??@6LTDjC=m-%3Cn6vvNC`D0lmudE3IaNah$tvk5X9in zl%kM;h!mwmsG*}&Ly;yR@EsiI&b{lO|E~3YSt~2&?Ckx%yFTwe``o!?Zp?d7_#hJ# z6EDKV5Xr>EjN1QlvICUXvq8eZj}YF-27ejnh7WS`M>FZU;+)Zv2p=bRG!pIPdfo3M zT8oK^)z8z?25)0_QNso2qu{j9qd@fW1*Dmnv~-BRPA=YPyreVQ-4hF!TCA#(lJsFhmEq_@aRfYE3efaV6MTHl%>K17HsF_| z0GEM@PQGA>f+E<*XWy=$+yQtb`foMWwqQB(hGoqv*7Qc}~`H`IsdsY3N35F=HXAxu$SRUM{mbY2~*ZUFmB7J&`G zJ7HbWf9>rF?EOnt@xRJy82F=|@Hl@<91inW7F_bc;c)>TIA2Kv1Ig3Yo>*60P=MTi zg#I+x5bf_7h;}{ikMoiI>0%Aff7l+4cG|bkRbJ7_$xR*&Q9;W)E32!?yFuNYm7vZp zXb40_>X*Fh|J8M1Kq>Hkp8Q+h{6zwiasTpPRRG-ltB%lEpm_X&Dv7^u^^}Q8cnx8w zXGt9VI?SE+h>+g6y!W={i=sWd=wtTi*rx){T>{xTFW45&6nF~T&8DEI3ZJZbHJchi ze)IWaRsBaf{+Tql)792bW<%Eu4%=Kic;U0ur&G856gca60;U2Lwzt=QWD?cecmlA> z(>$3Q^J^~A$^mX|KiUJTr}utjPH*moE)$2Cn6~a^$}llSfS5P|2&v^}VgobX7(1{} z&|PBrM}j1g8Q{2~tb1Rki7&IgZyv{(2#}D!eeQP9N=ZDR63-PdzN^r6*JY3XF?bUbeWWr)FKgKHw^BL6T1`!dAdZQ2L z#_@Nq2WnH3YpVhogJY+X?Z35UXJ><(`-*ZNbvWAE*=6>Z`_NgTL%wxwm6es|A{6W) zb@L?w3ab=R74P1j5&CF!aj!6hA!^IHQ&3Rg<3J#+B^TE0ZuuC;i!(a-{4B;zt@tUM z$0*gl>LGr9e#_7Nx9vQ|J|7g@2%RJ6X$LpUS5VIEF-m*T-ya-(Qm2=u-88nfwdKz> z9Cb^<-wc8HnD#bcdIW+Sp&%zk`*{cL$6ACVpy37fZ@Y9{o9=H4R4zVOy&wCjxfw^D zFRW5{U=fS?&W>3UsAj{3tp~cdJpRq<(xoLx3JXkxB`kc|2;@+DZK~x7wy5IWk!nfe zN_$hn$vk$Jn?j{*{#UPF9eA`d-JW?Zn5rj2;ei3V4@{P2HAkIk@-#9sdiLBbH83N@ zw#ApVHnlIyJ2FVg>}E(m3cZL)bANHd<2V*r@J(p52m9y9iHV8BQ#1%lhl+}TItEjj zQ&A#eE%(#-56+NDE_+fH8xFNWx*0-u+Zmy% zmAe3WmCpWma{ZZ0zq0L}?mK)GO!8Q)k+85Z3XY%Y7}1p+v)0axKTvo-$wNgrfgz_D z__co}hF3(bt*@u6PmV*WaZil(@`DEt#@fTy zUmrp&-5XysUz)J=oBf=^%E6bW*d{=)?DD3mzoa&Ji061se7JMs`pNh%0XDF}jQG>9 zhlNGnFv>N2IIB#3SP?i|G#XtpG2tAMJZ5AqO>|Mi&~x5rEX4^>OjL=crlyku6fu

kKhaelc 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 PcgzSQbHhUYYZ3ngtLSEh literal 0 HcmV?d00001 diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/images/android-sense-icon.png b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/images/android-sense-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4baf188bb1e7c0ddc75bd63ce3d3a0f235f0cb0c GIT binary patch literal 8464 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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/images/myDevices_analytics.png b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/images/myDevices_analytics.png new file mode 100644 index 0000000000000000000000000000000000000000..292097a4519333fdc117e5c203c470fee82c9ec1 GIT binary patch literal 286419 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 z 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

ptp$vc(zEtnZVxG&ieG-?=<(0BP4Oeaf1B0-(7 z=WQO3CG{MrQGrA3#hlzZ4sUWmrA-waHH16u`~)ObFFg|qTvS4ORsiPg&eS&<9#VUIrG^1n6d1;Kd>gB2GrNeX-LoZ9j!p?5}LC(DYP`(bX@0BO;V(d9iTSDLh&L4 z;zu0}p^ORHEn6olVbAUrfh|0A(!rE21i(**XAK(JV24S#VbPS>y#fIrU^&oFVNN3ZV&N$8f$CsOpRu0N z@UXg$LKxL6PIk|)9}I}6+8M&$A!cj_ZezDhPd!i9ozqWeFbmDx{*VKprMDdf)EBO}e-!uH zMn4i-Y)QE!2;i9MJRYU=qssSTKro?j9TuG%%26CL{yn4$ZWnwlcgugKdBun5X^$~A zLVT!+m9gZvav*m4Xu*0b5Cmor4uB#9-0V7f52iYQpv>&%8boJ(wn zn$iD>84PA?6--Jm*Vt>BS>Azy$cS?dY3&FgAoQS&-}^OpR)Mw{<-@jbP98ZBCER!E zJvp<{JH8r>c)Va=pYGxj(f9xW)$ADmNSU8Q_C67B=K3CHA8c171FxNq)T-pC>@vmk zlOPmm`b1G*-A4iUhY?T;6mGMiv0bZ_;L zw+|ks-s;!Q&vd33d!_=nEH_t&^4f{IY@HZiH!Y)HqEFQkh(I?sV3XfC3~Ls<3;phs{JFK$@MD^Pq+<%nkq^ z4>AikGA%uI@Dn6yKjtf{y2c1WFNWKNV?QVZ+9avp61wQ&BMUd*qNIC97Q9(lHB;6n zs-EB-5mew{6%}s?6q~O>Qvh*F_NlR}w-g-=;3FV1kuuv7K7X~n5L?x%LC^U|e4wdK zlDO3_m=FDEddlaYuEZc=O-@mO6AZ4I`3!y3#_mGt>ASZdt<4Bf$1aqOQn!ZY>9D#+ zl}#XyrMTJjYX(2mmLYv36?Txx7l_%{QUyPulE$HZno%Hkr)c;78m)G;eX~oFGT}K8 z3Jh~?D$~uC-KCRTVrc%rmS)8!`O@g$U=Gg9px?M!@J`#wxkJAvVN3?ILjXSt9N?1- zTAH%;=Wkh^CAm2;9s)EM!&j+Jtj_GN-`ot5m%G;lM9_wus`zA^7sSKcwendru=`Wy zDm{YxHe2x;2nzE5Nln;YUyK^&-6)wcSq|!;Si|a=DU`obUx+JxGaW}EVDm$Ne)n1+ zeJ{e)cKGdTm&tP+skX>!z09w~H;b~ttgprzPn?zfeyw|D^KgfTSl%DL420JZBIrrm7Z;F%gHY|r!h8tp`ST6>Re8kms?XVKr zb^dMGC;j_LVY5He@0xRt-&_%H_SmXt#jT)wcmkcCs_3UV9n*YHVe}evOe2naLssEL zslC@!=n=7m_Vx+qvUMJ-nwRVgoYenmhF=inc8l%6CQcS^u2(Dd<*36L2OoRpcod15 zG3EQ#+mYX6mJPlz?Tbe^5(Vz--rC0@^?eI?pvMT4ChGO_edBUP%0bLKYcJWjBQ_(9 z47y|LHKu&@*r30pPWz_T&rEx8N=E#<7)IfVw*Fm#k+xbYJfsJ8MXDH&DGeHuJ2v?w zg+kG6l#@cL+jj*70d`^=>6i?PQ6+ldlT%B6Ut#V>F9t1_$by$g-r^>?&Mku=&a%7x zv81JhzOh|a09ji}Mr~oyR>e0?*T3vWEi^nKx(5>MFgFPOFc$Pt`weUC_eT8De!)mc z%m4an~rqcyfiJ8$bA=5V{Epn@_?!cNV#5iQ}L5d==99fMs>xmI^C9?EO z_tu8ieQ)5ZK0Ez>{;V8SFpsP#U zyr8Sxc>#?m8N8lLyAuLH4y9@ol) z?jV}_Qf4oW%!MTY_O2ey3jtXRS7DIH2Ikuzk7imDRYOO=htaF;RJ97ngQZxu&!Y>}?>ipK z!v`RE;ucTo876W4KC;w+w{M>N_~PCivU35dUjfyuH)Yf-)@P}&fKKv}!Igrjy}P>c zHXsz7fGi@fqHsjBVU+9h@)v(>4pYYDHw*>1f5e?AGbo(2N)6B)V*aV4P#~%y&+%l$ zQs%u;93i=MVdZ4rD9bX17NC1Lc$*gqd={AEDYZ2Bg}8Aa8A&++;b(VNdcaa}B(7H_5 zErwQ7mFE+M1igwN-wld~i(+;FYW|*Qk5FF>&|aKqJLsBJ15F`y5jO+z%`9UhgRC}oEcjmNJa&D9O6Kyq7(@Elj zRx>IHvU!#`1Oyoc`*6L6=V6Q^uu^0Bb-S5sExsBf1&|lK97F)+#nsd%Z~ysb!q@zs|>pMamy3x zfl1@d#qftrQLvd~3VUdA){LMyLP1IU*rj+|L4hGE8JolAA3K{9ZN1J#O)ta=7dlSg^l=iS%)rK-LR%!3kbi642?4rU z8_2(M0kiidB28@jLh@FPlQ;348NRRWLw;>m2@i^3rU-aRjy7Wk!$6X7ChMt0;<)C- z4iGCQS%U&MnEe&19AKWX9ip-#p22X4WP>~Q!Rr*p{JC0?@g0fkM{QF%#@62fLNO-5 zFx~nV#Yar4SSyxTxEaZV-d?!C?Sh55Qt!+XI%@;a{!3MiE{6G69+=_aSV6zYYt=6} z*Du=TS0UHSD3}oLx;a7ImbWoZS2u<;xB_%SJEd_bq5Vm-F5`R=U-&WqDa z$hMBOD7=cO6rbLVm!?d}KIg(P+}S=+QM*@*)cu@O9}NZlS8;#L-%x@U1cuO;_qq#5 z#^CBOMkmVOT-GM#Opfs<)LUu@ozoCdc`Dj^IW?q{&8>u=WpPD0R<~pSmy?1NNwCJY zb`R?a?otLZ%>-nhXdM_NBE{w|4)iB(P$o)DkOrl#v-9%=hJ=K~_Xupc<$4HW02{#h()Miok%f&$m_QCO!Hh&wph#^2kAjA6bl?36`Da6 z3-7<4gU%}+Trrs@qW}8c6SG$4NPlJLb}E*lem;v&NCH*{{B^eZ0JDcyYU9w@r{X#$ zuQ_Pd?ur0xuv}-*SQdt-3ai@4v{@W2fmP`Y*o2CpBY^XDKk{Ry4nBs&q;J*r^T0jl z>{FZBPYI??7LN!mWXDX)m%>${4woKB7vWLGT6^Pe-hV!lfd1lavt-3V1KqMETRSp) zH*bf@ugvgpyFy+pHl8A&B&hoih zzMmfn2sI})lHT_yr0Ac1sb^nmDU;7l?rzXof8JuG8Jn+5WsR?(=9%I9Z8sHt-Qq#8k_4_)OMBMzP6~v0|H9*C@p@u#Io)Le zVyVkguZ&wkK*x^L;I+HP{J9}gY#_LuIdr7gD!1zDQ7c@Be2Qg_wWO=U zzw^>B-Qj{P8G$*I1Vq6K=xqvBkaLF|#G)a>g7LAdQLe&v9+pCQV`}S}4a*oZqHlb& z`UcY~Vcq&+vVj(BSUIdo>Dmii;e=Xf>fTCXN0W|KA}k8Mqe=G4Ca{yKffH`6Hy#cCSFJH%7d+t{rCaa>Zw*d)8o9#B%0L0w&jBf9X8r^ zV81;^PXhI+a8UX@Qrx=vl9mp|9Ku^WUPvmNG<~}NfqfWweGH4{Mbh+6(Wj&~MpKuR zB$}w6zH1$!6|$pMgrilmgly`>{(<&cw_d(bA}9D{mFmEvQ~mum-2OpfAHLMVK|dYG zv|h5*zzj!0LNs=W(1i2F@DDB?T`U~9n9NlhYgHyWFgvESamj~${tTS=xkr<$L&Uc} zKIR*Xrzv*N0lCd_Xt_Utz$DF6iIE~)>#y}iii(Nz(C;y6)sCp;2)cB48@rsGygYSO z&(FQHw!O2#S|%HgV)5IGZ$>sPEvY-%F>0Vi@qYSz)^@^L&Y?_$SJzohx?9eU+HW}H z1HiCB##TZLrf*dzqZQ=80q;C8GM>3&cbv5xV?9POEMK8?%fTNZ~Ke`30Elb9SYS`?II$Y?ipd1uV|)TkU1(6(5XqiD&ouF-3`B@zGQN0rlqlL!Yv8#1;6 z*t_?Y@`vMY1EnM5s;a6ul6EFmI$M07Dr=0a$5h`tw%C|U`gRmb?g@TQc@>ka#Ltsf1N$ZAVvlY1gnZ^`>!W$($?eSLxgx0=YYRl>r z)d!8y<1qEB_e&SDo;%TRSytDN8ms7ddjDw4PZe}pT6U40_*m1Qig~QNT7l=N8@j`G zcEvLCbLy!!FQjhFGskvmWa_mRi=AU>RVmcFdH+X=%!EN<6YI=P(^QoTW^tj5mP?kd z&?=d1rRP=!qP6j<uoYjj1n4x$UZ=T4CA- z#lB`Qv#P}08T-}En7;Y`71X@5PxGuAcF1xd&TmGJ%7GM+`$K~n<*SmN4vURbE1fV@ z@Z1k{CSFBNc;-@E(lO`U_jHyj?Aw=h?Fy)2ld6!|0Bn!uE`#en02Gn{T?Ars)=%i2 z6w{qvMkGWJLWs5Q@fe?Md};g<69LKczu`O`FQRf(ZjfSJvvOj`6^>)+cp!A*|0!tzT8-)j07g#*l8pee4w#4U9^iz8dhw0N zq#vtTybP-zF&t>UnjEm|B&-MBBzzVsojSR)d34Y;gN7_@A>AUI(ZIgt?TtDt?Quql z_#{|qhj6SY<%`cO4cg!Ryx#sd&>)}(FTGLe(<*CU?&tDGC8EnAvFo5AZxlmx>SOB7 z*Vb|6IGmK6|MjC_pVlw{##g<3WbE=$ zo(^POrEU(9lQS{PiWhORT0kZMG=vy0KpRXqXHF-4l{t*%^zZnBQkZ(68oOflf8->c zQoxW-Og5@xdNPg}<+BO`3$eG2KYJWETjFGeHbnkUln2BTG90(wr*qeQMV38BS93RW zn#vO`$xog)(w}IpoqRmw6-?QnBn!8dxGTl#YM&6{rJHg(`>4pa?3$RF1AF$`M*|I} zx#vZAXZzs+gPJeWb3gRWD^zL}m4%qTz9`p2qwMIUS}*AmmaNPoXbE|*p6~*H2*Joa`+$ zYqNvNzN>GRUs)vY2j0x|>A7FNHdz&8AnIM|f(J-X8i4=#3&V~yYt#Ox1!$0VVXT=5 zWFWfK>J5z8swVAZUiq=kxtCOgP54(ZlIZeN-Q6IrAO%bjHZ9Ucbe`&;8hA%B@@+CR zOd!TgT)mRIX-LOPe5SNrWA%sJS#_jq{7|6DAVrGECr3$?*R}8Db~+ukrv{(%NSMSc z$faQ>PY-Z}R+cby0w3+5DrIpBFyj}TmHZNjI0>o}Rv z3H7_TE(tc_4Vbto{UdmtOg1XI*?ccg0uM-c)@NJ(=B@VP{xSpWsU3D?PN>>X%oKHt zqmGiD1$GO1lGU|0VF>l!-3z6gMPu9Uh<4xhs&%@4&EyM$OBs?z;+{CnNDK!2@ zg%4(;e6w>BHC@Os*zwVFJK?5mpWL+bUqr8?G6s?TDlam&BZunw3Nj|6F*48|L8clnFw|F%9tsJ1b9t13)tB(_!^pIE&kH%|HPT&8lj zXIVyNZXs41Hs<2RVvLSFrx8h8XkBW$yEcDRP2!z+l~_OBu*6qV?^Y!CqvzC8pBT>t z=F)?o<6<(~H9dM@UOaupf ztCLyv0_So%_JYufQs+Cl%Dt!S?w3>JyEW3G8(#Rz9D~Uh``B1a+f~~$-O5ZP8q<*A z64JY-J%O{ziTx&TAXR9DAO#+cX&r2O81#_F2vkm2=qUE9Fvmgmi@x8nBRGsmr`z2> zg@soJhJZ#d-R8xf6WOQy91dKIZ(7TNnmEGN2Ul-nko#Rp6HCoIzK?D%#`st9P+-x=TO|tgCpNo55HkT|h>AMvLG8#lS5nYJ*l`=f8Gpu7k$y1& z#$WEPXhu+~C=#Z=4uh=Suo%X`m$mFM%d-Aim03A2xK3aSf{ZlfRpPZuy7vwhD$=l> zPLA8jT#;T)gC1T4PKgx`gUoR?Xw?eWYzN-&?DPj&>#i6TYzw?t{=KqaImR9Hn21(5 zM^}O)Uy;$)F>zyu#aUOKd zQ$gOlmNSVHKZ@IVHA1uo6A>~=w?Y{->Rs{l|66okdGsu6Wj7C*b5SF%qlE|W0$4j0 z;}=NwEarAFa8DyodzWAEw0lC2ZmSs3dW{}bS*x<58;Ox7R8xaTw{259{6Tn(E!59$ z1|5Yu?xil6<|-W6w+qXZGg)l~k*Pxb|13TJ+v88FD~?t{b)2aMzl*`uu5wz6kdaqt zD7x^xRIB|Vx1kQA%P2v3$@d~CfliSsT?Ms0Sb2vv%R@!{`w=4cFY$18nd4vrF>xhL z;X6tcM+{jT>a5Zlwb;gL6vvF8c1$-7yK4d0@rBd%K_Pc$bVWKcPdg%kVGM96lqCHr z@Zn^qmy77nBBZyCSqlb)2*F zQYM3Gd2KCKosAjZ$kJE9eue@`&0AC*T5menfxuT75FP|N3VEW@(##X^QO#zTvm=gK8sT zopfFB8D$fJtU7df;YD8W!7UiK9>k3hZ1&a%(G!Em-r8=(=SMNU_UV==PRC1GeGQLO z2VxZ}w;d^j0J;Y@~ylS|DA>B)kWeft~xN!xb=4VaNc zPxdzLkQ|NG)B$`UzMO_z2ixCYH?Ijb_*D2w#|5=<*~ENV93SO0Fn5u}9`5d#zX3ch zjzw0blPJ3!I%D1?n+i}2vqCTM?>oVj=8Soq%vJWUV*-^yAGF2g50 zmq$^tTj+eT$e%D{>GPw}Z+6~W2lsIbdFL`!BoeVb^MY@EQu z-&E8i0Uvago&q)8mB$6{j{4QB5c>7EjKCWxvuh9-#Q6+4>3nMt2p6A(J|m?&0I$Av z8XGU$7b<3IkZ78jS79xetLJ2lb(m1@j9YaaFuv598hU?^YS)Zg-#lR(CpcWQWBhli zc6^_VTdCYRFe*%q^{06RUJ{AMH$Y7p5+SY;knDBrlYlyV0`0{u$0?k_RDc(b*~$T; zS>*fpK7MFkMO{Z-y*n{z>b?zPW(Xm)IK|{Xup$M>Ety&c(bRxQ#=3f&*HM-ZHL(tk zB{`DQHC5#SdkX6uegf-*2hdEf8EOG+J7~=K9Qi=3V6TpKFpVfpPKTm=XdXbxkyzK- zlB<1^8=ikiL^KikhiiOSVW~U*g zWXRU_!&}dInCTGQkdV_-TnuFsj&4V0IKB1HK?gokk`r5t;%o}5hw^|+whltw@;Ll3 z&7)hSoAFX^X>jp-q%5$F>vYWiDFsN3pf}-9PbhfcUl7-pUr{3&ANuLWJ8`SBmY1^J z>==+P)i&_%t}E0-ND~Nm7@{Dxwk+_ImgM{4CEb_-?&yT@fY|O&iwvWaw2p4S#69EX zd+*IQ8NB_aW|y6r2)dQo3rhPB0!4795J~$*jj|4&cChnrW9sZ{S#ZNo>tzPy2)jUN zglGdW5Ux&qq#HG${2w;KND~}1O!Is&?)X%xi58Molr$Z_%p2YF997VHoq6mjA;p?? z@aan%5!F;#6GMVtBOUeZtLg3nDDR~$VV9jthzIJuy&QO^f_USS(lz_zxLQ5j+H*dq z+4D8)wqS#BuKC{^!K~mbvLSs@o2_S7KrFr2Ly zR@(diBP`Z0b_?Ugm`5kQxl!=1J2{90=42)wnZ7sHP@#F^%7?_%64$+B&{u?%DQ`wUF&i)-p59RWDauYD#J)7Dn%TgP# zWLs!&@;FVslH*|5#HOCTBY_N2uDY_?wRCq9f9+OIH0A*aRRONjPsFs-1$B@=IkBCw z1@IidxQb61T!E^)ga{5EM#!d{qb4D3h zdezB%)C4=|<=!Owd-V;82=!SNdfV$? z7^&P_nzq;{NAOaETDg-zb}#qQ+CBq1;ENVQePX+qS7yAj4_K_t)L2LU4^FtgLT&x{ zxWf_2D`{{rbK7yHmDVff&j4AkQ7N?BU|!6VPjP`U3ce~9-`6XF(vtCNkg9i;zB`jy zHl-hZrM1R;Y+S(+BzijPaW9AGb-3pMg7Jb~ENb~HD&D74)qO^sEx&%@b}n;lp74`= z?j%+0%ce9E&ZRxNL@dbrarPyzfsaH+SEd+4V`7zNyq#(e|sF4YWgzWCS8%b zA{m?DzkC{WH@{_EzoIkcUD1I94;x3yX<&RZCjp7nCt|BN98fUK6#Be4E5uT|FX9Li^JPH zi#s5?glnM={%TE=@v{nw2H`?&O2}3yqDq3NPx>B97Bo|ZLDsHCO|F3{z;Ca9U&xv9 zMn*ZoVd6~aEp;Tp^XT8-Zh2+@XoczivL$~UZkXs&%_Xu{izPVTQ3i9;w2T%*92%XU z_luCywStS`pUbYRav@OD9%ckslRiZt4BfOk6@2X5>~XO)qLcYMfxuog0U_FYKRJ0ngg38A$@pkD)@UoIQ^TmO&DHXWn3s?(-RJBMz1X}*i zBOZaBEVdZ}v4u4E&=y{)jjNi*+E6zZs9W<_B4n((hnbM@X)Q~$5wYV(hTyFY?Do7@DbYDZz$dZmZ5@=C8DW|uX%wEOH z>V~Q;)VvZ+TAhGe8fHO)kkO?RzE4xOmReaA7Bi1Z#yw&W#Uo3`z;R*yU{g%PCL`$T z6_&xUSckQ1RcM%13q2@`c#_kG8m{oD#@X^=p<4hK{9tze7JROvsdMUK>OmG zf0SxX_@}GtY8+EOtZ>ChBfe;rjS7|(RsS&4)%UXk>5fT&a#F@LDcGb5 zpHUL?nNa+nA2N}=Ed$3y36piTCKoy)5Gq{KBqd=j^74GJF4Q;f8Auakv-RK?D;5zW zcA3!Gqrwo}cD@RZ!T^_({28nDr|u6IW3n7x-ltT9=pQvPt%@omjh@!saFA5ukmg&T z%T_U!;v)vpdtwA~cno%oU7|6g^F9s3rRUQ;L766Hl3lg*Mw>}_3oLPFU!qbZ%%FYu^*AN#$Mpj!s1gjn!-#-pg^1sVu~<5DbKv*b~z6`DEY^v*6&5f;ur27jMJFlLN_vZ6)Nb{&1EIK@)L$$3g=9=JUGfX*(E zi>1WdBh)U5^&2#_tliGWXV#z9&A-&OG$nCqZFdnB{E4xaZ+>(uuXL&N$IzTB?P!%W zX5_jYafZT3T0W<I~gZzM+ACE5gks}NT@n(^@{;@XMSs=IrUin`;m{gg5>%6wx ztJ}7`+ouOccf|g77+LtI;cgo#2`}Baq=C%t`EPn_^~#X5e6M` zbNH6Hf>hv+SXq$pb$ZWj@>D7X)l7)*@*F~&G}KeLUGIrud}c;;t8J3rS;?ois0X1c zdS5eL?1v;73BFeRjJh#o$clNf_9Y6Vu?58#V!$iC-*$@|?j}x_dYmt)=My!I8vv!4 ze-_ZL7flteiVt(`p4j;^Bt*UpA!rMJepHFT9`X{Si-U_R-0dORM@734(r5i>_*p-d zZGz|Y5be1q%o$D;*xxy3R0=P7!#@v-)P34>!e&pR zop|!(_Q@B)Qf5-HN2e1lc{fkeze#;vv$Z0noSpUCs&jE0>Y(k9{h`|Jpk3gYfTNON zSFib<@X>^hmhV>!MgdnAxLCb%+l|&%_C(F z^-DZxnzXw*FJ;gr^VpNxeI9Kp`?yv3x><+)bSU{`_Uhd)(Ymlr@O5Xr|m8 zpJ)cZIjho#5CW%}bR4wt#ei#*+v8w^waMMhM?c9xyK0eDT6kvyI8PFdEiPPo#uV~X zJuRN9F*T*=){he`i}mAF`v(iBgsnf@D4V?>WpVR=CgIOX{O zf_L#CW9SXFj7PJ3@Osxx)?P4UfM`N2j*5Yxbg8$Mlnt)lS0i3>1w9C1!im|R)x`-c zkMWMz&OTl4Z%l-n_YC5LL^TPOK8a0K!Tfg_1EY}4)tI$?^>I_Ss4#>ks*tm|D}MVJ zx0Eq!5b9}-sWc8fLg*^Rg*^})^aV9e@-8VBw1o+tn9}(1GUoG?AO?*7rWH%7xF{9d?Vni>dYY4$9wXT`0&gZOHv1@ObBc*9n4W5Hb7t0@>YFz!*8jw zq9J7AP~b#i{INTS7qkyW={;Qvyi*0^wekj!VSfP?enffmnU+^+sgB#z|xO?d(=Ag3I=~Jk8hd&MH8QR z{zd=BVqqnLS=KMgu*C^N&Ceuk&>|8d)&DXbtcvM=5{lV+n6Bq3Lcdh7bV5diectL4 zKbD2>mwJJ%n(A^aQMOs)GD)+-$al+389#&8?HI%vkprS9CN)}av`9->&>>AT$S*)- zANycXI4e;t(rNZJgYc2%%@S`xs#rgDHl}oNXiwaIi7fC zrKKz1R96UBUn0dAFMi~0S#G4^%D{218jrPjGZ`la>bKzoTV|+RT=1Duo4XfbHm@jaX9q>G9|!j~mwmwa)c|##uSy2w%L2;N!H@c~4nETR8cyS=yv{Q|V-nSgDPcw@&Hw*ud+VsE zy0~q4P>~Wr0cj8rX^;jf0TJmOx)cVG4k_u7Zlt?mNCy~@4(Sq+Zif(%ZmDucS|vrP)6hNplo#t6^{?7ZyBg0 z&Djx$sf14tWm0H>E7Jh*QGE=Y`iXX=eOlHW7s@PgrCr)GLYRY-grTQ|k~0B@3Nn+9 zR?Sg~ip~?Xn$by7QG+}#NupQe2GC3<_Hq9pRXO9~8xELR!7BsIulAu&6gU-y->2Y~ zBFQVQaV|&hNMQGD#M)_>%g2ugb616xGRlSjBt%-Hs>LJ-(Mc4+)C(Byy+8G7?gXMH zPse3K<6lv3nNoJxTTw-5@#VA%ZANQjx8zilGLRScgx<1G%NZ-B;kZ*h9(>EY_~LhG zlJ~>ZD#3fgKO{$amj(E&`u)&xd_KaM#eP{7T2YtdLor(T4_ygZ((JbT<_}ck9A)uuxMhyc!-sw())erh(MQ41J;65%o@6%(U+$( z7h0}DQa8>wI##W<8yU?*ZjeWn~^OLHPyAyi4sXz8@9P!2M z5Hi1SxdM=vZ+wmvaTxp`yo$S%9{+SY!n~NharF{=(fz(GjU5*W4fZXoa{KFS!iNln zk`zwoB;8Dr=aP4PgI*ElxIYiD>l+mdW1C5_j-bz?FIUd-b>Z+T9-tzo?@RmNMGgeV4Ws5NDDF zSl*|GXj#g4-ss-K!N89Q`aU#}$KK~FzebQ4ig^W&DG~^6mH;pdayt+C13rxzxf%{N z=8uhpV4u=)6u`c3tn));Qj7&MAkT7XJD$C$LJFVbg?$&MeZC&W8Oi-#$LXtfBv1e6 z#FbxBPwN`0eA96sZIwH7--NxMp+qaX^o~?z8BKtXswr#g=47Q+@PxhW)P0Z27HbJuS;M zj=5IlqT7(_de}Ib6ipU<=zce1sg5sCU0F8MiHdzD7fWS5bo>=Zc+(`5)wt_w3K;=& zXnTr`H{(N1^65#%rB`Ty-Hz86baO;3?-%dh>Un?Eh#AtH~_Wd&wgI^BpwNjoPq7 z@>gm2Pk8vd)2;iT&eQ*UXOB?e-%hOmktlACoag+9L;v49_apz;g~5fQUGDw8tADTg z&*6>6dVjmC++57dhD9N*h|=Wi8#MmkRg+gbg?RcAL!zQ4|8$qn(@0?Ey$eOKuK9(& zYZd?ZQ0hmIcGXl#MU9>W;O~DomKY}5qUaVU3B>eU^lw%&{%ZucAhesI%J+kUQLgM+ zb`%)po9#%!!JFwp!S(jDjnER%&C%J=tJPN>`)_4&c*LXQT8ma$Yawsg~lvY0fhPu0Jaht<^Zo;DBvGh*KBC9|dWHyue#&WU$sGkjpC*s7@j>zUtm zbed!EWLs0Z8M&65pa8mWY8x^;-Y@B1bS4F@n24V|L!}-ylDZXPrS&K)FMt`Rh%4ys z;wep<&TSiGbOT3po2uIk;X^nszrh_u=bP0VJRNpCJom(;F}yT zL$l5l7&SV$Th6KJ&dbm@n;*&2_$uO|M<`+^t5;E_=$VLT@_%o`OF~_TX!EDp0`1kP zw`AivZuO^KoswOC=f#bZV3E)w<~Q0w9GWk1+>+u^tJ!Ojv_s~o4_*(k_q(hIZuU?~9nga`#M-r=3h z!0Wdkn0QWQ)$P%rLiN`_zJ$MhAKL5U-MZWFEphRi+?;-?Vk_%xWUXlKlF-ZSb|iH& zMhGyc4>Qg$giq4F7^h4+L66X%%Yq1$$k&F2_G8b}E#4p7o}uH?EidX0AYShAoj144 zPxfy#%uAYNNOaMCipzHM6(-F|1IQ>NKJNd97stnK@c?HDw5%_}yB}yELA%I`hnL z3%#;~%u4JTy0Cc>B0$#IoIX3Ar?VO1LnvsHAd!f!9+ON>e2bPUmjVw$51jYWt>{~_ zmXl*U*6wH&VgXx0m6s4jlC}^syArV|UCZ`&)r;f01wCsYTD8|QI=nq@d?Xq#$${(Z zY3g<4)y_3q@0APiz6j7Lo1+U^_SXcbWi>33)zxs0&r1UA;G7HE(x|rK$KGdRk3~EJ z4;Qh178=qC&3EiwU$!h&kH6eHmNpj!1BLLs0<)?*i}8WHoCG?g5M+JJeL85el1y0U z=w*bUZC8M7+S@jI2k{y$DYFD|hl_h5^D2c)jwbAmP9MvaIIC79ZtXY^fukO^O%tv+ z4A;Pt$fgOMzx1}=m;JVXhDJ#A=)QO)_%#oYlQf&+NabEbkGnMEUH#xae6Fy;;KwSr zzS*^trF|g3n&z``A+t-Rbd7k__=+5yuW&wq{KtSnw8igy*p!boT7n37hrT5)qf-LD zc|N2pyZTl@(L)R@C}Fge=_|WcQuP=KjZR^0dZN*v83Wus!oI=X(Qc}jU)~s~&P0C6 zv!|Fu;>;W@@=e>pf96c+`I7U>i*uBA+?N>ryf(4;l2>i`w?sZ*El3`Ygvs-%L&u8;RO*Q*DTnjrER%) zg2eM+f%DI?3nsr!?pbfew~OV(VB~h&vBwqN1BvpC!bHJp!fzkXG~z$@_UHRNG!3YD z*s0B4njg^f_7`nXEWHVtWY|MED|{H|*ZPw*W{e3kpIG9jKh>qgE+y<0nP2oXTYE<& zVn*Ul^08`}eW}|z16e5;xkESG9A;!q?&VHtU>Se?iyHE8-DwoB{uJ#7-q*S`264f7 zSL72)O|3F4NLd?O>sebhsx4##vv#}Px_XIMXm#dQ$wp2Iwr+;YBOfIkV@on&HP9dQ<1^Ee~({45w&ve`E&1EN!|q7B{TLj0*a%wY;D70k!Q!cv?9zT1@0 zmVy9w9Ee7Z634UiLqD>#O4C;k<79T01!}`({0hDm;{gQY@O~2Vgh7U}zjn91O)o2- z;Sq7Qb43-`?d|i3K4*qH`)JDc+r=-O(~{K6Iex=uRTLPJ1Z`NfH>qjo`rcONHTw6< z9nPssg70fe-!6l$Z(lB4QChfAfj_Jmaou%v9lU=%f~lQAp|=k2P|%1{sba<(%tG9u zt)NOTAb630FJrYoP3!-hWqjqEs)Xh_s)kES2)a_1V4Yc!T$z3VB^X0#mpR3kxL2~4 zq$m?;;61RK6gT(CLpS>4$?>cuk}t~ATFeI}T`@sg_=$vHY|`z ze=RTzh+|eXMcN&(ThvlLp7w%xI}s@2tN6}K{!_Ct&psy-yJ0_wQtqN7%vJkz$sT(3 zVBd@QEf?3vQreFR?ROSyo?eVIH|nw!2u$dFoY-i!ENjIjH_Lr$J;9B3&BN)Ms$P-$ zMySGLrm%$b@#urcIx+{cDxb#szkc^ve#g7P9oT0a%o7;iZE9=yD0K*Ar{Hd7sr&TU zan-J(tP}oQu9d=g$jy#D$-1&grfuj)VSbC&hz-->4C>b7j;fJ#icXoElDLl=fgn2c z54}$epIz){Dl6Uvo|AJqm6?0?ix_jE5mDkd8%uwrJ>u^qF3}O;B{#uR6;UD9P@1VB8t#-z$7_!2#N4vcIaW{L$77^ zeXZbCyidrbkhQ;D>VChk^n>#EwwBQXRpXe38CCUU3%m9{cF>T&&+*+w(Uv24OE4W9k+ z*_SWl1uT_4*@!zUl|jFk@z2&D^tEw<2K?@?fA9KK$u8CUPKj>OoM*tVamj#^ncwa1 zQH#Yl)DGVP-2|)&ShD@)3ajr8-bzu>TUS2^;Q(d_Ei#|Yc4vJ7aonrEyR)m3cTcVz5Y>b2Kn|{!8>**qweviE|SR;VC>ohDC+w@MVo5$HzVe{)_?D16qW@wce^J zYvF*g+8u?i0!yWkgU@sjtC{lSGby`U+@_;g1-blm+%`NGNwy_;4S)6yh@)1CmS_4U zN))>$tVn+7NX84QCrGe_P}hEyx}5+sK?SW<2=YSC+4_C|raF4J9sG`pYa;)JYr6e; zw8mqy@#~-7uQo>wK{mnQw`eSROsI(8K2?X_QWes5IUMbhNjHCacdyDentE%={5oX4 zx#5IKC(6rrI$ddTJoQ4Fepy694lVhqX_Pxx;Okdr4Rf%BBrNvgupDi2Qq02+s!(y_mBl+NCXfesG*68gRZ=FJy&w$1po;8j@-0@_v>R&kyRYTc zpY7&upl6<^RIDcSy-|0(jCXs$gl?kpLX%ELJtigH-3>cN{&8H4%H2;dnjeE}3)I`Q8mbbraN@CaYnH1i{=+waqAA+4L8{D>T8WLYTlp+ zkEG!DetRe}qurSn>5FAct;n#h?v~a*V(*aYIi*G3>7A8KT~p3bFXFxVlcbAX`tO(B z_`R40O#sQ#@reErw3xwLs<7_BlDLv2*he~8sYpCE4t6zh_`S!}x5U(^LQPAWijg+f z{T>^~D0#NsUOq8pGgBeY-VfhdJ&54P;w#g^_CxUkf|q6lx&@nz@) z=PGQr8=R7-Ram}z6%=X`{`QBYy@QX*4kuz;G*ktTS!wBq4(2%Wfz5){*0|)$RF@?e zK6#9#^BkocI88xCMXNBH+`uz@Q2F_@uX6+L%#{&Uj_iLu$Bj15H{Iur1}|E3=2+g0 zY*S*me{{3g2>*k>hrd!6^;X$wBmx7!cQV}l2iYSYD>1(bb4~a0=^L1@Iljm%G}c_x z^8`nf1Fc+WeRS-Xg6EQ?I*M>3v(pZ+nB7>*ixpB(=H9G? zfi<9(h72M0H-UP4N-t%5eXocg zW`bpxR*H?lj>k>|YA_=03CG@9PC&3UV_;!V{ctpusHB&J&s)2D$!3-M)K`7abGF=z z=B`Kp5vpoAq+GM>SFT1C-^`R4ok|%jtId`)G_ct;89x6E#ipbZ`Jo4cfl=DCh>M$! z-I~}LzcqwxSF;wse5ZBCDv2}kh)699r@e~UV|nXs1J&Y(y6Uttr<#BZ57IB}IHO-Q zQ*zRo9g7e=a~5M0DI;mgGLIJ|)`!Zv-e)weHd@S;P5T<1w<-N`)OluXs4y}_h*vXi ziuV#apa!#fxVfPlpPX31MJgH3WFpKZWnGq?%|Qq$8Ocw;EtIX_dd5R1Hypc*uW0@% zX&UO1soiYA20OX1coLyY|8rZLpPD9s6=!s=5-uWg#o zug02($}95cA!gj17>(CH5x_SwR9kNkk6Ip2H%I-s99C{P!oS*hI{Sefxu!;g>6Y#Y z5?34vZMyo$dgJ9LIzm8#l<-s(15I;+Kxy}=E4zO&^z>qkt!hogB1!mhxY7BDc80Z3 z`Fdc(Gq2c2Z2$x&48TY7a;O_$+s|QE+IgR1y^Y|B`vTpLsPz5?#=YhXvjFm1m4*XO z0A*;kW_&fbQWz%eG++E3yt8AC2{X-)E79j;ggp!lqtxiV)A{QM;aZ@`r#LiqUIIhY z&!nj&KNiQyF!^meEK6GrwSy*f{1>!?bZy)2Z;tgu6s1Hf&3lg)KXNzZ$;8|>D3;tW zt=hLXYS>vLktrjrW*r=Q!8Eugn;iLyX~+s!R(uspJwq~HyC&1_Lrj_^k<8efRthCy z&dcQEEE6oKsWJ?UQJVPWJQ$P2`PJ1YXF-bdw}gD4X@FLYcw)hfUdVU70G&q#Q5>ulWOI~!i?y0d zE=c^+9re9Ic!>4sVz6FT+C9#*C6;?$Cnrh5XJp8=+=G7asCcn+xgXoYegrS64;O~Z zc;!o5Jn^-FVIbz^Y%wOt3RRiOoS-RcM9tegxxFHIh9z z^3wVVpzHT4WGZglGtvP-46cCJ#xGa|RBi+PV`RwRBI2t?%|Vm?76pIQ8S5j|Jf7A0 ztf9@E)aD3>O5~rO`t#SBVz2fuut1@VU5i?}%X?}$Q#9ulJiQf#U%KEop>w%79CorH z>wh!Y(?K5$7A^aYgZCQ& z9Qx27esbV38QIMk5>0Jfr30mmg#Xb?(GSqR#fClF#(*6x%&Q;eb~i>LYv|E8r2)j* z`&-GKW1=`|J1Lcgf$us1wo;+A>6oklw7HyjaXiJ&GU6a6ZT?JDvj50vrd(T3>?pl4 zoGze|rG<&!peF5EP8qIp&)Py%W6<->~iYY>yfN# zRdMa|1SChw_mm!2-BBh>*lf3~$VMLEj||I(C=LIv}+c-$v|3ojnG~ zlW8ArJJ%@TKqsO?s7tGaEljP7tQ}i8zA(8wU)|Hs$dP{v@-yM3ah~*WH06a)C(-PA##U!~kt2*jawU z;qtf}Q=@h;H^_39{LfFpOPl&8%>cV!o+ z5*hDjsJj=eMp;`=J%t?mQ#Tn(SmMsu51Dqi5ZBYun@~w6d?D4;{>Z8$Q^d-d{(LOB zXkEgRURJ9dqdRKxrIUaPuFa3q<00!-!#SS`J>KgIGP#~a&w$4BFbFrII)FLMV$GP!O;yNxmcrA5*O^^V710W>jAwMh{kPyfZ_o;q`M18|Y})1Ao&C@ez8FGg z$`8v03WOiQ7@YoS%;4Sx|J;X1I3EI{`8N*XpF2oj3csA6lXj>JyKtp;+ktfM-jVIw zppn2aa3^Y;Mv~Hu>wF~V9ipO{%)sd1Q+Tsz)l>}kFuR-F)q~O9(=Z%ExkIz#0!ovz z_N~J4mvwF!BgdXE0f(DyyxlS-I3p=*Q%A=9@$F>CFEEMpI&cFZxcJ$5Vx${hV^n)z z5o1ocBv{-_f6c&%Au{S)doA+4qwq%jaJ|C_qr!mFEzTCbDY$(~^P3S)b?ZxkWzPXx zzaJ9FOiKZ{t;rE|@Ds`M9ml~w?uDK2V$gKX?1dRt3FCvC~AZQ6i7F3_7dLCpM3wpJ3&E0?UhTIg=|SXPu%J=-l?>*9EFp;~42n zXf7$DCA&hV9#NpMf!VHl{4tR`9mo>;6J$pdSwM8joeop6 zB0`X{02EpVLfM>cw5YeA!=yhemwj=YfQ$$ zbUh}w^jCdgAR@WZS`j5fo)~8S7S;#GYXrC8hYG0=kOk&ugIC)LxI*C|3npF*na9-% zzM)hsL(^*$Wi!|Eg4B+GC<}AM#hq~gB+afJ4#_pVI!f3|;#@;U$hAx&20MDWiPm4BiyC&h!3uSwF#08&AIUn8w-ts!*8dS%R0|V+=MxGcT zB?4s?a3Q*)=j*eNfg(z{%C!;2sM1AeGPm|R;|eRc+xx|z3-@0+0W-g|ymlU>M0tJu zGbg>PV`-XrMouy!!ZuAwL1837@m82h(lP?f)xQ3|^2?Xrf8JO;69I#OD`+E)DX6JY zzkXr*w?SaqIde9~#>TfTw;pXE{h%h_#xi2vNK+r{;-I~_?8%+;(`f(g)BoLtisBdZ zJICDT6k^X?NG7JAv}TaX8P~rR768I|QQPq?tw_?nUQN#c)|F)A`q($ld&RW;>8?_D zp^ucM(4Z!!?`K`@kkqok!S?Xq{((p&*@)1iTLFpHz`q4?zoiPx+lhP$fVBw9Qxwq-t_4~^xIE`# ziZbjzP=D^Z-&E;iYC6xW+xv01L8}+ezr36;G@m`cSJM!Mza<>7bzW)pryft2gC2O^ z;nj*5>d^4|5wjA|wHJ(z_^;6-Ffu#Jcp3B1k*s0&nA2b6VZGOOI$?SDGlb`EUO+7U z%SV#LZt*=ptM+-_?ej^1LtYyQK{4_Wdg7VFho);gawW#oziWQ{v`Ee}LWA0?hQE z+f4e*Sanzi(mGCU*O?PB(>yNi}%2h^bZ2 z%+7w@*x1m;;i*;)6Z$icmB7@>sScRpUX=`*Y6=D(qFyiYR;LLjC#kyr2jk#3c%79& zfSXS3q(mkOexb^9;N#a z#Llqe7z}BIrX@LlNig}^0f#*imAV|F3lKV4s<7!O;eX>M`rCyE*Qnk_{fV)Obi?c8 zX6eQgN|1|ax;*P5_$-9*l)X+rbi{mf@mmv@gE zGL0@5E-TDEk1bEj?xVJ-873{fE6P25rdDvQ(>uv@^jRmNjQU#;gF}{Eh=Hu{lNZzD zKPj+uBv(SF2*OM9XLK-&SOXvaJfh;az>QHHa=zChugUt$B5LoBEMcNo8M9-Li7*RB zzhA;F+KBA^-}24yX9PqL9H4@S&HQa0k=(yDc}=sB-W<^}o76H#AHOTFHT&wt-DzR1(_2kr5S#?yQsbBKP$yw5?+)67 zsF+NO$2-)iDqk6f6m%6s*gaZg~V4ol~_(=$eYX#n0Fw$fIVMAk;OdAN;l0i@)B6 zN8))+|C74;CE}XW3UG-7$ky-`MOo(MT@Zl+98+;Uf9vOd+z<@lG*ClMBC5t4TOsv-SmHF!2 znzGd0!g+}Ukgbo_ZIwZXS^E(s^RSS_BPPY`72A=^?rQ>|S%L0=$gzbB-W2NVhgW@M zr7k?7@zMO0LCFt1@<}hXSMk^HteE>aybsNB2|hF@ULT?QdP>M8XQch2(u;gBs`d@{FZc=2}q6~~L6F@5M4vmu%iuy}YFnCRWN>x4jMq{c_#aRUoCOXk9P zbLOU&y!pvH%q_^`Z;w;f19h0N`uM`A`4$20c5}tZv^5N424ST+a%Q=qcMX58*^)E< zlM9V%m#}zW{cNc@HpnuxR)fg4oN-P^tbwej? zEvvnBa|ek!$z7lnlsw#t45rJ&tZ-iM&sZC#kCtDGOo>kh#C3_1hsoF#MCwD6i$kUR z{xp!4EsTsX>{!%&P@B3+m_2@}Hg^4eVyt<;k08u1FgJ#epAQ%na!MyUNigk+Eg=LE zi0c-M?*&i(VrpKp6HkLjr=iT%Sn6yk@`>ElKx6%cq1s9Q1QEM_%vO+sgh3BdD7PPX-aRzb>P3KKx=`5S=@0zbpQ|qPv7*2^$Jy_)@+S z-UIFOfBJhCw1}Hh$B~e1?LHX05LV+56(S}I3evVkY$)oUtt-9qlS`EDR$VOC$}u+FOF z2CXd&=6{qn4(0vseiVm4jv{?7qn6ys9QjAqnV(muA$MtmjUI2im;`q@fKn>aKz*l+ zpGeSKms=vb4*7n0`G{`O~M>zh!W<_pKyy!ddqlqd*c<$M3$^>6=OB@AKwsT%yV36q z2ir7pxq8}gK(w0m6ZLP#DFaPJO~G;-{3iyB>Tvm=1qx^5DS;)mJdGzYv0J=j(Bj>8oDI)&v316jvH9)$ z*_04S=2l+Px9w5N?9MNm!n48zDydmKyYoD$9cyKn1o`N(uxaiHfNzxCHNjVGu-7x<#x!{sE0@kcI% zVvQB9sK}qdt)2Ox#2;seURG%>ZD^)oH?DtNYqr>{qLRLE+nk?nc=5X9oVtHYq2%N{ zKhIvgZAp!tHj=n9#z*YEH*|T>-o&n`v8BYsMY8ue5Yfc8f+5rw)>QqcpkSf-BNO@tJR;j!kX(&ddSAEIR4@s;e3TV} zKtvDP;=gUHJ-9J-i9EqdkUMY|LfctuomQ)g68X1N;zi>Z^feqUdAmQIYKHWq37n?P zU00#7Y-ZBpzK0;GKoj8dfvE1!p*$L-c&wX>`IQk+!SUp)h*ski!(Kd3buCTI|16lL?oIrr z)2)f=3jw!-keWk-nnQP(7)Ua(1_VW|U~}hfq7F$=j8HobWu+~-a8_7v?4Gb-y3E<= z0Rvh(zaUEmKO4hFPaRq})9w{(Xn}k0QnM=QRz4B;O&T7+P?u;>k}}7DMCUOp#Z&S? znR3rF&8zLD*S961ZnO-+gaho%jduDYoy0@~Hx@Sc4Hy*}X4x-zo7Iz7uMy8rWV2$P z#Vn_O4-X0}4jqZ<tUyv!kZ$+49w_irx z)Ku!<{Rmc&fk4$N@jRoiNht5nOv$&fV)b%0Jm0xc$Jn{ln`$d>@9a;xa9Pki>6}T$k3CC(_c!P7Q-=;xRG@oAQ&Qcp;|I`!?A9S@1`<~-k5ySkEbw~BJnArOd< znyH>)+wWMvDf%OWKlO;KjGu!IWaS=tBbQOUT21-6?$mvo@=QrBMRx0*KUYJxYdgTK`*IL_4&dc^y0;d=ybhQx#yrj6C`}ou}iTfnH zl2y*YOci|6S2RCG-H*4au&nza=kGpPI(%t({qhoT{*zc!)=e~VG=n(zYYcHjT&{7n z_MbOAzgX7ay4<)ncfB?ioWmOQ+S%bb+i)4vsbdXW@La?(!-&xl{~@epvs$PDE9atu z3(RUxFR08fsMxz^dAJsFk;18J&C0{{x-w(8f*hvxzs$Y6NehK*EAI=JPuaBDtA6BrzDs&LCOr?G*+eni{`61XS*=RRR*eCMF$yHYMWDogET zo+W)Uel-1*q9JaX_$P8TJT@}L(wFWQatWM>4kU0!fYG{bdu^Od{1Rp>S%l0ZSQd1+ z${8Anc$dJJ0?ZAvEpj2*t2%q49PW zU;0iYB{xCHsuw$v4xHTVc`4gnPh3qKi=Fh$^*lkTI6vt zM6%5wVeY<-^VRJ*6I7HzC2+yOgpV7vPY1=qpRBxE;85aVT7b1e_e z#@^9}CQ7NHxu_OvL}t3bZy8XF-z)ke_S~ZRY3|ea{b1r|d4jQZfluTZ_T9d-_Lj2! zB&6Mm6uMnUCKTQmW7ei$oeEQA>x$?Ye)r(IBRY!CL@aAooS(wdOueFDG6;R`E}ny* zMR(f^?b?yk{ro=+nzNH_FC${poj_9-wr!p8OgEM+&EZv3AN|m&D+R%!6V{JC#MZl| z*n2;Rn|rgEgpz5hzQX_H8j7*gn0;uZnQmEO@hA0KoPeWgJ|G^F@$-Ohw7S_u4RR{H zCS`T&#v-nNv=itnguo|1B(Wv?qG+M|n_g3X+uVuM;hxZj_pY4a8(k2sI$@eOS2Z#f zx3QIVo_r@kB+_K}s*Z3mz%~?^-g5;dNZy*MqY_rPyUqa3nJA@u#`aWzagOWlsZebQmf~ zE5_7spJvPB_fTnKOp2>vVR(@Keysfd3Z8SI$qUBW7MfVWHf1@e7##MZdTDPP}dJ zrs94W8l=_H?_Vzww<%<(JxHnNXe?kJ{(E!1OZRT)l(sCkyNwWS~%PCDf^BLvi&h z$Y5MyP}AbRrI^0=d2d}O)mw1!py83~`S#q@>`JgHB*#=O)t2;dZ_n{X!O`*$ zzu%8P%_PSX08GHJjBEg@K@eOMCk4(wdQqETESJD$>?i|K;z!lpM7v&NY4XlR4M5VA zb6%R4c;3ZreF~JG`{h;>YL;)b(@QFt8xP;Ay_-$`foA7?TCxwslCSd7sySy?yY;n5^^2gtNIi{ zmQgJlnlQB~4X8ptn0yHiFE%JCiJK@9h9-xrN9xLJpe$->so+CziDXKdso>09@!GUR zodVVUP~tCr!q)IL#7k&Kzk}Y?8{ICP<$$e%4YpuH#i8EQ=CmRf;|2w%yafv#>l)e{ zbj$Jyi+0MOq?kE&=+KH|F$t#4u;o|SvL^}dUjcnS{e-9Spqc98)Vpu8{302>=21ma z(*;5YCoz@O6`WqAoCm4sR;ykt|CU~NbHn>kjx%{6y^WLQVfIiL zUSKK)z}vtkAjxV>C_l)Qlrp+pvY?jS1qdL3=^MY|TtI})Y_wbk9(x}d_r}hKIASLz z5V7e)Ew6GKL$s${oD*XxM=bokw$8Zk|fPfwS0s=Ut^iKjgeGHVVFykke# z;f^cN8$7qv4*LR#Ub{_|ExBf-XEG&U>&Oq(@lE3C;JStvT5Bkho2FLqgf#+=c zgh*aZ8w1jM5cy9~4n3Q(Eq2;&)L9#{{Ml6tT}%GnLD^|p%d6E=W@0S0-f{l?VmA@~ z{iThW>il6EFd!iO;NwOxy8nS2V8R;&P=Hk#{yFt=KNo7P>v|MrWPMRrzQJ&-&e#cB zT;4CO^r?Hyt_BV$;XNolU2!>-=u4<8z-{?()d$qYvJc0ciUDd4$AFi}N^z;jn}MIu z?*joP{dxc@_dFbfl>-lDXVEs`-W)`VBpV424<4BSdZMA*HcJm6QvX3JbM0eL+PTVJ zcY)G8gG#nlpihGd#d4xHYE8*07i@10ck^NUh>_NEGu3hz55<$GT!(opH#U_o^#whz zKlcU9tNMDI9C4Z8mwaHs#>S#opJTTUZ(Mhq61Swtm_fB(D4kH1ZR&vaF2XU$%I7q$ zFAaY!`t!%1xDaKuz;Uim#eOvGv7M&Ai^g%QUi!+!I1$;%VMH5P0QoPrvQ0} zqTDR}o4=IRv(AlWua!IrjT`FGoBelx1LA@sR-$QLkFBIun;-r;2F8bb6dAL z0Vt!O0*pdc9{^Bbu(v$woWfSqs&K$A;_)K$Xf4^D#pZ`pozzrZRNXWqSbl@I7&J=~ z2#%Ml-MPy7%k4IMT-l~nloWp?*bBSO$xM~KMudkifOVJ(oH9_nW40)5?*vruq!Mzp z%1d+imAmZX`eZk$uJfeE`A?isc7usSmn#i7Z_V`%D6Xcc>h<`{^!d^C*ig&WYve?% zSVPfqyylN)56QMezgb14g0Tb?1{`*2FRnB9J9C2v`k}I{ttqPd`dEqgbeVDtmVG={ zVZH>qMUMOlgmH?x`nZFk}E$=G3 zc}uTfA%Xk>2r!Xi~VFOCKYm&?BG3tXtE0o_G9p$QE zZW^u*h`8$2z}r#85EqyDq5;c3q-m?3O(37?@+9Dd|8 zZtq9tkg()cx?O@Xo&*ySs&*Azj5J^UGao;qJvV}6=uSl3u5>bTB{jdHk! z6=&s=5^y>TyEPHqZLh5Nwz>u`vT6$#*-vdebLwKB4SX-0x+mnrKErUafu4qbJcuf|EZ|81))Rk( zv#P|e!S}VG8JF8T`V$B%X(VxvLu2=TrB1>o|1h~6ysSVf9hIhD&dWw$CLI+qvd1rA zGs2`DbwvTWD8EdF_3vAeazu|NE7#e{s8x6x0#OQxX!JsOgtqge=9kDn)R(g7w%77q z_9>kZgDfKpMSvTCIwq*%qNh*M%Z2PS$$Nj|8(RjY*G-iLM%vlwb?}rTuCQDc@-7*Q z*Cb36;+VmM*_Y!%S~J5@0`N_pNL1QDs@LkZ!i5$ZL_nmQ#n)qD;_$F~qdviU=8g-* zr_dkPi%2)sm`P&mwLXc&GeH?^U+Nxgc%jH6IMT$l!^|F3qn&t$P=5wl2Z~DU z_CnijFKgNXAWBM4Bsek=&g4siKrTX}*2MA6ot-sQt4OX&A6MUoEHW9*9LwYb^=9~6 z;mR;Y5Y>0@Akhg zwOWY<3cIJ1zSv$JoEfHF@I=OB=GeCBFUJ5mRcWJMBc*`3e}8!oRgsQOp97c8MSZ@ta@s}>=i&O2Ez?%nZWpdEh+;Y~< zSQB|L`R_xx#%|VQ`z_XH@OSl`c#H{EIbCLJ$#YldSe*H7|zvXL$o{p9I|+TVy5};Ng@C1QUBkpdOsDk2*a4u;H!Vn zJ0t+6%YwJy-PqLFNxuZn424SB{pS&H5@5X~bTi1zloo=UBch7ovM}u%vG(s~27sFS zjdIIFT}0XK2G^MQ_pGOHZ&>i|C+eb1q(8u1H=9q@@J;LdyZMaX=r;dt|3ACM|C_D; zKOX2fc?Tp1`N60E-m+cS*q;c?Y;cTEp@QLe_SAn((n_i!s}rABU(VK#U^M;j`9RCg Zw4iBZcp|YU_%Ltw%19}`ESE6y`#<)I!s7q{ literal 0 HcmV?d00001 diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/images/publishDataView.png b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/images/publishDataView.png new file mode 100644 index 0000000000000000000000000000000000000000..993610c4db802b7775b98c3ff3a28b2f59d5351d GIT binary patch literal 36622 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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/images/registerView.png b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/images/registerView.png new file mode 100644 index 0000000000000000000000000000000000000000..385713e13daed2c04b45e7625146e8c8e794b03c GIT binary patch literal 17868 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 z literal 0 HcmV?d00001 diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/images/selectSensorView.png b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/images/selectSensorView.png new file mode 100644 index 0000000000000000000000000000000000000000..94af4a48a024089aa24fd6d5dae0c77af22eb307 GIT binary patch literal 38015 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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/images/thumb.png b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/images/thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..1524c56fe77e4f59ccb87cffb7684dd1c5498795 GIT binary patch literal 7888 zcmbVx2UJt();3Lw6cN!#M??@6LTDjC=m-%3Cn6vvNC`D0lmudE3IaNah$tvk5X9in zl%kM;h!mwmsG*}&Ly;yR@EsiI&b{lO|E~3YSt~2&?Ckx%yFTwe``o!?Zp?d7_#hJ# z6EDKV5Xr>EjN1QlvICUXvq8eZj}YF-27ejnh7WS`M>FZU;+)Zv2p=bRG!pIPdfo3M zT8oK^)z8z?25)0_QNso2qu{j9qd@fW1*Dmnv~-BRPA=YPyreVQ-4hF!TCA#(lJsFhmEq_@aRfYE3efaV6MTHl%>K17HsF_| z0GEM@PQGA>f+E<*XWy=$+yQtb`foMWwqQB(hGoqv*7Qc}~`H`IsdsY3N35F=HXAxu$SRUM{mbY2~*ZUFmB7J&`G zJ7HbWf9>rF?EOnt@xRJy82F=|@Hl@<91inW7F_bc;c)>TIA2Kv1Ig3Yo>*60P=MTi zg#I+x5bf_7h;}{ikMoiI>0%Aff7l+4cG|bkRbJ7_$xR*&Q9;W)E32!?yFuNYm7vZp zXb40_>X*Fh|J8M1Kq>Hkp8Q+h{6zwiasTpPRRG-ltB%lEpm_X&Dv7^u^^}Q8cnx8w zXGt9VI?SE+h>+g6y!W={i=sWd=wtTi*rx){T>{xTFW45&6nF~T&8DEI3ZJZbHJchi ze)IWaRsBaf{+Tql)792bW<%Eu4%=Kic;U0ur&G856gca60;U2Lwzt=QWD?cecmlA> z(>$3Q^J^~A$^mX|KiUJTr}utjPH*moE)$2Cn6~a^$}llSfS5P|2&v^}VgobX7(1{} z&|PBrM}j1g8Q{2~tb1Rki7&IgZyv{(2#}D!eeQP9N=ZDR63-PdzN^r6*JY3XF?bUbeWWr)FKgKHw^BL6T1`!dAdZQ2L z#_@Nq2WnH3YpVhogJY+X?Z35UXJ><(`-*ZNbvWAE*=6>Z`_NgTL%wxwm6es|A{6W) zb@L?w3ab=R74P1j5&CF!aj!6hA!^IHQ&3Rg<3J#+B^TE0ZuuC;i!(a-{4B;zt@tUM z$0*gl>LGr9e#_7Nx9vQ|J|7g@2%RJ6X$LpUS5VIEF-m*T-ya-(Qm2=u-88nfwdKz> z9Cb^<-wc8HnD#bcdIW+Sp&%zk`*{cL$6ACVpy37fZ@Y9{o9=H4R4zVOy&wCjxfw^D zFRW5{U=fS?&W>3UsAj{3tp~cdJpRq<(xoLx3JXkxB`kc|2;@+DZK~x7wy5IWk!nfe zN_$hn$vk$Jn?j{*{#UPF9eA`d-JW?Zn5rj2;ei3V4@{P2HAkIk@-#9sdiLBbH83N@ zw#ApVHnlIyJ2FVg>}E(m3cZL)bANHd<2V*r@J(p52m9y9iHV8BQ#1%lhl+}TItEjj zQ&A#eE%(#-56+NDE_+fH8xFNWx*0-u+Zmy% zmAe3WmCpWma{ZZ0zq0L}?mK)GO!8Q)k+85Z3XY%Y7}1p+v)0axKTvo-$wNgrfgz_D z__co}hF3(bt*@u6PmV*WaZil(@`DEt#@fTy zUmrp&-5XysUz)J=oBf=^%E6bW*d{=)?DD3mzoa&Ji061se7JMs`pNh%0XDF}jQG>9 zhlNGnFv>N2IIB#3SP?i|G#XtpG2tAMJZ5AqO>|Mi&~x5rEX4^>OjL=crlyku6fu

kKhaelc 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 PcgzSQbHhUYYZ3ngtLSEh literal 0 HcmV?d00001 diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/js/download.js b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/js/download.js new file mode 100644 index 000000000..f963c0e0c --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/js/download.js @@ -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. + */ + +var modalPopup = ".wr-modalpopup"; +var modalPopupContainer = modalPopup + " .modalpopup-container"; +var modalPopupContent = modalPopup + " .modalpopup-content"; +var body = "body"; + +/* + * set popup maximum height function. + */ +function setPopupMaxHeight() { + $(modalPopupContent).css('max-height', ($(body).height() - ($(body).height() / 100 * 30))); + $(modalPopupContainer).css('margin-top', (-($(modalPopupContainer).height() / 2))); +} + +/* + * show popup function. + */ +function showPopup() { + $(modalPopup).show(); + setPopupMaxHeight(); + $('#downloadForm').validate({ + rules: { + deviceName: { + minlength: 4, + required: true + } + }, + highlight: function (element) { + $(element).closest('.control-group').removeClass('success').addClass('error'); + }, + success: function (element) { + $(element).closest('.control-group').removeClass('error').addClass('success'); + $('label[for=deviceName]').remove(); + } + }); + var deviceType = ""; + $('.deviceType').each(function () { + if (this.value != "") { + deviceType = this.value; + } + }); + if (deviceType == 'virtual_firealarm') { + $('.sketchType').remove(); + $('input[name="sketchType"][value="virtual_firealarm"]').prop('checked', true); + $("label[for='virtual_firealarm']").text("Simple Agent"); + $("label[for='virtual_firealarm_advanced']").text("Advanced Agent"); + } else { + $('.sketchTypes').remove(); + } +} + +/* + * hide popup function. + */ +function hidePopup() { + $('label[for=deviceName]').remove(); + $('.control-group').removeClass('success').removeClass('error'); + $(modalPopupContent).html(''); + $(modalPopup).hide(); +} + +/* + * DOM ready functions. + */ +$(document).ready(function () { + attachEvents(); +}); + +function attachEvents() { + /** + * Following click function would execute + * when a user clicks on "Download" link + * on Device Management page in WSO2 DC Console. + */ + $("a.download-link").click(function () { + var sketchType = $(this).data("sketchtype"); + var deviceType = $(this).data("devicetype"); + var downloadDeviceAPI = "/devicemgt/api/devices/sketch/generate_link"; + var payload = {"sketchType": sketchType, "deviceType": deviceType}; + $(modalPopupContent).html($('#download-device-modal-content').html()); + showPopup(); + var deviceName; + $("a#download-device-download-link").click(function () { + $('.new-device-name').each(function () { + if (this.value != "") { + deviceName = this.value; + } + }); + $('label[for=deviceName]').remove(); + if (deviceName && deviceName.length >= 4) { + payload.deviceName = deviceName; + invokerUtil.post( + downloadDeviceAPI, + payload, + function (data, textStatus, jqxhr) { + doAction(data); + }, + function (data) { + doAction(data); + } + ); + } else if (deviceName) { + $('.controls').append(''); + $('.control-group').removeClass('success').addClass('error'); + } else { + $('.controls').append(''); + $('.control-group').removeClass('success').addClass('error'); + } + }); + + $("a#download-device-cancel-link").click(function () { + hidePopup(); + }); + + }); +} + +//Device owner removed. +function downloadAgent() { + $('#downloadForm').submit(); + hidePopup(); + $(modalPopupContent).html($('#device-agent-downloading-content').html()); + showPopup(); + setTimeout(function () { + hidePopup(); + }, 1000); +} + +function doAction(data) { + //if it is saml redirection response + if (data.status == null) { + document.write(data); + } + if (data.status == "200") { + $(modalPopupContent).html($('#download-device-modal-content-links').html()); + $("input#download-device-url").val(data.responseText); + $("input#download-device-url").focus(function () { + $(this).select(); + }); + showPopup(); + } else if (data.status == "401") { + $(modalPopupContent).html($('#device-401-content').html()); + $("#device-401-link").click(function () { + window.location = "/devicemgt/login"; + }); + showPopup(); + } else if (data == "403") { + $(modalPopupContent).html($('#device-403-content').html()); + $("#device-403-link").click(function () { + window.location = "/devicemgt/login"; + }); + showPopup(); + } else { + $(modalPopupContent).html($('#device-unexpected-error-content').html()); + $("a#device-unexpected-error-link").click(function () { + hidePopup(); + }); + } +} \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/js/jquery.validate.js b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/js/jquery.validate.js new file mode 100644 index 000000000..fe7ecf07a --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/public/js/jquery.validate.js @@ -0,0 +1,1227 @@ +/* + * 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. + */ + +(function($) { + +$.extend($.fn, { + // http://docs.jquery.com/Plugins/Validation/validate + validate: function( options ) { + + // if nothing is selected, return nothing; can't chain anyway + if ( !this.length ) { + if ( options && options.debug && window.console ) { + console.warn( "Nothing selected, can't validate, returning nothing." ); + } + return; + } + + // check if a validator for this form was already created + var validator = $.data( this[0], "validator" ); + if ( validator ) { + return validator; + } + + // Add novalidate tag if HTML5. + this.attr( "novalidate", "novalidate" ); + + validator = new $.validator( options, this[0] ); + $.data( this[0], "validator", validator ); + + if ( validator.settings.onsubmit ) { + + this.validateDelegate( ":submit", "click", function( event ) { + if ( validator.settings.submitHandler ) { + validator.submitButton = event.target; + } + // allow suppressing validation by adding a cancel class to the submit button + if ( $(event.target).hasClass("cancel") ) { + validator.cancelSubmit = true; + } + }); + + // validate the form on submit + this.submit( function( event ) { + if ( validator.settings.debug ) { + // prevent form submit to be able to see console output + event.preventDefault(); + } + function handle() { + var hidden; + if ( validator.settings.submitHandler ) { + if ( validator.submitButton ) { + // insert a hidden input as a replacement for the missing submit button + hidden = $("").attr("name", validator.submitButton.name).val(validator.submitButton.value).appendTo(validator.currentForm); + } + validator.settings.submitHandler.call( validator, validator.currentForm, event ); + if ( validator.submitButton ) { + // and clean up afterwards; thanks to no-block-scope, hidden can be referenced + hidden.remove(); + } + return false; + } + return true; + } + + // prevent submit for invalid forms or custom submit handlers + if ( validator.cancelSubmit ) { + validator.cancelSubmit = false; + return handle(); + } + if ( validator.form() ) { + if ( validator.pendingRequest ) { + validator.formSubmitted = true; + return false; + } + return handle(); + } else { + validator.focusInvalid(); + return false; + } + }); + } + + return validator; + }, + // http://docs.jquery.com/Plugins/Validation/valid + valid: function() { + if ( $(this[0]).is("form")) { + return this.validate().form(); + } else { + var valid = true; + var validator = $(this[0].form).validate(); + this.each(function() { + valid &= validator.element(this); + }); + return valid; + } + }, + // attributes: space seperated list of attributes to retrieve and remove + removeAttrs: function( attributes ) { + var result = {}, + $element = this; + $.each(attributes.split(/\s/), function( index, value ) { + result[value] = $element.attr(value); + $element.removeAttr(value); + }); + return result; + }, + // http://docs.jquery.com/Plugins/Validation/rules + rules: function( command, argument ) { + var element = this[0]; + + if ( command ) { + var settings = $.data(element.form, "validator").settings; + var staticRules = settings.rules; + var existingRules = $.validator.staticRules(element); + switch(command) { + case "add": + $.extend(existingRules, $.validator.normalizeRule(argument)); + staticRules[element.name] = existingRules; + if ( argument.messages ) { + settings.messages[element.name] = $.extend( settings.messages[element.name], argument.messages ); + } + break; + case "remove": + if ( !argument ) { + delete staticRules[element.name]; + return existingRules; + } + var filtered = {}; + $.each(argument.split(/\s/), function( index, method ) { + filtered[method] = existingRules[method]; + delete existingRules[method]; + }); + return filtered; + } + } + + var data = $.validator.normalizeRules( + $.extend( + {}, + $.validator.classRules(element), + $.validator.attributeRules(element), + $.validator.dataRules(element), + $.validator.staticRules(element) + ), element); + + // make sure required is at front + if ( data.required ) { + var param = data.required; + delete data.required; + data = $.extend({required: param}, data); + } + + return data; + } +}); + +// Custom selectors +$.extend($.expr[":"], { + // http://docs.jquery.com/Plugins/Validation/blank + blank: function( a ) { return !$.trim("" + a.value); }, + // http://docs.jquery.com/Plugins/Validation/filled + filled: function( a ) { return !!$.trim("" + a.value); }, + // http://docs.jquery.com/Plugins/Validation/unchecked + unchecked: function( a ) { return !a.checked; } +}); + +// constructor for validator +$.validator = function( options, form ) { + this.settings = $.extend( true, {}, $.validator.defaults, options ); + this.currentForm = form; + this.init(); +}; + +$.validator.format = function( source, params ) { + if ( arguments.length === 1 ) { + return function() { + var args = $.makeArray(arguments); + args.unshift(source); + return $.validator.format.apply( this, args ); + }; + } + if ( arguments.length > 2 && params.constructor !== Array ) { + params = $.makeArray(arguments).slice(1); + } + if ( params.constructor !== Array ) { + params = [ params ]; + } + $.each(params, function( i, n ) { + source = source.replace( new RegExp("\\{" + i + "\\}", "g"), function() { + return n; + }); + }); + return source; +}; + +$.extend($.validator, { + + defaults: { + messages: {}, + groups: {}, + rules: {}, + errorClass: "error", + validClass: "valid", + errorElement: "label", + focusInvalid: true, + errorContainer: $([]), + errorLabelContainer: $([]), + onsubmit: true, + ignore: ":hidden", + ignoreTitle: false, + onfocusin: function( element, event ) { + this.lastActive = element; + + // hide error label and remove error class on focus if enabled + if ( this.settings.focusCleanup && !this.blockFocusCleanup ) { + if ( this.settings.unhighlight ) { + this.settings.unhighlight.call( this, element, this.settings.errorClass, this.settings.validClass ); + } + this.addWrapper(this.errorsFor(element)).hide(); + } + }, + onfocusout: function( element, event ) { + if ( !this.checkable(element) && (element.name in this.submitted || !this.optional(element)) ) { + this.element(element); + } + }, + onkeyup: function( element, event ) { + if ( event.which === 9 && this.elementValue(element) === "" ) { + return; + } else if ( element.name in this.submitted || element === this.lastElement ) { + this.element(element); + } + }, + onclick: function( element, event ) { + // click on selects, radiobuttons and checkboxes + if ( element.name in this.submitted ) { + this.element(element); + } + // or option elements, check parent select in that case + else if ( element.parentNode.name in this.submitted ) { + this.element(element.parentNode); + } + }, + highlight: function( element, errorClass, validClass ) { + if ( element.type === "radio" ) { + this.findByName(element.name).addClass(errorClass).removeClass(validClass); + } else { + $(element).addClass(errorClass).removeClass(validClass); + } + }, + unhighlight: function( element, errorClass, validClass ) { + if ( element.type === "radio" ) { + this.findByName(element.name).removeClass(errorClass).addClass(validClass); + } else { + $(element).removeClass(errorClass).addClass(validClass); + } + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/setDefaults + setDefaults: function( settings ) { + $.extend( $.validator.defaults, settings ); + }, + + messages: { + required: "This field is required.", + remote: "Please fix this field.", + email: "Please enter a valid email address.", + url: "Please enter a valid URL.", + date: "Please enter a valid date.", + dateISO: "Please enter a valid date (ISO).", + number: "Please enter a valid number.", + digits: "Please enter only digits.", + creditcard: "Please enter a valid credit card number.", + equalTo: "Please enter the same value again.", + maxlength: $.validator.format("Please enter no more than {0} characters."), + minlength: $.validator.format("Please enter at least {0} characters."), + rangelength: $.validator.format("Please enter a value between {0} and {1} characters long."), + range: $.validator.format("Please enter a value between {0} and {1}."), + max: $.validator.format("Please enter a value less than or equal to {0}."), + min: $.validator.format("Please enter a value greater than or equal to {0}.") + }, + + autoCreateRanges: false, + + prototype: { + + init: function() { + this.labelContainer = $(this.settings.errorLabelContainer); + this.errorContext = this.labelContainer.length && this.labelContainer || $(this.currentForm); + this.containers = $(this.settings.errorContainer).add( this.settings.errorLabelContainer ); + this.submitted = {}; + this.valueCache = {}; + this.pendingRequest = 0; + this.pending = {}; + this.invalid = {}; + this.reset(); + + var groups = (this.groups = {}); + $.each(this.settings.groups, function( key, value ) { + if ( typeof value === "string" ) { + value = value.split(/\s/); + } + $.each(value, function( index, name ) { + groups[name] = key; + }); + }); + var rules = this.settings.rules; + $.each(rules, function( key, value ) { + rules[key] = $.validator.normalizeRule(value); + }); + + function delegate(event) { + var validator = $.data(this[0].form, "validator"), + eventType = "on" + event.type.replace(/^validate/, ""); + if ( validator.settings[eventType] ) { + validator.settings[eventType].call(validator, this[0], event); + } + } + $(this.currentForm) + .validateDelegate(":text, [type='password'], [type='file'], select, textarea, " + + "[type='number'], [type='search'] ,[type='tel'], [type='url'], " + + "[type='email'], [type='datetime'], [type='date'], [type='month'], " + + "[type='week'], [type='time'], [type='datetime-local'], " + + "[type='range'], [type='color'] ", + "focusin focusout keyup", delegate) + .validateDelegate("[type='radio'], [type='checkbox'], select, option", "click", delegate); + + if ( this.settings.invalidHandler ) { + $(this.currentForm).bind("invalid-form.validate", this.settings.invalidHandler); + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/form + form: function() { + this.checkForm(); + $.extend(this.submitted, this.errorMap); + this.invalid = $.extend({}, this.errorMap); + if ( !this.valid() ) { + $(this.currentForm).triggerHandler("invalid-form", [this]); + } + this.showErrors(); + return this.valid(); + }, + + checkForm: function() { + this.prepareForm(); + for ( var i = 0, elements = (this.currentElements = this.elements()); elements[i]; i++ ) { + this.check( elements[i] ); + } + return this.valid(); + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/element + element: function( element ) { + element = this.validationTargetFor( this.clean( element ) ); + this.lastElement = element; + this.prepareElement( element ); + this.currentElements = $(element); + var result = this.check( element ) !== false; + if ( result ) { + delete this.invalid[element.name]; + } else { + this.invalid[element.name] = true; + } + if ( !this.numberOfInvalids() ) { + // Hide error containers on last error + this.toHide = this.toHide.add( this.containers ); + } + this.showErrors(); + return result; + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/showErrors + showErrors: function( errors ) { + if ( errors ) { + // add items to error list and map + $.extend( this.errorMap, errors ); + this.errorList = []; + for ( var name in errors ) { + this.errorList.push({ + message: errors[name], + element: this.findByName(name)[0] + }); + } + // remove items from success list + this.successList = $.grep( this.successList, function( element ) { + return !(element.name in errors); + }); + } + if ( this.settings.showErrors ) { + this.settings.showErrors.call( this, this.errorMap, this.errorList ); + } else { + this.defaultShowErrors(); + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/resetForm + resetForm: function() { + if ( $.fn.resetForm ) { + $(this.currentForm).resetForm(); + } + this.submitted = {}; + this.lastElement = null; + this.prepareForm(); + this.hideErrors(); + this.elements().removeClass( this.settings.errorClass ).removeData( "previousValue" ); + }, + + numberOfInvalids: function() { + return this.objectLength(this.invalid); + }, + + objectLength: function( obj ) { + var count = 0; + for ( var i in obj ) { + count++; + } + return count; + }, + + hideErrors: function() { + this.addWrapper( this.toHide ).hide(); + }, + + valid: function() { + return this.size() === 0; + }, + + size: function() { + return this.errorList.length; + }, + + focusInvalid: function() { + if ( this.settings.focusInvalid ) { + try { + $(this.findLastActive() || this.errorList.length && this.errorList[0].element || []) + .filter(":visible") + .focus() + // manually trigger focusin event; without it, focusin handler isn't called, findLastActive won't have anything to find + .trigger("focusin"); + } catch(e) { + // ignore IE throwing errors when focusing hidden elements + } + } + }, + + findLastActive: function() { + var lastActive = this.lastActive; + return lastActive && $.grep(this.errorList, function( n ) { + return n.element.name === lastActive.name; + }).length === 1 && lastActive; + }, + + elements: function() { + var validator = this, + rulesCache = {}; + + // select all valid inputs inside the form (no submit or reset buttons) + return $(this.currentForm) + .find("input, select, textarea") + .not(":submit, :reset, :image, [disabled]") + .not( this.settings.ignore ) + .filter(function() { + if ( !this.name ) { + if ( window.console ) { + console.error( "%o has no name assigned", this ); + } + throw new Error( "Failed to validate, found an element with no name assigned. See console for element reference." ); + } + + // select only the first element for each name, and only those with rules specified + if ( this.name in rulesCache || !validator.objectLength($(this).rules()) ) { + return false; + } + + rulesCache[this.name] = true; + return true; + }); + }, + + clean: function( selector ) { + return $(selector)[0]; + }, + + errors: function() { + var errorClass = this.settings.errorClass.replace(" ", "."); + return $(this.settings.errorElement + "." + errorClass, this.errorContext); + }, + + reset: function() { + this.successList = []; + this.errorList = []; + this.errorMap = {}; + this.toShow = $([]); + this.toHide = $([]); + this.currentElements = $([]); + }, + + prepareForm: function() { + this.reset(); + this.toHide = this.errors().add( this.containers ); + }, + + prepareElement: function( element ) { + this.reset(); + this.toHide = this.errorsFor(element); + }, + + elementValue: function( element ) { + var type = $(element).attr("type"), + val = $(element).val(); + + if ( type === "radio" || type === "checkbox" ) { + return $("input[name='" + $(element).attr("name") + "']:checked").val(); + } + + if ( typeof val === "string" ) { + return val.replace(/\r/g, ""); + } + return val; + }, + + check: function( element ) { + element = this.validationTargetFor( this.clean( element ) ); + + var rules = $(element).rules(); + var dependencyMismatch = false; + var val = this.elementValue(element); + var result; + + for (var method in rules ) { + var rule = { method: method, parameters: rules[method] }; + try { + + result = $.validator.methods[method].call( this, val, element, rule.parameters ); + + // if a method indicates that the field is optional and therefore valid, + // don't mark it as valid when there are no other rules + if ( result === "dependency-mismatch" ) { + dependencyMismatch = true; + continue; + } + dependencyMismatch = false; + + if ( result === "pending" ) { + this.toHide = this.toHide.not( this.errorsFor(element) ); + return; + } + + if ( !result ) { + this.formatAndAdd( element, rule ); + return false; + } + } catch(e) { + if ( this.settings.debug && window.console ) { + console.log( "Exception occured when checking element " + element.id + ", check the '" + rule.method + "' method.", e ); + } + throw e; + } + } + if ( dependencyMismatch ) { + return; + } + if ( this.objectLength(rules) ) { + this.successList.push(element); + } + return true; + }, + + // return the custom message for the given element and validation method + // specified in the element's HTML5 data attribute + customDataMessage: function( element, method ) { + return $(element).data("msg-" + method.toLowerCase()) || (element.attributes && $(element).attr("data-msg-" + method.toLowerCase())); + }, + + // return the custom message for the given element name and validation method + customMessage: function( name, method ) { + var m = this.settings.messages[name]; + return m && (m.constructor === String ? m : m[method]); + }, + + // return the first defined argument, allowing empty strings + findDefined: function() { + for(var i = 0; i < arguments.length; i++) { + if ( arguments[i] !== undefined ) { + return arguments[i]; + } + } + return undefined; + }, + + defaultMessage: function( element, method ) { + return this.findDefined( + this.customMessage( element.name, method ), + this.customDataMessage( element, method ), + // title is never undefined, so handle empty string as undefined + !this.settings.ignoreTitle && element.title || undefined, + $.validator.messages[method], + "Warning: No message defined for " + element.name + "" + ); + }, + + formatAndAdd: function( element, rule ) { + var message = this.defaultMessage( element, rule.method ), + theregex = /\$?\{(\d+)\}/g; + if ( typeof message === "function" ) { + message = message.call(this, rule.parameters, element); + } else if (theregex.test(message)) { + message = $.validator.format(message.replace(theregex, "{$1}"), rule.parameters); + } + this.errorList.push({ + message: message, + element: element + }); + + this.errorMap[element.name] = message; + this.submitted[element.name] = message; + }, + + addWrapper: function( toToggle ) { + if ( this.settings.wrapper ) { + toToggle = toToggle.add( toToggle.parent( this.settings.wrapper ) ); + } + return toToggle; + }, + + defaultShowErrors: function() { + var i, elements; + for ( i = 0; this.errorList[i]; i++ ) { + var error = this.errorList[i]; + if ( this.settings.highlight ) { + this.settings.highlight.call( this, error.element, this.settings.errorClass, this.settings.validClass ); + } + this.showLabel( error.element, error.message ); + } + if ( this.errorList.length ) { + this.toShow = this.toShow.add( this.containers ); + } + if ( this.settings.success ) { + for ( i = 0; this.successList[i]; i++ ) { + this.showLabel( this.successList[i] ); + } + } + if ( this.settings.unhighlight ) { + for ( i = 0, elements = this.validElements(); elements[i]; i++ ) { + this.settings.unhighlight.call( this, elements[i], this.settings.errorClass, this.settings.validClass ); + } + } + this.toHide = this.toHide.not( this.toShow ); + this.hideErrors(); + this.addWrapper( this.toShow ).show(); + }, + + validElements: function() { + return this.currentElements.not(this.invalidElements()); + }, + + invalidElements: function() { + return $(this.errorList).map(function() { + return this.element; + }); + }, + + showLabel: function( element, message ) { + var label = this.errorsFor( element ); + if ( label.length ) { + // refresh error/success class + label.removeClass( this.settings.validClass ).addClass( this.settings.errorClass ); + + // check if we have a generated label, replace the message then + if ( label.attr("generated") ) { + label.html(message); + } + } else { + // create label + label = $("<" + this.settings.errorElement + "/>") + .attr({"for": this.idOrName(element), generated: true}) + .addClass(this.settings.errorClass) + .html(message || ""); + if ( this.settings.wrapper ) { + // make sure the element is visible, even in IE + // actually showing the wrapped element is handled elsewhere + label = label.hide().show().wrap("<" + this.settings.wrapper + "/>").parent(); + } + if ( !this.labelContainer.append(label).length ) { + if ( this.settings.errorPlacement ) { + this.settings.errorPlacement(label, $(element) ); + } else { + label.insertAfter(element); + } + } + } + if ( !message && this.settings.success ) { + label.text(""); + if ( typeof this.settings.success === "string" ) { + label.addClass( this.settings.success ); + } else { + this.settings.success( label, element ); + } + } + this.toShow = this.toShow.add(label); + }, + + errorsFor: function( element ) { + var name = this.idOrName(element); + return this.errors().filter(function() { + return $(this).attr("for") === name; + }); + }, + + idOrName: function( element ) { + return this.groups[element.name] || (this.checkable(element) ? element.name : element.id || element.name); + }, + + validationTargetFor: function( element ) { + // if radio/checkbox, validate first element in group instead + if ( this.checkable(element) ) { + element = this.findByName( element.name ).not(this.settings.ignore)[0]; + } + return element; + }, + + checkable: function( element ) { + return (/radio|checkbox/i).test(element.type); + }, + + findByName: function( name ) { + return $(this.currentForm).find("[name='" + name + "']"); + }, + + getLength: function( value, element ) { + switch( element.nodeName.toLowerCase() ) { + case "select": + return $("option:selected", element).length; + case "input": + if ( this.checkable( element) ) { + return this.findByName(element.name).filter(":checked").length; + } + } + return value.length; + }, + + depend: function( param, element ) { + return this.dependTypes[typeof param] ? this.dependTypes[typeof param](param, element) : true; + }, + + dependTypes: { + "boolean": function( param, element ) { + return param; + }, + "string": function( param, element ) { + return !!$(param, element.form).length; + }, + "function": function( param, element ) { + return param(element); + } + }, + + optional: function( element ) { + var val = this.elementValue(element); + return !$.validator.methods.required.call(this, val, element) && "dependency-mismatch"; + }, + + startRequest: function( element ) { + if ( !this.pending[element.name] ) { + this.pendingRequest++; + this.pending[element.name] = true; + } + }, + + stopRequest: function( element, valid ) { + this.pendingRequest--; + // sometimes synchronization fails, make sure pendingRequest is never < 0 + if ( this.pendingRequest < 0 ) { + this.pendingRequest = 0; + } + delete this.pending[element.name]; + if ( valid && this.pendingRequest === 0 && this.formSubmitted && this.form() ) { + $(this.currentForm).submit(); + this.formSubmitted = false; + } else if (!valid && this.pendingRequest === 0 && this.formSubmitted) { + $(this.currentForm).triggerHandler("invalid-form", [this]); + this.formSubmitted = false; + } + }, + + previousValue: function( element ) { + return $.data(element, "previousValue") || $.data(element, "previousValue", { + old: null, + valid: true, + message: this.defaultMessage( element, "remote" ) + }); + } + + }, + + classRuleSettings: { + required: {required: true}, + email: {email: true}, + url: {url: true}, + date: {date: true}, + dateISO: {dateISO: true}, + number: {number: true}, + digits: {digits: true}, + creditcard: {creditcard: true} + }, + + addClassRules: function( className, rules ) { + if ( className.constructor === String ) { + this.classRuleSettings[className] = rules; + } else { + $.extend(this.classRuleSettings, className); + } + }, + + classRules: function( element ) { + var rules = {}; + var classes = $(element).attr("class"); + if ( classes ) { + $.each(classes.split(" "), function() { + if ( this in $.validator.classRuleSettings ) { + $.extend(rules, $.validator.classRuleSettings[this]); + } + }); + } + return rules; + }, + + attributeRules: function( element ) { + var rules = {}; + var $element = $(element); + + for (var method in $.validator.methods) { + var value; + + // support for in both html5 and older browsers + if ( method === "required" ) { + value = $element.get(0).getAttribute(method); + // Some browsers return an empty string for the required attribute + // and non-HTML5 browsers might have required="" markup + if ( value === "" ) { + value = true; + } + // force non-HTML5 browsers to return bool + value = !!value; + } else { + value = $element.attr(method); + } + + if ( value ) { + rules[method] = value; + } else if ( $element[0].getAttribute("type") === method ) { + rules[method] = true; + } + } + + // maxlength may be returned as -1, 2147483647 (IE) and 524288 (safari) for text inputs + if ( rules.maxlength && /-1|2147483647|524288/.test(rules.maxlength) ) { + delete rules.maxlength; + } + + return rules; + }, + + dataRules: function( element ) { + var method, value, + rules = {}, $element = $(element); + for (method in $.validator.methods) { + value = $element.data("rule-" + method.toLowerCase()); + if ( value !== undefined ) { + rules[method] = value; + } + } + return rules; + }, + + staticRules: function( element ) { + var rules = {}; + var validator = $.data(element.form, "validator"); + if ( validator.settings.rules ) { + rules = $.validator.normalizeRule(validator.settings.rules[element.name]) || {}; + } + return rules; + }, + + normalizeRules: function( rules, element ) { + // handle dependency check + $.each(rules, function( prop, val ) { + // ignore rule when param is explicitly false, eg. required:false + if ( val === false ) { + delete rules[prop]; + return; + } + if ( val.param || val.depends ) { + var keepRule = true; + switch (typeof val.depends) { + case "string": + keepRule = !!$(val.depends, element.form).length; + break; + case "function": + keepRule = val.depends.call(element, element); + break; + } + if ( keepRule ) { + rules[prop] = val.param !== undefined ? val.param : true; + } else { + delete rules[prop]; + } + } + }); + + // evaluate parameters + $.each(rules, function( rule, parameter ) { + rules[rule] = $.isFunction(parameter) ? parameter(element) : parameter; + }); + + // clean number parameters + $.each(["minlength", "maxlength", "min", "max"], function() { + if ( rules[this] ) { + rules[this] = Number(rules[this]); + } + }); + $.each(["rangelength", "range"], function() { + var parts; + if ( rules[this] ) { + if ( $.isArray(rules[this]) ) { + rules[this] = [Number(rules[this][0]), Number(rules[this][1])]; + } else if ( typeof rules[this] === "string" ) { + parts = rules[this].split(/[\s,]+/); + rules[this] = [Number(parts[0]), Number(parts[1])]; + } + } + }); + + if ( $.validator.autoCreateRanges ) { + // auto-create ranges + if ( rules.min && rules.max ) { + rules.range = [rules.min, rules.max]; + delete rules.min; + delete rules.max; + } + if ( rules.minlength && rules.maxlength ) { + rules.rangelength = [rules.minlength, rules.maxlength]; + delete rules.minlength; + delete rules.maxlength; + } + } + + return rules; + }, + + // Converts a simple string to a {string: true} rule, e.g., "required" to {required:true} + normalizeRule: function( data ) { + if ( typeof data === "string" ) { + var transformed = {}; + $.each(data.split(/\s/), function() { + transformed[this] = true; + }); + data = transformed; + } + return data; + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/addMethod + addMethod: function( name, method, message ) { + $.validator.methods[name] = method; + $.validator.messages[name] = message !== undefined ? message : $.validator.messages[name]; + if ( method.length < 3 ) { + $.validator.addClassRules(name, $.validator.normalizeRule(name)); + } + }, + + methods: { + + // http://docs.jquery.com/Plugins/Validation/Methods/required + required: function( value, element, param ) { + // check if dependency is met + if ( !this.depend(param, element) ) { + return "dependency-mismatch"; + } + if ( element.nodeName.toLowerCase() === "select" ) { + // could be an array for select-multiple or a string, both are fine this way + var val = $(element).val(); + return val && val.length > 0; + } + if ( this.checkable(element) ) { + return this.getLength(value, element) > 0; + } + return $.trim(value).length > 0; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/remote + remote: function( value, element, param ) { + if ( this.optional(element) ) { + return "dependency-mismatch"; + } + + var previous = this.previousValue(element); + if (!this.settings.messages[element.name] ) { + this.settings.messages[element.name] = {}; + } + previous.originalMessage = this.settings.messages[element.name].remote; + this.settings.messages[element.name].remote = previous.message; + + param = typeof param === "string" && {url:param} || param; + + if ( previous.old === value ) { + return previous.valid; + } + + previous.old = value; + var validator = this; + this.startRequest(element); + var data = {}; + data[element.name] = value; + $.ajax($.extend(true, { + url: param, + mode: "abort", + port: "validate" + element.name, + dataType: "json", + data: data, + success: function( response ) { + validator.settings.messages[element.name].remote = previous.originalMessage; + var valid = response === true || response === "true"; + if ( valid ) { + var submitted = validator.formSubmitted; + validator.prepareElement(element); + validator.formSubmitted = submitted; + validator.successList.push(element); + delete validator.invalid[element.name]; + validator.showErrors(); + } else { + var errors = {}; + var message = response || validator.defaultMessage( element, "remote" ); + errors[element.name] = previous.message = $.isFunction(message) ? message(value) : message; + validator.invalid[element.name] = true; + validator.showErrors(errors); + } + previous.valid = valid; + validator.stopRequest(element, valid); + } + }, param)); + return "pending"; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/minlength + minlength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || length >= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/maxlength + maxlength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || length <= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/rangelength + rangelength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || ( length >= param[0] && length <= param[1] ); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/min + min: function( value, element, param ) { + return this.optional(element) || value >= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/max + max: function( value, element, param ) { + return this.optional(element) || value <= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/range + range: function( value, element, param ) { + return this.optional(element) || ( value >= param[0] && value <= param[1] ); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/email + email: function( value, element ) { + // contributed by Scott Gonzalez: http://projects.scottsplayground.com/email_address_validation/ + return this.optional(element) || /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/url + url: function( value, element ) { + // contributed by Scott Gonzalez: http://projects.scottsplayground.com/iri/ + return this.optional(element) || /^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/date + date: function( value, element ) { + return this.optional(element) || !/Invalid|NaN/.test(new Date(value).toString()); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/dateISO + dateISO: function( value, element ) { + return this.optional(element) || /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/number + number: function( value, element ) { + return this.optional(element) || /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/digits + digits: function( value, element ) { + return this.optional(element) || /^\d+$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/creditcard + // based on http://en.wikipedia.org/wiki/Luhn + creditcard: function( value, element ) { + if ( this.optional(element) ) { + return "dependency-mismatch"; + } + // accept only spaces, digits and dashes + if ( /[^0-9 \-]+/.test(value) ) { + return false; + } + var nCheck = 0, + nDigit = 0, + bEven = false; + + value = value.replace(/\D/g, ""); + + for (var n = value.length - 1; n >= 0; n--) { + var cDigit = value.charAt(n); + nDigit = parseInt(cDigit, 10); + if ( bEven ) { + if ( (nDigit *= 2) > 9 ) { + nDigit -= 9; + } + } + nCheck += nDigit; + bEven = !bEven; + } + + return (nCheck % 10) === 0; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/equalTo + equalTo: function( value, element, param ) { + // bind to the blur event of the target in order to revalidate whenever the target field is updated + // TODO find a way to bind the event just once, avoiding the unbind-rebind overhead + var target = $(param); + if ( this.settings.onfocusout ) { + target.unbind(".validate-equalTo").bind("blur.validate-equalTo", function() { + $(element).valid(); + }); + } + return value === target.val(); + } + + } + +}); + +// deprecated, use $.validator.format instead +$.format = $.validator.format; + +}(jQuery)); + +// ajax mode: abort +// usage: $.ajax({ mode: "abort"[, port: "uniqueport"]}); +// if mode:"abort" is used, the previous request on that port (port can be undefined) is aborted via XMLHttpRequest.abort() +(function($) { + var pendingRequests = {}; + // Use a prefilter if available (1.5+) + if ( $.ajaxPrefilter ) { + $.ajaxPrefilter(function( settings, _, xhr ) { + var port = settings.port; + if ( settings.mode === "abort" ) { + if ( pendingRequests[port] ) { + pendingRequests[port].abort(); + } + pendingRequests[port] = xhr; + } + }); + } else { + // Proxy ajax + var ajax = $.ajax; + $.ajax = function( settings ) { + var mode = ( "mode" in settings ? settings : $.ajaxSettings ).mode, + port = ( "port" in settings ? settings : $.ajaxSettings ).port; + if ( mode === "abort" ) { + if ( pendingRequests[port] ) { + pendingRequests[port].abort(); + } + return (pendingRequests[port] = ajax.apply(this, arguments)); + } + return ajax.apply(this, arguments); + }; + } +}(jQuery)); + +// provides delegate(type: String, delegate: Selector, handler: Callback) plugin for easier event delegation +// handler is only called when $(event.target).is(delegate), in the scope of the jquery-object for event.target +(function($) { + $.extend($.fn, { + validateDelegate: function( delegate, type, handler ) { + return this.bind(type, function( event ) { + var target = $(event.target); + if ( target.is(delegate) ) { + return handler.apply(target, arguments); + } + }); + } + }); +}(jQuery)); diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/type-view.hbs b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/type-view.hbs new file mode 100644 index 000000000..6548564fa --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/type-view.hbs @@ -0,0 +1,337 @@ +

+

Android Sense

+
+
+ +
+ +
+ +
+ +
+ +
+

What it Does

+
+

Connect an Android device to WSO2 IoT Server and visualize sensor + data.

+
+

What You Need

+
+

You should have an Android Device to get started.

+
    +
  • + STEP 01 +    Go ahead and click [Enroll Device]. +
  • +
  • + STEP 02 +    You can either scan the QR code or directly download Android agent. +
  • +
  • + STEP 03 +    Install Android agent into your Android Device. +
  • +
  • STEP 04 +    Proceed to [Prepare] section. +
+
+ + Enroll Device +

+ + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+

+
+ +
+

Prepare

+
+
    +
  • + 01 +    Connect Android device to network. +
  • +
  • + 02 +    Start Android Sense app in your device. +
  • +
  • + 03 +    Fill login form with the credentials. + (Use server URL as [ https://<WSO2_IoT_SERVER_HOST>:< + HTTPS_SERVER_PORT> ] and click on + Register Device button.) +
  • +
  • + + + +
  • + +
  • + 04 +    Once the device is enrolled, click on [+] button to + select the sensors. + + + +
  • +
  • + 05 +    Click on [Publish data] button to publish sensor readings to + IoT server. + + + +
  • +
+
+
+ +
+

Try Out

+
+
    +
  • + + + + +    You can view all your connected devices + [Device Management] page. +
  • +
  • + + + + +    You can select any one of your connected devices for available + operations and Real-Time data monitoring. +
  • +
  • + + + +    For historical analytics of sensor data navigate to device + analytics page. +
  • +
+
+

Click on the image to zoom

+
+ + + +
+
+ + + + + +{{#zone "bottomJs"}} + {{js "/js/download.js"}} + + {{js "/js/jquery.validate.js"}} +{{/zone}} diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/type-view.js b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/type-view.js new file mode 100644 index 000000000..6f277602a --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/type-view.js @@ -0,0 +1,26 @@ +/* + * 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. + */ + +function onRequest(context){ + var viewModel = {}; + var process = require("process"); + var serverIP = process.getProperty("carbon.local.ip"); + var serverPort = process.getProperty("carbon.http.port"); + viewModel.enrollmentURL = "http://"+serverIP+":"+serverPort+"/android_sense_mgt/manager/device/android_sense/download/"; + return viewModel; +} \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/type-view.json b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/type-view.json new file mode 100644 index 000000000..9eecd8f5b --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/org.wso2.carbon.device.mgt.iot.androidsense.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.android_sense.type-view/type-view.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0" +} \ No newline at end of file diff --git a/components/iot-plugins/androidsense-plugin/pom.xml b/components/iot-plugins/androidsense-plugin/pom.xml new file mode 100644 index 000000000..59a343d4c --- /dev/null +++ b/components/iot-plugins/androidsense-plugin/pom.xml @@ -0,0 +1,62 @@ + + + + + + + org.wso2.carbon.devicemgt-plugins + iot-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + androidsense-plugin + pom + WSO2 Carbon - Arduino Plugin + http://wso2.org + + + org.wso2.carbon.device.mgt.iot.androidsense.analytics + org.wso2.carbon.device.mgt.iot.androidsense.controller.api + org.wso2.carbon.device.mgt.iot.androidsense.manager.api + org.wso2.carbon.device.mgt.iot.androidsense.plugin + org.wso2.carbon.device.mgt.iot.androidsense.ui + + + + + + + org.apache.felix + maven-scr-plugin + 1.7.2 + + + generate-scr-scrdescriptor + + scr + + + + + + + + diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/build.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/build.xml new file mode 100644 index 000000000..a877b1141 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/build.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/pom.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/pom.xml new file mode 100644 index 000000000..5591c22e5 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/pom.xml @@ -0,0 +1,91 @@ + + + + + + + arduino-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.arduino.analytics + WSO2 Carbon - IoT Server Arduino Analytics capp + pom + + + + + maven-clean-plugin + 2.4.1 + + + auto-clean + initialize + + clean + + + + + + maven-antrun-plugin + 1.7 + + + process-resources + + + + + + + run + + + + + + maven-assembly-plugin + 2.5.5 + + ${project.artifactId}-${carbon.device.mgt.version} + false + + src/assembly/src.xml + + + + + create-archive + package + + single + + + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/assembly/src.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/assembly/src.xml new file mode 100644 index 000000000..a5a375010 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/assembly/src.xml @@ -0,0 +1,36 @@ + + + + src + + zip + + false + ${basedir}/src + + + ${basedir}/target/carbonapps + / + true + + + \ No newline at end of file diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/EventReceiver_temperature.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/EventReceiver_temperature.xml new file mode 100644 index 000000000..28b710c27 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/EventReceiver_temperature.xml @@ -0,0 +1,26 @@ + + + + + + false + + + + diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/artifact.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/artifact.xml new file mode 100644 index 000000000..25df56734 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + EventReceiver_temperature.xml + diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/artifact.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/artifact.xml new file mode 100644 index 000000000..ccfb3b314 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + org_wso2_iot_devices_temperature.xml + diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/org_wso2_iot_devices_temperature.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/org_wso2_iot_devices_temperature.xml new file mode 100644 index 000000000..d06f73b14 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/org_wso2_iot_devices_temperature.xml @@ -0,0 +1,62 @@ + + + + + + org.wso2.iot.devices.temperature:1.0.0 + + EVENT_STORE + + + meta_owner + true + true + false + STRING + + + meta_deviceType + true + true + false + STRING + + + meta_deviceId + true + true + false + STRING + + + meta_time + true + true + false + LONG + + + temperature + false + false + false + FLOAT + + + \ No newline at end of file diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/artifact.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/artifact.xml new file mode 100644 index 000000000..27ec69702 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/artifact.xml @@ -0,0 +1,23 @@ + + + + + org.wso2.iot.devices.temperature_1.0.0.json + + diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/org.wso2.iot.devices.temperature_1.0.0.json b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/org.wso2.iot.devices.temperature_1.0.0.json new file mode 100644 index 000000000..5d94b9821 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/org.wso2.iot.devices.temperature_1.0.0.json @@ -0,0 +1,20 @@ +{ + "name": "org.wso2.iot.devices.temperature", + "version": "1.0.0", + "nickName": "Temperature Data", + "description": "Temperature data received from the Device", + "metaData": [ + {"name":"owner","type":"STRING"}, + {"name":"deviceType","type":"STRING"}, + {"name":"deviceId","type":"STRING"}, + {"name":"time","type":"LONG"} + ], + "payloadData": [ + { + "name": "temperature","type": "FLOAT" + } + ] +} + + + diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/Temperature_Sensor_Script.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/Temperature_Sensor_Script.xml new file mode 100644 index 000000000..41938dd4f --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/Temperature_Sensor_Script.xml @@ -0,0 +1,31 @@ + + + + + IoTServer_Sensor_Script + + 0 * * * * ? + diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/artifact.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/artifact.xml new file mode 100644 index 000000000..9b4228e30 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + Temperature_Sensor_Script.xml + diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/artifacts.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/artifacts.xml new file mode 100644 index 000000000..c4580f909 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.analytics/src/main/resources/carbonapps/Temperature_Sensor/artifacts.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/pom.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/pom.xml new file mode 100644 index 000000000..9ef464d1a --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/pom.xml @@ -0,0 +1,170 @@ + + + + + + arduino-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl + war + WSO2 Carbon - IoT Server Arduino Controller API + WSO2 Carbon - Arduino Service Controller API Implementation + http://wso2.org + + + + + org.wso2.carbon + org.wso2.carbon.utils + provided + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.core + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.analytics + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + provided + + + org.apache.cxf + 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 + provided + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.arduino.plugin.impl + provided + + + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-jaxrs + + + javax + javaee-web-api + provided + + + javax.ws.rs + jsr311-api + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.annotations + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.extensions + provided + + + + + + + + maven-compiler-plugin + + UTF-8 + ${wso2.maven.compiler.source} + ${wso2.maven.compiler.target} + + + + maven-war-plugin + + arduino + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/ArduinoControllerService.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/ArduinoControllerService.java new file mode 100644 index 000000000..7c5be93c0 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/ArduinoControllerService.java @@ -0,0 +1,300 @@ +/* + * 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.device.mgt.iot.arduino.controller.service.impl; + +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.apimgt.annotations.api.API; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.device.mgt.analytics.common.AnalyticsDataRecord; +import org.wso2.carbon.device.mgt.analytics.exception.DeviceManagementAnalyticsException; +import org.wso2.carbon.device.mgt.analytics.service.DeviceAnalyticsService; +import org.wso2.carbon.device.mgt.extensions.feature.mgt.annotations.DeviceType; +import org.wso2.carbon.device.mgt.extensions.feature.mgt.annotations.Feature; +import org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl.dto.DeviceData; +import org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl.dto.SensorData; +import org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl.transport.ArduinoMQTTConnector; +import org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl.util.ArduinoServiceUtils; +import org.wso2.carbon.device.mgt.iot.arduino.plugin.constants.ArduinoConstants; +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 javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +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; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentHashMap; + + +@API( name="arduino", version="1.0.0", context="/arduino", tags = {"arduino"}) +@DeviceType( value = "arduino") +public class ArduinoControllerService { + + private static Log log = LogFactory.getLog(ArduinoControllerService.class); + private static Map> replyMsgQueue = new HashMap<>(); + private static Map> internalControlsQueue = new HashMap<>(); + @Context //injected response proxy supporting multiple thread + private HttpServletResponse response; + private ArduinoMQTTConnector arduinoMQTTConnector; + private ConcurrentHashMap deviceToIpMap = new ConcurrentHashMap<>(); + + /** + * @return the queue containing all the MQTT reply messages from all Arduinos communicating to this service + */ + public static Map> getReplyMsgQueue() { + return replyMsgQueue; + } + + /** + * @return the queue containing all the MQTT controls received to be sent to any Arduinos connected to this server + */ + public static Map> getInternalControlsQueue() { + return internalControlsQueue; + } + + private boolean waitForServerStartup() { + while (!IoTServerStartupListener.isServerReady()) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + return true; + } + } + return false; + } + + /** + * @return the "ArduinoMQTTConnector" object of this ArduinoControllerService instance + */ + @SuppressWarnings("unused") + public ArduinoMQTTConnector getArduinoMQTTConnector() { + return arduinoMQTTConnector; + } + + /** + * @param arduinoMQTTConnector an object of type "ArduinoMQTTConnector" specific for this ArduinoControllerService + */ + @SuppressWarnings("unused") + public void setArduinoMQTTConnector(final ArduinoMQTTConnector arduinoMQTTConnector) { + Runnable connector = new Runnable() { + public void run() { + if (waitForServerStartup()) { + return; + } + ArduinoControllerService.this.arduinoMQTTConnector = arduinoMQTTConnector; + if (MqttConfig.getInstance().isEnabled()) { + arduinoMQTTConnector.connect(); + } else { + log.warn("MQTT disabled in 'devicemgt-config.xml'. Hence, ArduinoMQTTConnector not started."); + } + } + }; + Thread connectorThread = new Thread(connector); + connectorThread.setDaemon(true); + connectorThread.start(); + } + + @Path("controller/register/device/{deviceId}/{ip}/{port}") + @POST + public String registerDeviceIP(@PathParam("deviceId") String deviceId, @PathParam("ip") String deviceIP, + @PathParam("port") String devicePort, @Context HttpServletResponse response, + @Context HttpServletRequest request) { + String result; + if (log.isDebugEnabled()) { + log.debug("Got register call from IP: " + deviceIP + " for Device ID: " + deviceId + " of owner: "); + } + String deviceHttpEndpoint = deviceIP + ":" + devicePort; + deviceToIpMap.put(deviceId, deviceHttpEndpoint); + result = "Device-IP Registered"; + response.setStatus(Response.Status.OK.getStatusCode()); + if (log.isDebugEnabled()) { + log.debug(result); + } + return result; + } + + @Path("controller/device/{deviceId}/bulb") + @POST + @Feature(code = "bulb", name = "Control Bulb", type = "operation", description = "Control Bulb on Arduino Uno") + public void switchBulb(@PathParam("deviceId") String deviceId, @QueryParam("protocol") String protocol, + @FormParam("state") String state, @Context HttpServletResponse response) { + + LinkedList deviceControlList = internalControlsQueue.get(deviceId); + String operation = "BULB:" + state.toUpperCase(); + log.info(operation); + if (deviceControlList == null) { + deviceControlList = new LinkedList<>(); + deviceControlList.add(operation); + internalControlsQueue.put(deviceId, deviceControlList); + } else { + deviceControlList.add(operation); + } + response.setStatus(Response.Status.OK.getStatusCode()); + } + + @Path("controller/device/{deviceId}/temperature") + @GET + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Feature( code="temperature", name="Temperature", type="monitor", description="Request temperature reading from Arduino agent") + public SensorRecord requestTemperature(@PathParam("deviceId") String deviceId, @QueryParam("protocol") String protocol, + @Context HttpServletResponse response) { + SensorRecord sensorRecord = null; + try { + sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, + ArduinoConstants.SENSOR_TEMPERATURE); + } catch (DeviceControllerException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } + response.setStatus(Response.Status.OK.getStatusCode()); + return sensorRecord; + } + + @Path("controller/sensor") + @POST + @Consumes(MediaType.APPLICATION_JSON) + public void pushData(DeviceData dataMsg, @Context HttpServletResponse response) { + String owner = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUsername(); + String deviceId = dataMsg.deviceId; + float pinData = dataMsg.value; + SensorDataManager.getInstance().setSensorRecord(deviceId, ArduinoConstants.SENSOR_TEMPERATURE, + String.valueOf(pinData), + Calendar.getInstance().getTimeInMillis()); + if (!ArduinoServiceUtils.publishToDAS(dataMsg.deviceId, dataMsg.value)) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.warn("An error occured whilst trying to publish pin data of Arduino with ID [" + + deviceId + "] of owner [" + owner + "]"); + } + } + + @Path("controller/device/{deviceId}/controls") + @GET + public String readControls(@PathParam("deviceId") String deviceId, @QueryParam("protocol") String protocol, + @Context HttpServletResponse response) { + String result; + LinkedList deviceControlList = internalControlsQueue.get(deviceId); + String owner = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUsername(); + if (deviceControlList == null) { + result = "No controls have been set for device " + deviceId + " of owner " + owner; + response.setStatus(HttpStatus.SC_NO_CONTENT); + } else { + try { + result = deviceControlList.remove(); + response.setStatus(HttpStatus.SC_ACCEPTED); + } catch (NoSuchElementException ex) { + result = "There are no more controls for device " + deviceId + " of owner " + owner; + response.setStatus(HttpStatus.SC_NO_CONTENT); + } + } + if (log.isDebugEnabled()) { + log.debug(result); + } + return result; + } + + @Path("controller/temperature") + @POST + @Consumes(MediaType.APPLICATION_JSON) + public void pushTemperatureData(final DeviceData dataMsg, @Context HttpServletResponse response, + @Context HttpServletRequest request) { + String owner = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUsername(); + String deviceId = dataMsg.deviceId; + float temperature = dataMsg.value; + SensorDataManager.getInstance().setSensorRecord(deviceId, ArduinoConstants.SENSOR_TEMPERATURE, + String.valueOf(temperature), + Calendar.getInstance().getTimeInMillis()); + if (!ArduinoServiceUtils.publishToDAS(dataMsg.deviceId, dataMsg.value)) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.warn("An error occured whilst trying to publish temperature data of Arduino with ID [" + deviceId + + "] of owner [" + owner + "]"); + } + } + + /** + * Retreive Sensor data for the device type + */ + @Path("controller/stats/device/{deviceId}/sensors/temperature") + @GET + @Consumes("application/json") + @Produces("application/json") + public SensorData[] getArduinoTemperatureStats(@PathParam("deviceId") String deviceId, + @QueryParam("from") long from, + @QueryParam("to") 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; + 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); + } + return sensorDatas.toArray(new SensorData[sensorDatas.size()]); + } catch (DeviceManagementAnalyticsException e) { + String errorMsg = "Error on retrieving stats on table " + sensorTableName + " with query " + query; + log.error(errorMsg); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return sensorDatas.toArray(new SensorData[sensorDatas.size()]); + } + } + +} diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/dto/DeviceData.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/dto/DeviceData.java new file mode 100644 index 000000000..00de218bd --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/dto/DeviceData.java @@ -0,0 +1,35 @@ +/* + * 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.device.mgt.iot.arduino.controller.service.impl.dto; + +import org.codehaus.jackson.annotate.JsonIgnoreProperties; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement + +@JsonIgnoreProperties(ignoreUnknown = true) +public class DeviceData { + @XmlElement(required = true) public String deviceId; + @XmlElement(required = true) public String reply; + @XmlElement public Long time; + @XmlElement public String key; + @XmlElement public float value; +} diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/dto/SensorData.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/dto/SensorData.java new file mode 100644 index 000000000..22a58f0f8 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/dto/SensorData.java @@ -0,0 +1,44 @@ +package org.wso2.carbon.device.mgt.iot.arduino.controller.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.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/exception/ArduinoException.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/exception/ArduinoException.java new file mode 100644 index 000000000..82766a0db --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/exception/ArduinoException.java @@ -0,0 +1,31 @@ +/* + * 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.device.mgt.iot.arduino.controller.service.impl.exception; + +public class ArduinoException extends Exception { + private static final long serialVersionUID = 118512086957330189L; + + public ArduinoException(String errorMessage) { + super(errorMessage); + } + + public ArduinoException(String errorMessage, Throwable throwable) { + super(errorMessage, throwable); + } +} diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/transport/ArduinoMQTTConnector.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/transport/ArduinoMQTTConnector.java new file mode 100644 index 000000000..246fb7903 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/transport/ArduinoMQTTConnector.java @@ -0,0 +1,193 @@ +/* + * 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.device.mgt.iot.arduino.controller.service.impl.transport; + +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.device.mgt.iot.arduino.plugin.constants.ArduinoConstants; +import org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl.ArduinoControllerService; +import org.wso2.carbon.device.mgt.iot.config.server.DeviceManagementConfigurationManager; +import org.wso2.carbon.device.mgt.iot.controlqueue.mqtt.MqttConfig; +import org.wso2.carbon.device.mgt.iot.transport.TransportHandlerException; +import org.wso2.carbon.device.mgt.iot.transport.mqtt.MQTTTransportHandler; + +import java.io.File; +import java.util.LinkedList; +import java.util.UUID; + +public class ArduinoMQTTConnector extends MQTTTransportHandler { + private static Log log = LogFactory.getLog(ArduinoMQTTConnector.class); + private static final String subscribeTopic = "wso2/" + ArduinoConstants.DEVICE_TYPE + "/#"; + private static final String iotServerSubscriber = UUID.randomUUID().toString().substring(0, 5); + + private static final String MESSAGE_TO_SEND = "IN"; + private static final String MESSAGE_RECEIVED = "OUT"; + + private ArduinoMQTTConnector() { + super(iotServerSubscriber, ArduinoConstants.DEVICE_TYPE, + MqttConfig.getInstance().getMqttQueueEndpoint(), subscribeTopic); + } + + @Override + public void connect() { + Runnable connector = new Runnable() { + public void run() { + while (!isConnected()) { + try { + String brokerUsername = MqttConfig.getInstance().getMqttQueueUsername(); + String brokerPassword = MqttConfig.getInstance().getMqttQueuePassword(); + setUsernameAndPassword(brokerUsername, brokerPassword); + 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); + } + } + } + }; + + + Thread connectorThread = new Thread(connector); + connectorThread.setDaemon(true); + connectorThread.start(); + } + + @Override + public void processIncomingMessage(MqttMessage message, String... messageParams) throws TransportHandlerException { + 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 deviceId = topicParams[3]; + + if (log.isDebugEnabled()) { + log.debug("Received MQTT message for: [DEVICE.ID-" + deviceId + "]"); + } + + int lastIndex = message.toString().lastIndexOf(":"); + String msgContext = message.toString().substring(lastIndex + 1); + + LinkedList deviceControlList; + LinkedList replyMessageList; + + if (msgContext.equals(MESSAGE_TO_SEND) || msgContext.equals(ArduinoConstants.STATE_ON) || msgContext.equals( + ArduinoConstants.STATE_OFF)) { + + if (log.isDebugEnabled()) { + log.debug("Received a control message: "); + log.debug("Control message topic: " + topic); + log.debug("Control message: " + message.toString()); + } + + synchronized (ArduinoControllerService.getInternalControlsQueue()) { + deviceControlList = ArduinoControllerService.getInternalControlsQueue().get(deviceId); + if (deviceControlList == null) { + ArduinoControllerService.getInternalControlsQueue() + .put(deviceId, deviceControlList = new LinkedList()); + } + } + deviceControlList.add(message.toString()); + + } else if (msgContext.equals(MESSAGE_RECEIVED)) { + + if (log.isDebugEnabled()) { + log.debug("Received reply from a device: "); + log.debug("Reply message topic: " + topic); + log.debug("Reply message: " + message.toString().substring(0, lastIndex)); + } + + synchronized (ArduinoControllerService.getReplyMsgQueue()) { + replyMessageList = ArduinoControllerService.getReplyMsgQueue().get(deviceId); + if (replyMessageList == null) { + ArduinoControllerService.getReplyMsgQueue() + .put(deviceId, replyMessageList = new LinkedList<>()); + } + } + replyMessageList.add(message.toString()); + } + } + } + + @Override + public void disconnect() { + Runnable stopConnection = new Runnable() { + public void run() { + while (isConnected()) { + try { + closeConnection(); + } catch (MqttException e) { + if (log.isDebugEnabled()) { + log.warn("Unable to 'STOP' MQTT connection at broker at: " + mqttBrokerEndPoint + + " for device-type - " + ArduinoConstants.DEVICE_TYPE, e); + } + + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException e1) { + log.error("MQTT-Terminator: Thread Sleep Interrupt Exception at device-type - " + + ArduinoConstants.DEVICE_TYPE, e1); + } + } + } + } + }; + + Thread terminatorThread = new Thread(stopConnection); + terminatorThread.setDaemon(true); + terminatorThread.start(); + } + + @Override + public void processIncomingMessage() throws TransportHandlerException { + + } + + @Override + public void processIncomingMessage(MqttMessage message) throws TransportHandlerException { + + } + + @Override + public void publishDeviceData() throws TransportHandlerException { + + } + + @Override + public void publishDeviceData(MqttMessage publishData) throws TransportHandlerException { + + } + + @Override + public void publishDeviceData(String... publishData) throws TransportHandlerException { + + } +} diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/util/ArduinoServiceUtils.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/util/ArduinoServiceUtils.java new file mode 100644 index 000000000..bdba9fef1 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/controller/service/impl/util/ArduinoServiceUtils.java @@ -0,0 +1,217 @@ +/* + * 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.device.mgt.iot.arduino.controller.service.impl.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.concurrent.FutureCallback; +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.exception.DataPublisherConfigurationException; +import org.wso2.carbon.device.mgt.analytics.service.DeviceAnalyticsService; +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.arduino.plugin.constants.ArduinoConstants; +import org.wso2.carbon.device.mgt.iot.sensormgt.SensorDataManager; +import org.wso2.carbon.utils.multitenancy.MultitenantUtils; + +import javax.ws.rs.HttpMethod; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.URL; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; + +public class ArduinoServiceUtils { + private static final Log log = LogFactory.getLog(ArduinoServiceUtils.class); + + //TODO; replace this tenant domain + private static final String SUPER_TENANT = "carbon.super"; + private static final String TEMPERATURE_STREAM_DEFINITION = "org.wso2.iot.devices.temperature"; + + public static String sendCommandViaHTTP(final String deviceHTTPEndpoint, String urlContext, + boolean fireAndForgot) throws DeviceManagementException { + + String responseMsg = ""; + String urlString = ArduinoConstants.URL_PREFIX + deviceHTTPEndpoint + urlContext; + + if (log.isDebugEnabled()) { + log.debug(urlString); + } + + if (!fireAndForgot) { + HttpURLConnection httpConnection = getHttpConnection(urlString); + + try { + httpConnection.setRequestMethod(HttpMethod.GET); + } catch (ProtocolException e) { + String errorMsg = + "Protocol specific error occurred when trying to set method to GET" + + " for:" + urlString; + log.error(errorMsg); + throw new DeviceManagementException(errorMsg, e); + } + + responseMsg = readResponseFromGetRequest(httpConnection); + + } else { + CloseableHttpAsyncClient httpclient = null; + try { + + httpclient = HttpAsyncClients.createDefault(); + httpclient.start(); + HttpGet request = new HttpGet(urlString); + final CountDownLatch latch = new CountDownLatch(1); + Future future = httpclient.execute( + request, new FutureCallback() { + @Override + public void completed(HttpResponse httpResponse) { + latch.countDown(); + } + + @Override + public void failed(Exception e) { + latch.countDown(); + } + + @Override + public void cancelled() { + latch.countDown(); + } + }); + + latch.await(); + + } catch (InterruptedException e) { + if (log.isDebugEnabled()) { + log.debug("Sync Interrupted"); + } + } finally { + try { + if (httpclient != null) { + httpclient.close(); + + } + } catch (IOException e) { + if (log.isDebugEnabled()) { + log.debug("Failed on close"); + } + } + } + } + + return responseMsg; + } + + /* --------------------------------------------------------------------------------------- + Utility methods relevant to creating and sending http requests + --------------------------------------------------------------------------------------- */ + + /* This methods creates and returns a http connection object */ + public static HttpURLConnection getHttpConnection(String urlString) throws DeviceManagementException { + URL connectionUrl = null; + HttpURLConnection httpConnection; + + try { + connectionUrl = new URL(urlString); + httpConnection = (HttpURLConnection) connectionUrl.openConnection(); + } catch (MalformedURLException e) { + String errorMsg = + "Error occured whilst trying to form HTTP-URL from string: " + urlString; + log.error(errorMsg); + throw new DeviceManagementException(errorMsg, e); + } catch (IOException e) { + String errorMsg = "Error occured whilst trying to open a connection to: " + + connectionUrl.toString(); + log.error(errorMsg); + throw new DeviceManagementException(errorMsg, e); + } + + return httpConnection; + } + + /* This methods reads and returns the response from the connection */ + + public static String readResponseFromGetRequest(HttpURLConnection httpConnection) + throws DeviceManagementException { + BufferedReader bufferedReader; + try { + bufferedReader = new BufferedReader(new InputStreamReader( + httpConnection.getInputStream())); + } catch (IOException e) { + String errorMsg = + "There is an issue with connecting the reader to the input stream at: " + + httpConnection.getURL(); + log.error(errorMsg); + throw new DeviceManagementException(errorMsg, e); + } + + String responseLine; + StringBuilder completeResponse = new StringBuilder(); + + try { + while ((responseLine = bufferedReader.readLine()) != null) { + completeResponse.append(responseLine); + } + } catch (IOException e) { + String errorMsg = + "Error occured whilst trying read from the connection stream at: " + + httpConnection.getURL(); + log.error(errorMsg); + throw new DeviceManagementException(errorMsg, e); + } + try { + bufferedReader.close(); + } catch (IOException e) { + log.error( + "Could not succesfully close the bufferedReader to the connection at: " + + httpConnection.getURL()); + } + + return completeResponse.toString(); + } + + public static boolean publishToDAS(String deviceId, float temperature) { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + String owner = ctx.getUsername(); + Object metdaData[] = {owner, ArduinoConstants.DEVICE_TYPE, deviceId, System.currentTimeMillis()}; + Object payloadData[] = {temperature}; + DeviceAnalyticsService deviceAnalyticsService = (DeviceAnalyticsService) ctx + .getOSGiService(DeviceAnalyticsService.class, null); + if (deviceAnalyticsService != null) { + try { + deviceAnalyticsService.publishEvent(TEMPERATURE_STREAM_DEFINITION, "1.0.0", metdaData, + new Object[0], payloadData); + } catch (DataPublisherConfigurationException e) { + return false; + } + return true; + } + return false; + } +} diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/webapp/META-INF/permissions.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/webapp/META-INF/permissions.xml new file mode 100644 index 000000000..f2595154a --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/webapp/META-INF/permissions.xml @@ -0,0 +1,87 @@ + + + + + + + + + + Register Device + /login + /controller/register/device/{deviceId}/{ip}/{port} + POST + + + + get device bulb statjs + /login + /controller/device/{deviceId}/bulb + GET + + + + get device temperature + /login + /controller/device/{deviceId}/temperature + GET + + + + get device temperature + /login + /controller/device/{deviceId}/temperature + GET + + + + get device temperature + /login + /controller/sensor + POST + + + + get controls + /login + /controller/device/{deviceId}/controls + POST + + + + push temperature + /login + /controller/temperature + POST + + + + get temperature + /login + /controller/stats/device/{deviceId}/sensors/temperature + GET + + + \ No newline at end of file diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/webapp/META-INF/webapp-classloading.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/webapp/META-INF/webapp-classloading.xml new file mode 100644 index 000000000..fa4461919 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/webapp/META-INF/webapp-classloading.xml @@ -0,0 +1,33 @@ + + + + + + + + + false + + + CXF,Carbon + diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml new file mode 100644 index 000000000..291443c92 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/webapp/WEB-INF/web.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..c62aa6100 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,62 @@ + + + Arduino + Arduino + + + CXFServlet + org.apache.cxf.transport.servlet.CXFServlet + 1 + + + + + CXFServlet + /* + + + + isAdminService + false + + + doAuthentication + false + + + isSharedWithAllTenants + true + + + providerTenantDomain + carbon.super + + + + managed-api-enabled + true + + + managed-api-owner + admin + + + managed-api-context-template + /arduino/{version} + + + managed-api-application + arduino + + + managed-api-isSecured + true + + + + + diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/pom.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/pom.xml new file mode 100644 index 000000000..9824474d5 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/pom.xml @@ -0,0 +1,180 @@ + + + + + + arduino-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl + war + WSO2 Carbon - IoT Server Arduino ManagerService API + WSO2 Carbon - Arduino ManagerService API Implementation + http://wso2.org + + + + + org.wso2.carbon + org.wso2.carbon.utils + provided + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.core + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.analytics + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + provided + + + org.apache.cxf + 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 + provided + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.arduino.plugin.impl + provided + + + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-jaxrs + + + javax + javaee-web-api + provided + + + javax.ws.rs + jsr311-api + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.annotations + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.webapp.publisher + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.jwt.client.extension + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.application.extension + provided + + + + + + + + maven-compiler-plugin + + UTF-8 + ${wso2.maven.compiler.source} + ${wso2.maven.compiler.target} + + + + maven-war-plugin + + arduino_mgt + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/manager/service/impl/ArduinoManagerService.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/manager/service/impl/ArduinoManagerService.java new file mode 100644 index 000000000..6d5969c58 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/manager/service/impl/ArduinoManagerService.java @@ -0,0 +1,274 @@ +/* + * 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.device.mgt.iot.arduino.manager.service.impl; + +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.common.Device; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.EnrolmentInfo; +import org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl.util.APIUtil; +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.util.ZipArchive; +import org.wso2.carbon.device.mgt.iot.util.ZipUtil; +import org.wso2.carbon.device.mgt.jwt.client.extension.JWTClient; +import org.wso2.carbon.device.mgt.jwt.client.extension.JWTClientManager; +import org.wso2.carbon.device.mgt.jwt.client.extension.dto.AccessTokenInfo; +import org.wso2.carbon.device.mgt.jwt.client.extension.exception.JWTClientException; +import org.wso2.carbon.user.api.UserStoreException; + +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +public class ArduinoManagerService { + + @Context //injected response proxy supporting multiple thread + private HttpServletResponse response; + private static final String KEY_TYPE = "PRODUCTION"; + private static ApiApplicationKey apiApplicationKey; + + @Path("manager/device/{device_id}") + @DELETE + public void removeDevice(@PathParam("device_id") String deviceId, @Context HttpServletResponse response) { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(ArduinoConstants.DEVICE_TYPE); + try { + boolean removed = APIUtil.getDeviceManagementService().disenrollDevice(deviceIdentifier); + if (removed) { + response.setStatus(Response.Status.OK.getStatusCode()); + + } else { + response.setStatus(Response.Status.NOT_ACCEPTABLE.getStatusCode()); + + } + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } + } + + @Path("manager/device/{device_id}") + @PUT + public boolean updateDevice(@PathParam("device_id") String deviceId, @QueryParam("name") String name, + @Context HttpServletResponse response) { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(ArduinoConstants.DEVICE_TYPE); + try { + Device device = APIUtil.getDeviceManagementService().getDevice(deviceIdentifier); + device.setDeviceIdentifier(deviceId); + device.getEnrolmentInfo().setDateOfLastUpdate(new Date().getTime()); + device.setName(name); + device.setType(ArduinoConstants.DEVICE_TYPE); + boolean updated = APIUtil.getDeviceManagementService().modifyEnrollment(device); + if (updated) { + response.setStatus(Response.Status.OK.getStatusCode()); + } else { + response.setStatus(Response.Status.NOT_ACCEPTABLE.getStatusCode()); + } + return updated; + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } + } + + @Path("manager/device/{device_id}") + @GET + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Device getDevice(@PathParam("device_id") String deviceId) { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(ArduinoConstants.DEVICE_TYPE); + try { + return APIUtil.getDeviceManagementService().getDevice(deviceIdentifier); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } + } + + @Path("manager/devices") + @GET + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Device[] getArduinoDevices() { + try { + List userDevices = APIUtil.getDeviceManagementService().getDevicesOfUser( + APIUtil.getAuthenticatedUser()); + ArrayList userDevicesforArduino = new ArrayList<>(); + for (Device device : userDevices) { + if (device.getType().equals(ArduinoConstants.DEVICE_TYPE) && + device.getEnrolmentInfo().getStatus().equals(EnrolmentInfo.Status.ACTIVE)) { + userDevicesforArduino.add(device); + } + } + return userDevicesforArduino.toArray(new Device[]{}); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } + } + + @Path("manager/device/download") + @GET + @Produces("application/octet-stream") + public Response downloadSketch(@QueryParam("deviceName") String customDeviceName) { + try { + ZipArchive zipFile = createDownloadFile(APIUtil.getAuthenticatedUser(), customDeviceName); + Response.ResponseBuilder rb = Response.ok(zipFile.getZipFile()); + rb.header("Content-Disposition", "attachment; filename=\"" + zipFile.getFileName() + "\""); + return rb.build(); + } catch (IllegalArgumentException ex) { + return Response.status(400).entity(ex.getMessage()).build();//bad request + } catch (DeviceManagementException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (DeviceControllerException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (JWTClientException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (APIManagerException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (UserStoreException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } + } + + @Path("manager/device/generate_link") + @GET + public Response generateSketchLink(@QueryParam("deviceName") String deviceName) { + try { + ZipArchive zipFile = createDownloadFile(APIUtil.getAuthenticatedUser(), deviceName); + Response.ResponseBuilder rb = Response.ok(zipFile.getDeviceId()); + return rb.build(); + } catch (IllegalArgumentException ex) { + return Response.status(400).entity(ex.getMessage()).build();//bad request + } catch (DeviceManagementException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (JWTClientException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (DeviceControllerException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (APIManagerException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (UserStoreException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } + } + + private ZipArchive createDownloadFile(String owner, String deviceName) + throws DeviceManagementException, JWTClientException, DeviceControllerException, APIManagerException, + UserStoreException { + if (owner == null) { + throw new IllegalArgumentException("Error on createDownloadFile() Owner is null!"); + } + //create new device id + String deviceId = shortUUID(); + String applicationUsername = + PrivilegedCarbonContext.getThreadLocalCarbonContext().getUserRealm().getRealmConfiguration() + .getAdminUserName(); + if (apiApplicationKey == null) { + APIManagementProviderService apiManagementProviderService = APIUtil.getAPIManagementProviderService(); + String[] tags = {ArduinoConstants.DEVICE_TYPE}; + apiApplicationKey = apiManagementProviderService.generateAndRetrieveApplicationKeys( + ArduinoConstants.DEVICE_TYPE, tags, KEY_TYPE, applicationUsername, true); + } + JWTClient jwtClient = JWTClientManager.getInstance().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(); + //Register the device with CDMF + boolean status = register(deviceId, deviceName); + if (!status) { + String msg = "Error occurred while registering the device with " + "id: " + deviceId + " owner:" + owner; + throw new DeviceManagementException(msg); + } + ZipUtil ziputil = new ZipUtil(); + ZipArchive zipFile = ziputil.createZipFile(owner, APIUtil.getTenantDomainOftheUser(), + ArduinoConstants.DEVICE_TYPE, deviceId, + deviceName, accessToken, refreshToken); + zipFile.setDeviceId(deviceId); + return zipFile; + } + + private static String shortUUID() { + UUID uuid = UUID.randomUUID(); + long l = ByteBuffer.wrap(uuid.toString().getBytes(StandardCharsets.UTF_8)).getLong(); + return Long.toString(l, Character.MAX_RADIX); + } + + private boolean register(String deviceId, String name) { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(ArduinoConstants.DEVICE_TYPE); + try { + if (APIUtil.getDeviceManagementService().isEnrolled(deviceIdentifier)) { + response.setStatus(Response.Status.CONFLICT.getStatusCode()); + return false; + } + Device device = new Device(); + device.setDeviceIdentifier(deviceId); + EnrolmentInfo enrolmentInfo = new EnrolmentInfo(); + enrolmentInfo.setDateOfEnrolment(new Date().getTime()); + enrolmentInfo.setDateOfLastUpdate(new Date().getTime()); + enrolmentInfo.setStatus(EnrolmentInfo.Status.ACTIVE); + enrolmentInfo.setOwnership(EnrolmentInfo.OwnerShip.BYOD); + device.setName(name); + device.setType(ArduinoConstants.DEVICE_TYPE); + enrolmentInfo.setOwner(APIUtil.getAuthenticatedUser()); + device.setEnrolmentInfo(enrolmentInfo); + boolean added = APIUtil.getDeviceManagementService().enrollDevice(device); + if (added) { + response.setStatus(Response.Status.OK.getStatusCode()); + } else { + response.setStatus(Response.Status.NOT_ACCEPTABLE.getStatusCode()); + } + return added; + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } + } +} diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/manager/service/impl/util/APIUtil.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/manager/service/impl/util/APIUtil.java new file mode 100644 index 000000000..5ffe0198c --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/manager/service/impl/util/APIUtil.java @@ -0,0 +1,55 @@ +package org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl.util; + +import org.apache.commons.logging.Log; +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; + +/** + * This class provides utility functions used by REST-API. + */ +public class APIUtil { + + private static Log log = LogFactory.getLog(APIUtil.class); + + public static String getAuthenticatedUser() { + PrivilegedCarbonContext threadLocalCarbonContext = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + String username = threadLocalCarbonContext.getUsername(); + String tenantDomain = threadLocalCarbonContext.getTenantDomain(); + if (username.endsWith(tenantDomain)) { + return username.substring(0, username.lastIndexOf("@")); + } + 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 = + (DeviceManagementProviderService) ctx.getOSGiService(DeviceManagementProviderService.class, null); + if (deviceManagementProviderService == null) { + String msg = "Device Management service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return deviceManagementProviderService; + } + + 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; + } +} diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/webapp/META-INF/permissions.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/webapp/META-INF/permissions.xml new file mode 100644 index 000000000..5d829b850 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/webapp/META-INF/permissions.xml @@ -0,0 +1,59 @@ + + + + + + + + + + Get device + /device-mgt/user/devices/list + /manager/device/{device_id} + GET + + + + Remove device + /device-mgt/user/devices/remove + /manager/device/{device_id} + DELETE + + + + Download device + /device-mgt/user/devices/add + /manager/device/download + GET + + + + Update device + /device-mgt/user/devices/update + /manager/device/{device_id} + PUT + + + \ No newline at end of file diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/webapp/META-INF/webapp-classloading.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/webapp/META-INF/webapp-classloading.xml new file mode 100644 index 000000000..fa4461919 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/webapp/META-INF/webapp-classloading.xml @@ -0,0 +1,33 @@ + + + + + + + + + false + + + CXF,Carbon + diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml new file mode 100644 index 000000000..98bfb6d53 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/webapp/WEB-INF/web.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..e2c67f55a --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,51 @@ + + + Arduino + Arduino + + + CXFServlet + org.apache.cxf.transport.servlet.CXFServlet + 1 + + + + CXFServlet + /* + + + + isAdminService + false + + + doAuthentication + true + + + + + managed-api-enabled + false + + + managed-api-owner + admin + + + managed-api-context-template + /arduino/{version} + + + managed-api-application + arduino + + + managed-api-isSecured + true + + diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/pom.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/pom.xml new file mode 100644 index 000000000..0c870b611 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/pom.xml @@ -0,0 +1,124 @@ + + + + + + + arduino-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.arduino.plugin.impl + bundle + WSO2 Carbon - IoT Server Arduino Management Plugin + WSO2 Carbon - Arduino Management/Control Plugin Implementation + http://wso2.org + + + + + org.apache.felix + maven-scr-plugin + + + maven-compiler-plugin + + 1.7 + 1.7 + + 2.3.2 + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + ${project.artifactId} + ${project.artifactId} + ${carbon.devicemgt.plugins.version} + IoT Server Arduino Impl Bundle + org.wso2.carbon.device.mgt.iot.arduino.plugin.internal + + org.osgi.framework, + org.osgi.service.component, + org.apache.commons.logging, + javax.xml.bind.*;resolution:=optional, + javax.naming;resolution:=optional, + javax.sql;resolution:=optional, + javax.xml.bind.annotation.*;resolution:=optional, + javax.xml.parsers;resolution:=optional, + javax.net;resolution:=optional, + javax.net.ssl;resolution:=optional, + org.w3c.dom;resolution:=optional, + org.wso2.carbon.device.mgt.common.*, + org.wso2.carbon.device.mgt.common, + org.wso2.carbon.context.*, + org.wso2.carbon.ndatasource.core, + org.wso2.carbon.device.mgt.iot.*, + org.wso2.carbon.device.mgt.extensions.feature.mgt.*, + org.wso2.carbon.utils.* + + + + !org.wso2.carbon.device.mgt.iot.arduino.plugin.internal, + org.wso2.carbon.device.mgt.iot.arduino.plugin.* + + + + + + + + + + org.eclipse.osgi + org.eclipse.osgi + + + org.eclipse.osgi + org.eclipse.osgi.services + + + org.wso2.carbon + org.wso2.carbon.logging + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + + + org.wso2.carbon + org.wso2.carbon.ndatasource.core + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.extensions + + + org.wso2.carbon + org.wso2.carbon.utils + + + \ No newline at end of file diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/constants/ArduinoConstants.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/constants/ArduinoConstants.java new file mode 100644 index 000000000..6a9350075 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/constants/ArduinoConstants.java @@ -0,0 +1,39 @@ +/* + * 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.device.mgt.iot.arduino.plugin.constants; + +public class ArduinoConstants { + public final static String DEVICE_TYPE = "arduino"; + public final static String DEVICE_PLUGIN_DEVICE_NAME = "DEVICE_NAME"; + public final static String DEVICE_PLUGIN_DEVICE_ID = "ARDUINO_DEVICE_ID"; + public final static String STATE_ON = "ON"; + public final static String STATE_OFF = "OFF"; + + public static final String URL_PREFIX = "http://"; + public static final String BULB_CONTEXT = "/BULB/"; + public static final String SONAR_CONTEXT = "/HUMIDITY/"; + public static final String TEMPERATURE_CONTEXT = "/TEMPERATURE/"; + + //type of the sensor + public static final String SENSOR_TEMPERATURE = "temperature"; + //sensor events summerized table name + public static final String TEMPERATURE_EVENT_TABLE = "DEVICE_TEMPERATURE_SUMMARY"; + public static final String DATA_SOURCE_NAME = "jdbc/ArduinoDM_DB"; + +} diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/exception/ArduinoDeviceMgtPluginException.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/exception/ArduinoDeviceMgtPluginException.java new file mode 100644 index 000000000..93dc15683 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/exception/ArduinoDeviceMgtPluginException.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.device.mgt.iot.arduino.plugin.exception; + + +public class ArduinoDeviceMgtPluginException extends Exception{ + + private String errorMessage; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public ArduinoDeviceMgtPluginException(String msg, Exception nestedEx) { + super(msg, nestedEx); + setErrorMessage(msg); + } + + public ArduinoDeviceMgtPluginException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public ArduinoDeviceMgtPluginException(String msg) { + super(msg); + setErrorMessage(msg); + } + + public ArduinoDeviceMgtPluginException() { + super(); + } + + public ArduinoDeviceMgtPluginException(Throwable cause) { + super(cause); + } + +} diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/ArduinoManager.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/ArduinoManager.java new file mode 100644 index 000000000..5c7174d96 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/ArduinoManager.java @@ -0,0 +1,259 @@ +/* + * 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.device.mgt.iot.arduino.plugin.impl; + + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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.common.DeviceManager; +import org.wso2.carbon.device.mgt.common.EnrolmentInfo; +import org.wso2.carbon.device.mgt.common.FeatureManager; +import org.wso2.carbon.device.mgt.common.configuration.mgt.TenantConfiguration; +import org.wso2.carbon.device.mgt.common.license.mgt.License; +import org.wso2.carbon.device.mgt.common.license.mgt.LicenseManagementException; +import org.wso2.carbon.device.mgt.iot.arduino.plugin.exception.ArduinoDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.arduino.plugin.impl.dao.ArduinoDAO; +import org.wso2.carbon.device.mgt.iot.arduino.plugin.impl.feature.ArduinoFeatureManager; +import java.util.List; + + +/** + * This represents the Arduino implementation of DeviceManagerService. + */ +public class ArduinoManager implements DeviceManager { + + private static final ArduinoDAO arduinoDAO = new ArduinoDAO(); + private static final Log log = LogFactory.getLog(ArduinoManager.class); + private ArduinoFeatureManager arduinoFeatureManager = new ArduinoFeatureManager(); + + @Override + public FeatureManager getFeatureManager() { + return arduinoFeatureManager; + } + + @Override + public boolean saveConfiguration(TenantConfiguration tenantConfiguration) + throws DeviceManagementException { + //TODO implement this + return false; + } + + @Override + public TenantConfiguration getConfiguration() throws DeviceManagementException { + //TODO implement this + return null; + } + + @Override + public boolean enrollDevice(Device device) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Enrolling a new Arduino device : " + device.getDeviceIdentifier()); + } + ArduinoDAO.beginTransaction(); + status = arduinoDAO.getDeviceDAO().addDevice(device); + ArduinoDAO.commitTransaction(); + } catch (ArduinoDeviceMgtPluginException e) { + try { + ArduinoDAO.rollbackTransaction(); + } catch (ArduinoDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the device enrol transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while enrolling the Arduino device : " + device.getDeviceIdentifier(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean modifyEnrollment(Device device) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Modifying the Arduino device enrollment data"); + } + ArduinoDAO.beginTransaction(); + status = arduinoDAO.getDeviceDAO().updateDevice(device); + ArduinoDAO.commitTransaction(); + } catch (ArduinoDeviceMgtPluginException e) { + try { + ArduinoDAO.rollbackTransaction(); + } catch (ArduinoDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the update device transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while updating the enrollment of the Arduino device : " + + device.getDeviceIdentifier(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean disenrollDevice(DeviceIdentifier deviceId) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Dis-enrolling Arduino device : " + deviceId); + } + ArduinoDAO.beginTransaction(); + status = arduinoDAO.getDeviceDAO().deleteDevice(deviceId.getId()); + ArduinoDAO.commitTransaction(); + } catch (ArduinoDeviceMgtPluginException e) { + try { + ArduinoDAO.rollbackTransaction(); + } catch (ArduinoDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the device dis enrol transaction :" + deviceId.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while removing the Arduino device : " + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean isEnrolled(DeviceIdentifier deviceId) throws DeviceManagementException { + boolean isEnrolled = false; + try { + if (log.isDebugEnabled()) { + log.debug("Checking the enrollment of Arduino device : " + deviceId.getId()); + } + Device iotDevice = arduinoDAO.getDeviceDAO().getDevice(deviceId.getId()); + if (iotDevice != null) { + isEnrolled = true; + } + } catch (ArduinoDeviceMgtPluginException e) { + String msg = "Error while checking the enrollment status of Arduino device : " + + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return isEnrolled; + } + + @Override + public boolean isActive(DeviceIdentifier deviceId) throws DeviceManagementException { + return true; + } + + @Override + public boolean setActive(DeviceIdentifier deviceId, boolean status) + throws DeviceManagementException { + return true; + } + + @Override + public Device getDevice(DeviceIdentifier deviceId) throws DeviceManagementException { + Device device; + try { + if (log.isDebugEnabled()) { + log.debug("Getting the details of Arduino device : " + deviceId.getId()); + } + device = arduinoDAO.getDeviceDAO().getDevice(deviceId.getId()); + } catch (ArduinoDeviceMgtPluginException e) { + String msg = "Error while fetching the Arduino device : " + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return device; + } + + @Override + public boolean setOwnership(DeviceIdentifier deviceId, String ownershipType) + throws DeviceManagementException { + return true; + } + + public boolean isClaimable(DeviceIdentifier deviceIdentifier) throws DeviceManagementException { + return false; + } + + @Override + public boolean setStatus(DeviceIdentifier deviceId, String currentOwner, + EnrolmentInfo.Status status) throws DeviceManagementException { + return false; + } + + @Override + public License getLicense(String s) throws LicenseManagementException { + return null; + } + + @Override + public void addLicense(License license) throws LicenseManagementException { + + } + + @Override + public boolean requireDeviceAuthorization() { + return false; + } + + @Override + public boolean updateDeviceInfo(DeviceIdentifier deviceIdentifier, Device device) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug( + "updating the details of Arduino device : " + deviceIdentifier); + } + ArduinoDAO.beginTransaction(); + status = arduinoDAO.getDeviceDAO().updateDevice(device); + ArduinoDAO.commitTransaction(); + } catch (ArduinoDeviceMgtPluginException e) { + try { + ArduinoDAO.rollbackTransaction(); + } catch (ArduinoDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the update device info transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = + "Error while updating the Arduino device : " + deviceIdentifier; + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public List getAllDevices() throws DeviceManagementException { + List devices; + try { + if (log.isDebugEnabled()) { + log.debug("Fetching the details of all Arduino devices"); + } + devices = arduinoDAO.getDeviceDAO().getAllDevices(); + } catch (ArduinoDeviceMgtPluginException e) { + String msg = "Error while fetching all Arduino devices."; + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return devices; + } + +} \ No newline at end of file diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/ArduinoManagerService.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/ArduinoManagerService.java new file mode 100644 index 000000000..15f1ff60c --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/ArduinoManagerService.java @@ -0,0 +1,108 @@ +/* + * 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.device.mgt.iot.arduino.plugin.impl; + +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.DeviceManager; +import org.wso2.carbon.device.mgt.common.app.mgt.Application; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManagementException; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManager; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; +import org.wso2.carbon.device.mgt.iot.arduino.plugin.constants.ArduinoConstants; + +import java.util.List; + +public class ArduinoManagerService implements DeviceManagementService { + private DeviceManager deviceManager; + + @Override + public String getType() { + return ArduinoConstants.DEVICE_TYPE; + } + + @Override + public String getProviderTenantDomain() { + return "carbon.super"; + } + + @Override + public boolean isSharedWithAllTenants() { + return true; + } + + @Override + public void init() throws DeviceManagementException { + deviceManager=new ArduinoManager(); + + } + + @Override + public DeviceManager getDeviceManager() { + return deviceManager; + } + + @Override + public ApplicationManager getApplicationManager() { + return null; + } + + @Override + public void notifyOperationToDevices(Operation operation, List deviceIds) + throws DeviceManagementException { + + } + + @Override + public Application[] getApplications(String domain, int pageNumber, int size) + throws ApplicationManagementException { + return new Application[0]; + } + + @Override + public void updateApplicationStatus(DeviceIdentifier deviceId, Application application, + String status) throws ApplicationManagementException { + + } + + @Override + public String getApplicationStatus(DeviceIdentifier deviceId, Application application) + throws ApplicationManagementException { + return null; + } + + @Override + public void installApplicationForDevices(Operation operation, List deviceIdentifiers) + throws ApplicationManagementException { + + } + + @Override + public void installApplicationForUsers(Operation operation, List userNameList) + throws ApplicationManagementException { + + } + + @Override + public void installApplicationForUserRoles(Operation operation, List userRoleList) + throws ApplicationManagementException { + + } +} diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/dao/ArduinoDAO.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/dao/ArduinoDAO.java new file mode 100644 index 000000000..133f0c74d --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/dao/ArduinoDAO.java @@ -0,0 +1,125 @@ +/* + * 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.device.mgt.iot.arduino.plugin.impl.dao; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.iot.arduino.plugin.constants.ArduinoConstants; +import org.wso2.carbon.device.mgt.iot.arduino.plugin.exception.ArduinoDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.arduino.plugin.impl.dao.impl.ArduinoDeviceDAOImpl; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +public class ArduinoDAO { + + private static final Log log = LogFactory.getLog(ArduinoDAO.class); + static DataSource dataSource; + private static ThreadLocal currentConnection = new ThreadLocal(); + + public ArduinoDAO() { + initArduinoDAO(); + } + + public static void initArduinoDAO() { + try { + Context ctx = new InitialContext(); + dataSource = (DataSource) ctx.lookup(ArduinoConstants.DATA_SOURCE_NAME); + } catch (NamingException e) { + log.error("Error while looking up the data source: " + ArduinoConstants.DATA_SOURCE_NAME); + } + } + + public ArduinoDeviceDAOImpl getDeviceDAO() { + return new ArduinoDeviceDAOImpl(); + } + + public static void beginTransaction() throws ArduinoDeviceMgtPluginException { + try { + Connection conn = dataSource.getConnection(); + conn.setAutoCommit(false); + currentConnection.set(conn); + } catch (SQLException e) { + throw new ArduinoDeviceMgtPluginException("Error occurred while retrieving datasource connection", e); + } + } + + public static Connection getConnection() throws ArduinoDeviceMgtPluginException { + if (currentConnection.get() == null) { + try { + currentConnection.set(dataSource.getConnection()); + } catch (SQLException e) { + throw new ArduinoDeviceMgtPluginException("Error occurred while retrieving data source connection", e); + } + } + return currentConnection.get(); + } + + public static void commitTransaction() throws ArduinoDeviceMgtPluginException { + try { + Connection conn = currentConnection.get(); + if (conn != null) { + conn.commit(); + } else { + if (log.isDebugEnabled()) { + log.debug("Datasource connection associated with the current thread is null, hence commit " + + "has not been attempted"); + } + } + } catch (SQLException e) { + throw new ArduinoDeviceMgtPluginException("Error occurred while committing the transaction", e); + } finally { + closeConnection(); + } + } + + public static void closeConnection() throws ArduinoDeviceMgtPluginException { + + Connection con = currentConnection.get(); + if (con != null) { + try { + con.close(); + } catch (SQLException e) { + log.error("Error occurred while close the connection"); + } + } + currentConnection.remove(); + } + + public static void rollbackTransaction() throws ArduinoDeviceMgtPluginException { + try { + Connection conn = currentConnection.get(); + if (conn != null) { + conn.rollback(); + } else { + if (log.isDebugEnabled()) { + log.debug("Datasource connection associated with the current thread is null, hence rollback " + + "has not been attempted"); + } + } + } catch (SQLException e) { + throw new ArduinoDeviceMgtPluginException("Error occurred while rollback the transaction", e); + } finally { + closeConnection(); + } + } +} \ No newline at end of file diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/dao/impl/ArduinoDeviceDAOImpl.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/dao/impl/ArduinoDeviceDAOImpl.java new file mode 100644 index 000000000..1bb0f164f --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/dao/impl/ArduinoDeviceDAOImpl.java @@ -0,0 +1,196 @@ +/* + * 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.device.mgt.iot.arduino.plugin.impl.dao.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.iot.arduino.plugin.exception.ArduinoDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.arduino.plugin.impl.util.ArduinoUtils; +import org.wso2.carbon.device.mgt.iot.arduino.plugin.constants.ArduinoConstants; +import org.wso2.carbon.device.mgt.iot.arduino.plugin.impl.dao.ArduinoDAO; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * Implements CRUD for arduino Devices. + */ +public class ArduinoDeviceDAOImpl { + + + private static final Log log = LogFactory.getLog(ArduinoDeviceDAOImpl.class); + + public Device getDevice(String deviceId) throws ArduinoDeviceMgtPluginException { + Connection conn = null; + PreparedStatement stmt = null; + Device device = null; + ResultSet resultSet = null; + try { + conn = ArduinoDAO.getConnection(); + String selectDBQuery = + "SELECT ARDUINO_DEVICE_ID, DEVICE_NAME FROM ARDUINO_DEVICE WHERE ARDUINO_DEVICE_ID = ?"; + stmt = conn.prepareStatement(selectDBQuery); + stmt.setString(1, deviceId); + resultSet = stmt.executeQuery(); + + if (resultSet.next()) { + device = new Device(); + device.setName(resultSet.getString(ArduinoConstants.DEVICE_PLUGIN_DEVICE_NAME)); + if (log.isDebugEnabled()) { + log.debug("Arduino device " + deviceId + " data has been fetched from " + + "Arduino database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while fetching Arduino device : '" + deviceId + "'"; + log.error(msg, e); + throw new ArduinoDeviceMgtPluginException(msg, e); + } finally { + ArduinoUtils.cleanupResources(stmt, resultSet); + ArduinoDAO.closeConnection(); + } + return device; + } + + public boolean addDevice(Device iotDevice) throws ArduinoDeviceMgtPluginException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = ArduinoDAO.getConnection(); + String createDBQuery = + "INSERT INTO ARDUINO_DEVICE(ARDUINO_DEVICE_ID, DEVICE_NAME) VALUES (?, ?)"; + + stmt = conn.prepareStatement(createDBQuery); + stmt.setString(1, iotDevice.getDeviceIdentifier()); + stmt.setString(2,iotDevice.getName()); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Arduino device " + iotDevice.getDeviceIdentifier() + " data has been" + + " added to the Arduino database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while adding the Arduino device '" + + iotDevice.getDeviceIdentifier() + "' to the Arduino db."; + log.error(msg, e); + throw new ArduinoDeviceMgtPluginException(msg, e); + } finally { + ArduinoUtils.cleanupResources(stmt, null); + } + return status; + } + + public boolean updateDevice(Device iotDevice) throws ArduinoDeviceMgtPluginException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = ArduinoDAO.getConnection(); + String updateDBQuery = + "UPDATE ARDUINO_DEVICE SET DEVICE_NAME = ? WHERE ARDUINO_DEVICE_ID = ?"; + + stmt = conn.prepareStatement(updateDBQuery); + stmt.setString(1, iotDevice.getName()); + stmt.setString(2, iotDevice.getDeviceIdentifier()); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Arduino device " + iotDevice.getDeviceIdentifier() + " data has been" + + " modified."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while modifying the Arduino device '" + iotDevice.getDeviceIdentifier() + + "' data."; + log.error(msg, e); + throw new ArduinoDeviceMgtPluginException(msg, e); + } finally { + ArduinoUtils.cleanupResources(stmt, null); + } + return status; + } + + public boolean deleteDevice(String iotDeviceId) throws ArduinoDeviceMgtPluginException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = ArduinoDAO.getConnection(); + String deleteDBQuery = + "DELETE FROM ARDUINO_DEVICE WHERE ARDUINO_DEVICE_ID = ?"; + stmt = conn.prepareStatement(deleteDBQuery); + stmt.setString(1, iotDeviceId); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Arduino device " + iotDeviceId + " data has deleted" + + " from the Arduino database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while deleting Arduino device " + iotDeviceId; + log.error(msg, e); + throw new ArduinoDeviceMgtPluginException(msg, e); + } finally { + ArduinoUtils.cleanupResources(stmt, null); + } + return status; + } + + public List getAllDevices() throws ArduinoDeviceMgtPluginException { + + Connection conn = null; + PreparedStatement stmt = null; + ResultSet resultSet = null; + Device device; + List devices = new ArrayList(); + try { + conn = ArduinoDAO.getConnection(); + String selectDBQuery = + "SELECT ARDUINO_DEVICE_ID, DEVICE_NAME FROM ARDUINO_DEVICE"; + stmt = conn.prepareStatement(selectDBQuery); + resultSet = stmt.executeQuery(); + while (resultSet.next()) { + device = new Device(); + device.setDeviceIdentifier(resultSet.getString(ArduinoConstants.DEVICE_PLUGIN_DEVICE_ID)); + device.setName(resultSet.getString(ArduinoConstants.DEVICE_PLUGIN_DEVICE_NAME)); + } + if (log.isDebugEnabled()) { + log.debug("All Arduino device details have fetched from Arduino database."); + } + return devices; + } catch (SQLException e) { + String msg = "Error occurred while fetching all Arduino device data'"; + log.error(msg, e); + throw new ArduinoDeviceMgtPluginException(msg, e); + } finally { + ArduinoUtils.cleanupResources(stmt, resultSet); + ArduinoDAO.closeConnection(); + } + } + } \ No newline at end of file diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/feature/ArduinoFeatureManager.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/feature/ArduinoFeatureManager.java new file mode 100644 index 000000000..d693f8e5b --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/feature/ArduinoFeatureManager.java @@ -0,0 +1,59 @@ +/* + * 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.device.mgt.iot.arduino.plugin.impl.feature; + +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.Feature; +import org.wso2.carbon.device.mgt.common.FeatureManager; +import org.wso2.carbon.device.mgt.extensions.feature.mgt.GenericFeatureManager; +import org.wso2.carbon.device.mgt.iot.arduino.plugin.constants.ArduinoConstants; + +import java.util.List; + +public class ArduinoFeatureManager implements FeatureManager { + @Override + public boolean addFeature(Feature feature) throws DeviceManagementException { + return false; + } + + @Override + public boolean addFeatures(List features) throws DeviceManagementException { + return false; + } + + @Override + public Feature getFeature(String name) throws DeviceManagementException { + GenericFeatureManager genericFeatureManager = GenericFeatureManager.getInstance(); + return genericFeatureManager.getFeature(ArduinoConstants.DEVICE_TYPE, name); + } + + @Override + public List getFeatures() throws DeviceManagementException { + GenericFeatureManager genericFeatureManager = GenericFeatureManager.getInstance(); + return genericFeatureManager.getFeatures(ArduinoConstants.DEVICE_TYPE); + } + + @Override + public boolean removeFeature(String name) throws DeviceManagementException { + return false; + } + + @Override + public boolean addSupportedFeaturesToDB() throws DeviceManagementException { + return false; + } +} diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/util/ArduinoUtils.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/util/ArduinoUtils.java new file mode 100644 index 000000000..f5e364274 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/util/ArduinoUtils.java @@ -0,0 +1,114 @@ +/* + * 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.device.mgt.iot.arduino.plugin.impl.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.iot.arduino.plugin.constants.ArduinoConstants; +import org.wso2.carbon.device.mgt.iot.arduino.plugin.exception.ArduinoDeviceMgtPluginException; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; + +/** + * Contains utility methods used by Arduino plugin. + */ +public class ArduinoUtils { + + private static Log log = LogFactory.getLog(ArduinoUtils.class); + + public static String getDeviceProperty(List deviceProperties, String propertyKey) { + String deviceProperty = ""; + for(Device.Property property :deviceProperties){ + if(propertyKey.equals(property.getName())){ + deviceProperty = property.getValue(); + } + } + return deviceProperty; + } + + public static Device.Property getProperty(String property, String value) { + if (property != null) { + Device.Property prop = new Device.Property(); + prop.setName(property); + prop.setValue(value); + return prop; + } + return null; + } + + public static void cleanupResources(Connection conn, PreparedStatement stmt, ResultSet rs) { + if (rs != null) { + try { + rs.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing result set", e); + } + } + if (stmt != null) { + try { + stmt.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing prepared statement", e); + } + } + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing database connection", e); + } + } + } + + public static void cleanupResources(PreparedStatement stmt, ResultSet rs) { + cleanupResources(null, stmt, rs); + } + + /** + * Creates the device management schema. + */ + public static void setupDeviceManagementSchema() throws ArduinoDeviceMgtPluginException { + try { + Context ctx = new InitialContext(); + DataSource dataSource = (DataSource) ctx.lookup(ArduinoConstants.DATA_SOURCE_NAME); + DeviceSchemaInitializer initializer = + new DeviceSchemaInitializer(dataSource); + log.info("Initializing device management repository database schema"); + initializer.createRegistryDatabase(); + + } catch (NamingException e) { + log.error("Error while looking up the data source: " + ArduinoConstants.DATA_SOURCE_NAME); + } catch (Exception e) { + throw new ArduinoDeviceMgtPluginException("Error occurred while initializing Iot Device " + + "Management database schema", e); + } + } + + +} diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/util/DeviceSchemaInitializer.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/util/DeviceSchemaInitializer.java new file mode 100644 index 000000000..3567e776f --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/impl/util/DeviceSchemaInitializer.java @@ -0,0 +1,50 @@ +/* + * 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.device.mgt.iot.arduino.plugin.impl.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.utils.CarbonUtils; +import org.wso2.carbon.utils.dbcreator.DatabaseCreator; + +import javax.sql.DataSource; +import java.io.File; + +/** + * Provides methods for initializing the database script. + */ +public class DeviceSchemaInitializer extends DatabaseCreator{ + + private static final Log log = LogFactory.getLog(DeviceSchemaInitializer.class); + private static final String setupSQLScriptBaseLocation = CarbonUtils.getCarbonHome() + File.separator + "dbscripts" + + File.separator + "cdm" + File.separator + "plugins" + File.separator; + + public DeviceSchemaInitializer(DataSource dataSource) { + super(dataSource); + } + + @Override + protected String getDbScriptLocation(String databaseType) { + String scriptName = databaseType + ".sql"; + if (log.isDebugEnabled()) { + log.debug("Loading database script from :" + scriptName); + } + return setupSQLScriptBaseLocation.replaceFirst("DBTYPE", databaseType) + scriptName; + } +} diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/internal/ArduinoManagementServiceComponent.java b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/internal/ArduinoManagementServiceComponent.java new file mode 100644 index 000000000..fc5fe40a3 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/arduino/plugin/internal/ArduinoManagementServiceComponent.java @@ -0,0 +1,86 @@ +/* + * 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.device.mgt.iot.arduino.plugin.internal; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.ComponentContext; +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; +import org.wso2.carbon.device.mgt.iot.arduino.plugin.exception.ArduinoDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.arduino.plugin.impl.ArduinoManagerService; +import org.wso2.carbon.device.mgt.iot.arduino.plugin.impl.util.ArduinoUtils; +/** + * @scr.component name="org.wso2.carbon.device.mgt.iot.arduino.internal.ArduinoManagementServiceComponent" + * immediate="true" + */ +public class ArduinoManagementServiceComponent { + + private static final Log log = LogFactory.getLog(ArduinoManagementServiceComponent.class); + private ServiceRegistration arduinoServiceRegRef; + + protected void activate(ComponentContext ctx) { + if (log.isDebugEnabled()) { + log.debug("Activating Arduino Device Management Service Component"); + } + try { + BundleContext bundleContext = ctx.getBundleContext(); + arduinoServiceRegRef = + bundleContext.registerService(DeviceManagementService.class.getName(), + new ArduinoManagerService(), null); + String setupOption = System.getProperty("setup"); + if (setupOption != null) { + if (log.isDebugEnabled()) { + log.debug( + "-Dsetup is enabled. Iot Device management repository schema initialization is about " + + "to begin"); + } + try { + ArduinoUtils.setupDeviceManagementSchema(); + } catch (ArduinoDeviceMgtPluginException e) { + log.error("Exception occurred while initializing device management database schema", e); + } + } + if (log.isDebugEnabled()) { + log.debug("Arduino Device Management Service Component has been successfully activated"); + } + } catch (Throwable e) { + log.error("Error occurred while activating Arduino Device Management Service Component", e); + } + } + + protected void deactivate(ComponentContext ctx) { + if (log.isDebugEnabled()) { + log.debug("De-activating Arduino Device Management Service Component"); + } + try { + if (arduinoServiceRegRef != null) { + arduinoServiceRegRef.unregister(); + } + + if (log.isDebugEnabled()) { + log.debug( + "Arduino Device Management Service Component has been successfully de-activated"); + } + } catch (Throwable e) { + log.error("Error occurred while de-activating Arduino Device Management bundle", e); + } + } +} diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/pom.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/pom.xml new file mode 100644 index 000000000..950c0b438 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/pom.xml @@ -0,0 +1,62 @@ + + + + + + + + arduino-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.arduino.ui + WSO2 Carbon - IoT Server Arduino UI + pom + + + + + maven-assembly-plugin + 2.5.5 + + ${project.artifactId}-${carbon.device.mgt.version} + false + + src/assembly/src.xml + + + + + create-archive + package + + single + + + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/assembly/src.xml b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/assembly/src.xml new file mode 100644 index 000000000..2797034e0 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/assembly/src.xml @@ -0,0 +1,36 @@ + + + + src + + zip + + false + ${basedir}/src + + + ${basedir}/src/main/resources/jaggeryapps/devicemgt + / + true + + + \ No newline at end of file diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.device-view/device-view.hbs b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.device-view/device-view.hbs new file mode 100644 index 000000000..d4294f8d2 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.device-view/device-view.hbs @@ -0,0 +1,68 @@ +{{#zone "topCss"}} + +{{/zone}} + +{{#zone "device-thumbnail"}} + +{{/zone}} + +{{#zone "device-opetations"}} +
+ Operations +
+
+ {{unit "iot.unit.device.operation-bar" device=device}} +
+{{/zone}} + +{{#zone "device-detail-properties"}} +
+ +
+
+ +
+
Device Statistics
+ {{unit "iot.unit.device.stats" device=device}} +
+
+
Operations Log
+
+ +
+
+ Not available yet +
+
+
+
+
+
+
+
+{{/zone}} \ No newline at end of file diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.device-view/device-view.js b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.device-view/device-view.js new file mode 100644 index 000000000..3198cf40f --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.device-view/device-view.js @@ -0,0 +1,44 @@ +/* + * 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. + */ + +function onRequest(context) { + var log = new Log("device-view.js"); + var deviceType = context.uriParams.deviceType; + var deviceId = request.getParameter("id"); + + if (deviceType != null && deviceType != undefined && deviceId != null && deviceId != undefined) { + var deviceModule = require("/app/modules/device.js").deviceModule; + var device = deviceModule.viewDevice(deviceType, deviceId); + + if (device && device.status != "error") { + var viewModel = {}; + var deviceInfo = device.properties.DEVICE_INFO; + if (deviceInfo != undefined && String(deviceInfo.toString()).length > 0) { + deviceInfo = parse(stringify(deviceInfo)); + viewModel.system = device.properties.IMEI; + viewModel.machine = "Arduino"; + viewModel.vendor = device.properties.VENDOR; + } + device.viewModel = viewModel; + return {"device": device}; + } else { + response.sendError(404, "Device Id " + deviceId + "of type " + deviceType + " cannot be found!"); + exit(); + } + } +} \ No newline at end of file diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.device-view/device-view.json b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.device-view/device-view.json new file mode 100644 index 000000000..9eecd8f5b --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.device-view/device-view.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0" +} \ No newline at end of file diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.device-view/public/images/ardunio-icon.png b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.device-view/public/images/ardunio-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d478b0e303f60ccec7d7716e4609dfe8c757d66c GIT binary patch literal 21675 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 Qof+3u)l(@`dhzyu0YXp5wg3PC literal 0 HcmV?d00001 diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/public/images/ardunio-icon.png b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/public/images/ardunio-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d478b0e303f60ccec7d7716e4609dfe8c757d66c GIT binary patch literal 21675 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ⓈZTsFem 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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/public/images/schematicsGuide.png b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/public/images/schematicsGuide.png new file mode 100644 index 0000000000000000000000000000000000000000..d2d1466abf58998743761cf716b632f56835aac1 GIT binary patch literal 94571 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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/public/images/thumb.png b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/public/images/thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..ad8170f33d34cf765cfb68e8adc370622d514b98 GIT binary patch literal 11936 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 Qof+3u)l(@`dhzyu0YXp5wg3PC literal 0 HcmV?d00001 diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/public/js/download.js b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/public/js/download.js new file mode 100644 index 000000000..0c22e5099 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/public/js/download.js @@ -0,0 +1,184 @@ +/* + * 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. + */ + +var modalPopup = ".wr-modalpopup"; +var modalPopupContainer = modalPopup + " .modalpopup-container"; +var modalPopupContent = modalPopup + " .modalpopup-content"; +var body = "body"; + +/* + * set popup maximum height function. + */ +function setPopupMaxHeight() { + $(modalPopupContent).css('max-height', ($(body).height() - ($(body).height() / 100 * 30))); + $(modalPopupContainer).css('margin-top', (-($(modalPopupContainer).height() / 2))); +} + +/* + * show popup function. + */ +function showPopup() { + $(modalPopup).show(); + setPopupMaxHeight(); + $('#downloadForm').validate({ + rules: { + deviceName: { + minlength: 4, + required: true + } + }, + highlight: function (element) { + $(element).closest('.control-group').removeClass('success').addClass('error'); + }, + success: function (element) { + $(element).closest('.control-group').removeClass('error').addClass('success'); + $('label[for=deviceName]').remove(); + } + }); + var deviceType = ""; + $('.deviceType').each(function () { + if (this.value != "") { + deviceType = this.value; + } + }); +} + +/* + * hide popup function. + */ +function hidePopup() { + $('label[for=deviceName]').remove(); + $('.control-group').removeClass('success').removeClass('error'); + $(modalPopupContent).html(''); + $(modalPopup).hide(); +} + +/* + * DOM ready functions. + */ +$(document).ready(function () { + attachEvents(); +}); + +function attachEvents() { + /** + * Following click function would execute + * when a user clicks on "Download" link + * on Device Management page in WSO2 DC Console. + */ + $("a.download-link").click(function () { + var sketchType = $(this).data("sketchtype"); + var deviceType = $(this).data("devicetype"); + var downloadDeviceAPI = "/devicemgt/api/devices/sketch/generate_link"; + var payload = {"sketchType": sketchType, "deviceType": deviceType}; + $(modalPopupContent).html($('#download-device-modal-content').html()); + showPopup(); + var deviceName; + $("a#download-device-download-link").click(function () { + $('.new-device-name').each(function () { + if (this.value != "") { + deviceName = this.value; + } + }); + $('label[for=deviceName]').remove(); + if (deviceName && deviceName.length >= 4) { + payload.deviceName = deviceName; + invokerUtil.post( + downloadDeviceAPI, + payload, + function (data, textStatus, jqxhr) { + doAction(data); + }, + function (data) { + doAction(data); + } + ); + }else if(deviceName){ + $('.controls').append(''); + $('.control-group').removeClass('success').addClass('error'); + } else { + $('.controls').append(''); + $('.control-group').removeClass('success').addClass('error'); + } + }); + + $("a#download-device-cancel-link").click(function () { + hidePopup(); + }); + + }); +} + +function downloadAgent() { + var deviceName; + $('.new-device-name').each(function () { + if (this.value != "") { + deviceName = this.value; + } + }); + var deviceNameFormat = /^[^~?!#$:;%^*`+={}\[\]\\()|<>,'"]{1,30}$/; + if (deviceName && deviceNameFormat.test(deviceName)) { + $('#downloadForm').submit(); + hidePopup(); + $(modalPopupContent).html($('#device-agent-downloading-content').html()); + showPopup(); + setTimeout(function () { + hidePopup(); + }, 1000); + }else { + $("#invalid-username-error-msg span").text("Invalid device name"); + $("#invalid-username-error-msg").removeClass("hidden"); + } +} + +function doAction(data) { + //if it is saml redirection response + if (data.status == null) { + document.write(data); + } + + if (data.status == "200") { + $(modalPopupContent).html($('#download-device-modal-content-links').html()); + $("input#download-device-url").val(data.responseText); + $("input#download-device-url").focus(function () { + $(this).select(); + }); + showPopup(); + } else if (data.status == "401") { + $(modalPopupContent).html($('#device-401-content').html()); + $("#device-401-link").click(function () { + window.location = "/devicemgt/login"; + }); + showPopup(); + } else if (data == "403") { + $(modalPopupContent).html($('#device-403-content').html()); + $("#device-403-link").click(function () { + window.location = "/devicemgt/login"; + }); + showPopup(); + } else { + $(modalPopupContent).html($('#device-unexpected-error-content').html()); + $("a#device-unexpected-error-link").click(function () { + hidePopup(); + }); + } +} \ No newline at end of file diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/public/js/jquery.validate.js b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/public/js/jquery.validate.js new file mode 100644 index 000000000..fe7ecf07a --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/public/js/jquery.validate.js @@ -0,0 +1,1227 @@ +/* + * 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. + */ + +(function($) { + +$.extend($.fn, { + // http://docs.jquery.com/Plugins/Validation/validate + validate: function( options ) { + + // if nothing is selected, return nothing; can't chain anyway + if ( !this.length ) { + if ( options && options.debug && window.console ) { + console.warn( "Nothing selected, can't validate, returning nothing." ); + } + return; + } + + // check if a validator for this form was already created + var validator = $.data( this[0], "validator" ); + if ( validator ) { + return validator; + } + + // Add novalidate tag if HTML5. + this.attr( "novalidate", "novalidate" ); + + validator = new $.validator( options, this[0] ); + $.data( this[0], "validator", validator ); + + if ( validator.settings.onsubmit ) { + + this.validateDelegate( ":submit", "click", function( event ) { + if ( validator.settings.submitHandler ) { + validator.submitButton = event.target; + } + // allow suppressing validation by adding a cancel class to the submit button + if ( $(event.target).hasClass("cancel") ) { + validator.cancelSubmit = true; + } + }); + + // validate the form on submit + this.submit( function( event ) { + if ( validator.settings.debug ) { + // prevent form submit to be able to see console output + event.preventDefault(); + } + function handle() { + var hidden; + if ( validator.settings.submitHandler ) { + if ( validator.submitButton ) { + // insert a hidden input as a replacement for the missing submit button + hidden = $("").attr("name", validator.submitButton.name).val(validator.submitButton.value).appendTo(validator.currentForm); + } + validator.settings.submitHandler.call( validator, validator.currentForm, event ); + if ( validator.submitButton ) { + // and clean up afterwards; thanks to no-block-scope, hidden can be referenced + hidden.remove(); + } + return false; + } + return true; + } + + // prevent submit for invalid forms or custom submit handlers + if ( validator.cancelSubmit ) { + validator.cancelSubmit = false; + return handle(); + } + if ( validator.form() ) { + if ( validator.pendingRequest ) { + validator.formSubmitted = true; + return false; + } + return handle(); + } else { + validator.focusInvalid(); + return false; + } + }); + } + + return validator; + }, + // http://docs.jquery.com/Plugins/Validation/valid + valid: function() { + if ( $(this[0]).is("form")) { + return this.validate().form(); + } else { + var valid = true; + var validator = $(this[0].form).validate(); + this.each(function() { + valid &= validator.element(this); + }); + return valid; + } + }, + // attributes: space seperated list of attributes to retrieve and remove + removeAttrs: function( attributes ) { + var result = {}, + $element = this; + $.each(attributes.split(/\s/), function( index, value ) { + result[value] = $element.attr(value); + $element.removeAttr(value); + }); + return result; + }, + // http://docs.jquery.com/Plugins/Validation/rules + rules: function( command, argument ) { + var element = this[0]; + + if ( command ) { + var settings = $.data(element.form, "validator").settings; + var staticRules = settings.rules; + var existingRules = $.validator.staticRules(element); + switch(command) { + case "add": + $.extend(existingRules, $.validator.normalizeRule(argument)); + staticRules[element.name] = existingRules; + if ( argument.messages ) { + settings.messages[element.name] = $.extend( settings.messages[element.name], argument.messages ); + } + break; + case "remove": + if ( !argument ) { + delete staticRules[element.name]; + return existingRules; + } + var filtered = {}; + $.each(argument.split(/\s/), function( index, method ) { + filtered[method] = existingRules[method]; + delete existingRules[method]; + }); + return filtered; + } + } + + var data = $.validator.normalizeRules( + $.extend( + {}, + $.validator.classRules(element), + $.validator.attributeRules(element), + $.validator.dataRules(element), + $.validator.staticRules(element) + ), element); + + // make sure required is at front + if ( data.required ) { + var param = data.required; + delete data.required; + data = $.extend({required: param}, data); + } + + return data; + } +}); + +// Custom selectors +$.extend($.expr[":"], { + // http://docs.jquery.com/Plugins/Validation/blank + blank: function( a ) { return !$.trim("" + a.value); }, + // http://docs.jquery.com/Plugins/Validation/filled + filled: function( a ) { return !!$.trim("" + a.value); }, + // http://docs.jquery.com/Plugins/Validation/unchecked + unchecked: function( a ) { return !a.checked; } +}); + +// constructor for validator +$.validator = function( options, form ) { + this.settings = $.extend( true, {}, $.validator.defaults, options ); + this.currentForm = form; + this.init(); +}; + +$.validator.format = function( source, params ) { + if ( arguments.length === 1 ) { + return function() { + var args = $.makeArray(arguments); + args.unshift(source); + return $.validator.format.apply( this, args ); + }; + } + if ( arguments.length > 2 && params.constructor !== Array ) { + params = $.makeArray(arguments).slice(1); + } + if ( params.constructor !== Array ) { + params = [ params ]; + } + $.each(params, function( i, n ) { + source = source.replace( new RegExp("\\{" + i + "\\}", "g"), function() { + return n; + }); + }); + return source; +}; + +$.extend($.validator, { + + defaults: { + messages: {}, + groups: {}, + rules: {}, + errorClass: "error", + validClass: "valid", + errorElement: "label", + focusInvalid: true, + errorContainer: $([]), + errorLabelContainer: $([]), + onsubmit: true, + ignore: ":hidden", + ignoreTitle: false, + onfocusin: function( element, event ) { + this.lastActive = element; + + // hide error label and remove error class on focus if enabled + if ( this.settings.focusCleanup && !this.blockFocusCleanup ) { + if ( this.settings.unhighlight ) { + this.settings.unhighlight.call( this, element, this.settings.errorClass, this.settings.validClass ); + } + this.addWrapper(this.errorsFor(element)).hide(); + } + }, + onfocusout: function( element, event ) { + if ( !this.checkable(element) && (element.name in this.submitted || !this.optional(element)) ) { + this.element(element); + } + }, + onkeyup: function( element, event ) { + if ( event.which === 9 && this.elementValue(element) === "" ) { + return; + } else if ( element.name in this.submitted || element === this.lastElement ) { + this.element(element); + } + }, + onclick: function( element, event ) { + // click on selects, radiobuttons and checkboxes + if ( element.name in this.submitted ) { + this.element(element); + } + // or option elements, check parent select in that case + else if ( element.parentNode.name in this.submitted ) { + this.element(element.parentNode); + } + }, + highlight: function( element, errorClass, validClass ) { + if ( element.type === "radio" ) { + this.findByName(element.name).addClass(errorClass).removeClass(validClass); + } else { + $(element).addClass(errorClass).removeClass(validClass); + } + }, + unhighlight: function( element, errorClass, validClass ) { + if ( element.type === "radio" ) { + this.findByName(element.name).removeClass(errorClass).addClass(validClass); + } else { + $(element).removeClass(errorClass).addClass(validClass); + } + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/setDefaults + setDefaults: function( settings ) { + $.extend( $.validator.defaults, settings ); + }, + + messages: { + required: "This field is required.", + remote: "Please fix this field.", + email: "Please enter a valid email address.", + url: "Please enter a valid URL.", + date: "Please enter a valid date.", + dateISO: "Please enter a valid date (ISO).", + number: "Please enter a valid number.", + digits: "Please enter only digits.", + creditcard: "Please enter a valid credit card number.", + equalTo: "Please enter the same value again.", + maxlength: $.validator.format("Please enter no more than {0} characters."), + minlength: $.validator.format("Please enter at least {0} characters."), + rangelength: $.validator.format("Please enter a value between {0} and {1} characters long."), + range: $.validator.format("Please enter a value between {0} and {1}."), + max: $.validator.format("Please enter a value less than or equal to {0}."), + min: $.validator.format("Please enter a value greater than or equal to {0}.") + }, + + autoCreateRanges: false, + + prototype: { + + init: function() { + this.labelContainer = $(this.settings.errorLabelContainer); + this.errorContext = this.labelContainer.length && this.labelContainer || $(this.currentForm); + this.containers = $(this.settings.errorContainer).add( this.settings.errorLabelContainer ); + this.submitted = {}; + this.valueCache = {}; + this.pendingRequest = 0; + this.pending = {}; + this.invalid = {}; + this.reset(); + + var groups = (this.groups = {}); + $.each(this.settings.groups, function( key, value ) { + if ( typeof value === "string" ) { + value = value.split(/\s/); + } + $.each(value, function( index, name ) { + groups[name] = key; + }); + }); + var rules = this.settings.rules; + $.each(rules, function( key, value ) { + rules[key] = $.validator.normalizeRule(value); + }); + + function delegate(event) { + var validator = $.data(this[0].form, "validator"), + eventType = "on" + event.type.replace(/^validate/, ""); + if ( validator.settings[eventType] ) { + validator.settings[eventType].call(validator, this[0], event); + } + } + $(this.currentForm) + .validateDelegate(":text, [type='password'], [type='file'], select, textarea, " + + "[type='number'], [type='search'] ,[type='tel'], [type='url'], " + + "[type='email'], [type='datetime'], [type='date'], [type='month'], " + + "[type='week'], [type='time'], [type='datetime-local'], " + + "[type='range'], [type='color'] ", + "focusin focusout keyup", delegate) + .validateDelegate("[type='radio'], [type='checkbox'], select, option", "click", delegate); + + if ( this.settings.invalidHandler ) { + $(this.currentForm).bind("invalid-form.validate", this.settings.invalidHandler); + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/form + form: function() { + this.checkForm(); + $.extend(this.submitted, this.errorMap); + this.invalid = $.extend({}, this.errorMap); + if ( !this.valid() ) { + $(this.currentForm).triggerHandler("invalid-form", [this]); + } + this.showErrors(); + return this.valid(); + }, + + checkForm: function() { + this.prepareForm(); + for ( var i = 0, elements = (this.currentElements = this.elements()); elements[i]; i++ ) { + this.check( elements[i] ); + } + return this.valid(); + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/element + element: function( element ) { + element = this.validationTargetFor( this.clean( element ) ); + this.lastElement = element; + this.prepareElement( element ); + this.currentElements = $(element); + var result = this.check( element ) !== false; + if ( result ) { + delete this.invalid[element.name]; + } else { + this.invalid[element.name] = true; + } + if ( !this.numberOfInvalids() ) { + // Hide error containers on last error + this.toHide = this.toHide.add( this.containers ); + } + this.showErrors(); + return result; + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/showErrors + showErrors: function( errors ) { + if ( errors ) { + // add items to error list and map + $.extend( this.errorMap, errors ); + this.errorList = []; + for ( var name in errors ) { + this.errorList.push({ + message: errors[name], + element: this.findByName(name)[0] + }); + } + // remove items from success list + this.successList = $.grep( this.successList, function( element ) { + return !(element.name in errors); + }); + } + if ( this.settings.showErrors ) { + this.settings.showErrors.call( this, this.errorMap, this.errorList ); + } else { + this.defaultShowErrors(); + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/resetForm + resetForm: function() { + if ( $.fn.resetForm ) { + $(this.currentForm).resetForm(); + } + this.submitted = {}; + this.lastElement = null; + this.prepareForm(); + this.hideErrors(); + this.elements().removeClass( this.settings.errorClass ).removeData( "previousValue" ); + }, + + numberOfInvalids: function() { + return this.objectLength(this.invalid); + }, + + objectLength: function( obj ) { + var count = 0; + for ( var i in obj ) { + count++; + } + return count; + }, + + hideErrors: function() { + this.addWrapper( this.toHide ).hide(); + }, + + valid: function() { + return this.size() === 0; + }, + + size: function() { + return this.errorList.length; + }, + + focusInvalid: function() { + if ( this.settings.focusInvalid ) { + try { + $(this.findLastActive() || this.errorList.length && this.errorList[0].element || []) + .filter(":visible") + .focus() + // manually trigger focusin event; without it, focusin handler isn't called, findLastActive won't have anything to find + .trigger("focusin"); + } catch(e) { + // ignore IE throwing errors when focusing hidden elements + } + } + }, + + findLastActive: function() { + var lastActive = this.lastActive; + return lastActive && $.grep(this.errorList, function( n ) { + return n.element.name === lastActive.name; + }).length === 1 && lastActive; + }, + + elements: function() { + var validator = this, + rulesCache = {}; + + // select all valid inputs inside the form (no submit or reset buttons) + return $(this.currentForm) + .find("input, select, textarea") + .not(":submit, :reset, :image, [disabled]") + .not( this.settings.ignore ) + .filter(function() { + if ( !this.name ) { + if ( window.console ) { + console.error( "%o has no name assigned", this ); + } + throw new Error( "Failed to validate, found an element with no name assigned. See console for element reference." ); + } + + // select only the first element for each name, and only those with rules specified + if ( this.name in rulesCache || !validator.objectLength($(this).rules()) ) { + return false; + } + + rulesCache[this.name] = true; + return true; + }); + }, + + clean: function( selector ) { + return $(selector)[0]; + }, + + errors: function() { + var errorClass = this.settings.errorClass.replace(" ", "."); + return $(this.settings.errorElement + "." + errorClass, this.errorContext); + }, + + reset: function() { + this.successList = []; + this.errorList = []; + this.errorMap = {}; + this.toShow = $([]); + this.toHide = $([]); + this.currentElements = $([]); + }, + + prepareForm: function() { + this.reset(); + this.toHide = this.errors().add( this.containers ); + }, + + prepareElement: function( element ) { + this.reset(); + this.toHide = this.errorsFor(element); + }, + + elementValue: function( element ) { + var type = $(element).attr("type"), + val = $(element).val(); + + if ( type === "radio" || type === "checkbox" ) { + return $("input[name='" + $(element).attr("name") + "']:checked").val(); + } + + if ( typeof val === "string" ) { + return val.replace(/\r/g, ""); + } + return val; + }, + + check: function( element ) { + element = this.validationTargetFor( this.clean( element ) ); + + var rules = $(element).rules(); + var dependencyMismatch = false; + var val = this.elementValue(element); + var result; + + for (var method in rules ) { + var rule = { method: method, parameters: rules[method] }; + try { + + result = $.validator.methods[method].call( this, val, element, rule.parameters ); + + // if a method indicates that the field is optional and therefore valid, + // don't mark it as valid when there are no other rules + if ( result === "dependency-mismatch" ) { + dependencyMismatch = true; + continue; + } + dependencyMismatch = false; + + if ( result === "pending" ) { + this.toHide = this.toHide.not( this.errorsFor(element) ); + return; + } + + if ( !result ) { + this.formatAndAdd( element, rule ); + return false; + } + } catch(e) { + if ( this.settings.debug && window.console ) { + console.log( "Exception occured when checking element " + element.id + ", check the '" + rule.method + "' method.", e ); + } + throw e; + } + } + if ( dependencyMismatch ) { + return; + } + if ( this.objectLength(rules) ) { + this.successList.push(element); + } + return true; + }, + + // return the custom message for the given element and validation method + // specified in the element's HTML5 data attribute + customDataMessage: function( element, method ) { + return $(element).data("msg-" + method.toLowerCase()) || (element.attributes && $(element).attr("data-msg-" + method.toLowerCase())); + }, + + // return the custom message for the given element name and validation method + customMessage: function( name, method ) { + var m = this.settings.messages[name]; + return m && (m.constructor === String ? m : m[method]); + }, + + // return the first defined argument, allowing empty strings + findDefined: function() { + for(var i = 0; i < arguments.length; i++) { + if ( arguments[i] !== undefined ) { + return arguments[i]; + } + } + return undefined; + }, + + defaultMessage: function( element, method ) { + return this.findDefined( + this.customMessage( element.name, method ), + this.customDataMessage( element, method ), + // title is never undefined, so handle empty string as undefined + !this.settings.ignoreTitle && element.title || undefined, + $.validator.messages[method], + "Warning: No message defined for " + element.name + "" + ); + }, + + formatAndAdd: function( element, rule ) { + var message = this.defaultMessage( element, rule.method ), + theregex = /\$?\{(\d+)\}/g; + if ( typeof message === "function" ) { + message = message.call(this, rule.parameters, element); + } else if (theregex.test(message)) { + message = $.validator.format(message.replace(theregex, "{$1}"), rule.parameters); + } + this.errorList.push({ + message: message, + element: element + }); + + this.errorMap[element.name] = message; + this.submitted[element.name] = message; + }, + + addWrapper: function( toToggle ) { + if ( this.settings.wrapper ) { + toToggle = toToggle.add( toToggle.parent( this.settings.wrapper ) ); + } + return toToggle; + }, + + defaultShowErrors: function() { + var i, elements; + for ( i = 0; this.errorList[i]; i++ ) { + var error = this.errorList[i]; + if ( this.settings.highlight ) { + this.settings.highlight.call( this, error.element, this.settings.errorClass, this.settings.validClass ); + } + this.showLabel( error.element, error.message ); + } + if ( this.errorList.length ) { + this.toShow = this.toShow.add( this.containers ); + } + if ( this.settings.success ) { + for ( i = 0; this.successList[i]; i++ ) { + this.showLabel( this.successList[i] ); + } + } + if ( this.settings.unhighlight ) { + for ( i = 0, elements = this.validElements(); elements[i]; i++ ) { + this.settings.unhighlight.call( this, elements[i], this.settings.errorClass, this.settings.validClass ); + } + } + this.toHide = this.toHide.not( this.toShow ); + this.hideErrors(); + this.addWrapper( this.toShow ).show(); + }, + + validElements: function() { + return this.currentElements.not(this.invalidElements()); + }, + + invalidElements: function() { + return $(this.errorList).map(function() { + return this.element; + }); + }, + + showLabel: function( element, message ) { + var label = this.errorsFor( element ); + if ( label.length ) { + // refresh error/success class + label.removeClass( this.settings.validClass ).addClass( this.settings.errorClass ); + + // check if we have a generated label, replace the message then + if ( label.attr("generated") ) { + label.html(message); + } + } else { + // create label + label = $("<" + this.settings.errorElement + "/>") + .attr({"for": this.idOrName(element), generated: true}) + .addClass(this.settings.errorClass) + .html(message || ""); + if ( this.settings.wrapper ) { + // make sure the element is visible, even in IE + // actually showing the wrapped element is handled elsewhere + label = label.hide().show().wrap("<" + this.settings.wrapper + "/>").parent(); + } + if ( !this.labelContainer.append(label).length ) { + if ( this.settings.errorPlacement ) { + this.settings.errorPlacement(label, $(element) ); + } else { + label.insertAfter(element); + } + } + } + if ( !message && this.settings.success ) { + label.text(""); + if ( typeof this.settings.success === "string" ) { + label.addClass( this.settings.success ); + } else { + this.settings.success( label, element ); + } + } + this.toShow = this.toShow.add(label); + }, + + errorsFor: function( element ) { + var name = this.idOrName(element); + return this.errors().filter(function() { + return $(this).attr("for") === name; + }); + }, + + idOrName: function( element ) { + return this.groups[element.name] || (this.checkable(element) ? element.name : element.id || element.name); + }, + + validationTargetFor: function( element ) { + // if radio/checkbox, validate first element in group instead + if ( this.checkable(element) ) { + element = this.findByName( element.name ).not(this.settings.ignore)[0]; + } + return element; + }, + + checkable: function( element ) { + return (/radio|checkbox/i).test(element.type); + }, + + findByName: function( name ) { + return $(this.currentForm).find("[name='" + name + "']"); + }, + + getLength: function( value, element ) { + switch( element.nodeName.toLowerCase() ) { + case "select": + return $("option:selected", element).length; + case "input": + if ( this.checkable( element) ) { + return this.findByName(element.name).filter(":checked").length; + } + } + return value.length; + }, + + depend: function( param, element ) { + return this.dependTypes[typeof param] ? this.dependTypes[typeof param](param, element) : true; + }, + + dependTypes: { + "boolean": function( param, element ) { + return param; + }, + "string": function( param, element ) { + return !!$(param, element.form).length; + }, + "function": function( param, element ) { + return param(element); + } + }, + + optional: function( element ) { + var val = this.elementValue(element); + return !$.validator.methods.required.call(this, val, element) && "dependency-mismatch"; + }, + + startRequest: function( element ) { + if ( !this.pending[element.name] ) { + this.pendingRequest++; + this.pending[element.name] = true; + } + }, + + stopRequest: function( element, valid ) { + this.pendingRequest--; + // sometimes synchronization fails, make sure pendingRequest is never < 0 + if ( this.pendingRequest < 0 ) { + this.pendingRequest = 0; + } + delete this.pending[element.name]; + if ( valid && this.pendingRequest === 0 && this.formSubmitted && this.form() ) { + $(this.currentForm).submit(); + this.formSubmitted = false; + } else if (!valid && this.pendingRequest === 0 && this.formSubmitted) { + $(this.currentForm).triggerHandler("invalid-form", [this]); + this.formSubmitted = false; + } + }, + + previousValue: function( element ) { + return $.data(element, "previousValue") || $.data(element, "previousValue", { + old: null, + valid: true, + message: this.defaultMessage( element, "remote" ) + }); + } + + }, + + classRuleSettings: { + required: {required: true}, + email: {email: true}, + url: {url: true}, + date: {date: true}, + dateISO: {dateISO: true}, + number: {number: true}, + digits: {digits: true}, + creditcard: {creditcard: true} + }, + + addClassRules: function( className, rules ) { + if ( className.constructor === String ) { + this.classRuleSettings[className] = rules; + } else { + $.extend(this.classRuleSettings, className); + } + }, + + classRules: function( element ) { + var rules = {}; + var classes = $(element).attr("class"); + if ( classes ) { + $.each(classes.split(" "), function() { + if ( this in $.validator.classRuleSettings ) { + $.extend(rules, $.validator.classRuleSettings[this]); + } + }); + } + return rules; + }, + + attributeRules: function( element ) { + var rules = {}; + var $element = $(element); + + for (var method in $.validator.methods) { + var value; + + // support for in both html5 and older browsers + if ( method === "required" ) { + value = $element.get(0).getAttribute(method); + // Some browsers return an empty string for the required attribute + // and non-HTML5 browsers might have required="" markup + if ( value === "" ) { + value = true; + } + // force non-HTML5 browsers to return bool + value = !!value; + } else { + value = $element.attr(method); + } + + if ( value ) { + rules[method] = value; + } else if ( $element[0].getAttribute("type") === method ) { + rules[method] = true; + } + } + + // maxlength may be returned as -1, 2147483647 (IE) and 524288 (safari) for text inputs + if ( rules.maxlength && /-1|2147483647|524288/.test(rules.maxlength) ) { + delete rules.maxlength; + } + + return rules; + }, + + dataRules: function( element ) { + var method, value, + rules = {}, $element = $(element); + for (method in $.validator.methods) { + value = $element.data("rule-" + method.toLowerCase()); + if ( value !== undefined ) { + rules[method] = value; + } + } + return rules; + }, + + staticRules: function( element ) { + var rules = {}; + var validator = $.data(element.form, "validator"); + if ( validator.settings.rules ) { + rules = $.validator.normalizeRule(validator.settings.rules[element.name]) || {}; + } + return rules; + }, + + normalizeRules: function( rules, element ) { + // handle dependency check + $.each(rules, function( prop, val ) { + // ignore rule when param is explicitly false, eg. required:false + if ( val === false ) { + delete rules[prop]; + return; + } + if ( val.param || val.depends ) { + var keepRule = true; + switch (typeof val.depends) { + case "string": + keepRule = !!$(val.depends, element.form).length; + break; + case "function": + keepRule = val.depends.call(element, element); + break; + } + if ( keepRule ) { + rules[prop] = val.param !== undefined ? val.param : true; + } else { + delete rules[prop]; + } + } + }); + + // evaluate parameters + $.each(rules, function( rule, parameter ) { + rules[rule] = $.isFunction(parameter) ? parameter(element) : parameter; + }); + + // clean number parameters + $.each(["minlength", "maxlength", "min", "max"], function() { + if ( rules[this] ) { + rules[this] = Number(rules[this]); + } + }); + $.each(["rangelength", "range"], function() { + var parts; + if ( rules[this] ) { + if ( $.isArray(rules[this]) ) { + rules[this] = [Number(rules[this][0]), Number(rules[this][1])]; + } else if ( typeof rules[this] === "string" ) { + parts = rules[this].split(/[\s,]+/); + rules[this] = [Number(parts[0]), Number(parts[1])]; + } + } + }); + + if ( $.validator.autoCreateRanges ) { + // auto-create ranges + if ( rules.min && rules.max ) { + rules.range = [rules.min, rules.max]; + delete rules.min; + delete rules.max; + } + if ( rules.minlength && rules.maxlength ) { + rules.rangelength = [rules.minlength, rules.maxlength]; + delete rules.minlength; + delete rules.maxlength; + } + } + + return rules; + }, + + // Converts a simple string to a {string: true} rule, e.g., "required" to {required:true} + normalizeRule: function( data ) { + if ( typeof data === "string" ) { + var transformed = {}; + $.each(data.split(/\s/), function() { + transformed[this] = true; + }); + data = transformed; + } + return data; + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/addMethod + addMethod: function( name, method, message ) { + $.validator.methods[name] = method; + $.validator.messages[name] = message !== undefined ? message : $.validator.messages[name]; + if ( method.length < 3 ) { + $.validator.addClassRules(name, $.validator.normalizeRule(name)); + } + }, + + methods: { + + // http://docs.jquery.com/Plugins/Validation/Methods/required + required: function( value, element, param ) { + // check if dependency is met + if ( !this.depend(param, element) ) { + return "dependency-mismatch"; + } + if ( element.nodeName.toLowerCase() === "select" ) { + // could be an array for select-multiple or a string, both are fine this way + var val = $(element).val(); + return val && val.length > 0; + } + if ( this.checkable(element) ) { + return this.getLength(value, element) > 0; + } + return $.trim(value).length > 0; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/remote + remote: function( value, element, param ) { + if ( this.optional(element) ) { + return "dependency-mismatch"; + } + + var previous = this.previousValue(element); + if (!this.settings.messages[element.name] ) { + this.settings.messages[element.name] = {}; + } + previous.originalMessage = this.settings.messages[element.name].remote; + this.settings.messages[element.name].remote = previous.message; + + param = typeof param === "string" && {url:param} || param; + + if ( previous.old === value ) { + return previous.valid; + } + + previous.old = value; + var validator = this; + this.startRequest(element); + var data = {}; + data[element.name] = value; + $.ajax($.extend(true, { + url: param, + mode: "abort", + port: "validate" + element.name, + dataType: "json", + data: data, + success: function( response ) { + validator.settings.messages[element.name].remote = previous.originalMessage; + var valid = response === true || response === "true"; + if ( valid ) { + var submitted = validator.formSubmitted; + validator.prepareElement(element); + validator.formSubmitted = submitted; + validator.successList.push(element); + delete validator.invalid[element.name]; + validator.showErrors(); + } else { + var errors = {}; + var message = response || validator.defaultMessage( element, "remote" ); + errors[element.name] = previous.message = $.isFunction(message) ? message(value) : message; + validator.invalid[element.name] = true; + validator.showErrors(errors); + } + previous.valid = valid; + validator.stopRequest(element, valid); + } + }, param)); + return "pending"; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/minlength + minlength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || length >= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/maxlength + maxlength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || length <= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/rangelength + rangelength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || ( length >= param[0] && length <= param[1] ); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/min + min: function( value, element, param ) { + return this.optional(element) || value >= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/max + max: function( value, element, param ) { + return this.optional(element) || value <= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/range + range: function( value, element, param ) { + return this.optional(element) || ( value >= param[0] && value <= param[1] ); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/email + email: function( value, element ) { + // contributed by Scott Gonzalez: http://projects.scottsplayground.com/email_address_validation/ + return this.optional(element) || /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/url + url: function( value, element ) { + // contributed by Scott Gonzalez: http://projects.scottsplayground.com/iri/ + return this.optional(element) || /^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/date + date: function( value, element ) { + return this.optional(element) || !/Invalid|NaN/.test(new Date(value).toString()); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/dateISO + dateISO: function( value, element ) { + return this.optional(element) || /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/number + number: function( value, element ) { + return this.optional(element) || /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/digits + digits: function( value, element ) { + return this.optional(element) || /^\d+$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/creditcard + // based on http://en.wikipedia.org/wiki/Luhn + creditcard: function( value, element ) { + if ( this.optional(element) ) { + return "dependency-mismatch"; + } + // accept only spaces, digits and dashes + if ( /[^0-9 \-]+/.test(value) ) { + return false; + } + var nCheck = 0, + nDigit = 0, + bEven = false; + + value = value.replace(/\D/g, ""); + + for (var n = value.length - 1; n >= 0; n--) { + var cDigit = value.charAt(n); + nDigit = parseInt(cDigit, 10); + if ( bEven ) { + if ( (nDigit *= 2) > 9 ) { + nDigit -= 9; + } + } + nCheck += nDigit; + bEven = !bEven; + } + + return (nCheck % 10) === 0; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/equalTo + equalTo: function( value, element, param ) { + // bind to the blur event of the target in order to revalidate whenever the target field is updated + // TODO find a way to bind the event just once, avoiding the unbind-rebind overhead + var target = $(param); + if ( this.settings.onfocusout ) { + target.unbind(".validate-equalTo").bind("blur.validate-equalTo", function() { + $(element).valid(); + }); + } + return value === target.val(); + } + + } + +}); + +// deprecated, use $.validator.format instead +$.format = $.validator.format; + +}(jQuery)); + +// ajax mode: abort +// usage: $.ajax({ mode: "abort"[, port: "uniqueport"]}); +// if mode:"abort" is used, the previous request on that port (port can be undefined) is aborted via XMLHttpRequest.abort() +(function($) { + var pendingRequests = {}; + // Use a prefilter if available (1.5+) + if ( $.ajaxPrefilter ) { + $.ajaxPrefilter(function( settings, _, xhr ) { + var port = settings.port; + if ( settings.mode === "abort" ) { + if ( pendingRequests[port] ) { + pendingRequests[port].abort(); + } + pendingRequests[port] = xhr; + } + }); + } else { + // Proxy ajax + var ajax = $.ajax; + $.ajax = function( settings ) { + var mode = ( "mode" in settings ? settings : $.ajaxSettings ).mode, + port = ( "port" in settings ? settings : $.ajaxSettings ).port; + if ( mode === "abort" ) { + if ( pendingRequests[port] ) { + pendingRequests[port].abort(); + } + return (pendingRequests[port] = ajax.apply(this, arguments)); + } + return ajax.apply(this, arguments); + }; + } +}(jQuery)); + +// provides delegate(type: String, delegate: Selector, handler: Callback) plugin for easier event delegation +// handler is only called when $(event.target).is(delegate), in the scope of the jquery-object for event.target +(function($) { + $.extend($.fn, { + validateDelegate: function( delegate, type, handler ) { + return this.bind(type, function( event ) { + var target = $(event.target); + if ( target.is(delegate) ) { + return handler.apply(target, arguments); + } + }); + } + }); +}(jQuery)); diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/type-view.hbs b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/type-view.hbs new file mode 100644 index 000000000..1d34030f0 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/type-view.hbs @@ -0,0 +1,336 @@ +

+

Arduino

+
+
+ +
+ +
+ +
+ +
+ +
+

What it Does

+
+

Connect Arduino UNO board to WSO2 IoT Server and visualize sensor + data.

+
+

What You Need

+
+
    +
  • + ITEM 01 +    Arduino UNO Board. +
  • +
  • + ITEM 02 +    Adafruit Wifi Shield for Arduino. +
  • +
  • + ITEM 03 +    LED bulb connected to Pin 13. + (If not available, will use the one on the board.) +
  • +
  • + ITEM 04 +    Resister( e.g 330 ohms ) +
  • +
  • + STEP 05 +    Proceed to [Prepare] section. +
+
+ + View API + + + Download Sketch + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+ +

+
+ +
+

Prepare

+
+
    +
  • + 01 +    Wifi-Shield mounted onto the Arduino-UNO board +
  • +
  • + 02 +    Connect LED bulb to arduino as follows +

    + + + +
    +
  • + +
  • + 03 +    Download Arduino-Sketch from [Download Sketch] link above. +
  • +
  • + 04 +    Unzip the downloaded Arduino Agent +
  • +
  • + 05 +    Create a folder called "ArduinoBoardSketch" and move all source files + inside +
  • +
  • + 06 +    Open ArduinoBoardSketch.h and provide appropriate values for + [WLAN_SSID], + [WLAN_PASS], + [SERVICE_PORT], + [server] and + [deviceIP] + variables according to your network. +
  • +
  • + 07 +    Burn the sketch onto your Arduino board and let the program run. +
  • +
  • +                + + + + +    Arduino will publish it's internal temperature to WSO2-IoT-Server +

    +                + + + + +    LED on PIN 13 can be controlled from Device Management page. +
  • +
+
+
+ +
+
+

Try Out

+
+
    +
  • + 01 +    You can view all your connected devices at + [Device Management] page. +
  • +
  • + 02 +    Select one of connected devices and check for available control + operations and monitor Real-Time data. +
  • +
  • + 03 +    You can also view analytics of the data published to IoT-Server by + navigating to Device Analytics page. +
  • +
+
+
+
+

Click on the image to zoom

+
+ + + +
+
+
+ +{{#zone "topCss"}} + +{{/zone}} + +{{#zone "bottomJs"}} + {{js "/js/download.js"}} + {{js "/js/jquery.validate.js"}} + +{{/zone}} +Status API Training Shop Blog About Pricing diff --git a/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/type-view.json b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/type-view.json new file mode 100644 index 000000000..9eecd8f5b --- /dev/null +++ b/components/iot-plugins/arduino-plugin/org.wso2.carbon.device.mgt.iot.arduino.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.arduino.type-view/type-view.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0" +} \ No newline at end of file diff --git a/components/iot-plugins/arduino-plugin/pom.xml b/components/iot-plugins/arduino-plugin/pom.xml new file mode 100644 index 000000000..9e08a4120 --- /dev/null +++ b/components/iot-plugins/arduino-plugin/pom.xml @@ -0,0 +1,63 @@ + + + + + + + org.wso2.carbon.devicemgt-plugins + iot-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.devicemgt-plugins + arduino-plugin + pom + WSO2 Carbon - Arduino Plugin + http://wso2.org + + + org.wso2.carbon.device.mgt.iot.arduino.analytics + org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl + org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl + org.wso2.carbon.device.mgt.iot.arduino.plugin.impl + org.wso2.carbon.device.mgt.iot.arduino.ui + + + + + + + org.apache.felix + maven-scr-plugin + 1.7.2 + + + generate-scr-scrdescriptor + + scr + + + + + + + + diff --git a/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.controller.service.impl/pom.xml b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.controller.service.impl/pom.xml new file mode 100644 index 000000000..b1930e8f0 --- /dev/null +++ b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.controller.service.impl/pom.xml @@ -0,0 +1,261 @@ + + + + + + camera-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.camera.controller.service.impl + war + WSO2 Carbon - IoT Server Camera API + WSO2 Carbon - Camera Service Controller API Implementation + http://wso2.org + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.core + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.analytics + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.certificate.mgt.core + provided + + + commons-codec.wso2 + commons-codec + + + + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + provided + + + org.apache.cxf + 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 + provided + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.camera.plugin.impl + provided + + + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-jaxrs + + + javax + javaee-web-api + provided + + + javax.ws.rs + jsr311-api + provided + + + commons-httpclient.wso2 + commons-httpclient + provided + + + + org.wso2.carbon + org.wso2.carbon.utils + provided + + + org.bouncycastle.wso2 + bcprov-jdk15on + + + org.wso2.carbon + org.wso2.carbon.user.api + + + org.wso2.carbon + org.wso2.carbon.queuing + + + org.wso2.carbon + org.wso2.carbon.base + + + org.apache.axis2.wso2 + axis2 + + + org.igniterealtime.smack.wso2 + smack + + + org.igniterealtime.smack.wso2 + smackx + + + jaxen + jaxen + + + commons-fileupload.wso2 + commons-fileupload + + + org.apache.ant.wso2 + ant + + + org.apache.ant.wso2 + ant + + + commons-httpclient.wso2 + commons-httpclient + + + org.eclipse.equinox + javax.servlet + + + org.wso2.carbon + org.wso2.carbon.registry.api + + + + + + commons-codec.wso2 + commons-codec + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.annotations + provided + + + + org.igniterealtime.smack.wso2 + smack + provided + + + org.igniterealtime.smack.wso2 + smackx + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.webapp.publisher + provided + + + + + + + + + maven-compiler-plugin + + UTF-8 + ${wso2.maven.compiler.source} + ${wso2.maven.compiler.target} + + + + maven-war-plugin + + camera + + + + + + diff --git a/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.manager.service.impl/pom.xml b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.manager.service.impl/pom.xml new file mode 100644 index 000000000..98667f028 --- /dev/null +++ b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.manager.service.impl/pom.xml @@ -0,0 +1,261 @@ + + + + + + camera-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.camera.manager.service.impl + war + WSO2 Carbon - IoT Server Camera API + WSO2 Carbon - Camera Service Management API Implementation + http://wso2.org + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.core + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.analytics + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.certificate.mgt.core + provided + + + commons-codec.wso2 + commons-codec + + + + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + provided + + + org.apache.cxf + 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 + provided + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.camera.plugin.impl + provided + + + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-jaxrs + + + javax + javaee-web-api + provided + + + javax.ws.rs + jsr311-api + provided + + + commons-httpclient.wso2 + commons-httpclient + provided + + + + org.wso2.carbon + org.wso2.carbon.utils + provided + + + org.bouncycastle.wso2 + bcprov-jdk15on + + + org.wso2.carbon + org.wso2.carbon.user.api + + + org.wso2.carbon + org.wso2.carbon.queuing + + + org.wso2.carbon + org.wso2.carbon.base + + + org.apache.axis2.wso2 + axis2 + + + org.igniterealtime.smack.wso2 + smack + + + org.igniterealtime.smack.wso2 + smackx + + + jaxen + jaxen + + + commons-fileupload.wso2 + commons-fileupload + + + org.apache.ant.wso2 + ant + + + org.apache.ant.wso2 + ant + + + commons-httpclient.wso2 + commons-httpclient + + + org.eclipse.equinox + javax.servlet + + + org.wso2.carbon + org.wso2.carbon.registry.api + + + + + + commons-codec.wso2 + commons-codec + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.annotations + provided + + + + org.igniterealtime.smack.wso2 + smack + provided + + + org.igniterealtime.smack.wso2 + smackx + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.webapp.publisher + provided + + + + + + + + + maven-compiler-plugin + + UTF-8 + ${wso2.maven.compiler.source} + ${wso2.maven.compiler.target} + + + + maven-war-plugin + + camera_mgt + + + + + + diff --git a/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/pom.xml b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/pom.xml new file mode 100644 index 000000000..bd3c82af6 --- /dev/null +++ b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/pom.xml @@ -0,0 +1,123 @@ + + + + + + + camera-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.camera.plugin.impl + bundle + WSO2 Carbon - IoT Server Camera Management Plugin + WSO2 Carbon - Camera Management/Control Plugin Implementation + http://wso2.org + + + + + org.apache.felix + maven-scr-plugin + + + maven-compiler-plugin + + 1.7 + 1.7 + + 2.3.2 + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + ${project.artifactId} + ${project.artifactId} + ${carbon.devicemgt.plugins.version} + IoT Server Camera Impl Bundle + org.wso2.carbon.device.mgt.iot.camera.plugin.internal + + org.osgi.framework, + org.osgi.service.component, + org.apache.commons.logging, + javax.xml.bind.*, + javax.naming, + javax.sql, + javax.xml.bind.annotation.*, + javax.xml.parsers, + javax.net, + javax.net.ssl, + org.w3c.dom, + org.wso2.carbon.device.mgt.common.*, + org.wso2.carbon.device.mgt.common, + org.wso2.carbon.context.*, + org.wso2.carbon.ndatasource.core, + org.wso2.carbon.device.mgt.iot.*, + + + + !org.wso2.carbon.device.mgt.iot.camera.plugin.internal, + org.wso2.carbon.device.mgt.iot.camera.plugin.* + + + + + + + + + + org.eclipse.osgi + org.eclipse.osgi + + + org.eclipse.osgi + org.eclipse.osgi.services + + + + org.wso2.carbon + org.wso2.carbon.logging + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + + + org.wso2.carbon + org.wso2.carbon.ndatasource.core + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot + + + + + \ No newline at end of file diff --git a/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/constants/CameraConstants.java b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/constants/CameraConstants.java new file mode 100644 index 000000000..fa9b91d83 --- /dev/null +++ b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/constants/CameraConstants.java @@ -0,0 +1,27 @@ +/* + * 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.device.mgt.iot.camera.plugin.constants; + +public class CameraConstants { + public final static String DEVICE_TYPE = "camera"; + public final static String DEVICE_PLUGIN_DEVICE_NAME = "DEVICE_NAME"; + public final static String DEVICE_PLUGIN_DEVICE_ID = "CAMERA_DEVICE_ID"; + public final static String STATE_ON = "ON"; + public final static String STATE_OFF = "OFF"; +} diff --git a/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/CameraManager.java b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/CameraManager.java new file mode 100644 index 000000000..1498a4b58 --- /dev/null +++ b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/CameraManager.java @@ -0,0 +1,281 @@ +/* + * 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.device.mgt.iot.camera.plugin.impl; + + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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.common.DeviceManager; +import org.wso2.carbon.device.mgt.common.EnrolmentInfo; +import org.wso2.carbon.device.mgt.common.FeatureManager; +import org.wso2.carbon.device.mgt.common.configuration.mgt.TenantConfiguration; +import org.wso2.carbon.device.mgt.common.license.mgt.License; +import org.wso2.carbon.device.mgt.common.license.mgt.LicenseManagementException; +import org.wso2.carbon.device.mgt.iot.camera.plugin.impl.dao.CameraDAO; +import org.wso2.carbon.device.mgt.iot.util.iotdevice.dao.IotDeviceManagementDAOException; +import org.wso2.carbon.device.mgt.iot.util.iotdevice.dao.IotDeviceManagementDAOFactory; +import org.wso2.carbon.device.mgt.iot.util.iotdevice.dto.IotDevice; +import org.wso2.carbon.device.mgt.iot.util.iotdevice.util.IotDeviceManagementUtil; + +import java.util.ArrayList; +import java.util.List; + + +/** + * This represents the FireAlarm implementation of DeviceManagerService. + */ +public class CameraManager implements DeviceManager { + + private static final IotDeviceManagementDAOFactory iotDeviceManagementDAOFactory = new CameraDAO(); + private static final Log log = LogFactory.getLog(CameraManager.class); + + + @Override + public FeatureManager getFeatureManager() { + return null; + } + + @Override + public boolean saveConfiguration(TenantConfiguration tenantConfiguration) + throws DeviceManagementException { + //TODO implement this + return false; + } + + @Override + public TenantConfiguration getConfiguration() throws DeviceManagementException { + //TODO implement this + return null; + } + + @Override + public boolean enrollDevice(Device device) throws DeviceManagementException { + boolean status; + IotDevice iotDevice = IotDeviceManagementUtil.convertToIotDevice(device); + try { + if (log.isDebugEnabled()) { + log.debug("Enrolling a new Camera device : " + device.getDeviceIdentifier()); + } + CameraDAO.beginTransaction(); + status = iotDeviceManagementDAOFactory.getIotDeviceDAO().addIotDevice( + iotDevice); + CameraDAO.commitTransaction(); + } catch (IotDeviceManagementDAOException e) { + try { + CameraDAO.rollbackTransaction(); + } catch (IotDeviceManagementDAOException iotDAOEx) { + String msg = "Error occurred while roll back the device enrol transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while enrolling the Camera device : " + device.getDeviceIdentifier(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean modifyEnrollment(Device device) throws DeviceManagementException { + boolean status; + IotDevice iotDevice = IotDeviceManagementUtil.convertToIotDevice(device); + try { + if (log.isDebugEnabled()) { + log.debug("Modifying the Camera device enrollment data"); + } + CameraDAO.beginTransaction(); + status = iotDeviceManagementDAOFactory.getIotDeviceDAO() + .updateIotDevice(iotDevice); + CameraDAO.commitTransaction(); + } catch (IotDeviceManagementDAOException e) { + try { + CameraDAO.rollbackTransaction(); + } catch (IotDeviceManagementDAOException iotDAOEx) { + String msg = "Error occurred while roll back the update device transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while updating the enrollment of the Camera device : " + + device.getDeviceIdentifier(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean disenrollDevice(DeviceIdentifier deviceId) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Dis-enrolling Camera device : " + deviceId); + } + CameraDAO.beginTransaction(); + status = iotDeviceManagementDAOFactory.getIotDeviceDAO() + .deleteIotDevice(deviceId.getId()); + CameraDAO.commitTransaction(); + } catch (IotDeviceManagementDAOException e) { + try { + CameraDAO.rollbackTransaction(); + } catch (IotDeviceManagementDAOException iotDAOEx) { + String msg = "Error occurred while roll back the device dis enrol transaction :" + deviceId.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while removing the Camera device : " + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean isEnrolled(DeviceIdentifier deviceId) throws DeviceManagementException { + boolean isEnrolled = false; + try { + if (log.isDebugEnabled()) { + log.debug("Checking the enrollment of Camera device : " + deviceId.getId()); + } + IotDevice iotDevice = + iotDeviceManagementDAOFactory.getIotDeviceDAO().getIotDevice( + deviceId.getId()); + if (iotDevice != null) { + isEnrolled = true; + } + } catch (IotDeviceManagementDAOException e) { + String msg = "Error while checking the enrollment status of Camera device : " + + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return isEnrolled; + } + + @Override + public boolean isActive(DeviceIdentifier deviceId) throws DeviceManagementException { + return true; + } + + @Override + public boolean setActive(DeviceIdentifier deviceId, boolean status) + throws DeviceManagementException { + return true; + } + + @Override + public Device getDevice(DeviceIdentifier deviceId) throws DeviceManagementException { + Device device; + try { + if (log.isDebugEnabled()) { + log.debug("Getting the details of Camera device : " + deviceId.getId()); + } + IotDevice iotDevice = iotDeviceManagementDAOFactory.getIotDeviceDAO(). + getIotDevice(deviceId.getId()); + device = IotDeviceManagementUtil.convertToDevice(iotDevice); + } catch (IotDeviceManagementDAOException e) { + String msg = "Error while fetching the Camera device : " + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return device; + } + + @Override + public boolean setOwnership(DeviceIdentifier deviceId, String ownershipType) + throws DeviceManagementException { + return true; + } + + public boolean isClaimable(DeviceIdentifier deviceIdentifier) throws DeviceManagementException { + return false; + } + + @Override + public boolean setStatus(DeviceIdentifier deviceId, String currentOwner, + EnrolmentInfo.Status status) throws DeviceManagementException { + return false; + } + + @Override + public License getLicense(String s) throws LicenseManagementException { + return null; + } + + @Override + public void addLicense(License license) throws LicenseManagementException { + + } + + @Override + public boolean requireDeviceAuthorization() { + return false; + } + + @Override + public boolean updateDeviceInfo(DeviceIdentifier deviceIdentifier, Device device) throws DeviceManagementException { + boolean status; + IotDevice iotDevice = IotDeviceManagementUtil.convertToIotDevice(device); + try { + if (log.isDebugEnabled()) { + log.debug( + "updating the details of Camera device : " + deviceIdentifier); + } + CameraDAO.beginTransaction(); + status = iotDeviceManagementDAOFactory.getIotDeviceDAO() + .updateIotDevice(iotDevice); + CameraDAO.commitTransaction(); + } catch (IotDeviceManagementDAOException e) { + try { + CameraDAO.rollbackTransaction(); + } catch (IotDeviceManagementDAOException iotDAOEx) { + String msg = "Error occurred while roll back the update device info transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = + "Error while updating the Camera device : " + deviceIdentifier; + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public List getAllDevices() throws DeviceManagementException { + List devices = null; + try { + if (log.isDebugEnabled()) { + log.debug("Fetching the details of all Camera devices"); + } + List iotDevices = + iotDeviceManagementDAOFactory.getIotDeviceDAO().getAllIotDevices(); + if (iotDevices != null) { + devices = new ArrayList(); + for (IotDevice iotDevice : iotDevices) { + devices.add(IotDeviceManagementUtil.convertToDevice(iotDevice)); + } + } + } catch (IotDeviceManagementDAOException e) { + String msg = "Error while fetching all Camera devices."; + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return devices; + } + +} \ No newline at end of file diff --git a/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/CameraManagerService.java b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/CameraManagerService.java new file mode 100644 index 000000000..a6241b940 --- /dev/null +++ b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/CameraManagerService.java @@ -0,0 +1,112 @@ +/* + * 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.device.mgt.iot.camera.plugin.impl; + +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.DeviceManager; +import org.wso2.carbon.device.mgt.common.app.mgt.Application; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManagementException; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManager; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; +import org.wso2.carbon.device.mgt.iot.camera.plugin.constants.CameraConstants; + +import java.util.List; + +public class CameraManagerService implements DeviceManagementService{ + private DeviceManager deviceManager; + @Override + public String getType() { + return CameraConstants.DEVICE_TYPE; + } + + + @Override + public String getProviderTenantDomain() { + return "carbon.super"; + } + + @Override + public boolean isSharedWithAllTenants() { + return true; + } + + @Override + public String[] getSharedTenantsDomain() { + return new String[0]; + } + + @Override + public void init() throws DeviceManagementException { + this.deviceManager=new CameraManager(); + } + + @Override + public DeviceManager getDeviceManager() { + return deviceManager; + } + + @Override + public ApplicationManager getApplicationManager() { + return null; + } + + @Override + public void notifyOperationToDevices(Operation operation, List deviceIds) + throws DeviceManagementException { + + } + + @Override + public Application[] getApplications(String domain, int pageNumber, int size) + throws ApplicationManagementException { + return new Application[0]; + } + + @Override + public void updateApplicationStatus(DeviceIdentifier deviceId, Application application, + String status) throws ApplicationManagementException { + + } + + @Override + public String getApplicationStatus(DeviceIdentifier deviceId, Application application) + throws ApplicationManagementException { + return null; + } + + @Override + public void installApplicationForDevices(Operation operation, List deviceIdentifiers) + throws ApplicationManagementException { + + } + + @Override + public void installApplicationForUsers(Operation operation, List userNameList) + throws ApplicationManagementException { + + } + + @Override + public void installApplicationForUserRoles(Operation operation, List userRoleList) + throws ApplicationManagementException { + + } +} diff --git a/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/dao/CameraDAO.java b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/dao/CameraDAO.java new file mode 100644 index 000000000..35571d851 --- /dev/null +++ b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/dao/CameraDAO.java @@ -0,0 +1,121 @@ +/* + * 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.device.mgt.iot.camera.plugin.impl.dao; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.iot.camera.plugin.constants.CameraConstants; +import org.wso2.carbon.device.mgt.iot.camera.plugin.impl.dao.impl.CameraDeviceDAOImpl; +import org.wso2.carbon.device.mgt.iot.util.iotdevice.dao.IotDeviceDAO; +import org.wso2.carbon.device.mgt.iot.util.iotdevice.dao.IotDeviceManagementDAOException; +import org.wso2.carbon.device.mgt.iot.util.iotdevice.dao.IotDeviceManagementDAOFactory; +import org.wso2.carbon.device.mgt.iot.util.iotdevice.dao.IotDeviceManagementDAOFactoryInterface; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +public class CameraDAO extends IotDeviceManagementDAOFactory implements IotDeviceManagementDAOFactoryInterface { + + private static final Log log = LogFactory.getLog(CameraDAO.class); + static DataSource dataSource; + private static ThreadLocal currentConnection = new ThreadLocal(); + + public CameraDAO() { + initFireAlarmDAO(); + } + + public static void initFireAlarmDAO() { + dataSource = getDataSourceMap().get(CameraConstants.DEVICE_TYPE); + } + + @Override public IotDeviceDAO getIotDeviceDAO() { + return new CameraDeviceDAOImpl(); + } + + public static void beginTransaction() throws IotDeviceManagementDAOException { + try { + Connection conn = dataSource.getConnection(); + conn.setAutoCommit(false); + currentConnection.set(conn); + } catch (SQLException e) { + throw new IotDeviceManagementDAOException("Error occurred while retrieving datasource connection", e); + } + } + + public static Connection getConnection() throws IotDeviceManagementDAOException { + if (currentConnection.get() == null) { + try { + currentConnection.set(dataSource.getConnection()); + } catch (SQLException e) { + throw new IotDeviceManagementDAOException("Error occurred while retrieving data source connection", e); + } + } + return currentConnection.get(); + } + + public static void commitTransaction() throws IotDeviceManagementDAOException { + try { + Connection conn = currentConnection.get(); + if (conn != null) { + conn.commit(); + } else { + if (log.isDebugEnabled()) { + log.debug("Datasource connection associated with the current thread is null, hence commit " + + "has not been attempted"); + } + } + } catch (SQLException e) { + throw new IotDeviceManagementDAOException("Error occurred while committing the transaction", e); + } finally { + closeConnection(); + } + } + + public static void closeConnection() throws IotDeviceManagementDAOException { + + Connection con = currentConnection.get(); + if (con != null) { + try { + con.close(); + } catch (SQLException e) { + log.error("Error occurred while close the connection"); + } + } + currentConnection.remove(); + } + + public static void rollbackTransaction() throws IotDeviceManagementDAOException { + try { + Connection conn = currentConnection.get(); + if (conn != null) { + conn.rollback(); + } else { + if (log.isDebugEnabled()) { + log.debug("Datasource connection associated with the current thread is null, hence rollback " + + "has not been attempted"); + } + } + } catch (SQLException e) { + throw new IotDeviceManagementDAOException("Error occurred while rollback the transaction", e); + } finally { + closeConnection(); + } + } +} \ No newline at end of file diff --git a/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/dao/impl/CameraDeviceDAOImpl.java b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/dao/impl/CameraDeviceDAOImpl.java new file mode 100644 index 000000000..59e68335b --- /dev/null +++ b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/dao/impl/CameraDeviceDAOImpl.java @@ -0,0 +1,237 @@ +/* + * 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.device.mgt.iot.camera.plugin.impl.dao.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.iot.camera.plugin.constants.CameraConstants; +import org.wso2.carbon.device.mgt.iot.camera.plugin.impl.dao.CameraDAO; +import org.wso2.carbon.device.mgt.iot.util.iotdevice.dao.IotDeviceDAO; +import org.wso2.carbon.device.mgt.iot.util.iotdevice.dao.IotDeviceManagementDAOException; +import org.wso2.carbon.device.mgt.iot.util.iotdevice.dao.util.IotDeviceManagementDAOUtil; +import org.wso2.carbon.device.mgt.iot.util.iotdevice.dto.IotDevice; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Implements IotDeviceDAO for camera Devices. + */ +public class CameraDeviceDAOImpl implements IotDeviceDAO{ + + + private static final Log log = LogFactory.getLog(CameraDeviceDAOImpl.class); + + @Override + public IotDevice getIotDevice(String iotDeviceId) + throws IotDeviceManagementDAOException { + Connection conn = null; + PreparedStatement stmt = null; + IotDevice iotDevice = null; + ResultSet resultSet = null; + try { + conn = CameraDAO.getConnection(); + String selectDBQuery = + "SELECT CAMERA_DEVICE_ID, DEVICE_NAME" + + " FROM CAMERA_DEVICE WHERE CAMERA_DEVICE_ID = ?"; + stmt = conn.prepareStatement(selectDBQuery); + stmt.setString(1, iotDeviceId); + resultSet = stmt.executeQuery(); + + if (resultSet.next()) { + iotDevice = new IotDevice(); + iotDevice.setIotDeviceName(resultSet.getString( + CameraConstants.DEVICE_PLUGIN_DEVICE_NAME)); + Map propertyMap = new HashMap(); + + + + iotDevice.setDeviceProperties(propertyMap); + + if (log.isDebugEnabled()) { + log.debug("Camera device " + iotDeviceId + " data has been fetched from " + + "Camera database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while fetching Camera device : '" + iotDeviceId + "'"; + log.error(msg, e); + throw new IotDeviceManagementDAOException(msg, e); + } finally { + IotDeviceManagementDAOUtil.cleanupResources(stmt, resultSet); + CameraDAO.closeConnection(); + } + + return iotDevice; + } + + @Override + public boolean addIotDevice(IotDevice iotDevice) + throws IotDeviceManagementDAOException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = CameraDAO.getConnection(); + String createDBQuery = + "INSERT INTO CAMERA_DEVICE(CAMERA_DEVICE_ID, DEVICE_NAME) VALUES (?, ?)"; + + stmt = conn.prepareStatement(createDBQuery); + stmt.setString(1, iotDevice.getIotDeviceId()); + stmt.setString(2,iotDevice.getIotDeviceName()); + if (iotDevice.getDeviceProperties() == null) { + iotDevice.setDeviceProperties(new HashMap()); + } + + + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Camera device " + iotDevice.getIotDeviceId() + " data has been" + + " added to the Camere database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while adding the Camera device '" + + iotDevice.getIotDeviceId() + "' to the Camera db."; + log.error(msg, e); + throw new IotDeviceManagementDAOException(msg, e); + } finally { + IotDeviceManagementDAOUtil.cleanupResources(stmt, null); + } + return status; + } + + @Override + public boolean updateIotDevice(IotDevice iotDevice) + throws IotDeviceManagementDAOException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = CameraDAO.getConnection(); + String updateDBQuery = + "UPDATE CAMERA_DEVICE SET DEVICE_NAME = ? WHERE CAMERA_DEVICE_ID = ?"; + + stmt = conn.prepareStatement(updateDBQuery); + + if (iotDevice.getDeviceProperties() == null) { + iotDevice.setDeviceProperties(new HashMap()); + } + stmt.setString(1, iotDevice.getIotDeviceName()); + + stmt.setString(2, iotDevice.getIotDeviceId()); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Camera device " + iotDevice.getIotDeviceId() + " data has been" + + " modified."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while modifying the Camera device '" + + iotDevice.getIotDeviceId() + "' data."; + log.error(msg, e); + throw new IotDeviceManagementDAOException(msg, e); + } finally { + IotDeviceManagementDAOUtil.cleanupResources(stmt, null); + } + return status; + } + + @Override + public boolean deleteIotDevice(String iotDeviceId) + throws IotDeviceManagementDAOException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = CameraDAO.getConnection(); + String deleteDBQuery = + "DELETE FROM CAMERA_DEVICE WHERE CAMERA_DEVICE_ID = ?"; + stmt = conn.prepareStatement(deleteDBQuery); + stmt.setString(1, iotDeviceId); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Camera device " + iotDeviceId + " data has deleted from the Camera database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while deleting Camera device " + iotDeviceId; + log.error(msg, e); + throw new IotDeviceManagementDAOException(msg, e); + } finally { + IotDeviceManagementDAOUtil.cleanupResources(stmt, null); + } + return status; + } + + @Override + public List getAllIotDevices() + throws IotDeviceManagementDAOException { + + Connection conn = null; + PreparedStatement stmt = null; + ResultSet resultSet = null; + IotDevice iotDevice; + List iotDevices = new ArrayList(); + + try { + conn = CameraDAO.getConnection(); + String selectDBQuery = + "SELECT CAMERA_DEVICE_ID, DEVICE_NAME " + + "FROM CAMERA_DEVICE"; + stmt = conn.prepareStatement(selectDBQuery); + resultSet = stmt.executeQuery(); + while (resultSet.next()) { + iotDevice = new IotDevice(); + iotDevice.setIotDeviceId(resultSet.getString(CameraConstants.DEVICE_PLUGIN_DEVICE_ID)); + iotDevice.setIotDeviceName(resultSet.getString(CameraConstants.DEVICE_PLUGIN_DEVICE_NAME)); + + Map propertyMap = new HashMap(); + + iotDevice.setDeviceProperties(propertyMap); + iotDevices.add(iotDevice); + } + if (log.isDebugEnabled()) { + log.debug("All Camera device details have fetched from Firealarm database."); + } + return iotDevices; + } catch (SQLException e) { + String msg = "Error occurred while fetching all Camera device data'"; + log.error(msg, e); + throw new IotDeviceManagementDAOException(msg, e); + } finally { + IotDeviceManagementDAOUtil.cleanupResources(stmt, resultSet); + CameraDAO.closeConnection(); + } + + } + + } \ No newline at end of file diff --git a/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/util/VirtualFireAlarmUtils.java b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/util/VirtualFireAlarmUtils.java new file mode 100644 index 000000000..b47c6d84a --- /dev/null +++ b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/impl/util/VirtualFireAlarmUtils.java @@ -0,0 +1,45 @@ +/* + * 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.device.mgt.iot.camera.plugin.impl.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.util.Map; + +/** + * Contains utility methods used by FireAlarm plugin. + */ +public class VirtualFireAlarmUtils { + + private static Log log = LogFactory.getLog(VirtualFireAlarmUtils.class); + + public static String getDeviceProperty(Map deviceProperties, String property) { + + String deviceProperty = deviceProperties.get(property); + + if (deviceProperty == null) { + return ""; + } + + return deviceProperty; + } + + +} diff --git a/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/internal/CameraManagementServiceComponent.java b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/internal/CameraManagementServiceComponent.java new file mode 100644 index 000000000..3546dd987 --- /dev/null +++ b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/camera/plugin/internal/CameraManagementServiceComponent.java @@ -0,0 +1,98 @@ +/* + * 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.device.mgt.iot.camera.plugin.internal; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.ComponentContext; +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; +import org.wso2.carbon.device.mgt.iot.camera.plugin.impl.CameraManagerService; +import org.wso2.carbon.device.mgt.iot.service.DeviceTypeService; + + +/** + * @scr.component name="org.wso2.carbon.device.mgt.iot.camera.plugin.internal + * .CameraManagementServiceComponent" + * immediate="true" + * @scr.reference name="org.wso2.carbon.device.mgt.iot.service.DeviceTypeServiceImpl" + * interface="org.wso2.carbon.device.mgt.iot.service.DeviceTypeService" + * cardinality="1..1" + * policy="dynamic" + * bind="setDeviceTypeService" + * unbind="unsetDeviceTypeService" + */ +public class CameraManagementServiceComponent { + + private static final Log log = LogFactory.getLog(CameraManagementServiceComponent.class); + private ServiceRegistration firealarmServiceRegRef; + + protected void activate(ComponentContext ctx) { + if (log.isDebugEnabled()) { + log.debug("Activating Camera Device Management Service Component"); + } + try { + BundleContext bundleContext = ctx.getBundleContext(); + firealarmServiceRegRef = + bundleContext.registerService(DeviceManagementService.class.getName(), + new CameraManagerService(), null); + + if (log.isDebugEnabled()) { + log.debug( + "Camera Device Management Service Component has been successfully activated"); + } + } catch (Throwable e) { + log.error( + "Error occurred while activating Camera Device Management Service Component", e); + } + } + + protected void deactivate(ComponentContext ctx) { + if (log.isDebugEnabled()) { + log.debug("De-activating Camera Device Management Service Component"); + } + try { + if (firealarmServiceRegRef != null) { + firealarmServiceRegRef.unregister(); + } + + if (log.isDebugEnabled()) { + log.debug( + "Camera Device Management Service Component has been successfully de-activated"); + } + } catch (Throwable e) { + log.error( + "Error occurred while de-activating Camera Device Management bundle", e); + } + } + + protected void setDeviceTypeService(DeviceTypeService deviceTypeService) { + /* This is to avoid this component getting initialized before the common registered */ + if (log.isDebugEnabled()) { + log.debug("Data source service set to mobile service component"); + } + } + + protected void unsetDeviceTypeService(DeviceTypeService deviceTypeService) { + //do nothing + } + + +} diff --git a/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.ui/pom.xml b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.ui/pom.xml new file mode 100644 index 000000000..89bc215a2 --- /dev/null +++ b/components/iot-plugins/camera-plugin/org.wso2.carbon.device.mgt.iot.camera.ui/pom.xml @@ -0,0 +1,62 @@ + + + + + + + + camera-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.camera.ui + WSO2 Carbon - IoT Server Camera UI + pom + + + + + maven-assembly-plugin + 2.5.5 + + ${project.artifactId}-${carbon.device.mgt.version} + false + + src/assembly/src.xml + + + + + create-archive + package + + single + + + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/camera-plugin/pom.xml b/components/iot-plugins/camera-plugin/pom.xml new file mode 100644 index 000000000..ebeed88cc --- /dev/null +++ b/components/iot-plugins/camera-plugin/pom.xml @@ -0,0 +1,63 @@ + + + + + + + org.wso2.carbon.devicemgt-plugins + iot-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.devicemgt-plugins + camera-plugin + pom + WSO2 Carbon - Arduino Plugin + http://wso2.org + + + org.wso2.carbon.device.mgt.iot.camera.controller.service.impl + org.wso2.carbon.device.mgt.iot.camera.manager.service.impl + org.wso2.carbon.device.mgt.iot.camera.plugin.impl + org.wso2.carbon.device.mgt.iot.camera.ui + + + + + + + org.apache.felix + maven-scr-plugin + 1.7.2 + + + generate-scr-scrdescriptor + + scr + + + + + + + + + diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/pom.xml b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/pom.xml new file mode 100644 index 000000000..104e025e7 --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/pom.xml @@ -0,0 +1,243 @@ + + + + + digital-display-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api + war + WSO2 Carbon - IoT Server DigitalDisplay API + WSO2 Carbon - Digital Display Service API Implementation + http://wso2.org + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.core + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.analytics + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.certificate.mgt.core + provided + + + commons-codec.wso2 + commons-codec + + + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + provided + + + org.apache.cxf + 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 + provided + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin + provided + + + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-jaxrs + + + javax + javaee-web-api + provided + + + javax.ws.rs + jsr311-api + provided + + + + commons-httpclient.wso2 + commons-httpclient + provided + + + + org.wso2.carbon + org.wso2.carbon.utils + provided + + + org.bouncycastle.wso2 + bcprov-jdk15on + + + org.wso2.carbon + org.wso2.carbon.user.api + + + org.wso2.carbon + org.wso2.carbon.queuing + + + org.wso2.carbon + org.wso2.carbon.base + + + org.apache.axis2.wso2 + axis2 + + + org.igniterealtime.smack.wso2 + smack + + + org.igniterealtime.smack.wso2 + smackx + + + jaxen + jaxen + + + commons-fileupload.wso2 + commons-fileupload + + + org.apache.ant.wso2 + ant + + + org.apache.ant.wso2 + ant + + + commons-httpclient.wso2 + commons-httpclient + + + org.eclipse.equinox + javax.servlet + + + org.wso2.carbon + org.wso2.carbon.registry.api + + + + + + commons-codec.wso2 + commons-codec + + + + org.igniterealtime.smack.wso2 + smack + provided + + + org.igniterealtime.smack.wso2 + smackx + provided + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.annotations + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.extensions + provided + + + + + + + + maven-compiler-plugin + + UTF-8 + ${wso2.maven.compiler.source} + ${wso2.maven.compiler.target} + + + + maven-war-plugin + + digital_display + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/DigitalDisplayControllerService.java b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/DigitalDisplayControllerService.java new file mode 100644 index 000000000..dde096d50 --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/DigitalDisplayControllerService.java @@ -0,0 +1,432 @@ +/* + * 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.device.mgt.iot.digitaldisplay.controller.api; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.apimgt.annotations.api.API; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.extensions.feature.mgt.annotations.DeviceType; +import org.wso2.carbon.device.mgt.extensions.feature.mgt.annotations.Feature; +import org.wso2.carbon.device.mgt.iot.controlqueue.mqtt.MqttConfig; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api.exception.DigitalDisplayException; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api.util.DigitalDisplayMQTTConnector; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.constants.DigitalDisplayConstants; +import org.wso2.carbon.device.mgt.iot.service.IoTServerStartupListener; +import org.wso2.carbon.device.mgt.iot.transport.TransportHandlerException; +import javax.servlet.http.HttpServletResponse; +import javax.websocket.server.PathParam; +import javax.ws.rs.FormParam; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; + + +@API(name = "digital_display", version = "1.0.0", context = "/digital_display", tags = {"digital_display"}) +@DeviceType(value = "digital_display") +public class DigitalDisplayControllerService { + + private static Log log = LogFactory.getLog(DigitalDisplayControllerService.class); + private static DigitalDisplayMQTTConnector digitalDisplayMQTTConnector; + + private boolean waitForServerStartup() { + while (!IoTServerStartupListener.isServerReady()) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + return true; + } + } + return false; + } + + public DigitalDisplayMQTTConnector getDigitalDisplayMQTTConnector() { + return DigitalDisplayControllerService.digitalDisplayMQTTConnector; + } + + public void setDigitalDisplayMQTTConnector(final + DigitalDisplayMQTTConnector digitalDisplayMQTTConnector) { + + Runnable connector = new Runnable() { + public void run() { + if (waitForServerStartup()) { + return; + } + DigitalDisplayControllerService.digitalDisplayMQTTConnector = digitalDisplayMQTTConnector; + if (MqttConfig.getInstance().isEnabled()) { + digitalDisplayMQTTConnector.connect(); + } else { + log.warn("MQTT disabled in 'devicemgt-config.xml'. " + + "Hence, DigitalDisplayMQTTConnector not started."); + } + } + }; + Thread connectorThread = new Thread(connector); + connectorThread.setDaemon(true); + connectorThread.start(); + } + + /** + * Restart the running browser in the given digital display. + * + * @param deviceId id of the controlling digital display + * @param sessionId web socket id of the method invoke client + * @param response response type of the method + */ + @Path("device/{deviceId}/restart-browser") + @POST + @Feature(code = "restart-browser", name = "Restart Browser", type = "operation", + description = "Restart Browser in Digital Display") + public void restartBrowser(@PathParam("deviceId") String deviceId, + @HeaderParam("sessionId") String sessionId, + @Context HttpServletResponse response) { + try { + sendCommandViaMQTT(deviceId, sessionId + "::" + DigitalDisplayConstants.RESTART_BROWSER_CONSTANT + "::", ""); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceManagementException e) { + log.error(e); + response.setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); + } catch (DigitalDisplayException e) { + log.error(e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + /** + * Terminate all running processes. If this execute we have to reboot digital display manually. + * + * @param deviceId id of the controlling digital display + * @param sessionId web socket id of the method invoke client + * @param response response type of the method + */ + @Path("device/{deviceId}/terminate-display") + @POST + @Feature(code = "terminate-display", name = "Terminate Display", type = "operation", + description = "Terminate all running process in Digital Display") + public void terminateDisplay(@PathParam("deviceId") String deviceId, @HeaderParam("sessionId") String sessionId, + @Context HttpServletResponse response) { + try { + sendCommandViaMQTT(deviceId, sessionId + "::" + DigitalDisplayConstants.TERMINATE_DISPLAY_CONSTANT + "::", ""); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceManagementException e) { + log.error(e); + response.setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); + } catch (DigitalDisplayException e) { + log.error(e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + + } + + /** + * Reboot running digital display + * + * @param deviceId id of the controlling digital display + * @param sessionId web socket id of the method invoke client + * @param response response type of the method + */ + @Path("device/{deviceId}/restart-display") + @POST + @Feature(code = "restart-display", name = "Restart Display", type = "operation", + description = "Restart Digital Display") + public void restartDisplay(@PathParam("deviceId") String deviceId, + @HeaderParam("sessionId") String sessionId, + @Context HttpServletResponse response) { + try { + sendCommandViaMQTT(deviceId, sessionId + "::" + DigitalDisplayConstants.RESTART_DISPLAY_CONSTANT + "::", ""); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceManagementException e) { + log.error(e); + response.setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); + } catch (DigitalDisplayException e) { + log.error(e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + /** + * Search through the sequence and edit requested resource + * + * @param deviceId id of the controlling digital display + * @param sessionId web socket id of the method invoke client + * @param response response type of the method + * @param name name of page need to change + * @param attribute this can be path,time or type + * @param newValue page is used to replace path + */ + @Path("device/{deviceId}/edit-sequence") + @POST + @Feature(code = "edit-sequence", name = "Edit Sequence", type = "operation", + description = "Search through the sequence and edit requested resource in Digital Display") + public void editSequence(@PathParam("deviceId") String deviceId, @FormParam("name") String name, + @FormParam("attribute") String attribute, @FormParam("new-value") String newValue, + @HeaderParam("sessionId") String sessionId, @Context HttpServletResponse response) { + try { + String params = name + "|" + attribute + "|" + newValue; + sendCommandViaMQTT(deviceId, sessionId + "::" + DigitalDisplayConstants.EDIT_SEQUENCE_CONSTANT + "::", params); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceManagementException e) { + log.error(e); + response.setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); + } catch (DigitalDisplayException e) { + log.error(e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + + } + + @Path("device/{deviceId}/upload-content") + @POST + @Feature(code = "upload-content", name = "Upload Content", type = "operation", + description = "Search through the sequence and edit requested resource in Digital Display") + public void uploadContent(@PathParam("deviceId") String deviceId, @FormParam("remote-path") String remotePath, + @FormParam("screen-name") String screenName, @HeaderParam("sessionId") String sessionId, + @Context HttpServletResponse response) { + try { + String params = remotePath + "|" + screenName; + sendCommandViaMQTT(deviceId, sessionId + "::" + DigitalDisplayConstants.UPLOAD_CONTENT_CONSTANT + "::", + params); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceManagementException e) { + log.error(e); + response.setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); + } catch (DigitalDisplayException e) { + log.error(e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + /** + * Add new resource end to the existing sequence + * + * @param deviceId id of the controlling digital display + * @param sessionId web socket id of the method invoke client + * @param response response type of the method + * @param type type of new resource + * @param time new resource visible time + * @param path URL of the new resource + */ + @Path("device/{deviceId}/add-resource") + @POST + @Feature(code = "add-resource", name = "Add Resource", type = "operation", + description = "Add new resource end to the existing sequence in Digital Display") + public void addNewResource(@PathParam("deviceId") String deviceId, + @FormParam("type") String type, + @FormParam("time") String time, + @FormParam("path") String path, + @FormParam("name") String name, + @FormParam("position") String position, + @HeaderParam("sessionId") String sessionId, + @Context HttpServletResponse response) { + String params; + try { + if (position.isEmpty()) { + params = type + "|" + time + "|" + path + "|" + name; + } else { + params = type + "|" + time + "|" + path + "|" + name + + "|" + "after=" + position; + } + sendCommandViaMQTT(deviceId, sessionId + "::" + DigitalDisplayConstants.ADD_NEW_RESOURCE_CONSTANT + "::", params); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceManagementException e) { + log.error(e); + response.setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); + } catch (DigitalDisplayException e) { + log.error(e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + /** + * Delete a resource in sequence + * + * @param deviceId id of the controlling digital display + * @param sessionId web socket id of the method invoke client + * @param response response type of the method + * @param name name of the page no need to delete + */ + @Path("device/{deviceId}/remove-resource") + @POST + @Feature(code = "remove-resource", name = "Remove Resource", type = "operation", + description = "Delete a resource from sequence in Digital Display") + public void removeResource(@PathParam("deviceId") String deviceId, @FormParam("name") String name, + @HeaderParam("sessionId") String sessionId, @Context HttpServletResponse response) { + try { + sendCommandViaMQTT(deviceId, sessionId + "::" + DigitalDisplayConstants.REMOVE_RESOURCE_CONSTANT + "::", name); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceManagementException e) { + log.error(e); + response.setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); + } catch (DigitalDisplayException e) { + log.error(e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + /** + * Restart HTTP in running display + * + * @param deviceId id of the controlling digital display + * @param sessionId web socket id of the method invoke client + * @param response response type of the method + */ + @Path("device/{deviceId}/restart-server") + @POST + @Feature(code = "restart-server", name = "Restart Server", type = "operation", + description = "Stop HTTP Server running in Digital Display") + public void restartServer(@PathParam("deviceId") String deviceId, @HeaderParam("sessionId") String sessionId, + @Context HttpServletResponse response) { + try { + sendCommandViaMQTT(deviceId, sessionId + "::" + DigitalDisplayConstants.RESTART_SERVER_CONSTANT + "::", ""); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceManagementException e) { + log.error(e); + response.setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); + } catch (DigitalDisplayException e) { + log.error(e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + /** + * Get screenshot of running display + * + * @param deviceId id of the controlling digital display + * @param sessionId web socket id of the method invoke client + * @param response response type of the method + */ + @Path("device/{deviceId}/screenshot") + @POST + @Feature(code = "screenshot", name = "Take Screenshot", type = "operation", + description = "Show current view in Digital Display") + public void showScreenshot(@PathParam("deviceId") String deviceId, + @HeaderParam("sessionId") String sessionId, + @Context HttpServletResponse response) { + try { + sendCommandViaMQTT(deviceId, sessionId + "::" + DigitalDisplayConstants.SCREENSHOT_CONSTANT + "::", ""); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceManagementException e) { + log.error(e); + response.setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); + } catch (DigitalDisplayException e) { + log.error(e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + /** + * Get statistics of running display + * + * @param deviceId id of the controlling digital display + * @param sessionId web socket id of the method invoke client + * @param response response type of the method + */ + @Path("device/{deviceId}/get-device-status") + @POST + @Feature(code = "get-device-status", name = "Get Device Statistics", type = "operation", + description = "Current status in Digital Display") + public void getDevicestatus(@PathParam("deviceId") String deviceId, @HeaderParam("sessionId") String sessionId, + @Context HttpServletResponse response) { + try { + sendCommandViaMQTT(deviceId, sessionId + "::" + DigitalDisplayConstants.GET_DEVICE_STATUS_CONSTANT + "::", ""); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceManagementException e) { + log.error(e); + response.setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); + } catch (DigitalDisplayException e) { + log.error(e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + /** + * Stop specific display + * + * @param deviceId id of the controlling digital display + * @param sessionId web socket id of the method invoke client + * @param response response type of the method + */ + @Path("device/{deviceId}/get-content-list") + @POST + @Feature(code = "get-content-list", name = "Get Content List", type = "operation", + description = "Content List in Digital Display") + public void getResources(@PathParam("deviceId") String deviceId, @HeaderParam("sessionId") String sessionId, + @Context HttpServletResponse response) { + try { + sendCommandViaMQTT(deviceId, sessionId + "::" + DigitalDisplayConstants.GET_CONTENTLIST_CONSTANT + "::", ""); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceManagementException e) { + log.error(e); + response.setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); + } catch (DigitalDisplayException e) { + log.error(e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + /** + * Send message via MQTT protocol + * + * @param deviceId id of the target digital display + * @param operation operation need to execute + * @param param parameters need to given operation + * @throws DeviceManagementException + * @throws DigitalDisplayException + */ + private void sendCommandViaMQTT(String deviceId, String operation, String param) + throws DeviceManagementException, DigitalDisplayException { + String topic = String.format(DigitalDisplayConstants.PUBLISH_TOPIC, deviceId); + String payload = operation + param; + try { + digitalDisplayMQTTConnector.publishToDigitalDisplay(topic, payload, 2, false); + } catch (TransportHandlerException e) { + String errorMessage = "Error publishing data to device with ID " + deviceId; + throw new DigitalDisplayException(errorMessage, e); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + +} diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/dto/DeviceJSON.java b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/dto/DeviceJSON.java new file mode 100644 index 000000000..309a6cf9f --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/dto/DeviceJSON.java @@ -0,0 +1,33 @@ +/* + * 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.device.mgt.iot.digitaldisplay.controller.api.dto; + +import org.codehaus.jackson.annotate.JsonIgnoreProperties; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement + +@JsonIgnoreProperties(ignoreUnknown = true) +public class DeviceJSON { + @XmlElement(required = true) public String owner; + @XmlElement(required = true) public String deviceId; + @XmlElement(required = true) public String reply; +} diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/exception/DigitalDisplayException.java b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/exception/DigitalDisplayException.java new file mode 100644 index 000000000..c80d9e61a --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/exception/DigitalDisplayException.java @@ -0,0 +1,44 @@ +package org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api.exception; + +/** + * Created by nuwan on 12/2/15. + */ +public class DigitalDisplayException extends Exception { + + private static final long serialVersionUID = 2736466230451105441L; + + private String errorMessage; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public DigitalDisplayException(String msg, DigitalDisplayException nestedEx) { + super(msg, nestedEx); + setErrorMessage(msg); + } + + public DigitalDisplayException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public DigitalDisplayException(String msg) { + super(msg); + setErrorMessage(msg); + } + + public DigitalDisplayException() { + super(); + } + + public DigitalDisplayException(Throwable cause) { + super(cause); + } + + +} diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/model/ScreenShotModel.java b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/model/ScreenShotModel.java new file mode 100644 index 000000000..e20f6991a --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/model/ScreenShotModel.java @@ -0,0 +1,33 @@ +package org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api.model; + +public class ScreenShotModel { + + private String[] screenShotData; + private int length; + + public ScreenShotModel(){ + + } + + public ScreenShotModel(String[] screenShotData , int length){ + this.screenShotData = screenShotData; + this.length = length; + } + + public void setScreenShotData(String[] screrenShotData){ + this.screenShotData = screenShotData; + } + + public void setLength(int length){ + this.length = length; + } + + public String[] getScreenShotData(){ + return this.screenShotData; + } + + public int getLength(){ + return this.length; + } + +} diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/util/DigitalDisplayMQTTConnector.java b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/util/DigitalDisplayMQTTConnector.java new file mode 100644 index 000000000..b356640ca --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/util/DigitalDisplayMQTTConnector.java @@ -0,0 +1,185 @@ +package org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api.util; + +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.json.JSONObject; +import org.wso2.carbon.device.mgt.iot.controlqueue.mqtt.MqttConfig; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api.model.ScreenShotModel; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api.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 java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ScheduledFuture; + +@SuppressWarnings("no JAX-WS annotation") +public class DigitalDisplayMQTTConnector extends MQTTTransportHandler { + + private static Log log = LogFactory.getLog(DigitalDisplayMQTTConnector.class); + 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 String iotServerSubscriber = UUID.randomUUID().toString().substring(0, 5); + + private ScheduledFuture dataPushServiceHandler; + + private Map screenshots = new HashMap<>(); + + private DigitalDisplayMQTTConnector() { + super(iotServerSubscriber, DigitalDisplayConstants.DEVICE_TYPE, + MqttConfig.getInstance().getMqttQueueEndpoint(), subscribeTopic); + } + + @Override + public void connect() { + Runnable connector = new Runnable() { + public void run() { + while (!isConnected()) { + try { + String brokerUsername = MqttConfig.getInstance().getMqttQueueUsername(); + String brokerPassword = MqttConfig.getInstance().getMqttQueuePassword(); + setUsernameAndPassword(brokerUsername, brokerPassword); + 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); + } + } + } + }; + + Thread connectorThread = new Thread(connector); + connectorThread.setDaemon(true); + connectorThread.start(); + } + + @Override + public void processIncomingMessage(MqttMessage message, String... messageParams) { + String topic = messageParams[0]; + String[] topicParams = topic.split("/"); + String owner = topicParams[2]; + String deviceId = topicParams[4]; + String[] messageData = message.toString().split("::"); + + if (log.isDebugEnabled()) { + log.debug("Received MQTT message for: [OWNER-" + owner + "] & [DEVICE.ID-" + deviceId + "]"); + } + + String sessionId = messageData[0]; + if (messageData.length == 2) { + String responseMessage = messageData[1]; + DigitalDisplayWebSocketServerEndPoint.sendMessage(sessionId, new StringBuilder(responseMessage)); + } else if (messageData.length == 3) { + String response = messageData[2]; + JSONObject schreenShot = new JSONObject(response); + String picId = schreenShot.getString("pic_id"); + String data = schreenShot.getString("data"); + int pos = schreenShot.getInt("pos"); + int length = schreenShot.getInt("size"); + createScreenShot(sessionId, picId, pos, length, data); + } + } + + private void createScreenShot(String sessionId, String picId, int pos, int length, String data) { + + ScreenShotModel screenShotModel = screenshots.get(picId); + + if (screenShotModel == null) { + screenShotModel = new ScreenShotModel(); + screenShotModel.setScreenShotData(new String[length + 1]); + screenShotModel.setLength(0); + screenshots.put(picId, screenShotModel); + } + if (screenShotModel.getLength() <= length) { + screenShotModel.getScreenShotData()[pos] = data; + screenShotModel.setLength(screenShotModel.getLength() + 1); + if (screenShotModel.getLength() == (length + 1)) { + StringBuilder displayScreenShot = new StringBuilder("Screenshot||"); + for (String screenshot : screenShotModel.getScreenShotData()) { + displayScreenShot.append(screenshot); + } + screenshots.remove(picId); + DigitalDisplayWebSocketServerEndPoint.sendMessage(sessionId, displayScreenShot); + } + } + } + + public void publishToDigitalDisplay(String topic, String payLoad, int qos, boolean retained) + throws TransportHandlerException { + if (log.isDebugEnabled()) { + log.debug("Publishing message [" + payLoad + "to topic [" + topic + "]."); + } + publishToQueue(topic, payLoad, qos, retained); + } + + @Override + public void disconnect() { + Runnable stopConnection = new Runnable() { + public void run() { + while (isConnected()) { + try { + closeConnection(); + } catch (MqttException e) { + if (log.isDebugEnabled()) { + log.warn("Unable to 'STOP' MQTT connection at broker at: " + mqttBrokerEndPoint); + } + + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException e1) { + log.error("MQTT-Terminator: Thread Sleep Interrupt Exception"); + } + } + } + } + }; + + Thread terminatorThread = new Thread(stopConnection); + terminatorThread.setDaemon(true); + terminatorThread.start(); + } + + + @Override + public void publishDeviceData() throws TransportHandlerException { + + } + + @Override + public void publishDeviceData(MqttMessage publishData) throws TransportHandlerException { + + } + + @Override + public void publishDeviceData(String... publishData) throws TransportHandlerException { + + } + + @Override + public void processIncomingMessage() { + + } + + @Override + public void processIncomingMessage(MqttMessage message) throws TransportHandlerException { + + } + +} diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/websocket/DigitalDisplayWebSocketServerEndPoint.java b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/websocket/DigitalDisplayWebSocketServerEndPoint.java new file mode 100644 index 000000000..5ac54331e --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/controller/api/websocket/DigitalDisplayWebSocketServerEndPoint.java @@ -0,0 +1,64 @@ +package org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api.websocket; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import javax.inject.Singleton; +import javax.websocket.*; +import javax.websocket.server.PathParam; +import javax.websocket.server.ServerEndpoint; +import java.util.HashMap; +import java.util.Map; + +@ServerEndpoint(value = "/{sessionId}") +@Singleton +public class DigitalDisplayWebSocketServerEndPoint { + + private static Log log = LogFactory.getLog(DigitalDisplayWebSocketServerEndPoint.class); + private static Map clientSessions = new HashMap<>(); + + /** + * This method will be invoked when a client requests for a + * WebSocket connection. + * + * @param userSession the userSession which is opened. + */ + @OnOpen + public void onOpen(Session userSession, @PathParam("sessionId") String sessionId) { + clientSessions.put(sessionId, userSession); + } + + /** + * This method will be invoked when a client closes a WebSocket + * connection. + * + * @param userSession the userSession which is opened. + */ + @OnClose + public void onClose(Session userSession) { + clientSessions.values().remove(userSession); + + } + + @OnError + public void onError(Throwable t) { + log.error("Error occurred " + t); + } + + /** + * This method will be invoked when a message received from device + * to send client. + * + * @param sessionId the client of message to be sent. + * @param message the message sent by device to client + */ + public static void sendMessage(String sessionId, StringBuilder message) { + Session session = clientSessions.get(sessionId); + if (session != null) { + session.getAsyncRemote().sendText(message.toString()); + } else { + log.error("Client already disconnected."); + } + } + +} diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/webapp/META-INF/webapp-classloading.xml b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/webapp/META-INF/webapp-classloading.xml new file mode 100644 index 000000000..fa4461919 --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/webapp/META-INF/webapp-classloading.xml @@ -0,0 +1,33 @@ + + + + + + + + + false + + + CXF,Carbon + diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/webapp/WEB-INF/cxf-servlet.xml b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/webapp/WEB-INF/cxf-servlet.xml new file mode 100644 index 000000000..edb998126 --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/webapp/WEB-INF/cxf-servlet.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/webapp/WEB-INF/web.xml b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..c6fb5fe50 --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,77 @@ + + + + Digital-Display-Agent-Webapp + + JAX-WS/JAX-RS MDM Android Endpoint + JAX-WS/JAX-RS Servlet + CXFServlet + + org.apache.cxf.transport.servlet.CXFServlet + + 1 + + + CXFServlet + /* + + + + isSharedWithAllTenants + true + + + providerTenantDomain + carbon.super + + + isAdminService + false + + + doAuthentication + true + + + + + managed-api-enabled + true + + + managed-api-owner + admin + + + managed-api-context-template + /digital_display/{version} + + + managed-api-application + digital_display + + + managed-api-isSecured + true + + + diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/pom.xml b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/pom.xml new file mode 100644 index 000000000..b68cd2afd --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/pom.xml @@ -0,0 +1,246 @@ + + + + + digital-display-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api + war + WSO2 Carbon - IoT Server DigitalDisplay API + WSO2 Carbon - Digital Display Service API Implementation + http://wso2.org + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.core + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.analytics + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.certificate.mgt.core + provided + + + commons-codec.wso2 + commons-codec + + + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + provided + + + org.apache.cxf + 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 + provided + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin + provided + + + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-jaxrs + + + javax + javaee-web-api + provided + + + javax.ws.rs + jsr311-api + provided + + + + commons-httpclient.wso2 + commons-httpclient + provided + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.webapp.publisher + provided + + + + org.wso2.carbon + org.wso2.carbon.utils + provided + + + org.bouncycastle.wso2 + bcprov-jdk15on + + + org.wso2.carbon + org.wso2.carbon.user.api + + + org.wso2.carbon + org.wso2.carbon.queuing + + + org.wso2.carbon + org.wso2.carbon.base + + + org.apache.axis2.wso2 + axis2 + + + org.igniterealtime.smack.wso2 + smack + + + org.igniterealtime.smack.wso2 + smackx + + + jaxen + jaxen + + + commons-fileupload.wso2 + commons-fileupload + + + org.apache.ant.wso2 + ant + + + org.apache.ant.wso2 + ant + + + commons-httpclient.wso2 + commons-httpclient + + + org.eclipse.equinox + javax.servlet + + + org.wso2.carbon + org.wso2.carbon.registry.api + + + + + + commons-codec.wso2 + commons-codec + + + org.igniterealtime.smack.wso2 + smack + provided + + + org.igniterealtime.smack.wso2 + smackx + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.jwt.client.extension + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.application.extension + provided + + + + + + + maven-compiler-plugin + + UTF-8 + ${wso2.maven.compiler.source} + ${wso2.maven.compiler.target} + + + + maven-war-plugin + + digital_display_mgt + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/manager/api/DigitalDisplayManagerService.java b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/manager/api/DigitalDisplayManagerService.java new file mode 100644 index 000000000..063a23d39 --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/manager/api/DigitalDisplayManagerService.java @@ -0,0 +1,243 @@ +/* + * 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.device.mgt.iot.digitaldisplay.manager.api; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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.common.Device; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.EnrolmentInfo; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api.util.APIUtil; +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.device.mgt.iot.digitaldisplay.plugin.constants.DigitalDisplayConstants; +import org.wso2.carbon.device.mgt.jwt.client.extension.JWTClient; +import org.wso2.carbon.device.mgt.jwt.client.extension.JWTClientManager; +import org.wso2.carbon.device.mgt.jwt.client.extension.dto.AccessTokenInfo; +import org.wso2.carbon.device.mgt.jwt.client.extension.exception.JWTClientException; +import org.wso2.carbon.user.api.UserStoreException; + +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.UUID; + +public class DigitalDisplayManagerService { + + private static Log log = LogFactory.getLog(DigitalDisplayManagerService.class); + @Context //injected response proxy supporting multiple thread + private HttpServletResponse response; + private static final String KEY_TYPE = "PRODUCTION"; + private static ApiApplicationKey apiApplicationKey; + + @Path("manager/device") + @POST + public boolean register(@QueryParam("deviceId") String deviceId, @QueryParam("name") String name) { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(DigitalDisplayConstants.DEVICE_TYPE); + try { + if (APIUtil.getDeviceManagementService().isEnrolled(deviceIdentifier)) { + response.setStatus(Response.Status.CONFLICT.getStatusCode()); + return false; + } + Device device = new Device(); + device.setDeviceIdentifier(deviceId); + EnrolmentInfo enrolmentInfo = new EnrolmentInfo(); + enrolmentInfo.setDateOfEnrolment(new Date().getTime()); + enrolmentInfo.setDateOfLastUpdate(new Date().getTime()); + enrolmentInfo.setStatus(EnrolmentInfo.Status.ACTIVE); + device.setName(name); + device.setType(DigitalDisplayConstants.DEVICE_TYPE); + enrolmentInfo.setOwner(APIUtil.getAuthenticatedUser()); + device.setEnrolmentInfo(enrolmentInfo); + boolean added = APIUtil.getDeviceManagementService().enrollDevice(device); + if (added) { + response.setStatus(Response.Status.OK.getStatusCode()); + } else { + response.setStatus(Response.Status.NOT_ACCEPTABLE.getStatusCode()); + } + + return added; + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/device/{device_id}") + @DELETE + public void removeDevice(@PathParam("device_id") String deviceId, @Context HttpServletResponse response) { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(DigitalDisplayConstants.DEVICE_TYPE); + try { + boolean removed = APIUtil.getDeviceManagementService().disenrollDevice( + deviceIdentifier); + if (removed) { + response.setStatus(Response.Status.OK.getStatusCode()); + } else { + response.setStatus(Response.Status.NOT_ACCEPTABLE.getStatusCode()); + } + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/device/{device_id}") + @PUT + public boolean updateDevice(@PathParam("device_id") String deviceId, + @QueryParam("name") String name, + @Context HttpServletResponse response) { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(DigitalDisplayConstants.DEVICE_TYPE); + try { + Device device = APIUtil.getDeviceManagementService().getDevice(deviceIdentifier); + device.setDeviceIdentifier(deviceId); + device.getEnrolmentInfo().setDateOfLastUpdate(new Date().getTime()); + device.setName(name); + device.setType(DigitalDisplayConstants.DEVICE_TYPE); + boolean updated = APIUtil.getDeviceManagementService().modifyEnrollment(device); + if (updated) { + response.setStatus(Response.Status.OK.getStatusCode()); + + } else { + response.setStatus(Response.Status.NOT_ACCEPTABLE.getStatusCode()); + } + return updated; + } catch (DeviceManagementException e) { + log.error(e.getErrorMessage()); + return false; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/device/{device_id}") + @GET + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Device getDevice(@PathParam("device_id") String deviceId) { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(DigitalDisplayConstants.DEVICE_TYPE); + try { + return APIUtil.getDeviceManagementService().getDevice(deviceIdentifier); + } catch (DeviceManagementException ex) { + log.error("Error occurred while retrieving device with Id " + deviceId + "\n" + ex); + return null; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/device/{sketch_type}/download") + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response downloadSketch(@QueryParam("deviceName") String deviceName, + @PathParam("sketch_type") String + sketchType) { + try { + ZipArchive zipFile = createDownloadFile(APIUtil.getAuthenticatedUser(), deviceName, sketchType); + Response.ResponseBuilder response = Response.ok(FileUtils.readFileToByteArray(zipFile.getZipFile())); + response.type("application/zip"); + response.header("Content-Disposition", "attachment; filename=\"" + zipFile.getFileName() + "\""); + return response.build(); + } catch (IllegalArgumentException ex) { + return Response.status(400).entity(ex.getMessage()).build();//bad request + } catch (DeviceManagementException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (JWTClientException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (DeviceControllerException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (APIManagerException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (IOException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (UserStoreException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + private ZipArchive createDownloadFile(String owner, String deviceName, String sketchType) + throws DeviceManagementException, JWTClientException, DeviceControllerException, APIManagerException, + UserStoreException { + if (owner == null) { + throw new IllegalArgumentException("Error on createDownloadFile() Owner is null!"); + } + //create new device id + String deviceId = shortUUID(); + if (apiApplicationKey == null) { + String applicationUsername = + PrivilegedCarbonContext.getThreadLocalCarbonContext().getUserRealm().getRealmConfiguration().getAdminUserName(); + APIManagementProviderService apiManagementProviderService = APIUtil.getAPIManagementProviderService(); + String[] tags = {DigitalDisplayConstants.DEVICE_TYPE}; + apiApplicationKey = apiManagementProviderService.generateAndRetrieveApplicationKeys( + DigitalDisplayConstants.DEVICE_TYPE, tags, KEY_TYPE, applicationUsername, true); + } + JWTClient jwtClient = JWTClientManager.getInstance().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 + 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; + throw new DeviceManagementException(msg); + } + ZipUtil ziputil = new ZipUtil(); + ZipArchive zipFile = ziputil.createZipFile(owner, APIUtil.getTenantDomainOftheUser(), sketchType, deviceId, + deviceName, accessToken, refreshToken); + zipFile.setDeviceId(deviceId); + return zipFile; + } + + private static String shortUUID() { + UUID uuid = UUID.randomUUID(); + long l = ByteBuffer.wrap(uuid.toString().getBytes(StandardCharsets.UTF_8)).getLong(); + return Long.toString(l, Character.MAX_RADIX); + } + +} diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/manager/api/util/APIUtil.java b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/manager/api/util/APIUtil.java new file mode 100644 index 000000000..a593b67cf --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/digitaldisplay/manager/api/util/APIUtil.java @@ -0,0 +1,54 @@ +package org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api.util; + +import org.apache.commons.logging.Log; +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; +/** + * This class provides utility functions used by REST-API. + */ +public class APIUtil { + + private static Log log = LogFactory.getLog(APIUtil.class); + + public static String getAuthenticatedUser() { + PrivilegedCarbonContext threadLocalCarbonContext = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + String username = threadLocalCarbonContext.getUsername(); + String tenantDomain = threadLocalCarbonContext.getTenantDomain(); + if (username.endsWith(tenantDomain)) { + return username.substring(0, username.lastIndexOf("@")); + } + 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 = + (DeviceManagementProviderService) ctx.getOSGiService(DeviceManagementProviderService.class, null); + if (deviceManagementProviderService == null) { + String msg = "Device Management service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return deviceManagementProviderService; + } + + 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; + } +} diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/webapp/META-INF/permissions.xml b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/webapp/META-INF/permissions.xml new file mode 100644 index 000000000..91ed83257 --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/webapp/META-INF/permissions.xml @@ -0,0 +1,66 @@ + + + + + + + + + + Get device + /device-mgt/user/devices/list + /manager/device/{device_id} + GET + + + + Add device + /device-mgt/user/devices/add + /manager/device + POST + + + + Remove device + /device-mgt/user/devices/remove + /manager/device/{device_id} + DELETE + + + + Download device + /device-mgt/user/devices/add + /manager/device/{sketch_type}/download + GET + + + + Update device + /device-mgt/user/devices/update + /manager/device/{device_id} + POST + + + \ No newline at end of file diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/webapp/META-INF/webapp-classloading.xml b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/webapp/META-INF/webapp-classloading.xml new file mode 100644 index 000000000..fa4461919 --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/webapp/META-INF/webapp-classloading.xml @@ -0,0 +1,33 @@ + + + + + + + + + false + + + CXF,Carbon + diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/webapp/WEB-INF/cxf-servlet.xml b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/webapp/WEB-INF/cxf-servlet.xml new file mode 100644 index 000000000..14cc35426 --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/webapp/WEB-INF/cxf-servlet.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/webapp/WEB-INF/web.xml b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..e5ae03197 --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,68 @@ + + + + Digital-Display-Agent-Webapp + + JAX-WS/JAX-RS MDM Android Endpoint + JAX-WS/JAX-RS Servlet + CXFServlet + org.apache.cxf.transport.servlet.CXFServlet + 1 + + + CXFServlet + /* + + + + isAdminService + false + + + doAuthentication + true + + + + + managed-api-enabled + false + + + managed-api-owner + admin + + + managed-api-context-template + /digital_display/{version} + + + managed-api-application + digital_display + + + managed-api-isSecured + true + + + + diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin/pom.xml b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin/pom.xml new file mode 100644 index 000000000..6cce70392 --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin/pom.xml @@ -0,0 +1,106 @@ + + + + + digital-display-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin + bundle + WSO2 Carbon - IoT Server DigitalDisplay Management Plugin + WSO2 Carbon - Digital Display Device-Type Plugin Implementation + http://wso2.org + + + + + org.apache.felix + maven-scr-plugin + + + maven-compiler-plugin + + 1.7 + 1.7 + + 2.3.2 + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + ${project.artifactId} + ${project.artifactId} + ${carbon.devicemgt.plugins.version} + IoT Server Impl Bundle + org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.internal + + org.osgi.framework, + org.osgi.service.component, + org.apache.commons.logging, + javax.xml.bind.*;resolution:=optional, + javax.naming;resolution:=optional, + javax.sql;resolution:=optional, + javax.xml.bind.annotation.*;resolution:=optional, + javax.xml.parsers;resolution:=optional, + javax.net, + javax.net.ssl, + org.w3c.dom, + org.wso2.carbon.device.mgt.common.*, + org.wso2.carbon.device.mgt.common, + org.wso2.carbon.context.*, + org.wso2.carbon.ndatasource.core, + org.wso2.carbon.device.mgt.iot.*, + org.wso2.carbon.device.mgt.extensions.feature.mgt.*, + org.wso2.carbon.utils.* + + + !org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.internal, + org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.* + + + + + + + + + + org.eclipse.osgi + org.eclipse.osgi + + + org.eclipse.osgi + org.eclipse.osgi.services + + + org.wso2.carbon + org.wso2.carbon.logging + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + + + org.wso2.carbon + org.wso2.carbon.ndatasource.core + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.extensions + + + org.wso2.carbon + org.wso2.carbon.utils + + + \ No newline at end of file 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 new file mode 100644 index 000000000..00ef15972 --- /dev/null +++ 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 @@ -0,0 +1,37 @@ +/* +* Copyright (c) 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.device.mgt.iot.digitaldisplay.plugin.constants; + +public class DigitalDisplayConstants { + + public final static String DEVICE_TYPE = "digital_display"; + public final static String DEVICE_PLUGIN_DEVICE_NAME = "DEVICE_NAME"; + public final static String DEVICE_PLUGIN_DEVICE_ID = "DIGITAL_DISPLAY_DEVICE_ID"; + public final static String RESTART_SERVER_CONSTANT = "restart_server"; + public final static String RESTART_DISPLAY_CONSTANT = "restart_display"; + public final static String RESTART_BROWSER_CONSTANT = "restart_browser"; + public final static String TERMINATE_DISPLAY_CONSTANT = "terminate_display"; + public final static String EDIT_SEQUENCE_CONSTANT = "edit_sequence"; + public final static String UPLOAD_CONTENT_CONSTANT = "upload_content"; + public final static String ADD_NEW_RESOURCE_CONSTANT = "add_new_resource"; + public final static String REMOVE_RESOURCE_CONSTANT = "remove_resources"; + public final static String SCREENSHOT_CONSTANT = "get_screenshot"; + public final static String GET_CONTENTLIST_CONSTANT = "get_content_list"; + 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"; +} 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/exception/DigitalDisplayDeviceMgtPluginException.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/exception/DigitalDisplayDeviceMgtPluginException.java new file mode 100644 index 000000000..6b184e57b --- /dev/null +++ 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/exception/DigitalDisplayDeviceMgtPluginException.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.device.mgt.iot.digitaldisplay.plugin.exception; + + +public class DigitalDisplayDeviceMgtPluginException extends Exception{ + + private String errorMessage; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public DigitalDisplayDeviceMgtPluginException(String msg, Exception nestedEx) { + super(msg, nestedEx); + setErrorMessage(msg); + } + + public DigitalDisplayDeviceMgtPluginException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public DigitalDisplayDeviceMgtPluginException(String msg) { + super(msg); + setErrorMessage(msg); + } + + public DigitalDisplayDeviceMgtPluginException() { + super(); + } + + public DigitalDisplayDeviceMgtPluginException(Throwable cause) { + super(cause); + } + +} 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/DigitalDisplayManager.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/DigitalDisplayManager.java new file mode 100644 index 000000000..f1e2befa3 --- /dev/null +++ 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/DigitalDisplayManager.java @@ -0,0 +1,251 @@ +/* + * 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.device.mgt.iot.digitaldisplay.plugin.impl; + + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.*; +import org.wso2.carbon.device.mgt.common.configuration.mgt.TenantConfiguration; +import org.wso2.carbon.device.mgt.common.license.mgt.License; +import org.wso2.carbon.device.mgt.common.license.mgt.LicenseManagementException; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.exception.DigitalDisplayDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.impl.dao.DigitalDisplayDAO; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.impl.feature.DigitalDisplayFeatureManager; +import java.util.List; + + +/** + * This represents the DigitalDisplay implementation of DeviceManagerService. + */ +public class DigitalDisplayManager implements DeviceManager { + + private static final DigitalDisplayDAO digitalDisplayDAO = new DigitalDisplayDAO(); + private static final Log log = LogFactory.getLog(DigitalDisplayManager.class); + private FeatureManager featureManager = new DigitalDisplayFeatureManager(); + @Override + public FeatureManager getFeatureManager() { + return featureManager; + } + + @Override + public boolean saveConfiguration(TenantConfiguration tenantConfiguration) + throws DeviceManagementException { + return false; + } + + @Override + public TenantConfiguration getConfiguration() throws DeviceManagementException { + return null; + } + + @Override + public boolean enrollDevice(Device device) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Enrolling a new DigitalDisplay device : " + device.getDeviceIdentifier()); + } + DigitalDisplayDAO.beginTransaction(); + status = digitalDisplayDAO.getDeviceDAO().addDevice(device); + DigitalDisplayDAO.commitTransaction(); + } catch (DigitalDisplayDeviceMgtPluginException e) { + try { + DigitalDisplayDAO.rollbackTransaction(); + } catch (DigitalDisplayDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the device enrol transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while enrolling the DigitalDisplay device : " + device.getDeviceIdentifier(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean modifyEnrollment(Device device) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Modifying the DigitalDisplay device enrollment data"); + } + DigitalDisplayDAO.beginTransaction(); + status = digitalDisplayDAO.getDeviceDAO().updateDevice(device); + DigitalDisplayDAO.commitTransaction(); + } catch (DigitalDisplayDeviceMgtPluginException e) { + try { + DigitalDisplayDAO.rollbackTransaction(); + } catch (DigitalDisplayDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the update device transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while updating the enrollment of the DigitalDisplay device : " + + device.getDeviceIdentifier(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean disenrollDevice(DeviceIdentifier deviceId) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Dis-enrolling DigitalDisplay device : " + deviceId); + } + DigitalDisplayDAO.beginTransaction(); + status = digitalDisplayDAO.getDeviceDAO().deleteDevice(deviceId.getId()); + DigitalDisplayDAO.commitTransaction(); + } catch (DigitalDisplayDeviceMgtPluginException e) { + try { + DigitalDisplayDAO.rollbackTransaction(); + } catch (DigitalDisplayDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the device dis enrol transaction :" + deviceId.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while removing the DigitalDisplay device : " + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean isEnrolled(DeviceIdentifier deviceId) throws DeviceManagementException { + boolean isEnrolled = false; + try { + if (log.isDebugEnabled()) { + log.debug("Checking the enrollment of DigitalDisplay device : " + deviceId.getId()); + } + Device iotDevice = digitalDisplayDAO.getDeviceDAO().getDevice(deviceId.getId()); + if (iotDevice != null) { + isEnrolled = true; + } + } catch (DigitalDisplayDeviceMgtPluginException e) { + String msg = "Error while checking the enrollment status of DigitalDisplay device : " + + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return isEnrolled; + } + + @Override + public boolean isActive(DeviceIdentifier deviceId) throws DeviceManagementException { + return true; + } + + @Override + public boolean setActive(DeviceIdentifier deviceId, boolean status) + throws DeviceManagementException { + return true; + } + + @Override + public Device getDevice(DeviceIdentifier deviceId) throws DeviceManagementException { + Device device; + try { + if (log.isDebugEnabled()) { + log.debug("Getting the details of DigitalDisplay device : " + deviceId.getId()); + } + device = digitalDisplayDAO.getDeviceDAO().getDevice(deviceId.getId()); + } catch (DigitalDisplayDeviceMgtPluginException e) { + String msg = "Error while fetching the DigitalDisplay device : " + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return device; + } + + @Override + public boolean setOwnership(DeviceIdentifier deviceId, String ownershipType) + throws DeviceManagementException { + return true; + } + + public boolean isClaimable(DeviceIdentifier deviceIdentifier) throws DeviceManagementException { + return false; + } + + @Override + public boolean setStatus(DeviceIdentifier deviceId, String currentOwner, + EnrolmentInfo.Status status) throws DeviceManagementException { + return false; + } + + @Override + public License getLicense(String s) throws LicenseManagementException { + return null; + } + + @Override + public void addLicense(License license) throws LicenseManagementException { + + } + + @Override + public boolean requireDeviceAuthorization() { + return true; + } + + @Override + public boolean updateDeviceInfo(DeviceIdentifier deviceIdentifier, Device device) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug( + "updating the details of DigitalDisplay device : " + deviceIdentifier); + } + DigitalDisplayDAO.beginTransaction(); + status = digitalDisplayDAO.getDeviceDAO().updateDevice(device); + DigitalDisplayDAO.commitTransaction(); + } catch (DigitalDisplayDeviceMgtPluginException e) { + try { + DigitalDisplayDAO.rollbackTransaction(); + } catch (DigitalDisplayDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the update device info transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = + "Error while updating the DigitalDisplay device : " + deviceIdentifier; + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public List getAllDevices() throws DeviceManagementException { + List devices; + try { + if (log.isDebugEnabled()) { + log.debug("Fetching the details of all DigitalDisplay devices"); + } + devices = digitalDisplayDAO.getDeviceDAO().getAllDevices(); + } catch (DigitalDisplayDeviceMgtPluginException e) { + String msg = "Error while fetching all DigitalDisplay devices."; + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return devices; + } + +} \ No newline at end of file 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 new file mode 100644 index 000000000..429694b41 --- /dev/null +++ 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 @@ -0,0 +1,101 @@ +/* + * 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.device.mgt.iot.digitaldisplay.plugin.impl; + +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.DeviceManager; +import org.wso2.carbon.device.mgt.common.app.mgt.Application; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManagementException; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManager; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.constants.DigitalDisplayConstants; + +import java.util.List; + +public class DigitalDisplayManagerService implements DeviceManagementService{ + private DeviceManager deviceManager; + @Override + public String getType() { + return DigitalDisplayConstants.DEVICE_TYPE; + } + + @Override + public String getProviderTenantDomain() { + return "carbon.super"; + } + + @Override + public boolean isSharedWithAllTenants() { + return true; + } + + @Override + public void init() throws DeviceManagementException { + deviceManager= new DigitalDisplayManager(); + } + + @Override + public DeviceManager getDeviceManager() { + return deviceManager; + } + + @Override + public ApplicationManager getApplicationManager() { + return null; + } + + @Override + public void notifyOperationToDevices(Operation operation, List list) throws DeviceManagementException { + + } + + @Override + public Application[] getApplications(String domain, int pageNumber, int size) + throws ApplicationManagementException { + return new Application[0]; + } + + @Override + public void updateApplicationStatus(DeviceIdentifier deviceId, Application application, + String status) throws ApplicationManagementException { + + } + + @Override + public String getApplicationStatus(DeviceIdentifier deviceId, Application application) + throws ApplicationManagementException { + return null; + } + + @Override + public void installApplicationForDevices(Operation operation, List list) throws ApplicationManagementException { + + } + + @Override + public void installApplicationForUsers(Operation operation, List list) throws ApplicationManagementException { + + } + + @Override + public void installApplicationForUserRoles(Operation operation, List list) throws ApplicationManagementException { + + } + +} 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/dao/DigitalDisplayDAO.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/dao/DigitalDisplayDAO.java new file mode 100644 index 000000000..01e9d702c --- /dev/null +++ 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/dao/DigitalDisplayDAO.java @@ -0,0 +1,126 @@ +/* + * 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.device.mgt.iot.digitaldisplay.plugin.impl.dao; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.exception.DigitalDisplayDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.constants.DigitalDisplayConstants; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.impl.dao.impl.DigitalDisplayDeviceDAOImpl; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +public class DigitalDisplayDAO { + + private static final Log log = LogFactory.getLog(DigitalDisplayDAO.class); + static DataSource dataSource; // package local variable + private static ThreadLocal currentConnection = new ThreadLocal(); + + public DigitalDisplayDAO() { + initDigitalDisplayDAO(); + } + + public DigitalDisplayDeviceDAOImpl getDeviceDAO() { + return new DigitalDisplayDeviceDAOImpl(); + } + + public static void initDigitalDisplayDAO(){ + try { + Context ctx = new InitialContext(); + dataSource = (DataSource) ctx.lookup(DigitalDisplayConstants.DATA_SOURCE_NAME); + } catch (NamingException e) { + log.error("Error while looking up the data source: " + DigitalDisplayConstants.DATA_SOURCE_NAME); + } + } + + public static void beginTransaction() throws DigitalDisplayDeviceMgtPluginException { + try { + Connection conn = dataSource.getConnection(); + conn.setAutoCommit(false); + currentConnection.set(conn); + } catch (SQLException e) { + throw new DigitalDisplayDeviceMgtPluginException("Error occurred while retrieving datasource connection", e); + } + } + + public static Connection getConnection() throws DigitalDisplayDeviceMgtPluginException { + if (currentConnection.get() == null) { + try { + currentConnection.set(dataSource.getConnection()); + } catch (SQLException e) { + throw new DigitalDisplayDeviceMgtPluginException("Error occurred while retrieving data source connection", + e); + } + } + return currentConnection.get(); + } + + public static void commitTransaction() throws DigitalDisplayDeviceMgtPluginException { + try { + Connection conn = currentConnection.get(); + if (conn != null) { + conn.commit(); + } else { + if (log.isDebugEnabled()) { + log.debug("Datasource connection associated with the current thread is null, hence commit " + + "has not been attempted"); + } + } + } catch (SQLException e) { + throw new DigitalDisplayDeviceMgtPluginException("Error occurred while committing the transaction", e); + } finally { + closeConnection(); + } + } + + public static void closeConnection() throws DigitalDisplayDeviceMgtPluginException { + Connection con = currentConnection.get(); + if(con != null){ + try { + con.close(); + } catch (SQLException e) { + log.error("Error occurred while close the connection"); + } + } + currentConnection.remove(); + } + + public static void rollbackTransaction() throws DigitalDisplayDeviceMgtPluginException { + try { + Connection conn = currentConnection.get(); + if (conn != null) { + conn.rollback(); + } else { + if (log.isDebugEnabled()) { + log.debug("Datasource connection associated with the current thread is null, hence rollback " + + "has not been attempted"); + } + } + } catch (SQLException e) { + throw new DigitalDisplayDeviceMgtPluginException("Error occurred while rollback the transaction", e); + } finally { + closeConnection(); + } + } +} \ No newline at end of file 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/dao/impl/DigitalDisplayDeviceDAOImpl.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/dao/impl/DigitalDisplayDeviceDAOImpl.java new file mode 100644 index 000000000..8df3567ff --- /dev/null +++ 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/dao/impl/DigitalDisplayDeviceDAOImpl.java @@ -0,0 +1,194 @@ +/* + * 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.device.mgt.iot.digitaldisplay.plugin.impl.dao.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.constants.DigitalDisplayConstants; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.exception.DigitalDisplayDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.impl.dao.DigitalDisplayDAO; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.impl.util.DigitalDisplayUtils; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * Implements CRUD for digital display Devices. + */ +public class DigitalDisplayDeviceDAOImpl { + private static final Log log = LogFactory.getLog(DigitalDisplayDeviceDAOImpl.class); + + public Device getDevice(String iotDeviceId) throws DigitalDisplayDeviceMgtPluginException { + Connection conn = null; + PreparedStatement stmt = null; + Device device = null; + ResultSet resultSet = null; + try { + conn = DigitalDisplayDAO.getConnection(); + String selectDBQuery = + "SELECT DIGITAL_DISPLAY_DEVICE_ID, DEVICE_NAME" + + " FROM DIGITAL_DISPLAY_DEVICE WHERE DIGITAL_DISPLAY_DEVICE_ID = ?"; + stmt = conn.prepareStatement(selectDBQuery); + stmt.setString(1, iotDeviceId); + resultSet = stmt.executeQuery(); + if (resultSet.next()) { + device = new Device(); + device.setName(resultSet.getString( + DigitalDisplayConstants.DEVICE_PLUGIN_DEVICE_NAME)); + if (log.isDebugEnabled()) { + log.debug("Digital Display device " + iotDeviceId + " data has been fetched from " + + "Digital Display database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while fetching Digital Display device : '" + iotDeviceId + "'"; + log.error(msg, e); + throw new DigitalDisplayDeviceMgtPluginException(msg, e); + } finally { + DigitalDisplayUtils.cleanupResources(stmt, resultSet); + DigitalDisplayDAO.closeConnection(); + } + return device; + } + + public boolean addDevice(Device device) throws DigitalDisplayDeviceMgtPluginException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = DigitalDisplayDAO.getConnection(); + String createDBQuery = + "INSERT INTO DIGITAL_DISPLAY_DEVICE(DIGITAL_DISPLAY_DEVICE_ID, DEVICE_NAME) VALUES (?, ?)"; + stmt = conn.prepareStatement(createDBQuery); + stmt.setString(1, device.getDeviceIdentifier()); + stmt.setString(2, device.getName()); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Digital Display device " + device.getDeviceIdentifier() + " data has been" + + " added to the Digital Display database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while adding the Digital Display device '" + + device.getDeviceIdentifier() + "' to the Digital Display db."; + log.error(msg, e); + throw new DigitalDisplayDeviceMgtPluginException(msg, e); + } finally { + DigitalDisplayUtils.cleanupResources(stmt, null); + } + return status; + } + + public boolean updateDevice(Device device) throws DigitalDisplayDeviceMgtPluginException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = DigitalDisplayDAO.getConnection(); + String updateDBQuery = + "UPDATE DIGITAL_DISPLAY_DEVICE SET DEVICE_NAME = ? WHERE DIGITAL_DISPLAY_DEVICE_ID = ?"; + stmt = conn.prepareStatement(updateDBQuery); + stmt.setString(1, device.getName()); + stmt.setString(2, device.getDeviceIdentifier()); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Digital Display device " + device.getDeviceIdentifier() + " data has been" + + " modified."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while modifying the Digital Display device '" + + device.getDeviceIdentifier() + "' data."; + log.error(msg, e); + throw new DigitalDisplayDeviceMgtPluginException(msg, e); + } finally { + DigitalDisplayUtils.cleanupResources(stmt, null); + } + return status; + } + + public boolean deleteDevice(String iotDeviceId) throws DigitalDisplayDeviceMgtPluginException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = DigitalDisplayDAO.getConnection(); + String deleteDBQuery = + "DELETE FROM DIGITAL_DISPLAY_DEVICE WHERE DIGITAL_DISPLAY_DEVICE_ID = ?"; + stmt = conn.prepareStatement(deleteDBQuery); + stmt.setString(1, iotDeviceId); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Digital Display device " + iotDeviceId + " data has deleted" + + " from the Digital Display database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while deleting Digital Display device " + iotDeviceId; + log.error(msg, e); + throw new DigitalDisplayDeviceMgtPluginException(msg, e); + } finally { + DigitalDisplayUtils.cleanupResources(stmt, null); + } + return status; + } + + public List getAllDevices() throws DigitalDisplayDeviceMgtPluginException { + Connection conn = null; + PreparedStatement stmt = null; + ResultSet resultSet = null; + Device iotDevice; + List iotDevices = new ArrayList(); + + try { + conn = DigitalDisplayDAO.getConnection(); + String selectDBQuery = + "SELECT DIGITAL_DISPLAY_DEVICE_ID, DEVICE_NAME FROM DIGITAL_DISPLAY_DEVICE"; + stmt = conn.prepareStatement(selectDBQuery); + resultSet = stmt.executeQuery(); + while (resultSet.next()) { + iotDevice = new Device(); + iotDevice.setDeviceIdentifier(resultSet.getString(DigitalDisplayConstants.DEVICE_PLUGIN_DEVICE_ID)); + iotDevice.setName(resultSet.getString(DigitalDisplayConstants.DEVICE_PLUGIN_DEVICE_NAME)); + iotDevices.add(iotDevice); + } + if (log.isDebugEnabled()) { + log.debug("All Digital Display device details have fetched from Digital Display database."); + } + return iotDevices; + } catch (SQLException e) { + String msg = "Error occurred while fetching all Digital Display device data'"; + log.error(msg, e); + throw new DigitalDisplayDeviceMgtPluginException(msg, e); + } finally { + DigitalDisplayUtils.cleanupResources(stmt, resultSet); + DigitalDisplayDAO.closeConnection(); + } + } +} \ No newline at end of file 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/feature/DigitalDisplayFeatureManager.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/feature/DigitalDisplayFeatureManager.java new file mode 100644 index 000000000..5ed07461a --- /dev/null +++ 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/feature/DigitalDisplayFeatureManager.java @@ -0,0 +1,59 @@ +/* + * 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.device.mgt.iot.digitaldisplay.plugin.impl.feature; + +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.Feature; +import org.wso2.carbon.device.mgt.common.FeatureManager; +import org.wso2.carbon.device.mgt.extensions.feature.mgt.GenericFeatureManager; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.constants.DigitalDisplayConstants; + +import java.util.List; + +public class DigitalDisplayFeatureManager implements FeatureManager { + @Override + public boolean addFeature(Feature feature) throws DeviceManagementException { + return false; + } + + @Override + public boolean addFeatures(List features) throws DeviceManagementException { + return false; + } + + @Override + public Feature getFeature(String name) throws DeviceManagementException { + GenericFeatureManager genericFeatureManager = GenericFeatureManager.getInstance(); + return genericFeatureManager.getFeature(DigitalDisplayConstants.DEVICE_TYPE, name); + } + + @Override + public List getFeatures() throws DeviceManagementException { + GenericFeatureManager genericFeatureManager = GenericFeatureManager.getInstance(); + return genericFeatureManager.getFeatures(DigitalDisplayConstants.DEVICE_TYPE); + } + + @Override + public boolean removeFeature(String name) throws DeviceManagementException { + return false; + } + + @Override + public boolean addSupportedFeaturesToDB() throws DeviceManagementException { + return false; + } +} 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/util/DeviceSchemaInitializer.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/util/DeviceSchemaInitializer.java new file mode 100644 index 000000000..a527d26d7 --- /dev/null +++ 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/util/DeviceSchemaInitializer.java @@ -0,0 +1,50 @@ +/* + * 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.device.mgt.iot.digitaldisplay.plugin.impl.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.utils.CarbonUtils; +import org.wso2.carbon.utils.dbcreator.DatabaseCreator; + +import javax.sql.DataSource; +import java.io.File; + +/** + * Provides methods for initializing the database script. + */ +public class DeviceSchemaInitializer extends DatabaseCreator{ + + private static final Log log = LogFactory.getLog(DeviceSchemaInitializer.class); + private static final String setupSQLScriptBaseLocation = CarbonUtils.getCarbonHome() + File.separator + "dbscripts" + + File.separator + "cdm" + File.separator + "plugins" + File.separator; + + public DeviceSchemaInitializer(DataSource dataSource) { + super(dataSource); + } + + @Override + protected String getDbScriptLocation(String databaseType) { + String scriptName = databaseType + ".sql"; + if (log.isDebugEnabled()) { + log.debug("Loading database script from :" + scriptName); + } + return setupSQLScriptBaseLocation.replaceFirst("DBTYPE", databaseType) + scriptName; + } +} 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/util/DigitalDisplayUtils.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/util/DigitalDisplayUtils.java new file mode 100644 index 000000000..2e071d361 --- /dev/null +++ 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/util/DigitalDisplayUtils.java @@ -0,0 +1,112 @@ +/* + * 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.device.mgt.iot.digitaldisplay.plugin.impl.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.constants.DigitalDisplayConstants; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.exception.DigitalDisplayDeviceMgtPluginException; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; + +/** + * Contains utility methods used by Digital Display plugin. + */ +public class DigitalDisplayUtils { + + private static Log log = LogFactory.getLog(DigitalDisplayUtils.class); + + public static String getDeviceProperty(List deviceProperties, String propertyKey) { + String deviceProperty = ""; + for(Device.Property property :deviceProperties){ + if(propertyKey.equals(property.getName())){ + deviceProperty = property.getValue(); + } + } + return deviceProperty; + } + + public static Device.Property getProperty(String property, String value) { + if (property != null) { + Device.Property prop = new Device.Property(); + prop.setName(property); + prop.setValue(value); + return prop; + } + return null; + } + + public static void cleanupResources(Connection conn, PreparedStatement stmt, ResultSet rs) { + if (rs != null) { + try { + rs.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing result set", e); + } + } + if (stmt != null) { + try { + stmt.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing prepared statement", e); + } + } + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing database connection", e); + } + } + } + + public static void cleanupResources(PreparedStatement stmt, ResultSet rs) { + cleanupResources(null, stmt, rs); + } + + /** + * Creates the device management schema. + */ + public static void setupDeviceManagementSchema() throws DigitalDisplayDeviceMgtPluginException { + try { + Context ctx = new InitialContext(); + DataSource dataSource = (DataSource) ctx.lookup(DigitalDisplayConstants.DATA_SOURCE_NAME); + DeviceSchemaInitializer initializer = + new DeviceSchemaInitializer(dataSource); + log.info("Initializing device management repository database schema"); + initializer.createRegistryDatabase(); + + } catch (NamingException e) { + log.error("Error while looking up the data source: " + DigitalDisplayConstants.DATA_SOURCE_NAME); + } catch (Exception e) { + throw new DigitalDisplayDeviceMgtPluginException("Error occurred while initializing Iot Device " + + "Management database schema", e); + } + } +} 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/internal/DigitalDisplayManagementServiceComponent.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/internal/DigitalDisplayManagementServiceComponent.java new file mode 100644 index 000000000..52a6fdec8 --- /dev/null +++ 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/internal/DigitalDisplayManagementServiceComponent.java @@ -0,0 +1,85 @@ +/* + * 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.device.mgt.iot.digitaldisplay.plugin.internal; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.ComponentContext; +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.exception.DigitalDisplayDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.impl.DigitalDisplayManagerService; +import org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.impl.util.DigitalDisplayUtils; + +/** + * @scr.component name="org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin.internal.DigitalDisplayManagementServiceComponent" + * immediate="true" + */ +public class DigitalDisplayManagementServiceComponent { + + private ServiceRegistration digitalDisplayServiceRegRef; + private static final Log log = LogFactory.getLog(DigitalDisplayManagementServiceComponent.class); + + protected void activate(ComponentContext ctx) { + if (log.isDebugEnabled()) { + log.debug("Activating Digital Display Management Service Component"); + } + try { + BundleContext bundleContext = ctx.getBundleContext(); + digitalDisplayServiceRegRef = + bundleContext.registerService(DeviceManagementService.class.getName(), new + DigitalDisplayManagerService(), null); + String setupOption = System.getProperty("setup"); + if (setupOption != null) { + if (log.isDebugEnabled()) { + log.debug("-Dsetup is enabled. Iot Device management repository schema initialization is about " + + "to begin"); + } + try { + DigitalDisplayUtils.setupDeviceManagementSchema(); + } catch (DigitalDisplayDeviceMgtPluginException e) { + log.error("Exception occurred while initializing device management database schema", e); + } + } + if (log.isDebugEnabled()) { + log.debug("Digital Display Management Service Component has been successfully activated"); + } + } catch (Throwable e) { + log.error("Error occurred while activating Digital Display Management Service Component", e); + } + } + + protected void deactivate(ComponentContext ctx) { + if (log.isDebugEnabled()) { + log.debug("De-activating DigitalDisplay Management Service Component"); + } + try { + if (digitalDisplayServiceRegRef != null) { + digitalDisplayServiceRegRef.unregister(); + } + if (log.isDebugEnabled()) { + log.debug("DigitalDisplay Management Service Component has been successfully de-activated"); + } + } catch (Throwable e) { + log.error("Error occurred while de-activating Iot Device Management bundle", e); + } + } + +} diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/pom.xml b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/pom.xml new file mode 100644 index 000000000..ea1b8629a --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/pom.xml @@ -0,0 +1,62 @@ + + + + + + + + digital-display-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.digitaldisplay.ui + WSO2 Carbon - IoT Server Digital Display UI + pom + + + + + maven-assembly-plugin + 2.5.5 + + ${project.artifactId}-${carbon.device.mgt.version} + false + + src/assembly/src.xml + + + + + create-archive + package + + single + + + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/assembly/src.xml b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/assembly/src.xml new file mode 100644 index 000000000..2797034e0 --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/assembly/src.xml @@ -0,0 +1,36 @@ + + + + src + + zip + + false + ${basedir}/src + + + ${basedir}/src/main/resources/jaggeryapps/devicemgt + / + true + + + \ No newline at end of file diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/device-view.hbs b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/device-view.hbs new file mode 100644 index 000000000..64d5eac67 --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/device-view.hbs @@ -0,0 +1,98 @@ +{{#zone "topCss"}} + +{{/zone}} + +{{#zone "device-thumbnail"}} + +{{/zone}} + +{{#zone "operation-status"}} + +{{/zone}} + +{{#zone "device-opetations"}} +
+ Operations +
+
+ {{unit "iot.unit.device.operation-bar" device=device}} +
+{{/zone}} + +{{#zone "device-detail-properties"}} + +
+
+ +
+
+ +
+
+ Display Status +
+
+
+
Display View
+ + + +
+
+
Content List
+
    +
    +
    +
    Device Statistics
    +
      +
      +
      +
      + +
      +
      +
      Operations Log
      +
      + +
      +
      + Not available yet +
      +
      +
      +
      +
      +
      +
      +
      +{{/zone}} + +{{#zone "bottomJs"}} + + {{js "js/websocket.js"}} +{{/zone}} \ No newline at end of file diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/device-view.js b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/device-view.js new file mode 100644 index 000000000..9c203853b --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/device-view.js @@ -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. + */ + +function onRequest(context) { + + var log = new Log("device-view.js"); + var deviceType = context.uriParams.deviceType; + var deviceId = request.getParameter("id"); + + var getProperty = require("process").getProperty; + var port = getProperty("carbon.https.port"); + var host = getProperty("carbon.local.ip"); + var sessionId = session.getId(); + if (deviceType != null && deviceType != undefined && deviceId != null && deviceId != undefined) { + var deviceModule = require("/app/modules/device.js").deviceModule; + var device = deviceModule.viewDevice(deviceType, deviceId); + + if (device && device.status != "error") { + return {"device": device}; + } else { + response.sendError(404, "Device Id " + deviceId + "of type " + deviceType + " cannot be found!"); + exit(); + } + } +} \ No newline at end of file diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/device-view.json b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/device-view.json new file mode 100644 index 000000000..9eecd8f5b --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/device-view.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0" +} \ No newline at end of file diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/public/images/default-screen.png b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/public/images/default-screen.png new file mode 100644 index 0000000000000000000000000000000000000000..df11c247a267953f023956b27f3122d3e7a0960c GIT binary patch literal 55235 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 z�|wN&J)_EA6Jbjp|;5O4K+c+UM3$H}#7wQ4GF{z=&Nq zvtSxl%Z2j&Z8obAjf1b>I##krsrT9uO@%ku82Y{2mf|uUq&QXe=Xz~-3r=aDT*i<0 z<*-YxfbgRQPfK(>yg@`2p63PAZXtR3OhO{q4FKE)7H{t(v_Wwy&77fbm08tD*l)HB z&o%Z8-|-R(q-7s;b=-jTxYXSCS2cReBn7zCrxRtLAM~#7`%isg7;fS&p9FfRJu
    • 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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/public/images/thumb.png b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/public/images/thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..3d2a829c6f0a9fb2653eb552f6ab98ae230f2835 GIT binary patch literal 6246 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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/public/js/websocket.js b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/public/js/websocket.js new file mode 100644 index 000000000..3075ba326 --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.device-view/public/js/websocket.js @@ -0,0 +1,81 @@ +/* + * 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. + */ + +var displayClient = null; + +window.onload = function () { + connect(); +}; + +window.onbeforeunload = function () { + disconnect(); +} + +function connect() { + displayClient = new WebSocket("wss://" + host + ":" + port + "/digital_display/" + sessionId); + + displayClient.onmessage = function (event) { + var message = event.data; + var operationDiv = document.getElementById("div-operation-status"); + var type = message.split('||')[0]; + var reply = message.split('||')[1]; + + if (type.valueOf() == new String("Screenshot").valueOf()) { + document.getElementById('img').setAttribute('src', 'data:image/png;base64,' + reply); + document.getElementById('zoom-image').setAttribute('href', 'data:image/png;base64,' + reply); + } else if (type.valueOf() == new String("Success").valueOf()) { + $('#div-operation-status').removeClass('hidden'); + $('#div-operation-status').text(reply); + operationDiv.style.backgroundColor = '#DFF2BF'; + operationDiv.style.color = '#4F8A10'; + } else if (type.valueOf() == new String("Failed").valueOf()) { + $('#div-operation-status').removeClass('hidden'); + $('#div-operation-status').text(reply); + operationDiv.style.backgroundColor = '#FFBABA'; + operationDiv.style.color = '#D8000C'; + } else if (type.valueOf() == new String("ContentList").valueOf()) { + var resources = reply.split("-"); + var ul = document.getElementById("content-list"); + ul.innerHTML = ""; + for (i = 0; i < resources.length; i++) { + var li = document.createElement("li"); + li.appendChild(document.createTextNode(resources[i])); + ul.appendChild(li); + } + } else if (type.valueOf() == new String("DeviceStatus").valueOf()) { + var resources = reply.split("-"); + var ul = document.getElementById("device-statics"); + ul.innerHTML = ""; + for (i = 0; i < resources.length; i++) { + var li = document.createElement("li"); + li.appendChild(document.createTextNode(resources[i])); + ul.appendChild(li); + } + } + + setTimeout(function () { + $('#div-operation-status').addClass('hidden'); + }, 10000); + + }; + +} + +function disconnect() { + displayClient.close(); +} \ No newline at end of file diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/display-icon.png b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/display-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..90342742a6d7275d137edec30c85c9a690c75d95 GIT binary patch literal 4963 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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/myDevices_analytics.png b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/myDevices_analytics.png new file mode 100644 index 0000000000000000000000000000000000000000..8f55999a3cc67dc30ca686e871ae4a1f4f77ad17 GIT binary patch literal 195921 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%0AtQlWM#H2)$dBI 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�zNNR{>H;&s)`CHKstC>X?nd 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

      ISby2!A2}G`3*_anJE`angbv4riiuX{70{=m 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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/add-resource.png b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/add-resource.png new file mode 100644 index 0000000000000000000000000000000000000000..d00ee206b189654aa0a146c4885c5580b75db92f GIT binary patch literal 4108 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 C`TxxT literal 0 HcmV?d00001 diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/edit-sequence.png b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/edit-sequence.png new file mode 100644 index 0000000000000000000000000000000000000000..a194713b5e9ca7f3a92fffeb7a84f2a5b4ff3461 GIT binary patch literal 8099 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�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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/get-device-status.png b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/get-device-status.png new file mode 100644 index 0000000000000000000000000000000000000000..caf3f39ef350a4164d024d527adfdb54383556f4 GIT binary patch literal 3589 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

      1jK$^`2 zw_nj;Ac9%ph!77>1fbOPB02R5P7}Pqi2#IoNPrKeo+Ot5R0v@ax2F&TGWf{wlZw?}Rl|p2#Y-lD80S|5DwJixkrv*Zl?yozc1yes&G=Wgp2oXB+r?cP zz+`oR(PE2;$qWV~Uc(?xF>PV|F9hLW0L2d>e=CIjofiqCS>Pwv4~7{d^A+DSM*>i} znm$tF2jvies&xFFSF2;OaF`Pok4uwu&hNIJhPVG!p78&X(#_5cFxjMg4~Y;#uLpuH zO#&$rJ-A14B$zrZPtiSdBmm)hB*4c~PLM-@r}6M0&sGzt_rEE1+hTpn<)$rt| zV;B58Dcq8l4JM0n=MjxCAzusl-F46w40QN^WDve^(+C*dFJ1QUb0)yAkO2QJbMuUR*IK%M341XAdO$ z;Ok0w?voSkf14d?LeJM~myqlOZ+#^U&$TF0K7lg<2$WLxDeIS|65u{O{F--*ZyV); zMWfhUzP?}>zWe5G!Iw`k+fn(M+9^MZVL2xF<&;cC$|tBmfcudEzhV8dR02@I=N-IT zEM@rr%bqHDX7h33v(ah?dv;&#mEXz6jvS~!fH#l;i&?)cl>ij>btTW%x-15Ge|{dW zt=XEg_?^9QsM;gU^^Uy$s?YiH;Hi9q3IzB93BVkkC6xf=ZRyLiwG#@h!UJW&>H1b! z^w!tHXIpk5m~H&6{jR`U`2-aRP=N&K$NFVv0*LkYTcn)Bf{A^tFtylApPG z2y4o!g>}9?yO3uyx&?)we}8&~|O0+c`{0!%;x>|*(%6auKS2S6USmZ}o~j9GWKtU^^hea!6* zg`ibXen9z;focR`-V=~Q0967&Lw>K&o0woQf-`@hZeihq0|-Z}M4Ll$p%MX@14X3} zU>+X+%)6z5mL~8wu_Z^C>}i6#R}Zv>!vaxxDxaVd0p=qCm;!cmCqN3SfIk5yynJW| z4!x@80HnYNXjjeLdG}DhQ@}T0+CSB3wrte<=2GY$xJoJA^~2$tSiZ|lfU(c*nr?BX z|4r|kOQCxNTirK&b^rMFEZ=1&!04y9&$VUbN{%{BL3{Y!^*8SM@#TME`7Sd7Mn3V` ze0z4`YTiq*pH1c)TiOz6gWv9ULrYMf5-v9kvBUTRHZSF-<6hWN!FN?beidIs^@6g8 zu4XdWnYB6M@sDTQbNa2Jh+3Xl|L^>kb)G;N9{#jJZqWrUcx`45^k=M5^fPQLWfJoQ zc`1GMm2>tz@*SqMab^MxU%sgnM}Q3`E8i4tUIa)6=3%UTICw+lGc9*9P+-Kr$$w@T1GpVdf=lSwn~F0&wrfdhTrq zv^1j)P&@g+V^hCi`93=VmTf38J2MZVA;`7CN(qoG%FnSI;mz5(U^B5jQYtR>hcf;z zBKI}bQZB>L1ItPevwU9)0kVu%d%YuvIo5y)N(kWb1>vU~E8swF$Bh}2)d>cpwl#$A znUDsvO4v84?)>mHoOBz&Xy&`EO&e8Vzc>59vS|%0-Ic<_i(cyddnXR-b4(u(ULj z^&DvgShnG7dv4Jf9vL@L$R$8yYXELvxd+a9&i8(6dM;R8%=gX(RujMrr8!WX$-X*T zvFRwR-B$ySJ_EpL;+JAUISby}@;%u5$FiOyl>mRTq~|QKy7=`lB~t*UgWvn7{SXYd zfi)u!EKctEs@-DA73nZ_5PQ0K0}^-P>$}0%W&~%SZ*Xmk-_rnr=DO8LfSXv)kxGE2 zXa_9gX$L4Z0yiBz2TyNr61Ix9#?2#~@Vzn8uKT}RPQtr8D}uEP6`0A>6?~>C2rF

      T3CkF0HuQ+ zF#l4Aq_Q=!ptbqKYY)MeqY<~S*@4DiCST((Ca>ZaB)~gT&Xq%eK6t1wm@Ots&`>&$ zB_e>}KA*hzX?kYcQFZaXVYCoK63Uyv65@kS!2}bs@z@&aQp;DQZ z(Y4zdR_-W=SGS&Szd~f~&$0I-!CGt2Q1ueZQ0a>VsF8BMth+-J`~wfy@UUd#2@qBO zz6!Tcf@Q_rOX2c?{QM3+G!oCcL&6K9TJbYI02t97I5Sh;fTq1k0K_k>PJX`m!zIAmjJW`pmEJ?&n;3^j44Wh zM9PnXNbtn$p)ir-NT4QYsrSUjqr&vleh=7DsKfAt==!CPwCovx1gMny8?whcB!TdH zIHkFIM1Zc89|cVZKfZ1d%;Zp;Pc6`VOkTcMK0npL&QN37M-Z2O^zkE9zO2{DDIvgI zJP1Le_MCnoH&STzyqtu}r&UJK>%JiUdfu1;5InW%D136buEW1m zuculDykjZjQtEC*0x$)E#zP4KXkA1>2ZN~kCXqnSO%@xf6DpsUn4M+e4#{)E!_$U9 zNgrjU4V=@^tZc1+2qy?)`jaE?u&kw!>Ui*ydEms5fSkd>#dh?Yo#_M%a? z%s#lrl^!nUsq9$jG2XvGd_%{@}7|TP7w7+Sz+;+i6&{xB7ym zr2&_EqJb4TZMecMM@pyB?#-4()oPoFP%sAh|>*`tQ?1+U+^GZg+ye^OTM zAS8fW?(Zp{n2-eRuZgD-fJOo-h!UW?NFHH2!1Zo?xz;E$?Pi#Z_Q9-?eV}iqIxm-^ z8bu#`T?Om**MP^Lc*GT^Lz?qdDqclBpGE)?525lEy;hzF0caFB0^-}kZCM4lHe{ch zi2$rtGHrR%0|@y#52mCW8e5bOQ%Z7SQgId(AsOV_Uo_XAY4E}q#~a|&Bek&iY}Xx) zIsgTxs8zp!idR#WQW5mw5+%Jq`n(haNpKe)UJ?GFseoL>Cbk~nch^f=B9T(4g15h! ztnKbMd-Bt*FrqLGhUU9qK$aax2OH!#Es$n2x347pp)k08A*gE!2rrvBRo4oKYdlbf z#%Yb4A#uZC1d5iHV*@eYcL<06+V-KJ@B>u-t4e>D7Xe7yyHQ5d-&z!KB)fn@pu>!{ zh13H1L8#ZOG2cfaSp7Kt#k|8{FUokimVYd`X$VPhH6Cb>KM zypv%|Wagd7(dE71^hV3y7@R;Eiv(!mbq($mMiN|uhs`JvD-aDNfdm|L30Od*0xcQo zA%+5892xw~(**6@Q6Wl`#V(MS*7aIfCKEP#NcnB;!7Rk)Yf<^1@w|>IcZDQahKHr+ zuVMrWCD*&Ur$CBOP#787=)tx{R}lumT}x|s>g{^Jot(%@A>J{{CoD(hFXMeJwFp3l zAQdT^cK%BGj08Om33@(Z1Ii2}0G~Hu#y~ZyBME2;upOo3{MC>I*#(-~1U(7DoL^rR zuYB5Yn}o`*llik%%Ihwx%NNdm%lw7+pTe`7mghAA84O_L22z)7iFUF=hkp*LSnShrH>*3FOvdC67WnDM#L83MO-KhBY z^=xlaJWa9i1ih7XdY6qgTfpMT5GrdWo3Tj79qmE}TAMnJTZ{o!@?I>Zkb_!C3IaqS zrk+0sVtip&py`3$8!YV0Yg`jB3W`yr;hhp#x)|nTL2-N!NKN5Z6G=q?0u`jh!y_n^ zfXX!a)6_sjps9lP>(!bjgyB8tk8Clc!V@@fIs;jMl(_i|)&h=NA?XMZ1rlKhM7ES~ zMqv-QL*>&d!f3V$WH8cRK5g;RRxcwlgl%KmGNw0!;&4t%4+nh_E51!q)e%Z*dKV&F zxksg(s1=fy08tL#Vcv=y3#y1(JbWg+zuM#AC5Wmp%cJC>?r<7Ac+C8ca6yXad9o2|+dT zu7Ze*nF=OTqKeYmG8BPJi5L{^OfK84sPN-@-zzU%R0N0z5`ylp2qGf`3E2_kainlH z8=N;PeWFm<6?y1r{yrf}IVxPAH&AJSi<$r_bdMAPdO1=A=;cTepqC>>fL@OO2lwc5 U`2nA#D*ylh07*qoM6N<$f*Ve`>Hq)$ literal 0 HcmV?d00001 diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/restart-server.png b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/restart-server.png new file mode 100644 index 0000000000000000000000000000000000000000..eb947a87fb30cb8274b39b7d53df8cc70054c9e2 GIT binary patch literal 3293 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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/screenshot.png b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..8a5bca3d1aaed2a64396e3d759646f255fa17c56 GIT binary patch literal 8786 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 IM6N<$f^RDZD*ylh literal 0 HcmV?d00001 diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/terminate-display.png b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/operations/terminate-display.png new file mode 100755 index 0000000000000000000000000000000000000000..9919cfbb5a0ce9f1fe9f2007a75f41a59bacf804 GIT binary patch literal 4346 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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/schematicsGuide.png b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/images/schematicsGuide.png new file mode 100644 index 0000000000000000000000000000000000000000..aec732f5f7b7fd3f8ab22907f0c2281a39bd22f4 GIT binary patch literal 393114 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 z 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

      f|M8Er3eiF`D@A1FvNPJrtc+}m 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�#}27l%|6Y-;OV`Bkg$k(u3B#!NI zzF7gU$@B89fUJirw?W^>zxj1ycfNj;9~v9q;SXu(=s?5vxd_}&3HMBr@<1J}&w8h> zz8m`MhOJxiT5P?EFZnm?tIxxTZ`zd=&LP177j(A&i9sO>crBS4CV7%XV|r7@-re1K zf>>SOA4HA0bjkfnzPs9Px_B;)8|a4+W_-|B$qlsFM*T*ha`r%dU zToVGL={P&|jDOo)c_DWMbSjs#Qr-L{dI#LyedW&fkJ=ra>k3jZVcjn`(A!;*^}cog zc*9Hui?9?Ctw0OE<|h=|vntvLy{PBTbcuCcu1X%$J8^OH2keifOUOric3x1oH4Jj# zMnpzM3VK5?sBtF0QZw6@d{4NH&rW#-U9Y=f5MpBZM0ai$(Ybf-cNWN(4q`@pnv0xq zzal+YQlRQhp5aSyLFFUoviN)T%F@r;6%Y$$-sj8}4Xys1{BYq%zxi5CLu9Yu^IXSd zQT&Sp>dg1K#eqoUa6_rnCqMk92VbJM)N2*ER@6W-Ou$Q`B}zkNT@CyQQyY55+R?>C z5a46Sc}%;FKP1sQ{dYPBooRsk6B>4X+T7P|`;48!sH-p??&+c>bCrDU+pi}BuT`WeA~xvjGA;VT@g zj7us_cp$9nR)a8}9&e=$x{Xo=kwLkxdccj5RJQ*P!Mogg$?(go&2I$%c^EqcUEdxU zQ!wM9c1dfmV)1J!d*c82@w$*>i?C9#@f1QqA?r*>NrCS%C99j*&c=xVVdI(Fx1OH2 z>L>Iy(==L6R-j&M1j1r+j`2`alCN3D9h?_hI_G5I$~UZfQ0{6xSs$C50U2&+lc_;v zk4OAi7=~EE%(m64-UMvkV-Qt=01E+A9H=@cUjNm_j54lCh&?98u}sYb<0dY{gaM$L z2`{^*qg;8TID-Ib9^HPA*mdu*>hZlgVUtydzSPlVgJ*~Naai9+o4t5qk|e}y$dEBN z1;hl>_;)b)6$J=+B}8GL9Nh{20OXSpI={5F?0dym73iHUl$VCeCSYGw6EeP(_Ly>? z=Vuq;upgu!IWZdEz2nz>x?P5y14hng3gd&d+1Y>#bMJ>Cy8Lvpu&Lwpw2ov~rMDtB zZ|?U%+=&5Uf4+0ZZ7*@q>yA81J>HLj0q<_vQSp2lh%3pJaGmopEGTKv;-Hu}#j<(p6#x;SpO z^lyGr(CK2}xWa@rW>Mr1$6y_HRSq_?Yg?mx4=*bx9f; zQ({0;9MoF5No$WNQ=`E_%-)FzZ=*GS*Hx3Ut6=Kjg(7UKJOi+p{4-q@A7?0AYC&0=eX1Q&9#mIMb&WCR zi`|7hg$D^zhJNAp)sSi|!-RbU4+7NU5zwj65E{DhjP#ZU{R6bng@Fmr?Cyf3_Ij3-{R1N2sEpF}g*w5kEeF3%Cl7&%_}=IjB@)->D4innh86xy^xu)sLyayo3u9NWbq)@WFFjfM zsWlWmva-QruYp-qZv>?&ps8TauJ%(ZI=-D@knbV7Ed?kZpY?dc9pD-F_KAfmXv~Lb zMvb2o!$0JS9gIEb{`UL@JAUAJ4Yn2a1?es>i8;D^pEVMqxQ7%1ys^;%5ld~5#^HGO%q~1-gFG z_k;?ytPF!D=$y@B-q5bHyAe}bOsLwSvF!kyOtgGyn~WI7-AY8X(5Js5M6Wmw)3n}!pTVl-@P4U1uoIlRYo zBL#|gaLBStCH3MdFaYp~qtdqSIUJ{E0ruSnXdN74%2EQ3BLnQ@`ecmAKT`oI?n)v< zj=aad*BMuwbjYcV`#I7VXjl{7|7i9|8jAi@s(ybO^GhrkeFeB~R0s&ACjC;m5|d z2XUoEAmpfz8E$`-xJ!e}32C9IO-T21w4TyO#R(~t7F&ofV5+qNcsZjmrjaf?Xh<;o`{W5# zHSXhteJ)K=#%<9{lS5J0e@6dOn39!hYI+K0EN0C!ZRZ9Hf2{uYP+uJ^T$UbG6OKlh z2*wf%1as4aOHo68OUA2HCko9P0P;ODDm>~Re`R%Lact7;q8XWcg|U zl|o|Z7^oP-yo({FhRslNE=vJDG;KjlE#oAy>^KBjYmktb3ny~G>Fz5kB@J^7Pdx=h zaTvhrG9BNSif=FtKOD5n)thn>hj4!k$C$Exk)?JZFcHGl;%L_VFL$dGx7ljoTeJ!E zM%ZOb;l0uwX;RW9H;DPA!%$$tM{D3H2J?A*fW}k1+lLhg> z+yKCALJEzrchPi#upLG?txD87pd)`SUM$Y*>i!*I5~Z3BaKt)ElHdp++^k4T7QcmU z#R&H&IiP5c2mD}wB*WfBR#5cG&vs_CSJH*T#3CE*Gq#MUpxK7-F+2_S)?ql3?(OLe z5_|d(kj4cFXv{om^J>VX9L&&0blg_)E)B`dI7J&$3#06<^ef~YWnlBeykwOt#6j*g z&SH3~f`ZyaWAa%}f+sSk9ij0h>D15eS6~*!xP+^yR5>FCc=87mq1Q5f#M-h~;4-Vj zEgheQXvQW?E%vRj7k9P0*i3wPHQ$sm0g2<3>TmsTgT!6FDsl(KSv>~~q(N6sEC$@w z{CF0NsUj9~*VcVp7&&96BJNmC(3%f)F*)=Y$TePy`hu?^%Ub?}{oDf_Za*$lwN9SZ zo|1me@#zd}k2$j=pHKWCG)@f~UmWnm+KZ%9UGcj7%uEGviH@4(^0V9c9*6q)Z6A#I z{Mse|yZ~;0zKFXv*}PKQyx1Q-7q9;Nw`lcy?upLPf|0qU2+_}|k%)iwL(Np#=+(U| zzcXj%uE!rq&CI5fucwGcKTMX01pWO%Z?i?=Vce$Aivi}*o&sK?pdeIwRB-m1$9TV- zZq*@|nYqxIxrnAUb&&m6Gn`}nk*}b;C@dDh z-mr+W4Y51V0PF@7Qb<4g1}b;}#y#%haqKOIG!>{uqf8q*yBmJzpcK1Kl3)ZP?T9zg zT+WL`n<|a{bPp4jlhiqNp!0(X8V16tF^cW`viMTcb_ZyB*i#dYRQDYolGUVQ=B@*b zxa?kFDyKOZ4TgKX#S(Wzmw6zvVu0V0eVc3{R`V&=8m$nE^z23@l-UsCu>IjD+i%BS z>CT>b#W=*~_?!hF9(e1(&joV-gkcqpnGyc^W@n?`JD8SYG5UC!x4R_yDmB&qBgAlY zVt8LLswancqaK0npD>31rf4f|^KnRgOmY}Ldg_1jV1vp#>cFgqr)|ms^E`PP%zXfu zLb~}#I2|sD1H=VI@{L3m3;2&UB;k|b%CigrUyGNh)Xsa}$77sc5VKVr8jn0ur>{CP z2&f{@Uo?KK@OjagTO`XCf(REGMs_LFmS%;Ir*HvwwYDDPm-DYoQw*2=xL8Cit zs1PgH2|H2HS%w`g(h26eN(uR40a2@&&5OUzuH~LpzbM9^hD%cCW}mE1Ut4?A zylSGm`c0w1+tWHWX?yXren5xiq4Ga$CCE=5$ssIt`LN&0K2;ll)8%uj2 z&L$s{O`8(ZjWGv{Ql7I8Vm(47nAxQSdY~*`p-yX5r~NO3yd}>X2u;&bQs@aG50H)J z#Y_yR<`R9GYEX{SX31{t?FtWQ^%!kB+zkoL^K73x-hDz;8cH4m5imh;m?QvsQ$@3e z-lj4D*?gSb>cCVJ%>H}UDJ4%V7GEz#s;~^o_G*$P41^@0AV|?O8^(4JBfHx))LQ5P zNi!g4CupS(*y(_Urng0`MIf}z=$ZGD7-4q^)gvWQDh=mM-1V?sGYc{D3O=^*r9Gj< zxr%5RbLJ2&A20@AVT>UBd#{$^Pu=3Q0{#cLarO~B%@YZqM*aXgLNJOg9fkBCGJ`~s znfpv%I!W-9yk)$>tpVw2-V*mr-7LobM6f1qwM*BM@a#YWvasyNGyWzgw{#Koyf2ze zZOz}Sx3fxt{Q2pb3aKPGjEq&;?_j5uE_TUdycb>qv-UA0!ryHIjm4aQR-dyg+ku$* z+IP2<5~cqJ^phESPeEL=OYs)4c&4EUH^1rV0Y}vBdQ>pez5KEJLR+9h3k%TOccUEe@!)c2@ww4y+K)( zN^u@6a6Sli$|y$llbn&K?SuINqEf&u8VTa$xKcy21&wotGcz-70M;Q|_)%GZAuc=q zh!Hj1NF?2wpZWZQO?aqbpjW}QrHC1`#Z86gqLtCmFi__V8H%Pq2+UWC)T95ker*<= zX#Q6gy>i&4$!x8qS){%^&WaYDi=OAVWs1~SmP^e4)q&qK_iuV8Y9)5`!{}^8?et`c z;8d#IlMiEU=8FqaFTtCT^88P1PJmWhW?MFEZY&;X51Jz*DDGyFVK-lXrkXd@jwYQi zV^*==vK$cvwD@|#=vhoc)4pb7Yk=iZ=Lkl&YKh>A*`SD20hFs(U*OQgM5u$K0l18M z%o-Lm2ReDk21%&XJWrVL^$=gLP%h^>!lqLmJ6)lB9InbTYIoDVb*6k*6`*5`N_91r z)pCtq3o+|rOOWu8;QfMyNz<6NCu%*is8tgTga?4HBP+BjUDVW(l;TJPaxF;uisE($ zaBhmsJoc2=xW{dzJKaV-nl{ra@n`63*6gN#vW*ck`SW_M=pQXbXL7Ppc%F0c76^7l z*qFm4?RF>y48eeJ@K?f#?t-~zEFdZa%5CrTOjSpL?axxWy2yi5!d*CN zI4c(<_~k1p<0T}wWTW5)m)##b+qIOr8av_ojXYH7mDX^#jhzRDl}BV~@Z>D9TG^>V z+v-cQxay)CFKBR|4rzQTkkz?imQ8sZN=1LEufcpLv5_&v#4?|A`vjr)} z?w-V$Iur!+u2iwMaoWI?hn_nf8)%)yOU1CXJdJ3Ny3_r*NoEl96`~8-bL#>fTsmR| zVtJZ1kx0(-7QxpUroc;PM2d9`=k`|aK$tvg9Nnp@Qs#R$3Bx*vP)K)nADGSytr6ti zB&rfC3TDD1y3@CH^LiuQC;s+0^V|Q|BS2csG#=ET+j+0Y(|){7xiQ}Lw(Fo=mV{cC z|M^LiIK#jnv%i{}8_m>GN2d5hqH7MzBg9ZAJVDKa`wIxbmwrii4umJm>FT=tTUeWW zvi58K)Y=dA)$gMwQEUCpt1~O3=4_>Ml8>! z7IX@v*Y2$Kt}Ro|msbmy|67|{O#j+eB$)d^y*6`g?%(f+04tGltZdH{qVxJ?!1&IP zWl5E44y%xLL2;Fzy>!J2*%Ju>6z6!Q6HFEVSlc|35L@aJK_8Fu8w@Is;U13*8~Xb(D;wuRu0Hp zykkeOQ?YnxYyDwJB6FaTo#0TK?z8*YuJjAH)PS%qxxNDX^t!Z>{D@C*O8_VlpZ?f@ zaic1g9X$YH$P{bYSfbv9gLX0`uSqDqgsxSFX!kthvmaI(JHAQYb0yG=U|SrhrObXg zos#M)m6ot)bJcmU2+sTZVSo*CrcAqzF8w%f#%-N1PR>AZ>d&3x(0W!b5;OkA8Vr?+y zu?A=9zrDf&Zd}!yS$?$AM z6Ze5&z;9;CtZigqv_qnZ+nuy@Atyi9G${Jd-AuW;g_`!`+an@OgGEt`pUqd_H2=#T zs`;1HKN|a0Zuv)l>QH3c(o$q#)KdJ+xr;8KxEX3*n?0+Z8oKn%WN|sPB?1v&RK5JY z{~BOQf^w>Petzub+=|?cZfa)jbx!EYubw9}&QZUAjrF%({oKIB3i!QC(aV!- z6HmfE73p5Prkgrg;3Aryj9A)Sk1eUbwAHgvzs~!rCHy5X&%4Yh=o%Y1(dlrIe7c$8 zAA_|pV`$&7kS$<&Vj4`YIUZ6=0PbBwr~+c7fdTW8s&HDi1PBtbRefjX0JbWzp7qQ}PoVPBaO#zG zJPoPOu&Qu+Zzt*b*-vMb4~wHbs>q8cD&-wz!BYTK#$;Nf`18f#S3gQo6j!oWJ&x+i z@zp}4L3a}*kX`RK!KxAfjwCMYXx8)wK=Z7NIX3XgqW1&zh!svCBjUii8$suCgiaMn z%A^p;9Y$7?iYvf86&dgRW-nKjNx2(&5X%{7GGX7noyQYOK9xF-b*Ef}JuXdwwrJ?G zzPeH)LH7B}ex`R>*cI03sL;NG+`SJ-L?A`W8F7kL;WtT=q|9Z*)b{ZhN|uYE4lpTtghsVS^^WVs=VL{&Bp^Pu)=5RJ7&|B`cwp1)K3H8bjuLi94|M4FfW#EQnX z^aogL%erg-=FS}#;KUa?CgYcupY?BEojPZ}_MiFUSoEUH^7K-Frh4>JQKT?v$fXm! zcXBSx4MWKzGiH%1<0}C(D+H`q1{{iKdD zI#3J|2CTV;fFBof4pzHY8iI%nZ*K@dd?}QZ6zWd0RX!8KRCmWnLA1xhhZ$W)B^YmfX{lxPg!Yja(`5OXatO=fsp&k{5fJbiqG2JLY*ssoHfBnbvVRbem9@5bjT;%nB>IXZ?V4{ ztfSea3)NXh{op=>-!^`sjWQjFoL?z<{UJDLAf7VVjU8R>F>g0Mv|S(EdT-z{!@NHL zcLqQhm&cE4TsFQyN36=TBTy*nl{KYdRuNNXEe6;oA?1cU5d3W^eY-w+;Kh+Q6=5>U`O}##4b(Pumx-w?*!2{ zDWoB$%+f7lgB0*7Cpjt<(f^LgN+5nI{eWqLs0Iw0jlAcHB4F=g=Y1#0Vlwv+@jElDnO(RdoZBp>@{-)&-5J z48$nwY@}(-<{;U916vE$hX}o!t|B2FRl$K0WgChF&)3~R(uq9&pw(*X=EMl0PVYuX z0ogg#RCVmY^NND>+x@Z7Tfv}UX-N}s;kd!;aB}byS5ztWgHWM=9RLR){%p_GL(t2o z?a~G7-^|vLaZXn_Cr|XtM7Z=dbI#DLCz4|la`~p=;f-4(LnGUjgiCWPALhpXu6~&u zU4Ks+M5`aw*S@=~lr^t*s5hBh?ChUg{Zq0!ks56*nrn()nXNTnTm;?4wbiki!QML_ z81v{*r-uKgB!%%K3g#>9rd1)edFFjl=#<&_>E9h?=1ZT|SAOx;)t*Gnb&QIhH!tzS ze^1w{M=#IL$ImpIUmFy8WL|5YRcQYEM$UZs*TTZ;^}Gzxlg`?+h3aO5VGs4gWLf2Y z<MK?GRcOeOE57mS{zA-WB)LFL3UcvdKZQf3_+3s4mRtc`%u6IrpxcUL(T zOKiml6otS5a=_rYJe;TK@p7FWi5ezxri6xAgnha(sHMYbw?4YN$L)Q6Q3|mYyKUoE z+PUOo|_GZhA#1zMd}vBCR3s zWE2?JPw6Dd?h?b`-%oTmQUy~g9jJq37!3$>J+6;P&-uj81=iJy4yh5*=Q`)=74Anw zF8*DVXlfFLaa)(37e>s@Pk)|#uP`|D_uuOKuW~BiqUK@>*S`0Eo$I(3Em{#gaRFP1 zmEQ{Hp(~3YX2O0ctgRNVes7!m+Pt`8zNBD&A%A=S{Oi${_;Vpgvw)Mm;(QFRYB zE9|Tlw56&S&ddz0374)lEzSxSed}k!S zEF0EN&MrFRL*b1;wR>rBgL1cGrd2u|d3uv12v)gyYmO1TzEs_#*l?%qS;%+~;2O2V z?knJszuc$+TX5RJz%0{^@&rd0otXvHKoFi!;`4ztiU9e;$y_ZnkX?w!}~rx(rU0B=@eKTsOV`+zkh#JajU^MAIE zvp@Co9MN=b6#y$CnBv0k_#aliWwilvpd90G$`h-ZR)Uzy4DUmBloKZqm~{8rr>IW9 zI!!>-Lnm3ol4)9Wm_hnxpw=xW9x|?Hosu*4pM-;mC|ftk8^?eiz++`EXz0Y$JcRmx zXydqs+cAnf@=pI=mTLDgpP#l5m~e88Ev3lW)Z5oIAAx4KmNNUYY!92TG~+d|-v{Hw zDbmEILnx^ubx25$odhldB61j0$E_kV9KoDFY=$3cp89hFdWIQSa_;&krGA82pqkl?lb|4|z`yx->)_X~i_1&3zg?H=0-GWlb#+BLMd>Da znVp48lg(qP(eu|sM{-k_sjH0uA?sl>4xZK#U6;j$!qqt!#5Dz(>(RgCXPTZ(kF5Zm zQwqstC@eahdlobpQF9BeBEd#E;eRttCZN2E2n|)`Hi`~Mi+-;Cj$YGU{XMig{#o$S ztSO40sn87O0rSUUcv%_eY|tmaE7?&>97s@AMzdqz+P|sLgkXefW*?Yb(&}g`=yFYT z(w#R)k{yY=3$|fue2G6TERhUcy2dmU@&~wi7E2G9&j7yo23mSQo}0(t3;N;pHscUY z{!+~KD=-OpdKrY-FNwh6E|^y9*+6s&O>5f50~j2dx0_e4F9SyrGvI)i1G@ZfRF<1l z^(E<=vWGu*J(aaCO==;(sRM-#^I6Afq%e@{HDkDqZSweF_gEOD-+*>je6a&8KL`#z za3C3fR*HPR=(3auNW8^Rfjf%$=^&qzh8o|z|>4Dswdg7uELXSf(`W0&;%xstqoN$ ztieS2zO5~dw1HqT-!SFIl{)QXiO?xiM%yLZx7dB;3=}{zRZ?}@G*S@}NnoPba14aS zQerO9A#&nlPsJY@7zS7YX+i+lx{VEicQe!b0nTcLpqyMKPT$SQ8FoN6+voPk4z7f?6ZX;VN!P( zw!2U4>U#sYGawhWmIS*5llFvRXfQlA(~gJgaV~$zG$Mijh_zow(=a8pbu8>Vj=EOh zA@K`?uDcX*{6>rl`rJMO{RVB9YhJdqpHnz@qt^doV8bLm2rmO)6qC=X5A#MjL*nv0 z>znp11hD?ps9F+Bkz?05ij#ZzEhqrVwe!u3Dcg#*FD-^kwI0W|g8Iahar47w$bGy9 zUKB&3Y*$Q&CX{JC(hr_0)Z&@p5>SZf04Y=pe4~w>+3|P_S^>&-zMGluNW@4dF~u?9 zl-4;82ww!CSmHYmb6$qK(yUd>pM_%yYWMcyKp=Wshi}eSXfRjkGx5OTkNFX>+&Z*W-_|Z1wiOkF4#Jv#w z3YlhIx{c}{*X#V$PK($G7Gj6SxD8X4HLTI1M4FO@Ay4rZiSB)tc=nKCO8GNeFSZ}> zN!8*r({5sAEC_)Pg@8R7W zIpKQCRjVIKhPI*IozS$3KMa6x(_^)8lWZvAJ5JCniG7WNyzm)m*40FyQ?e@U<|r9h zh2R)Dp;Lg9U$w`_W(UKfo9`|*Qp0-x(1+^-+`RH2so7xLzi#^vCJRCiTk~#vh>E)y z>7;^&$9{tKXYd3{GHQsMU&x%?5kOU90_W{ls7C5tX)tXQ+jgIPh&u#XsUiuGI0XCe z7K{jrehi6LDoJ~s2KAswhi(sCg5=BUypwjo4+LujIw(@8LEE=cPU&}lAdY|#p3aW! zHXU!^&U{)aEu^8LKqyBNrs=AYATs7&bhCQxSEBEi?9|gO4N*$C0=BPOjd_i!f zj}*gt`dQ#``!OKLgL%SWJA%6s30k<)rM111E>X3vUt3KkO`a?*HqMADl2zMSJ-0{o z_Yg-PZA9C8;VdpeFYf1K$WW3o`Bfq8G_?J|6Jv%Z#-c)A(o@Vr`++`km&NaMOEc~yLO5NexEF_S4V z#}|Ot$IXJ1*e}}>*NZbERT*DAxhy*|C{|__bdi7oUkqCO(El!l-g}`@;nB#+EN>y= zU&ms{DvTZhE!u9NOk_sw2yqTz$U$vYkSW$79t-X`&MoKmKXloqtqghR?XK(&6bKlH zVOe1q5Zobw2+_x_^p3kA0)N5-ZF?p9XMi%hlxJ*QoFxQiRBrQ&kDVXkDWw6VwvVc*YDKxam&~-O2JfwmK2A~;S<-)W zVd_%fyxL@%$LwZd;)xdN$n+3vB7Ar}@8s_p$wGA9@3FW_o|i}LOhs(AWN^1`?aFf1 zqHr>HBxH{1wIG%jd)5~(<@PQ9{^HYWRh@Za8go2mel*c5X8w~E&Fc4(QSD-VYHh80 zzwD&-Tw_y9mbp1>u9QafBAjN$L zEG@dAne}X0Q=w#2Nm#F>1`VklZ2BL@67*5xi_Eb2N5CLKp>REj1>%_>uZp<7#%it3 zTc}!}J)s`)se5o>%rEpQ92kG5OaLI$Q_tX9+Nq!SRK2GPkIEC7zO*zC%iUO&D3me! z%$`bKZUAKbl4ndP7$su)33v|<=ZNJS!=n(KgO6^(RS=c-AVKIC+^>qji(DH4k7wDH z8CZ5jhUpkWGRUU?9?5ZX7)D=8MenODF=48+i%kJzIbBz2l(m>YVob;0OSwD*(k= zPs&d~GpIG26kFXKneH6b{<8-=D1@%)Oc;mNDU=xgC{So!_Z%GK7{d~z>BKYrH$uI{ z%MJmLfG{FMon%!+7YomzfO9^$Se1AW6pA;;I|N>rscy#I^(dz_BC3-16k(yy@Ije~ z7oWOIc=C9IR!+&NJ3=A1@hIJP81@c&5&G@E-P`Tv`Zp68S-(G@r_%xDwD1nU<|ib- z`*_-ogCwYg{em~@dAb}MjU2)MkYwEs9Q24856eyvez<7qgg7=P?1N4^EwiKTb^1?IOcYmFYZMlMa%|3PT_exuMLd4XhMM zfNK!dCQqI%qZ5w7ZP_WXunck9k*_<=N|A34+Huke9iu*BSCxQ2ze_A{o6oxx5#J5W z!q3H{8T-Sfd20Ao?G8m49mX*^j4A?p7#(CN1A*7qyTU>}JJK20!w^04E50FC_R4p` zO`=kwPh?iu6Gxl%>RO(uFPvaNea1u-ZWUb1k*R15M&Yyc#N6S-jRl~= z#9jrj9PE90cy}oPM2xD*8P7*q%8F>jHK2Kk18I4(G+#I!zfLmDJ6;3;G3k1Ss;R8& zJl;r#nAacdioSWaWy*!_OF1nIjFQmP3^7te_a;-Dc>Jvy05)h>q47NF9tz|FGr^zf zkJ@j_ZM{Dlk7SG-T_!6RYFm;X_Tu4&7tB% z=T#+0MNWk-@x-EY+{onel2KiRG`Yn(OT6@JAYM8)GM1!!5jXvd zp*(*0AdnBuj~{j}zyzAOd&?7WV1I|;P|c}mCbSdbyrYPjg{vrgV(VNHF#S|u7(BWP zw1FZ2xkiAWj=D+>`^EDS0Jc8Ej&D2Kor393K&iseB@d&t#4<1o19Gl08o?QbGYb5fR+z7vD3~hYEz(w|wbX%!z59^OaQg_If9XSYG*&1H z^GcTIwKoV#h!3J>J((QJHaJ(?@{?GzkPPgw>)4lJV5Kb*r^#~$i4(%3NPPmyoC`6N zS}(%Tauv4#E8}B41HX0O;WZZ2a?@N}&BsKvF-Hc(i`U_iCAjuoCh4qQ1HD^4qx2!i zWTzOx!z$i-IsUs_zYu>oJ_B;7E714o%4UI@Dj0>tFaAOMW}VDP*!%`1i&BJO@a48k zB!cmCm|aEU+0Dc73krCMw%^&&m-`LuN)HzS$9K|t*r>z|8On%o89b;2KIzloHL625 zGR5*9=6)ucIjUO@;cvJN#Oa6Y?UZAZ^IoKBV%I-e14&&* ze}F4kDCJqX0=5$2){r#jmm5ld=`^#>%of3cb`sDT=g!*fAP*Ch>k+`{&j&s9-15$| z!{t2l#3{%9o@l|%@hi5sO!Pu6s-hERGO(lM@aP@oxvD=x=u)#NFUw!QDnTtzWQ}Kg zp}|-NE3lZ=B$50Xczwxda_qU_y!3X~^yrXaV(-XAY4+S*qu3EUuU)lSQmcO7xNlm( z{Fk~iJGtEJ+OqWPs_vYm!wY;s-%9&_tL8Jq&L74iTM`E^RRu{wq+i{-PWpE!wRUl_ zw54`=G0X|eJe{AT2CakeWkip+%B z;rb0j;=Ps<>*k%~Mw|M+o5%e+*|%H*#`xtED-B+Af9qx@mt^avy8F)9c3r%QO+Jh17u@i{!I9#neWO8XgdFg7>3cx5m3Q(as}Jttl@{fR(GmOx0#E zCZx9tt$^wZMbYR|?np*+4R&q4VZou9^!?D;kdzlZ3@}h@w6h%tZW!O%1wkHHv0o&h zHNm*yCg@fwN00O3XyI0 z9MjkOF4Lyc-ei_HoUg|UYv!xorV5EvCgKvB9se9+*B^jD12Q1Q958_(=Xx@vcoPNM zX^rK2qFJ}59^H?kr40*^_E%nQa)Uw1s_XV(OBdj7G^4xgODA3eLW@X-$(MB9tc@x# zWBOJWzLvP`#=iO0{zL}RZ-^m&v}lVc$mQ|X4wRibO%S7V00dye0mt_Wa*&3mm~~F< zkr@I(a0o>Is;DN@<)0sNU8G-IP=g8KM-hl{J5H(aP=#)tDa5Oj>j{3sW^7pTTnt*fb_mS1%;E?;O?18 z$;dkZ0^3d`G{?|VoA2gC>TeF4%oAt6D#=Tk=mi(Mj~Y+?`UQ?CW(e1*P#qaxL z=eJueU-6n_S%%h8zpwVBdmtrGCFvY)s7JaL}?)8aI3*+iPCI8hp@7Z<_UDy<*GAhRBCa z(_aeBhv24zcQV`{D43Z87+6ndojyaONBN$9jRh1g5j1?tlPTmOa?~h%&WV@H#hY}= z@!@+%%fTuGJe+1Du%2jA3u067Rre8nsZ|S36A#Jno#udTHnhv@gOy*AyCl=$9FTom&b(r9etZ831VKmtB(N$Yj|`J#`=e!#IXK16hhY z@RhuuCi46exn-q`f7>EQ6DvWh$w` z7;+|_jocaIN=59HM$RWhXuFeqsid#cQT z2tr|Oj0hS2k7`8ZOC|Z zljdqy^qmN%CvmjOhBr!QK_`Yhfdpx&=^($E-NHD%QvDxN=38NoBZvZAAf2P{K&v2Z zOCUp{9#7%xo2@p`F#fCzp1-ZFGFnA8L?*@gN3F>XnC2*%#h{rUEH@JH)G%1nXOu74QbDgl&aVV*ePhoBux{2`ex zRcVxXlu6nLr}}$6vW$aLn*^NXz>J0EwbkTj<>|jeux_wjGOQRxw^6a z%Y$*h4C>}}morzs)crDAc3t)bO}Of|&B)qPSNgxjo)Z@rTbA;d-)GN1 zuLz1O^;#^omj3lky;$q%S>}N3k@7yUn({h&7h)y?UBpq-z7os6rPX!wV{wbik}$v# zivEa`&dfX1{rS?jI6fUWr&aeiEKYLb;!3pFvcXDW-QTL}n#{1{X@8AFCKskdv@dA{ zp}QSq`0Z{WPeUCZztZJ1tUrT*@SvWQa2~vaP*5EAPjE!r1Mx5f>T3U^4?ouedn`79 zmi|k&VEjMr!Qu~(D&`*pmY*sNp;LaAJ45WpBl0pB90WQqm5b2fKsa|Tf6(wi;LGdf?+Qrg5WZlEj97>LEtZ&uEF~h>|O`GIqV1F z_9At)#5(PM{wW0yOC-+YA`H{-QX3?}L5m2t17ytx6Mhqz&|&;v0h*=kFx z=(yOW?z)AaaSL{JFL)Eo+E6RX9SFiX0+vOLR&JqE+RoL z7um+zp-_p&Y=h{!O%M(gS8< za|Wx!6J11LJYPS)=gcY?2H+R_p{M#sA59oSLy%sHnK}@qu6r-Ziy~7go97k}pX2D}&1rv0-N@ zRp7GMED;V8_fl(u5pE9>BsKdJ%;k3uv>y6JTus)&0cBu<`7)tI#MY>Q(7>S-PLF3@ zPhjzQ5zxT^`j}}7&*6VBY>;q;ALJp#J7T3sd{XJY+1`-Nm3&kDPF153XzxhIf_&>i zXAK@sy#ppu40y`RvSa!KcLredP!Dr3dIs$rjS{%_aU`trw*7x)Y`p3z zk!e`WxqISIX&<|P)-taDn2IRug7&6wE1CV=gZrE?^3Fpa1GD7)V6oJoa(Uuc>hSYu zU+5PMR5m=r#;4pR3}5UW_N{g#aroLDCI&Hb9v zU7F1Yr^$--GPCZtL*Gh{^}J-E4!ls>yY~Yv3s>Ax%G-SP@6S%x$%SQqdVJIB;2r5Q zTi#A_XewAU;RkcG`CB!`+4JwS!NVOlH)_>exhrYDs$;NkUb*|ne zk)NI=5?Ys@0bmc2o5?mu0=BodRhp>Yl>bq%mjh0H8igkIAnk(6auDvIZEBmhUqt}r zvX(I;3l4HQ9I&i~S5hFir8!>|moYw0Z@5i5IRbjDMG_fA2p2@Xid%qJUwh`%_LW8_g4>>=N{0SO+IvyI|r^oQY(Z1 zKP;6ZhbMWXo!+if10#fes(k?y(o~ay)-Y{)-^_iEmp;E)h)3O%MfDt((RaHQeIc(%ofB7Gazqa}hiAB?V@skBzxrjnUc&KN8RhCyFF2l?aA zjIn6F{7r922DV=V!?pF)TlF9g`xH|Rif0v!VxpaAhp(q%XdD+Y>6k7p**=#aX;FU3TI zLp{s&&;S+!G62enU~;NXBvO0(iWale$sN*YD9z}W~^kne1}S=#aG2#?x@ zChLf}=V{TC+OZ?j;_{I!98UNEfQ68`M*3nOzfyH>qk= zyg?H3xm4Gpwsz#He|<^(t=^fK^$gSw?+OKI?{S+|{Y)ADkJBU%GtAQ$59J|HwZ$t0 zXTuSmmfB5zhO^F9s46LfV#MG|#y|i*9OA(5feO-YXJJN#cq^l+di4aCo8hPaJoxxw z1wLm_)CJ!smx~zxlnldnB}I*@{?Xd(z@aS19Qb7uJp2#CA>|d*9IEUVAn(7wELK0d z5!!kUMB(5++f5nn0IsUM26l@t7phujXgm#eNp_d5<8rpdnxP4vXeXu})HW6Fqr3Cm%~ZMm7q1SdlY1&pYP$+-{N<-Id#pY<#sV!R%i<*iH09t}Mf` zf@JdU&KE;LC8HF=CEga-gr4ni%om%x0Xd_TS1(||l=NK2W;7J=cqNDk*CUu5p5`{n zLb?2$$L^&H8TPx_07>)UTPW!i8{YAtIg7d>1A zAL8H-H~828fQNBFq|#K+Wi|C%+g2COHP(M9Q*0NO@?EWfn?VKj=k~JKkYt(`uy_Vg z$}LBNceMB4AZ%6^N~(6GDIceG2T)Q@rfI(9ap7&S`vp5;=`2L6o(`D3t1!ZH831_m z6|b^&bd8*<`tZ6*a4PHAS?9s?UKOrln=`LKMMExL?)&JDUytbqc8Q(5MA!q-cdHM! zK9e%uO`S>!VbQ&;6Z8(vM4{MFX<_)`)ac=tH8nMsjU*imuGn>&M*F#McXdsc>lCnh zdsWB%9m+4Rr4G*wpo&&5B>If>OusbEo!BN~&2z`_&F zhG$U^lPS>4I|t?Q9^tOHoLJOwd@u#W*3m1L_dQQ2lx3KoL4SpUN%$fRpzpWql?)xl zVbQ7uJNR&o1k~Qz*`K(vW`7-B0+}ODudq+4x(AIiVYYyOx+^QUM8+sx^S->A%SdlN zx0DAM!l|1-ZpD>DJ6FTD%8fsBNrpu*@RlXK7pKSccM=%J#wA?2p4p5qs3WSz*rXqP ziSl+hMo!-wTz*hx;oui|`@ zoF}Rnk38q?9<6;gP~D~l@kVp_8a*E9Rh8Y6OxvyG57Xo?YZY?&J%M|%{ep2rw$)Yl zc8t1@jDYbhH2i#yhS33;mqvJ>Gp<@G{z~B%BLBMy`tSg3DPjwLT{3ZNzb2W-NI5Vr znS*}Y+Iy_~kz&>hR_r_5cAWN&A>8WiO16h6T^^?_ql-5H=itACjUoVF0$zEYIz*)1 zz_lU4(gF!bTpo6NE)P@XR4OPk6(Q4028Re&OI#eEW>iHNXS|85j-r205t^b6-XH>D z_YB<%A-}xBb{*Z8sE1820zzl^Y3JQv-_m=GgnKh^q2jZW{nl!Zdx`_kpb+akCNYQ+ ztc#*a=lXLvpcjWZAqj*5(%>chZ7O!(_?1BON+HCP8jfU4aP=UyV3Mz-rr718pa8`7 z&ku~oKY_LJ=w_G07**0<7HjzPn?v+h`9oEv^8JK?!cA=vJ-kcy9)~D}fLTk@>6dJU z)VHdIepcS4B7MiI81UaO=?j6icisE5>gE)z7p7hhR>VrpQxh-D&U*iS-(tNo z6!)!V$;(=p7-nrX4{}m-KeJali@}*8wqBGZs$}n8h?;tBROf!#(NSO6&Mv8GV-*0& z4j4~<5TJhl!>c!&eJEns7iGwnc<5wsd0J*@s3b@J`8HFi=T>DDd4_?$dAx>E1#!#o zhuDCp8L*fODJWtRfJ{I$rlcGfNXvcWyLR%>QRKKpn>zrNn1}?I@Zy4uqDBDit^nUc z;1Lb1zfzAjKm*q;5D7oFIu$``O&?MuQ4xa54~jem^VbsUX(ZuH`4+J1^5koFsd<09 z4ykIhxfb_OiWcD%l%5=rZy-nuHmn^4%Y~{nyOqP9>?3&`s9Z=7lOjue=^|Fh>TgfT zB{d95QM$#8Ky!g)6eeGG3;-NT&UM(Nx}An{pNISJ7atbCFA^i~e`tl5IF^m3#KS8y zS@I>Ql(hk_soZ>co4w^o5ibuo|G&#yK^zji0cI=uuwzkilVZ|$f(Y!zj|Q(v z*StER(+OR}6w9~;6mEhP;49B?WsFh+4w|RzYdmrW6mbu4P)G5PcBNEV8e%k<3lN?5{X+L+;}LYoaDtb zDqc%D>QX^EcR9F1Gn`@X2=*w@lkGT-8#yl-SQm69Z#>dZ{34TyD~ygCjp^2?ndQD^<*VjpmY|6Tq$-BMdH@}=YN+^;3wn3>GU z*yq1YDL$QRr#?tU)23w;b4w>Cug-V1{k<2c0~w8@_#ODynXEr$Gw;2Y%KGN#XU;~% z+GkgXSPf1tF4e6V)XkosnJem>69>gM*8TBXZdv)0{r7#`NYK*6bT(!AvoC0{x~d_$6;fL{<#$-CE$H!Jbz$%9A2ViCV6&B)6EjG z8az0;eqcO7yY0fgfKl_n-9gnc4{#JEh6JG^vYo57hqgL@=$z~**vFs5!b{5L2$=<4 zZ7fG$;z5qnRM86@_E(n&nesTm`(;G8D&)pgN`Eae%xx4XiW^X0CAdVj^Z`lIgUH~D zt4}jJmGb3h_+Ww8V{XQ=FI-dR+sh!MYWSZ8x89#o4(?{zwB;deKd66}@87w##W1hI zA@On%OVf-u_Gk?>j0iIyskRp($>cFO`lb?-Uj$+yP}rmnH%V3ojlTlh7|!GOlHzf2 zktr{!^r6cRrW_G_aNIDgFyQScMI}zxG0=E$t^k>=r{Z?xDbZ)EfAMI>jV!)^DE0JU zP5&xup_Y()5?HJW(t8+6KZGK04-4IlyLA|TI39y%-@ve4`1Q@J9-uUyKDct*dEfvn zc`ZU#gZ;w(goi)nhGRJv8>>eRzh9n-IKl?PMF)kBD`1WuVq#3N(tYy85ex2tD@DoZ zM41Qz2jg08?$@o3< zrjeGF@%_s^EdxVZx^vI7|324Un9>Cj)2E=7PtwTAnb*aAb+Nw(2G3-R14~m^qx*V$ zX|Qtf$p?6|(Ws4`r>BFf%rl0OJM+=5kXoxEhY~Uyk zqK*1;{Tgi_t%|D^;&9t~B&vb61I>WQdfP5^e?t6s3o6$){UIN9=J;*Xnev)`OKj;nM7x2&y%0?|7~Uh!|!At!_j z8ehOW59nzqBHHRJakCd5Y?*(suIjj<;p>BHs|PY?Z(YwgaAGI0B3+E)RkNjF3yEx8V2&ZMgBV=DR0v)z*2YvN*@`IwwM;$d`hf#hGFIW?u{%Q0CfC z#4MBsU7U7ZnVS4J-~M^Ky*4&5H`TLd!|%WQg&>Umv14p$?PPe})F-LBSDbrlvDVbD zaY;mCSXj3CoN4~a9A6&PUVBX#5$&OJL3XwpH(1*{@?-E(?22r$?(&aluh{ut=3sVK z$c~$(JyB^GoQkrVT*#M>yz`3c=n%Mmj6MoLG0v)k7^& zBG}A5D}S}K)Z3-D}iO`bF9F$RuDPBe0Wt;Yc(Bh@v1=`$H&H1%{GU-7MRbKqdVKFbbQmQSJ! z55MKy)LFe=5&o5U;XSM48SL`O|DA=Nq9a8RL&@!iU{H~E zMwoOJ7QC74AnU!BLG!jBHhvIp$8g%Nbg!9Z4^dmZJE~4H+y_7ssIUPJ(zs9rghl{e z8RR}d7}*6hrjS}kcl**S6gDet<2+=*C%Aj{oNmWkalo|wnCc!a@*%B<{78PYh9J{@ z@uQ*=g1C;&Jq@li|Kd3ibG@st0|HZpCs+>q*UKG1%J^@hBJKP<_vRKwFlq4-qZ^y6 zF=nz6KMV4_3tP#icd-1x++7XLk}S=B371R))DY4rE(l-buEqf0sc?#IP%)SEcE={W zQaI>B0i%YWjq1O`s-msl4EQPxU)}5o#6duy!$bq!J*e%*=5gd8hTKfLlDETfnu}16oRqk5jV8%YE`ky$H0_Seq+`L<^Pnthj;ct@Jm z+cF^QB^l0VZsX{HMwFLp{ZgBV#hx4Dx61mY*^=Fdl1{W3-LH_fp8nDY)?sa4z|S{1 zd2wpSO4uzqaI8maCpB97mby{~(y0qV-Ie!sD=Uk}e9wxJ^IpZ3NrSEH-u`}r zI4tWYuE`?l-}on8YEZ5!+)AyQxrv`kr3H2>#xjJ2GVCQ6hc-Pn*U=d3&nQ7&XX@qPPjPIoh*tlpj(M=^SkVI_ z@DX5Ti1Gk_=STW@)M2%&Naivj&xPrp1Tng-?ztnMVPxIb!E#}$81Hqc>*&?tbP>%i ziQ33scS}s7G(PD9`AlXO*9q_la{2H|^Xab_VRTy8=R=;+$*kSv(PkiF0<;kCd>5Z< z#*n`WPvV{A@{~q%#S*hzXAykYi1rrF8qk7sY->}2WScZ&n%arid!lhQl%~EK#ay>f z_#(bBhdcf((O&r_kAW}^C|-kaqruG4Y!n3Xf9wUft`xhEKt0A3cpxqp;hB=aOH|T? z`^FL;tX_bC*o_GwauNc=aTEQn!8vd?Y><1XC*r#qNgq#?VQwaHfPqT*kLrZ(M-Xly z3T23x0?9LFMLiCl1lz{<4^X~aC5|xu1d*?7-Y$Rrv?&A2bC7Ah=@MW-F6QEhW@x?f zT2wegY&jwT;Pj(Ni26JU4rdx73Jni~P+%n~FZce)QE&7kA#Eef`6ejdU%om(B#O2)zF0wOCoz zF8`V7_P$zGT#i#%`z5w5C^jNyIVA2|Q{RTvuA0p5Gij_ypu{Fc(1z!iPd|*-oLfxR zU09q_Ybo~XTUon2cfzx8VR>xELKoylYkcRD;-*I821b_K)caOmZ z;F~#0e08e)Ac1h?5PmP=M?cE(UW>yeRE|ut+@^Iq&mJH=CZ7IK{3eBY&toKDvZg&_ zWn{ytG%gpU$G}wpZTwm>H#g!mxp|O-$=xkiSU~dlN~IfsaSJ$b*n#4z65}B!ClbWa zor4+GIAnEJ@vc5{xmmpQREU%Pz?!-V$~q~Jwml(#qO zYt@vF^{8_pc*}wyfgyHs{t1`Mm3^{v<{ku^FvcYc=`8yqK$ZugEwKmr&kt54C2P%a z#(qnIw{WV{^Bd=o)9qfz5WAn$Qt-E;Yu_&recA>Zt^dNor3e z{_w$EE)@)(#^4BHaD@U-zjBGhMO8i6DX1XCN9hc`uoAr!FT?4o(!T#Nnl7`wCyoB< zqk`9a-z{3i^_W#(0n17RLlzc@sG?M$i`kIfAU zIwDhA&Q(dn=-o9FpB-u0-BH!Nzh+qJblJY?p~yJjBg;*|I6v*^hqUhRLupXl;`{_XZZWCu?^x=P*P=8oqsjH7pf>>L+p|Dh6scG*FMg@G5|KG# zq2kp$T*;gMQc?3^&F?Xz7Tn0-;NZ0AMDgFIm2deg^SVoM`9H@?4$tJ*{hf04>RV2X z1FLJP6iBN}amzzvcPlCehHk!~TYo!g@vA-fx)@fKds1|iRCGx@oL&fil=#C2piJA1 zlR$HQd-x>m$VXCv8lczQ5Wg*o7sjvh9D07o;pMT{Icm+%Pf zXIy#(?%)u7MSOV|c^Kvm{IH@?ken2Z&8+u19-hX!jr;=?Rwl!@&5#c~%w?@W)6#&! zOEfQ3OZViu?-{$(OtvXf*aD-In&i2A?j1`O6|`}*FWUerOdc5T*S7YB5stN=pt|VV!O=0f3rhIo;pb;Mla=93} zHe@q!UNJasN#F}$h>zK5kWpvS;*T>C0}p2xkM#nj3bxsk93S3TlSTksSV07XKUn+|)TAS85adXF(yBW{kvZ&$$; z^P@7j7c;{=zh{Uo1(Gp=KbM)6+|DYXC__Sy%bC%A{V&#)NL}0wotT#vZZt6n8<`;X z=(2D7=Sy#Bu4`K&CeNGl$1_>yC&qAhV?G&WwIp|3eEi^1q^EgyVYlFvL}DeKo|&D! z|4}2v^yB%-b9L29r7QDv-Iac?Wy#=OotNv6(Fej>tmn6{{00uCi3f(y;EUfXINiA+;~l zxCkUo3Szj>%^YV}mL{}i9UJu+*4-iC3yxYs58YF#ginHXe&_%D(QjZVCyPRrQKzxO*W=9*z{GNDN(I z*tI8dC&EuMi2pI{eS-32k6^S^uW)VkyuC1rt!7rg>>Fh@D0E8~6A@14jb%%i^+Q$_ zKu(9lGQ}M@AXdUJ6K{MjI16%3$~Tf!c}y&3C(${ixT?TFK-l?H3tSpdN6Tm!9X4%v zr$9USd<+K9(euuD(ZJGFH}0q4l+b-Vjtzu3=E;d24kXt zYTKeQ(rZAvN9~@`uJ0!8-{kIMYnj3;MBNSO7x>HG>h{op(pRftxR!;9549ueq0o|R zN5~t~n*&TwEA~2xN_+N%p3RPy0&I?P){`p0j{9fb8Y~Fa`5)T}86LxZ0mYNoBA%Ax zpzs-BLk-YX2n)OZVDBI8oK++xb-Nw$yO3Km!05e|yOmTa1qpde z`Q0{JqGp?8e9{w1SmEA7RYOGPk!S11nQw)tBNGhy9@& zw@ec*Je)@Mevg~4h@H^|ekL(^i(ek?Tio!7Jd*D<|5wF&3b3Fi7yqtECIJz7G;VJH zO2x`z-BW4C{nU=a2M3c}q!OWG_@s_3he!y3g&O3nF$h<6smJKKDDNZ|F#iGI{B&gzSxF1}$X-$fZ;TEDEjK^L7asmEw5aR1?RDDlAyb+x5 z7$rFcP@ga^38BsqOR!&hKZ(=a3!XerfOFaN5tAg~b7mYM#nbE@$;SBWRoGI<4}_0h zA#M78qsA1xYPV1^cybr8kGBQDY{VBy$EjoBlf7o(D;?d@b^wGxHTnS>2g3$j@{dPX zwH4Vzh7jbi`iJ2D;C;+Q?b)vTg%V&IamrggA@I*GPU8z?6z*f!LdKZA*sE2S^Y5%R zx@;?YCFPpoXU>NElHhz&r=A-ddV@)&u8z3yf{}1?l+(QCuDk}SpqDmGba*RZ@Dx5C zE#n=17p%wGlmSXRIi&4si~=-KRo}qrEubL~!ZH~*R*6YSNsTd{beLzJ3LbC85+17m zXwe2eH8$)U{JWr9lKDU2&+xIy6T5*y>G{Lp#4HV&7a zEgkESPWD+NF$B z`r^W3bx@1zJ#MrR<|#FkfnoYB&^z@Y_wOP5*XYY@+7~t0!)UHd=hf^S7iljJP9+sY zQGK~^cAFyxFM|0Y5J1?;+hj?4+`#>xPf9O0rA30_>jnFi>>=V+HmUL@Sy-Twf6(mL zjvq3U41Wq-{-z78bK-g&cU4E$Mo#xe`ALgzp&4?i52mJ$wlCWR)f!>gA zkLLf8Wisx`s{zP32dEv;T82y}jt7ttZ0AE!ph_g3``|hEAL0r8K-DRf8_l39bVIhd z=#i~da&)3uZWEt%1A?&!1e@!4JvpS&*RBv`j#e>lOMQ(%o6BH)2p1>$E0mxOkEhY(kjJhBm*s8{kOP|gfA;Ld>yBifrxpOntW~h=| zSgyrXl1{aBefN2ND+4kOvPmW>fe~l;%Fgo4Fit%X{G)@$6o>B62s*Nra?Gny1s;7P zwa<)|f~~D7wmY^}T1>tAa7}Tf#}$|Ms>Zb1dZ#qa%WCTHPKs}*SkuZ#_ZQVF9e7J! z!bEgjR_0^V(~o^0yw4(aM{0!GVmT+79&sHxfEk=U$hpTUzc%TsBg z)-`6cZ8h1`7GAMSGb4Q<8MiWH4Sd`q*+DT&p%l4Kap3MIxBEJo+;#xaFAeQ5Q@rNa_Q3KZ3}}A=>*VO9bHkGjOX+JYV2k zk5`LNV5Sf>bIh6lCfYMM14QNHM!c&$L_p@N-cbN5fkI=J@)5KF3HU&3=A1qp}wbR2AoqPr~ zJ^%+r{u&xxHGLX#Ly`!2viMb$E@r0pe`ScTLYx8B@neSl!$&TiU7HvY@1dyC(RlJ7 z_)d`-_A{YHyWEE|Tt?UKlrKQ?PiEwA5+@45*E`J( z6OPTiD9}R#t8?Y3XB#LUCsR-c>>S4dxVwi28y>t_r{R$k& zN@>)S9ER-~`hOIZqYR7@ybDq`o-mEzR!P{e$7S{^WoR;fs!mt(nn#+%SF+;89?m=P zm(*Yo5;~bk10Zn7@;P1{_QdC8`Z2t#?6bs*iv76c4?DAPWsGSAIIzvaHsFE_aUX}{ zJ5yV-NB+)aFV9c3#4g76F7~a6S0+{#>J~r5&9``su|*ffVL#6Bs$bQOFAcrOmL6OF zJ~ln{p}WO;CXtt&R`FV~&uVheBB(l(CjHSgTWu)~os<;P>pN7ughd^c)p7HeR@%K5 zfA;+~j~lC-FJAs#9EW=Ky94A5KGaN(yfR8%{F`07GB?(@2#T@7BYS&Xr8BRJRa88J zz4!6MdOYrXYjAlyUW2v**~ka}4JyYV(0C7s-Z;xW94>5wL(kU!2jlJ}LZH=3o|q>O zlyEXVlTpc4yeFGSx@Rv-0Oa^KdAAFcN#RZ3assJZYBlY$i*0iuBeXRiRa92)lNFW|RTC z3BSfwX-1X#Ppy={hs_Vn;{v&XFJN$8fJbhW2ox3o(b@!YfDePH_l&`{K`T7g5BPL+ zud@HDEhH&C{rFD6&tz;kLS(X@|A~=|?!Isc%>m8j`<*(^e}qcu2a&-Py2HOq$7rBP z;5=l!eh?CRe+lcpJq6=);~)GmygOK9Wa7I+h4s>bgfL!9hQ7$W+f*sH6LNXM$hxsw zU$4y2VxplT8N!ibH}TGeY{rour-15mWWm~O5$l!IjJucD!J;;bnG}H9dki2J?1Eo_ z1qu8T*G6}IMyhFajm{C{ofUdM$#YN$7gq|TWs?JEUM$X_Ni9V3Wy0GBGa_k5L*Den zgW#!dR(14o9)QY_+Nxdz#sSQE6JPGYwJQvlOL!Yv(zG9b7w;IaDa5?Uexckv!x4v$U(xCAgoLfc4gLUaoFo0PN_6*exf?NN9+UZ z>dpRpP8izqo1eWB3?BaOj?NaZzXwMu;;MHhmR1XYz3zTRSGTsB8@lVYWZa_e``~rc z#e&qR81s?vpKrtNb?C-Pi?e;xYZ{shiv=T1O=Hu`#mh@^3)aggmL=9JlgsXVsNUcy zfc|&LhWHj71WR!jW4>iMXZO{{#m1(<{cmK6J&a^@*gUftGc1~MUvmSD?Cc0}f?>GP za)OQ#3os|T8IBcF&5a5msmGTa|2or7-DGz%U3`4UV(R)OLHM~BlE-zK;yhRaF7 z=Hr9i%>Qw8?(s~w{~!NsGc<>ESPl^#@0!BhoR3RIw@$+*5vH<9&SE)_(vfuE-^6NI zB-;=j9I~*El(iAdaVW8J*pgVx;dk}>FONq(n(ededLLe|=L=%t(m^0ZjvL^um#RU< zwge0lVU)uednO-)M446KA_D4EETFwW7y=PP*nunI5WVeyxvjd*H_;_H*!L;C>;RFd zMM6is+!FfL+L|}J0>-`0CXAR=XZol?Rj=HtLD)Cw!N}pJm(KKR#op!EQ=mWe0a|@) zW&*9(57+Bnw>E>CtNRfNhVIv(NCCSyZl^KG+GU7@wx{WjR2NG0y)4)mG?lEs5fouwl-W%x9tU*}6FX02D4%DbR4vmw2r*o53`$pt7p_}J4xayuNh_fVNT z8tO%!q^&OJ900)`+Ba307zfhimJrMJJ%oP??(2leKY@`8-HLkFzkyb3(>7i#iJ!ey zr+N8&vI#SxAkmbc8MITw47)WG-WLu8?^w@O*bD5wafWB&B^@S1cYgZ~yH_@@m&pO| zvHce|oN*&}9y4t5)IXY48?$b-x@%fp@%)4Rf>e^d{Azh>d1(?vbjpi$^ZT_If4u|a z&49+##(@I))bRC%`SGr2a=BA--^ekir60WkOTXVW*Jk&Xg)A(zV6Vl=XEcR<{Ug%@ zcGLBWSGmBhoTM1(b6gM!6@A%G_ANI`FW-yp_HzYFAJf65C(F&LQ%9G30JQQ#hGU{r z1VuQWYv(u%-$ptUF>VCk4(63@4{g<-5?=tH0P7q$VnJ8NQT2vjDmLkA&lk_=8(qW)BrO~fmhog}kz+~KL0f41k-yI+Vgxp7W@6$8`S}Zx;vtf+R&IIS#CL)pA z@#N|HC*QzErK}h{Z?|b%v^w1mUpdV<-RdqzGFEtdTt>8=rOrTiHX&L(1ni4qzqEGZ zKcsEoqr0|lJH(8Bz+K@RLjgchgV@o#FA|2{>-Z=6exD?i(8w zh_)pia>*(&37rJxTsn*ia=nUayE1T*gnMury)>HUb1seJ2bSKe87p|&>i0^4R~~F6 zG-=vxR#0WZ;rLhD<(haM`Y2 zxo#ODXF+mD|-D|G6h|W;=Lm-^aP1x_U=fHSU~m9_HlJHQ{A9;O*X6R~nXaCSXvO z(pdPFL2;C+M5&ijq$Y`R@_WZ_6eI?8M46SDL`ZCaoYOE)Zq+n3U^_8Ccf2)Ya7kKf zn0h|(u4Hy33Gs{4V*M!G$N|@pg)M*;$6+VCqaW=tjYh^+Nmn#pnThMQgJ} zU6>mvi+8H2R7{Ort}IMl7-^kqHUw+7X<^~aOlw|Y;bH#7^z-F~FNKTCOY^0&WtZPS zXW|Q{86+pCrY3*Gx(zS-K3Ze#)`eH>wNPb>GNG`M?NxjBlmJN%0aV^#Y`BYt3hjzw zn%JBIcQvRq<46QxWa_M^=|3tW;J{4X-*amQE|aWWbBF%^{OGNV^;`-}2OQkIwY#tF zw0X>a>~_#%n!t(nES0Jk{a)RCPi#=I%N3_oU&iqr2F^^+6=Pd8kg6gE>C30KT{)n~pq+i^%c+d}#&j&OxYEUp7GE5B*g6{-2XlBuvONlF?6SuYVxtsXf zDkJ0jMFeZ8FA5}V?cXC}bEB^WlTp{!B9R;d5sI$OA(XF%;fd5*IALfuT9epOv@*O} zarL#NCPtv!ht!vi*zEN@W?%>b5&%8QeMef+_O19C7S|L=q~5z^LCF6q$mLQ$huYZD zXjhx3a&_*pl{BwkdT#)1EsTrIym@_$uG^up7r0&_uMhak?{=ar zKG8on`)FhGucfyuot`f*tt{l4y{H+uTWFr-D4(31G!f)ZCMlK{71X%7xhGC@GspZ* ze@s_{3FhymXW0KSfcC#Z7S~j3ns9!@;NU=<{5eQ*kVG^sy~zGdU-~q-&{3bdFkEXQ zpKVonb1d$+W9i^R$dY7vpm6zv)AIG@S;K|lfRs6s;L~^tu;l-*V}Rb?7 zL?*@MdS#KftyoJt;|o*5tdDBgRP>&%8%B)+wyb=cjNV4ZaW1#n^)=x^Tv(huZ_=;x zd6;GB$Ki=a7c1a4q;xk#()zEIuSJ*trweX}P$he=G09#07vXLZfnA$&Ff&JSuMB zY}l?H1f$V=kl)IC;e6nI#pvYV54gc*klaLK_i5|(>wjt){Yq#MxYLMK_keTf&YF?? z?zyeJ|Ab2!Fn-QkpWV{$gFe}xXehXiTH7|knpE2=dq{~s0LGE+p|_AjAA^Suz|C^@ z8DXCgvb@d_X>}%cn8xe*JwILAB_8b7wa?NGDp%gXPO|9#R{SCy!99m`k)vQ#3u$mw*I%Am zHM_d@&V^TTfm@O%2yl~hT8Ia-;0%)XK32)nq+nM!{kXqo;*Kcl`XeLi9HRB7L!X>& zyx3-hc6K5RBZ?P;=x4NF_tmN6FJH9j5Lp?Zpgp<^|9#UY2zohn+&Rb1@pYcgwzZN* z|5qO#AVUZhb@Ftd86Pc(R9v7Nz5*}Z4e1k9G!8J&B?=E3BI6=lk{iUV_Shno7T}#Nq^I`|6;q{BQEwtao6JuzY>1fmx>=oRt5|Il0Dul znfl0&e>BZ#ZJrsvY&aJ;_ewZ1I}?AT(wHhA_kN!GS(q|i=0KItBwWePGYu$|n^tDa z&Ie?ll9WbN0zX<|BZ^BhOnQ}j>DI0Vb{8vlN+R6lIIBy5&R#MeWr4MQn!=h z!^+f0g)+)$>)c#v_TX{~0ADOEPdLqIFTG0rDO{R!T4?P`i8${NGgO_t!M{VLaWZ8S zH9%GM-4G^>2qt#0A!W?dn>;x5ptGpjoVN2~S*Xh^>04EGBA5O6Bn;vW4QLnWmc?BF z%l-`zCqx8?I)S)K$}!{q%@&-q+LMVgsnXa`^&c@7Kve;aTi6%xpP13k@rwmuse`U! zuWTqLhVdIx&v|sdCo|8zxI^hFm5hsBb^lC`HqM|lRM@(#0}$EH5w8M%^dA8NL`a~aUX;K{6tnHjk1%DsQ65;}}Y5?4qhBAY9uyEr2+loyJ zYyb`B7sv^@+qTf*?W zFy03=wQxBczN+z1&CB#XCmf)ibp{4K^$BrZ&w!z|va7&`Yu7s(!*qLek6`q-j&d$^ zl8mvytB4RS4`0ax%G!`D!mAyap2~hH5K+>KdY&A zsCTybdDY~NK2eeA*9a2=EA!(Mdc~hAB+^>SK)t>u<;cY|G7fuRUs)8FKG0Qf*yRxvaOgv?)a;{Tc0w8RD?ar6C zKpdrv_xFlcxiLM|bgtoHx~j_7)9{{~LBz)%BBS#?dmNM}_*Lg}1IaX0o-S~O7~oV6 zT!@5RAVLMHz-@%DO7mpX?s*kW6Eb~^d547^ULVxk9w_6GTV6zgDI75A-TsK!dG!;Y z7k-urqRu*{4>!!TG?+>z1}Oc0LDcVpL!lP#8Y)?7MJS;pS;O@OvOm_P144F#Y(^c8 zeT8v>0(#$B@+{c#c*MpXE6YN(sE=Aw_%{@_vAB@MMz~ly>If zUcZ<~I7bb1(||CmS}GQ-x0Q>&KAqD8LiKG(vOa4~?_o76?V5^PL=iPUgpvJyQ zl@B`qccYzmq`Ea8^Bkv|=?TlhiU2F^@@)fUXx;0fuP^nFpxNf4APTuB?d01PJeO`Q ziqfp|7%&?Vm=5M)g4akRQGA@$!(pXhb;~Cw48UVchef_SY#$kO`OL1sA>w!Xzu=gd zd{Bhj0oO9=w8f4}6K=rakVdp1M)!UmeRAZ=Q(r={8~Xvq7rMuQfj}6uUI%Q^fh}Wh z<`XuEfw;WSC+g(?+imap48h{f-g(sP7v^HeZSB97NADBeV-tpE5rWn-v!a=)$0kM2=DNQICI4%22CPUZ_ z4?mIr@AsrERlX3J+A)uggjQY({1G%WKJuWAGQ4Ha{9J+f3`?ckzx zW_DOO*tj^A8zGZrQ@*9d+g<)39Qogl9R$^)V1^NxlL5ZRru)}cKuoYjQqo)8=!V->8j61i zC`}|`RNxrCwAp2>TY7eM;$c1anvdEQa502bb2&a7U_DoYU`ATn7ujI2qU=E-C&eAx z?|rq78D0V(_WTfN6;0if<7KerMu%T_FCEdp4l=$(vWAo)jCUGF-Mz&|*dt!8Gg!v| zw_(Tx`_>uCFjX%R7uO_XW(F;9$se1vWNv05SCCzq&kUL~n)m25+bh@mggQ zg_;dJI0aQ4;^|BMKtLG%QB8>}4KrGTyRkE2Vj{dzzh~p8lDF4~YTk{3pmz^Ku3?{<#OVdRp6pUHw7wTE@Wki*;jrr1+u+{oP zWW?2k{Di}2CH$|Y`ZTgNMKD=iS#T8u7uQCePboK%R~Kf36MRmj-Po`&H~i|AL}k!P zewj*fIe#}sHd7t2A$4Ztd4r5Z>bwLDk(T63GXcp%mP!4f&|+V}Xf$Nj36#e&iN*jYVTL7^ z)O<8kGsyUa(L;Cr1HY~13xyf!MWx+&5BB!Y1NVih5fcci__C5QKr^%8fC$iaRjbT+ z+u(%mP0jSH7>_6@-^U@x1I9r;g<_OR2<82_YC<`N(5q|UL)i(u<{=h4hYnqA)-YQQ zt!o3Q2BJGn*>5#fGA#!5||lnZv}rKk!d9d zln78UJL5uxVB$^GvzP~4b;>H#0&&H*zLRkj<;!93&NrAuCUruj{-Q4aO$q;OR45pY zZe0tNB~0?aLH}%l^Q5Q{m=aC_kA#>PNo#4!?TA%))KdYYE2RLt1!5tg36s01vb{X@ zKILOu-K$r3uKmH6-oUl#s6*@tWPyY|`?09Q1ite^3p@dHQkek&%x9oGS#yxZ?IZPLGoUx|T#6wSW)ixIVCSe$1_W2|0T+2t@tK~E|KWA^4T}1-TxfBj^ z8G4Bg9gL0=c;~10DnkRl0(G3{=b{e6vupO;^?ZAuUH6@}RNt?kmrDI5J>q)IS<#R; zD~j@01yqAMT--`Y7V|@0lEX&ubT;b(KOty zjTPmjA&XZJhip92wiBy4`ra1KXYx14?JWSbFY+a>IUPJ_UoI98=kQg)sGZ<5at(Oa96^sqr@${$Ca@d~P;OnOpe2vT350oBF)@ z_xHp>?LhyzDA^?X{Trtw#l(e5*+S$1EuwHRKpXM9EA_cdMwO>1zeJY zV5<5bj_@7#M>{bLHf=Qyn)tCD*U!J$XBH{65d&>=Bp% zAE+s62a9Gp?CV710VIpzdu;6vOBe>`eFdqNQ9<7!YQcuK^KU_!1HGTGd`QGkplbg^ zxsvjBU3bBTo`c0f_%>wp#-eV}c?;Cd zup%*W>g~DsN+uk?RqzXjaK*2CUmmW z_fy^9N#er+Q`z=$8_Ra2;cziXmu*mT)qA6Sl^8_J5F3Ep>Uud`VgvS~*v(fUWHGJH zt7@$#6jCxdKrdy!UGctqteSNpEb&~I`K-4AXt#guMtdlL;9aGusd7B9Aa+own5(F+ z&CWm79M|u4rem9j``(P#c`M9df443-uYJMtxOwvq3!O>2xIKME5h`kR>5Zck!!d$^ z#L4Q$LHV<152gH@ob7NPDxKZI+Gd93_C?D8><)xuMFR;L>8QCbD>-5+~ooL{jVPgX-E63t*{3N`eKI30$I$$&1?ivs$+rpoKR2QOA)j-6Z@r*Q9VfqDW>LEcU z;mD>fcy^Ah@;m4Ct2O;dfJW^B!ME&r6Ko$t6YuY=2_bV@kxaLW8*6qV9_JRp&X*HN zw#55mY3)pnYDm|3?^b95KMq9M@KfLDt~44A4iAR%+(M`V*AaVV-UC*?=eut_;;L%G zJ!i_F&`_E(&#lvPt~)(n7dP2h(V&=``u9aapq#^v_;a0*>~6TfL7tN)$FHOHe7LII z79)4~rY^2ChrD4lw{L-oqtnqje#GQgZRwc%&cjc*S8CV=2-0Td1j0-Wb}t0lm-L~A zvX@>o+sq@x{lJlA0f&0XmRP?+$Zo{%UFPne{CO3x8kQzP#8a*W^Kue+Cy zJD$$mIOXKuo0c}3W&s;)bXjEOJ1>?t}31`JtGTKOlu z#5;q}bzj=7Wr+^o>bnKo0g~F#T2O^;JdLKI+bNMcoH~s3B)0bNk1eSA?>Gdai-FJg zZpisx9R1q|Wl?YMy~s0~c$M8XKJuvdaP`KjkV|ezVB5py94F==s<=5D@UXL8I(06OND7Ly*-7!n$n{K&?tlqf@B)zSY#`lgfFCmBbhJhLYr_F9UZA zzOU}%WEY5&Q^OXlGkfM=)XDivU$mEI4=yi7q|AR;TOM(mzYMl*uK;o`wI?QJ>GR^; zaMDuBjfC^2|u zjF)*1!pO@U_cvCsK17>nY}CFc%LC10^;QhASiOEk2>>-=kW35+e{Imtqr<(ORnZ`3 zT@{d^bhVk>X>Y6jsyQC+IQ)0`p1-kZ)zA;y3AxJY?f`bgpr6;isEHj~y{WZ3NU%jF zS6B~K)hHpKtXB%AJ??u9f)6!_mpg777|smnxZmY?*fh%Fyn*&7B`*cuJ7S65Mb$sm znFTSo-TUO=0n6O_Bg%17YPMo;iEFo?NI7q}s&BuW%a*YsEoh?ASXf+>jwRlG&EllY z|5_mw$ChJga4_*Aiq@`yYP>YFj3~z;yzD2VR0@yr*qMh-jJOY;`)LbySMimwuE`PK7@k*g7mIMHiInw*8p?365SI@?!Bo7(x;W}Na@?`QL^5~c9jij>0P09 zZmxdmr^oAKg#6f`i(Wtvob?Zq26(I$gxF(R>mW*h$;W2g_HO!~kRTyHZbUzwJ*qFw zr(42^8E(MNOw@R!#T>i#gU4s?pBl(`mu4gBL;@3sn(q_ELs9X|&YUOLzd)~Jd;QQ4 zJ=EaWe0Eg5HWAJ#4ZOe9_*^>oyIWqN`E5EyX78GuIb2=|-O#^D|2fc@G@g{;nA%_I zR9$I0=#bJzd7CMQ?r>nbWuOFq58ylJyKN~tMmo3jMg8zIQ)Xu<)z zUIgUvGxoykR@}EykT(!%hC$uBG;tRB0WIi}AZfxxmqkpxOoHXXl~z-_X&w!r8W9j& zYY&`k#y!}5!@QHDrH}TA@D*Iy@Jt??G%HO4Fm5o8Lh9_RPN(b6t{(wig>!@arI+ks z@jt3LUI$_C{)_HAfHqFj@@`p2CdZ|zt-_WbrV#_L>ki7grq2W)CJBId0t&OegE?!n zx%>$?>Q}d$E)HRmHKdS&y)TKH#W5bY>D*=pX{0En|EIHit>l?p52C!D6{r$@F`*mr zYP2kznhq`N(T(@;uh3-TC@ztXh5ijj9n3KbhFoB2SJ?ow-X-coh}Bs0QMf!&`#tbWzg7x@=s^Xxo7PR*sRvvP6hZxK`k7}~-s=WgjOG~XnfAJDQt3-$ zbK(0(2P$GW(AwH>)!t3~JTtEhT?jjyb8R)SxIsI;tEj(&kRum?WmKruKEzIniFd|U{sK-xN z{5@Or(fz5|%1m(DrW@GVUKlQX;ZBV!&Nd~BO9anlu))uwrXP?|vyURN!~gFh#}n1?#W1r25H@D-U(S?n zKW7me!}1WuK;3RCdGQ$1XyE$O<+F_hSa5oVQVox6d@Pc)8i%|+o@s+E8DnkYeFK5G z1Pom7o0JISL7m%zm1C*n@B=&!0%pcx_^ir1LnT%-bh$Yj&Efrow{ApWY@qA1ije=J zJOaBuTXRmo*l^9PWsN+3tBeTv)KF?M#No{5Bl#{t1dTnZ<1#%ymt@)Na#AvsjNF3*Ojv(b0N#t;yPCd`M`pL_r? z`Y%f7)@u|< zY5Bu4cVwCh9GEtZ*-4Ho2OC8KKK*Fovj!#Wm(j3lX8^Yb>y+NF-_l5w2k;4~q5^ya zFwmY5B;v#;-QqYx$vwc;%%h9ZG)X*WMRdV6WiRb?oRDCN9T?!~46RT;yJnvfO$>5v zj$aHTuiHt#w4v~^A%SZF(TSv1f*}nF?0-1_gO0e@ z8Ac`ds__}(3iL6((BO$7!&n!O(6QI=O#c7+)6iM^r|wk4Px(iI{wAYXPAws zoPu6@1&T=Q38L%fdFR#B9!z@WfH`RxhAH*PKShLH@U_Nz)~37Py?&Dg>m=Gs!w;|U zh4D;CHZfP+U_g`%v*28EZAlay|Fac5hXG=uM<;dl4s7i9{qBAc021wdOpQt|#$mxV zO9<22^A~&HUw6B=D)rSIiRu@N%z@O|bA=f?6N}yn8WoeQHTtTseK^e@;b{m~eFeIV z^O_&JCxK)06smZJ9aaZ+-!(Xd5)6swVCDXMz*3P62NCb11HQ0Utqw?l=~RC&y&VD! zk4OUAU*9FR=wU%n8vXAyPP+Rkx7kz7_;Jkv?l>Ll{<+mPgKdYc7~)?6rTrLug$Mh! zRIumPMlHx|4M#UkhZmoYe%eP*i#tYeA9~B@55x)&g-suVw(Z8g7++L27igSRqRlqS z`1?TmaRm|PfZK(SAQ8lBhn^2lwx2mRWaFxCGJz=^NHU=JiVtQrUsXg28WcZcHyAqg zk1*JdHe6XnQaXBsD-#+nzB62&Ynm@J9rPbuKI!GCDa1}T#0D%(879vsrp^PK@%Jte zPj1=>!aZwK6~p7PGR1Ips5U+@IIi*#)m821Kz{t}KosNR<;8`$HT)5=3d|e(b2Vb4267!jVXvHxiN#v3Ch**mZeIq7c(z@}g z2Fb^l?WyLNT`Nsj+X)h%spQ!Y>Mjksc!&ujVO&XP$(_1!;1lNB0S0^NYVswPS}~f2 z3a&m`;v9Q#WYg}pt;WDhga{O2{%Tk4qQ8R~jcgk>f^amSTj3ZWk=o7_ak$1Rb#nM) zANaR;u>oNZkUsI~e#!`!sdAt`6q>{X?NJ5iB1a@eZL0h6AL6^p{g{c@W^oync8De0Tl7Vco2;XtPUjF zg&c4WZQ-2Id7KACcpU0EBUy#Y`*v*_+izA^cn^%kzz6#E&&s~iDemQ_#qWK^`6^joXzvia?_^#fxG%hb($UnOLwQ#|7X=r82+|lKu%Nj7nVyUbyAfPgr zL&^K^;gywI;M#@Eb5LziRFKcPA)u**X#bBH{G-oP?IQwpX(WYw`q;@)Hu*ug=)r27 zUzif@g2rENFdK|gi|7|rX0J`NH2` zLy)fN!f+YjxC61FCm_RY-eH6P*vAB_gEqj?$OUk))XCzba< zgsOH?8GB|L*cDjHrNl~~?&dTK*~pLw^T3gBP_*b8?UZY2a`UX$r^4RNDi zm0<~PP{229b8#)i+M)&EdLT&iYz2&LbCqd&P>#m#I}c7}oJvxpB1 zB?9gwht=s;q0MJQ(N4yVP7d_D(HxdKo+1)mJMEaeji5}pS+$x>e)sKJ z>eHFG)zIh_uB`Wkt~^e^t`7}!3AsyUd~P^yzGh5p6;{RN1%}IY3+MV3M36q-!v@-Mo2`9;@rYW z>RmleX>&*A(rl}&aba%y-Q4`-(d8cD2bumnnXP#>kv z$JkZv;$+Iw!s6`MVn~6KSA5gl&*9qaqr%16dFfQ&>|MhYg6YEZrRn9_<$3Mp*~-SG zS1WrAWY?1?)ktUI!)y`+5~4U-HAMGn57`?wv4h7&@8vfaCi(EV_&te zn)`?P1;7Q`Dz zgBjt%wA{5KRd5&wU~jfVz4nTt>4wNsz^_F>3>`88Jy6+A9I#9r@-rfneTe@YFM5XlMw|tt)9Ee;3RXT zIo6j3eT6F-f!9DJA4KAxiraM#Y}w~t@kNk%+y<;bzF|1sf|}y0zd5XJz9W2{-R%!E zMwO108wG0HHbsW1mr{MjwayPXnyM4y%t#L|BKx4Y>8D=+aF9c}GxBp}G-gS*I zX5sqj2-Zo9ZI{O0qkasi?R?XQIsnI`&9H6Ku^vAamvdAx>@s^_JyfL*lVtW;LER6( zyzU=u?3q5)pznw;v-0R`WMJnPx~Wd7@lA~rVd09(<+$aJ<)vfGt;;>jb&H>eJEr;H z`(i&2T)irYnfuh>noo2BLAZrY$$ce;cj=&DX^LN5`1wPB^s($`e`PcP1VQ)7oJH%> zFW`tL$2;cVpwyXdjy+;{S)%AjndmY-;h4Jgbg(IQdG_+)-0wYd@rKkSzwdqiycI`T z5gY5|H+G=mzBU*j?Z-+-aW8Sz5H#eK0Av#!FW$2)+x~*R2CAVj`N=~QCYdnMMAcg1 z1y{VH0>{Q+KwKA3D$=;~i6Sfsljkggap+HWojmy2-I!@B=^rSb*GsK=S4?onB{9-j|tQ;Q5Z=s<3PBt1ge`!l8(zKvwyARDgC=TBBBb2#gADE-i> z%cOH;DC*#ctTQtHD@&9bbhDm2$*~3-xMw#WhF-AkuJPFF8mY7;gFy$Yy09{eBSw8S zOb7DVOk2@5_SPK+txG3s-esI4cSF`|Nq@tlK~fH_S1f{p?Q!(9*| zLhqdfhR{}~GqClXF4NgZxax-YJtLqe%0~Hwgr_0$WI04`JT=!1oO>#wFfM z3mol9ZWul_`djDNwweB}T9+T>@##5xrxeAU=9i~W7P!lwBN~@7CC}xteRk$abM#A% zjkAj;aaPrv8)b?KdqdYbafH9bzDpj}J!kzyE;uBM9c|!!V+X_Xdc}6bEsDjZpQFHH zlbV#Eda{I+K%zFG;$ zL_C5@Sh28C_9me^0c)7^Arno5Q#6_I*L)oG!(P}x#1^XGinOIjlQ#kpB@ zswzRnSce>cr;^43^aXur-5ndA6sRmaiE-KWCtS6w?*v*C{XWn+Qa*xvRu#kJ^Kd2W zZ*FORmRO7_R>}w6!%wB%qHRV+uT~j@Y>Q&siEwalsVn^nAjx^J)ZZ{>Go3QnA4XY| z4i4Ny|3XzCK{6|~YZcRm!m+ew`g`Y#bJW`QpX>f|!-Q~YY-bzi6R~G&o@+)LnH6gE zPZr2R=tgcA>gM(Jr6sGinh5F=^F0r$@nDxWf4m0qw*7DDg;l3N(d0)8@D2cH8FV~P zoU`}XOZR;CI>P!#T2Ddntq~V64I)Q6Ggd+8s1DoZ$|T-y^#`9cc_*Me-LuqumH$MZ z)no7rN--R%fPwVrSMz8gq}{OwX^tE`Tk~k2ep)1Ty)41(hiM6L~LlC8?(vjGFm^pMbXs<=MosTZ|=+ki%GhluCJ7c*~;W?>q_BY2kj-GNY?@ez!lfnM@X83&0r_xc z%FJH(hvK^`%;d6Gut^`Y^tWd3kUfw`yo;>UE z+~k^Y%bLN)o`BjhI-x+1GL` z93VIZnTmpV%pC~g{EGs&g-3)y*D@^MEg<1$bosjcdvKsU&<3R_HP=9;T)A#NtDr{= zi8XGe0p3KFfT2~#27=46ebwO^&F)<|^?znSG$5G@o6#X@!%?4N1b2z@WOu>}nK@kiPt!fhmDsZnchG+Us zR0JBLz2D;p1>*qQ)ZB{{mwyI7Ej^h3BzA@%_`{)Dcc$7%m5>1W=kBS zr+CnR5zqsdjCQvo5@2jdRkM6(f-K(;>c%6gX0kdAO{K)wV1Mqx6*arxKPX~7v$&z> zSpl^K#`oC_dl_^i0Yx?&3nm| zT4#PUllvcUHp=ovz3Qy^*y5Y2U$*)I@-gB6K=)h;eKjYFV@5zDMfI&kiFsxB5S!WNJIwj40 zmL%Rt{dry1l&r995JmJY))-!{3}|wx3*k<};s(Z#a0E~Z7}srwhpGX*rhq@I>rS_5oL2M}0;>gx36fd<-DqlSjY*X_P};BUqHp3~7ef*$Z_mhAV@!;2z^{dGi!yoAMDiU@ptM21<}F+KqL^4qb^*hE zYpxvv1yqRX*p(nNRtrCZJG3?tqH zJF3jIR5M}R59+LN8U5;-p|Z}?AdWenR!`$R-0;AQxyPHKYw~rk1>D3aou>Ja5H|L| z0eeq2Rs$YkVx;13>VW;$5b=3&)6xaqfW`9=J~bSj8KZYWHh$uJyj z%OCz$xYXb@_t~mxai+2<&dzXb?&tifJ<66RsB(T?ZpGm+IL%K0%d-)E`oy?C@Vl=t zpeb?j(`e{KexTe|9l#OiE&2=>#ZJHfe3P1&e}LDLx)1<*ieF7jv(v`{tzCG8?{wWB z6cl-uH1iEKnyio#DF|E%2B0gj$6|WLu)>g$`Tu&dMZp$WqD>5k<6(|VL&cIY-6$6k zoi5JkY*GES{Uktx6pta-T4?{fhAB7)yMzRqI}9n3sw?7Z@8P)PkiEoqpw&ccrA1mD z@C%_{s<+4yWTMRysL4l4d_`Ub?uRx3EkdtfHHTJ|PMm@C2-xDwaU;;4du!A+9^gt| z!fsZ$lYp$CltaMfV6k`rXi~4Hxv@n2iAETjr3P`=6KT$6U(Zp=nNjzulCf~55ET~f z^A%Mim4D8`agdZ*XMN--l4bldI;Yn;^zMbP?1qCy5cGa~6~yAU9>_de{k*t%%mWR> z;9yl|&&)31k9UFK;tW0p8si)%75~{P42)laBn&^u;o$bWD8Bl7Z?yx(YGBC87P&`V zpo11C5UCpb;5{22lDp~wU^HqM!G_@6mprsy*+y?~hzy(-5c$Ds$%nrqvtn;9b7q56u)7tS|#m}pdUDdqj^=~qw$v@r{czG?FIZnC~+F9H1x z!tR~CgaCt92)%*_N8JYrgVk7075Eu~tk--~#;#=gzH{ep4|=AoUiu$;^2_En9JIG) z#ZBy!2DAfiFKk~TmCS%e*E*DG!FYVex;&+a3TupSOdEGxH|(t+f!UiZd+0IQ2-CX< z_wLDJzZq{afnKo7B-V;XGmf9lTN5mT$H|R2z^BJI)t?>0zalnFjP%$-LYbr~gPMZ6kfy$b++zaIjzTppZB7R#BPsES*)oOQy3NQc7lf+1q zsiQWZCj1{uRv2{U`dVxZckFyzZ5)+y3H3ruz7Z{(IRuryEPG7E;E5=v*N92UjXLh(U_Xna zOt16px9RsV_pgEY244_QZ*0Ic!E?;T`^~iuaK6KjR6FOKHv-BZBlS&dAsTUQ@F+p8 zp!5!Xauk|-MW2c{wuaHO`s4MR9v$o$7BO^haRGZ z*X_H6AcY`<+tgzz%G+Ovk-u=W%8~eR4Clz|Rc3nD@Mn`7RCDXz8MEZ~qdMGJpbybzNfleLQuHahF z6%B&%6k|=<5rQ#qnBnaBRqp606s!$;!gcJ3EDafb*3-a`_is&W0*f1xII*B6#b=tl?hmzM?? zRxbZcT~5tgDF=V8Nib*q;U)1Ai-*FW*`$L50YJ>XYojBL(JW2~c@tPOUQ@>Ve%;%z z7#Lv`V=zkFd!>RpKXfsH*~tbJGa?L}t-Xrvg0elJ6f2xy!_FaXvBAw{&j)0vE%$waBjBKoA5 zKhZ9A8$QX|EWy~!MK_gR_IJ<0Z6|?dQk;-=044^M_2ViySS#w(_#P_7)bt&&e4??C z2$h~n`^ereaR+?*6$)D6Xky_6huu)oUdwE8+)2R~VgpIxwTH3k#dLyPs-rHzI|5uw zFQ4uTnjA|HHKzi!b7bn9x1)Djj)eI35-vV)4A=t{4o<-!Tn4CNl+##r^H#HN zttf;=2rs0=8U|6T4`Mxuy)I&@rk6DPk(79rzM=l{SJi3-0}>;qpls3&;jA4$jXG`s z1j1TunTrG*dX{3UKq+ICy@zN>AqoVm+=tF`9e~))^Z*ztFX8ibIj1`wXr%xtDjqV( zKPH-~l-@AxpJv3tYkp9t!gu1fJxet#ge%}-krctIR1fb~o;44l1apC}OlU=L%Jsgc3=)f#^NT_5xP{be$+=*VCGG5U*x$qW8{`iG$>=T&D` z{V->7sXj43uO%jG`1*ohpcf-3?28#e6APq)L`#;q7XlPK01H#rz9l?GMe!ae(Jd6p z3C54)2)P2>6u2{6AVh9jQz=;v9T4Cz1K{wSO$Xl1__MYcgx7^~E;#FNtF}h~d)50hv~$2BJOzYhC1$ z!3B8p@c@XR1ye|-cnBlgB4Lzve6^wmG?L@6?|&sGq~`-*DO;8kLHz2L{SdP(J!lR< z#-jid2a7m~eORvx1rI8aNQw&fiEqW82kVl|i>;-xVy-OlcSR5F#+~0{{katl_6$q{n=)2xTi{eDg=H7L?-0DUo7%MnyXTO~F zlM{aruM?MRmhP>MPfMXcgNnY<{?LV4$BcOCDfvC;W|4bfJ1JfhVkNZK|L`qS zyLDXQ3__>}@j&?&Qt^>TCa*X6faeo0JF;^iT1mm|gAd%RPx;hk@EVsrHsl(a2Ecdl zfBD{WwF~0(BWYkS$BKNG)LR%2@Z6Hndnn$8pz|#i`Nm}l^@4ADb1-;M@&Orn2s|~g zy#%0sp0!p1$d-jVT_%OODR}tux*WF~WdL~s{MxAYHof@k3XKhaCf;;7Zl8Q+ThYhN z^XSZpIZ*c|t&{godF1E}zpqb}T6^kfVl>OV1h|*GJ!7;-G6e$u7Xqhyax7h~A%brk z`?m4Vs>$ky^wsiVgU0#7h{RV%JMUK-w*g6gM7ioFuR0<*VJ$LxO>^U29Iz?-{|j4h zwq5B1$=|iOd6TunajPZKtF4v8(RthBRwXMYM`wS{)(G~R%$At^?*7%X$(>*3Npcr7 zFrMJWwgE%x{{q9gb9?t9n%^bi&q35uK>nlQ0XK_~X0!k?UBd(*aahTu)B%g{jGs7W zfd#a5aIyleQZFAX6LKRK`zy5$G|_Div>8gjSp*ky8q5CPeIY2h{J;ZUpUOy*T1?{J z%$o{s?(#GHgLe6M|a0{Okz{&C;0Rs(i{ejzUa&j@8SRH(Ad|*#>6IgzYd~0tX><98H zn{=wE^)2X+#e=U0ZB-;XuN)t-U1u^_#*GjtRz<)vy@(L~Z)J>-w8R_dA5fNT41Ony$ zeKjR;A_hjEJ=%U{s(T@7Q^=FMuOMH-$Fm%JKx8s9zJ9Syn0`|n7&Q1nf)Qa|yKR&E zZRzwnxcZNxLv{ozy@#HV#N&aEkJDPPuURnbJ;nShmbS{m%l$_ezVOuJlN6lO7ueai zJJ@)g<#!D~+83zDq3O}uy4mrD&%uF{voTSPuEvSM@f=QeL)~!nas)?t?srR{>rwyc zzl&WlF*B>1!fGP8F$Yc@)fw!I4K`_vi`eMRuV2lHnrn-VXi}T1wt=&E`A%~I-g@$? zcKD@C*uv~=_r|3EMq(oXJ)dW9j1RBGt>6B!% zRrJm^9wG?mTuqZ>(NL_5dgmZW!&+F*j7CX;2ue`#`6)I&rMY_!o1@_d}bF*^-4vV%cfV_SV!yEzmBu3ICc(B<%Hcc8dFIi*i_|HTph0bH44m>EDUuNDq0s_pytU4j zp>aQgy|a?>>5(B`J%pQ5E~DO6e8yH)0UFMc1xDs?4Q_kjvHF$BjqryFn7D>4W6<3~Du!Eca}GHS6(@kX zNVz(NrRs|EBt^Qh)O-wjy*VK9QERF9%105LBEqr7TGRx>&CvL)0epPjJ*d0&Y8rrC zWgs1w5D9iL;!k$}K3KIMp%e|dE(Mu8JGT-)W^r?sUp~%CQ6h^_2v!P;?HCMC;eYLGw+X=g_eZEmW0A8MFSy(I*aV7 z7V`QA62eLj+UJh(5AF@QTDn6{ zz$G2p>{Zxc1>Od{>YJoz&Ys0FuskcJ_>#>p99`a}-6@-CNb8pW0Eu6~`#n&j+ItH7 zaelxh@N5_#Km@MB0!d!L(hwTvam|cnh}N!jvv8t_?55a-2-uSNvpr(+LN7YlmzBjK zN>V^|_-K}pm>BD4xk`KtF)Yhsypq`sMM(Q;8VG?z%F&uWNhXA*F!hdlN+T-MRh&SV z8*cRG(VEYtmc>qLxXH?AIm)+O6pHO3ee?r67e<)zsY6W~gKn)^-incd^g#$ta_rJSsf(_RO{Lr{)IqK|~nF&0%6sHV543Yir13`tM=);{Y6)l8rIk6e)Vl^ZCpBH0#r{YP<6lYmFvIp zW_UH%|5!}0$x!3{U{1QeXY$(5inz5ko!FI@OaUnS7F2X@eBXX-HVVNVqPBvVcDXJp*+|)O~tBMaOZ7oM806W|%c9 z%siAnjt;^L8%*LudYy&elq2zw6pgMBkk-r33r0iIGKS3}c?XF7@4y!ePr{UNLO7Vr zFG<2ss6_~b60%Dwh0H`NOD8%~;OR0&XSAqrUCMKCG^Ag;1<&xA@!RxX-PQgqW*2mA z%FvfA|NOCo6qqVX28%w+{(^!6Qs-y(Q+7eDMZB2Av@u7DwL?3MNbn&6BjVhA=~c8dwwwsnLc4UuzPh2R ziOyF&h*$8aP)^{j)ts8X>35+d2xWhTp8$bX6;vD~eNt!yPEnzGBz^pWpFML^;>V7F zcW%9y$%ZcrBB`Z-3Dd(1Nzgz^0V~x$Vi@cT9O2#5R{9jV$EPBN&U7LZrs*wqFp{^- zRpvKd?^wjc^ySYv%7#;^)tf1&o1PA>K31MA@VeG=V7y~~DRSQ_*GMRkG-c;O2@F-V z?K+)NI5coGs0;Etf?34&SRx^ekVcYql95Pp=#>SlYDH@}T@3bDCYJ#2vC5_+7XpGv zH?r)?h?yR&(A?J_-8kI6hOY@vUeX{Lp|^w=p;{DU*fw0Emb~w4>k)_uf3|$D%zeQ2 zq7)-4{;>ATk2#~oLovBop5tGXAF5RR`vv0iCVzB;k4+3kCZ}uHJOq`g{EvN?Jw~gK z3jNZ*K0B@7H`fp++HZPkq=;5?V^CRDm9r?8Gya@CQ1i!s-6>8m{BLgK<1l;s(S@=s z9cI|_$2NgeO#SlpRnSdbvT5TB0HgE?q9cQkbuRn_fH{-D%#B})?%Mg8?lFS3 zrcVF-*grw0$-I-DIE8)JY0&8uY&F>7=qU)JIF%x@0VKK@)WgFYLtlYtooDR;gK+yi zBxKMI^*C7;ibChYHR`g7ideid@vBcXWzYvK)HA2DcGqFis%Tqzm0nBII zxubG52rr@FTdg8?Z@0^~+Wg+@*Nal?dE z9{C=nBD2dw0(gxGW<|~To%$rf(E%1KnTo@5q^WqE!lub^xk&! zYn0kxNi;ER8_aMg{sY8uoQ^k#L0xEruEDpTg|*4=Tb*-vRqh9BhXQuM?YgxRp-l0x zG1QWrDui+c%pdG7mNpepSO^osH2Cos&N@GK6z1MYk{*Dh+IhE?$Epuf`=}^uTQ5OUG7TJcV%G$Z>d4|>u z@-Cx6G=L}_#f$m*=|tDIbKUV?*O2CJ+N@#`CZHKBoAp0TisED$@`?{L=B4_x(KAx7 zvE-xap}W#H3C8ws2cBzyxf+{2oorh;;rBawtY)~5*Lht~w7Ya6(A__u@4x=9WOcoz zGj6>*?r&S%-|^`+-P}(PZpmtBb&PxqSKKR<7H!nRGxSMvmi-K94J zEJU7Y?V`?l_3-zVPuthN3D)N}hCoaFG_PU(d&&P<#?JNB#y`yAXs|opKD;rvQNFP> z{4aZhxAC=c@o+=*WzH*hyOkx6l0@e>XZ+n>K0gx#uii^|}fL>K$6(3Bw_Q`|kSExCq%e*21 zNhT`n)^$A5WlE8{Z6m7D)~$~mP1=4M!$jhj-?iPj=cMLcRKY?!fu;utywN)5}x>!0K1@PK{=pA zzg@p^&lgjH~Uu^9VmQy$SdUqSKsiBUM3Rmp4ybvmS40K{;?V?;8} z01rJ<338|(0krpeu~e5bmbp;-0-HZbA5G1G;DBp7kSR=%cjh>ynUAlx{(*XV_h8%+IDpt9Yd?o@nJI zc~s@7yvQkhnkX=-KgvoePO#gCF4qqw<)Fpx!0-@Y$h6Zi{tCJbrXIcc4wNd58^s%- z5c^@Gc2)MUe&s`6Bp-tF$WS^GLcjb>sTX2P%IFZjx_Od{m-wg6oA`fK6PqmYhC~5^ zhVf*kcWzBT5^OZoH(iuxI8}dpg3mw1T9EZwY;bzqwy)nd|Jg^mT{}|#{{5wmnXJ;; z8WwKcH9lR@xN$q5eb}FU{-H&RcSp$Or(*`E-VAwv$zp09337iEs=a!>F%pa_ldmx? zn8~FVf{B(A&++xey`95oZp|icMJG0T z<(>OlS={*HjjwaDf9L(z{nu*%VidrF`Zj*W{WDpuZ(P)gi}C+wUf5guO~}Cort}XY zLPvzL-mI)r9LiCPa57|b&i~#Um;#vnL5T~RK-g+$r`0P@6p}>INaj*#AXR1U?!|c! z7)=rwAtBXb`^i(jr}1ewScaa0qA3{K60sAT-t26OReLba-rtwS2W8Is)Uy=fF`5{a zlZsBvK%6R*PE*m~u>5qKRTM-hWzAR&Ugprk4B%E#TucGpk5G;jVwsAgEJDhNAPqo) zK~7qP!q4$`HY5*9mZu_Bm}5A_hbI#75u~|eNPKl9afyZpHH?%7stV!%b3DEp)a^=o z1D1*|Iz%=Z|yxs{XKLR(d>p7p|pzdO+)m?CIcgIKGySc$`I1<0d{dJ(0BTw_{b``1P~(m zhQ7F40w>7vR>Wyu2pca4KMio?7ihCKyRU%qME5PSIFfnR zo`NJt3^Ky1{?Hvd=x1KUH%l61s4;32ZCG*#YndF(MK@z}3CaiXKOzBpyi@CR+Z{W= z^?pB2cB-jexKGtqMvUrcL+5%KU^H1(K1F%qIsArW$I>vhYGRIBWf;N%LYgvkVvD-X z=6!kI5)`aGL-U);x0{MN*TZKUj?~1KOcMeRM>$$u&MzugY-`F1f`d^M*S62CrG z`a8&q(xx(F2zf2DvU7_Q3OaK~;+8KDugN(ubw^6cnZAm=tk-kXTb<4eIL=j9!+8-D z-oj7v`LVx$YZY`?=3_fs+Lkx1$8FqMSsXpgido_Ht>=QuVPJ;iH3qwHTW{GI2Q^^p z9Y4EQUJd{GXR*=LxZ%Gsz0tbyogBM9*!eNODHHCS~Use978pnyfz2Dsx91 zy>(K`v8`V_yx7GYu6tpJ0c8iAz$Fq{H#lEH(D+BHZ{6zU_oa6J_Ar;%6xPNu6-gp{ zE>Ry!VJV|O5+!=*M$mqI$s^P9GToNqi*KA|58)JO&Xlwte;i-SodH`Xpv5yKARoJe zCJ+)*?Qa@Xs;%i`zlRXG(#m!}^fS5wfGRM{Oo`*E?T$ZT!XY*mXNm}Ull(Lb0;Cd> z3<8m$h`TxxxTbr>bU;*!bPz_28C)a}0=o zjO-SNPG51#RR#hk+bfte@I)z|C!tzW&N1u^lC7Hx?bTw5oZE7fU~%OW3hKv8?dvf^ zDdGy`YV3Qh5nfja)!_4mbAlIruAwH_6i&eD)&$etCCvm=D&iVc{a6``oSqus`CwyS zB&P0R`*}_bNfdHkpgns+HAx}~T}a_?+LN_g*}ME6(NDY%kqPyXehzkI6>HG7`pge^ zdCFo~+~IK<-2j*|j@qmdLa4*b-1e*PU`w({x~7I-rtGqS{2;r9o{0vCJ)a2xU4rXE zGzlvDAjbSISgxU{G+g^*gccN(#$p@KjssMeM=$DFh~QeoYsEU zpx7^=H37F@mHsH1+fO*$W&W5 zIL@j7S`tz!PauM=4+N1W81(;hTWr{cO|a4w7Y7(cgaxA_!H!nR4JO8tp=8A)= z7Vp3=2nP~H5F_hsim-VG=GpssU`|M}l(m-WUC2Sw5!l>MOM(vt zZi#1Dm%u5)kcccK%(3%u(7Y^S2`=6@atrK9dA#znua`*=vd%u2b^-nj;C9x=5t=b2 z%;RZwzQQK~McT47$quMJG~?E*ZDzw)w9VCrmmSy`pcki%Dd zO>FrgaP1BWNAIOR%HksyWdc3HQRfD%5NrWoz&H%}*yin!m_dg#e6YS^tk4_{d%pv~ zLh;1cH=s8&22XCP|`(S353ahoDlIn(k?r{x3arlFyb{RB}(rLr8@_j1SVEbDFgbOo7wjjQJd^V+b zA=DmHpIn*BG1I!XM#!)X(b8y9Fd{<%MA^7p>U zdW*@w`8l_K>rMN$#WVrzPg`ogYDC#L1ULl2>4Po+WGe!kT=jS)XvKCR6iNFMO2JwL z7^ljpQA|Z+anuPj?3t&>3wz1&qo8Zu z3u=GQ)JLomnr0kvX4PV72Vw6h8?IqjgOEuWYvd}K*p?UY6x$fF;wR{ zgzk3}@g&`+Ac#ARS$-G?S`K*V5MBX4kSmMx=yd`97nlUJ|(AwD; zvHO6;Qlxzw<3>@&>RA4S!H6V3r=n0B$W#k^QRdk%2xD6$3gIX5EbZp`rVHb#<)dF5 zpX4Ql9#w>T>U1#<6ZDqS5zc#Q6xS_;q#utd8FcKp++zufW%hxZxf2Bz?+`?B6~mRt zyl%6iOc!>l1HqKm?zjUJEc8f3`OMSLM^TfB5>lJ?Qy-47D|u^5xk9P+L`VVgixW?d z+BQs?)x*F?QmvgV$RYi_Ldf^CHan|8yl3j&8QaQddci4K@xG93@@dbl6q#&SQ)#T3 z&>`k*7x0%dZ`&TP$_`4u)5F34EVZsC7zY;^o&0K;RjrHA#hfO80=q;MjD{&mG5d)e zJMoH1eL`pb&*l;k?(Mq%)h(dl=p?M)6*fXjjp_`}1FHZ(-4tur!`3X`lH_|dqryU4 z)%e8r!$&7?Pb}3PwWyEgoeP_Y7|uSbbMlyMcDfK|VYZ}mIBs<5MFlxzc9z@_IMn!N z_)qP#&a%}PM?nPm`>SY`*u}+H09*KLy2c%F9Ev7qzM8D}_Qeo#YD%4xdtY!CJGdo+ z)xmn$n}3Vpf7Ig^Z@I^;u4FGXO&)%;e}vnov%X-mynU@HOeZ${-M8I!W$CCle|`%s zg9-u9xB!)?p~lq_P5|l5Yb#i9DlkTBgGwA+Q4eTKs)Cb91NBZKB99`b7^%Wc_No2g zEfXN}13vZSxX%GhFI%QLv=@%Glci`qG#4jU9yIqSfX*Z45!HnGp$x>&b~{r6JP6|Y zSm{Bd?AP@u0~U%(4}mF#)RF5bIiD1&{SNHHIq`T|@dRv$E%OMdovi>{jGdBpnERm0 z6!E|(`DLN?gN2#rD&Qwe%0)Si>tm*mZz}1CK&xiFkPB0UrG6^%!BJ)Ps#oQLG;#ZS zoHOhP2`y-Q*hoYkU0W9%3sl52j4%#|1Vh0?mo$kY5D`&~dYo8?B}V@!*TFul3C{iHj511V5OhDbSl?-g@WDL%xzx3WE?mFa7Pc~y~#-lbb| zL744xV`tmh$63Nez%AwRJcsDXyvx~HT9D(1!Y@e@6sC^xWDf8J;3*R`2}oeLHc$%bYME3;*f???-x)er&r_s5^6B$Z|*S?3rDyj$_?>#1wQ@)loYw zn7)ZiwLg=v}_e}nNyAR4+NH*z5S_&X^*WR$YblcL-teEvtSkO+h5PXvst`dCTla(i##WO*|)aNy91*Ohy7c?H#5=ycG*8Iz$0%Z zy#Ci%_vD(@x%vuj%RKPDjl~u%M-0{SqPU2>(GruDe`|uUtaN7Ta4g`FtQWm$Y>bOs z{qUa4b)H--1Z2=PL9}3LjO*U8@ohCGPH=IJmSGXszikv_vc~4lgv|}F{n?ujr2oi= zuU`*nbL@?nqeiB+)^@iy@%dAB;|@t!XR~0^Lz0MaPYUS}Sh#|BWuc!_H4I=&aJ^<# zX1IL=!FgIrZq;V*4&f0LqBjX|cN(4T<19u9xdc>7h;`EkLBuQI2sWR9DH(W^6|bsr z_{}|t5EQ5*BK!{qB2+#$f3TZ0_^@6`f{vr1!M2kTqDof=#fadv0SgK1>w)G^VGBjP z&Ofck7JR;vT-&ALCQ99d#UW3^ln{)^fr324-eQ-E;4^29)li`B&3w#ZK+VdtSkAYg zJlNpY8_Jl_Rq}aOI7KWeH&6ijN=IO{6 z1kW#?3-1h=qx%%3b+@WsT?VixSQU=|DS^HrH%g*WH8Yj&vfImTb{Q_L09a`KZYo!g zDr|nMY-y1p#1d7P4bDp;^^_(g_0a8iR6|EQzXXDl9_A%IKuq|C1l4EN{+9J7R7o@5 z)XiQ{>~ja{3-hA#pYNwYW2JH-_HJUA3Y4*_=F)Py1PU}_Q@~oXIMazdr~Zu%9`!7p9FW!&uG73qNt)YMcuS15OwC1VskYoO2g4K?bE>)uUDrYGwR| zR}s9vvi;xBoVDEiwQzU$>$CH-vnzaw#=qljnTq*Ce->xIavLI}@}m5=&*htRaJgKa z*rk>=ui@IvKE>EoosF4ElbFAM|NczpaDyMM{#^Op-5EQv78aM-7calNc|RW_*AfQs|;(B3I|1l{upCi0YC=&ifc*#39Es3RLq+Rk*=R zU}yrHpv}(OBlG+=cCv`1B#jm@J;jz3tIWf^X1eArFyy@;p#z4Tak@*!bvt1(vx^&LaCbx6j<9WtsE;Ekg`LQ{YIfHc=Ayd(smsfAwaw*Y$uqSbKAH3ztMJYh>kYS!LYw|Fi-G0{f9wM>c%2C9b^v24*4gWY)t-{HL2Mi z8>SZf@0G2|dTU=p!}_oB=x;jz%wp%lKq-d%+TUq#RQR0dGZdG$&h*!gZZ zj$5o)2lPY#b-sJ#l49(7cfMWkpBcVo0S!!(ZwcNE1SnaH0t3rigO_xNo!JJ$IJx`H za3Z9|x+Z!23=;kz{?>>jL>mF_-33UDI-@O2L5t0tCv*a^9j=Lf5Ym>UdcMJKgS#Ju zr+#-LiD{1zQBC+d~-`2sBDa7vY_Rh!jV|(VTj7gp9MW&`M>c#3lc5le^%&9*$Vr1FzkLNLHC5j{zEkBVI$ zXL+DVp7B%JwBbK5@u1%++ z0ft}b=hj1@7h8AgDq4Y3ehDIy5-briBYrKAuOX(T_$|i(5O|oboKw1Ed;~qKiE!b; zgBQ=OC3+C#9xV#4&Bvoh{5HV`7B2MA6v{XaNGH3Yb7q@xEPCxV)&Bl*S}Vv- zWbw;C+s$(d5ROeyYgX&oEjZYoY`KFXC$5P=6D(v;Odc#WOg_Q93n}uJAarPpDQ)77 zj|;(~y7b1F(2O=c8GGv(cbXj$&J@tGGpTNnXV3EP8EI7okZ`(Z*Xx3{BYl<9ce{7h zK*RK0MAaZKH-co|3$#}YLc1J^L@eVuy_*6iCw!D-B&;U+q53F(ar^h+7)rje^#vEw^!O*Jl(3CTpv4 zZ!)*XeT-Pm5qw-#Jkm5d$<5#RSF-UZf4yK8Bbd~QUA1Uj>+AO)3{IAfW+V}I=spBs z%&;g!&M8fNuEmXe!E-jfJ_IrR35xv{EaHZp9jYs^I7RDJh*-aJl{OX%>dCPNH;$zO zIyRw>E`xFe;U-{{2!ODqAoX;JqK5Sh^Ax6L-R+r$;_aOf(B6CI`O%`5xhg~wN=Bo% zP5~6DAfVK+FoUT*tQ_7poCJhh!dsbkr2dVprv#_ffp&`yN#~|5k&tsp7)%}ltSZg2 zc?UE3Q}n0uk&k*k`+1-n(U#d(@;^-pfLR2e|zaWxT3NlV3f%@v5lfD6_2OJmpvhuxJAA6yKbJL}j?V2qSIj*;jOm zlapcxJ{8yOuV`9~0B6$V`ypkpSre)hW5cde>?rUwtQc5WJU2D4)w_5Z9H|T-?Mg|= zYbVN~7a@b)M5O^L-OuO1VDO5Wmf_L_-P}VLXjh_WK2#*=3-q!7iLdSj0YBm`IDtAA0-Ihg+zSc2cg7^OHoO$VNx<>9Ms;2 zt)+vPCs>YWUURC-L{T)$5V}tfm}Y_K0Q~Oqw4SWcIaP*J`4~SJR4a%+hCm;InkF_0 zu}D)8LxZPHrweId5kKOcKV{i@KyO}*@b&l2RaAs5H9Kb9ml@5Ah^jwNQ3>N*b)8xo z=bk=89Z8w>$9N0d*kiXVq@@If6;BFdQU&*T`qbm@C>ss66qzI!_jbF!+x z1aCC-4UC1i?A3``_;oiE&;;&ZnTp-0iH%Wn-w@sS5GUZqwZyFsuZ4v#xK>n5pW1qo zz^&5LI3z~M0Rbt9u7!<1f00>aSzX@x6oHkHuO_<+<6w%6E3#BAgjbo!K6!}1WoSV@ zuFfG&3c|#A1_b=u5a^lorfThj2K;2YU{g|ek54rp-(O_KNT{aVb^&gn+k;yiMc$`I zavYMN06?!mEW)qiAqrid1wYWLsRrS>a2wfMb)z%V>YwbUy^uHd4+i&i-#b`nwYcJdkf-LKwEVp5W6WnV)re0-mfgzS_ zHF!U86EiHtusyi5Yx*)|`N&bAez)=*GdKKFzOqEVGC|W8Mob&rBb`*Y+!Dyg_iz zesX6d|Jd_eZXX9?Bpa6p_WD-{+@qK3bZml64NC=EBJ(#z%tx*o_^%XhpI>kX0Cgb) zW|YZCvv&X3jn3FLX5*Ue#`vc7%#F5otP%2)YEB2S=m4B@h7$Mmz?M9B=J< zBSlE6%B%pcTTH$pCjuce6h1MmAlWVn*kH4r9@!LxH;I<6kxw4;PE(G<2xdL@SkW$! zGxyHE6o$YKI0`{NN1&(W{BwQ$dn45{ zju1r`g_*OtywgwU)Vh90iC*=)bQB~}ZKp{2?vs6PO6^b`7U;vc0xseb*Bo|%TW4b> z)3^NgN)!$OSOJ5)kgZ;J4qA(}vED6NS=L+wvd3oE_~-Pa*3bpG;zg||oEO_upB5m-5t0`Hr64vgW{j}WvOmb$XV)t|9r6-~;Fdc*T3Bt zt$(c+{pZ(`lY7JP_+3rr-ufE%7WaQsCze-gp9i0r?u=Us`5hi{|JCZxBhPQ$ob(XqALb?&xJPcH-pa?H0+yh|Tm`|{@4>Kj4X_$|TY%B3RP zc%9f^9}maP|D9XQ-&g~GM~5TxT=#U5qrRD)kwAf~1Rbh1mN|C_)^A3J5jUTWKkh2a zO0ws|(eaLh&!G&04VSMkPOYj+5`ySdhUiOU8Lg_eA9B@N5U5JpLS-_=Daa~#Mv697 z5$9d@aO(5^5`&J=9po2Nd3(Ie7>Gf7C9eDVK^bS~8`dRSkSGp@I&Z1j4$<-F%+MWP}Ub)KWO{F9{5hZ6c3%zGM_ z>VE=m61UL%NDwCh5`J(xXp5dWq<-)Hi5jrSpg>k9P4VRHv^x1066E_&PfNJ|Sa#e& zb_?{i7MfJu^Fvqc_*L6dg{;6ZJ@kGGBZ)^oeaaPr9-?%~<5j19Z%N=Y>Clq|N*a=@ zFaJi4<+rcM*r2i)8H4*@*^6JUdIzJZho3O5Hck;IW;*iR?1Truq46P6HO&P#yGYl_ zW%eXesdcv_*O9<2xd9u5-+5@hUsP*oVyaG$O8&eB0e_H9zgWPe&BCB=4~pB>9mT%* zZ5kREs55ppJ3?WpkMo+F8MxCd#Kh+Wy~V8&kZtA zl~_sog@SS;WX|fb$y9F%&{uvk?670vGKCPYZsi&uO!q#(H+<3MY#DV4b#pVvu=LPa z8sBvG_3Qy}sqRo??lzk!#+|aS^Yo}O^~Q~o3X`>ueT_>MYg_ZzjUw|Jf5d?;^Dy|7 z48@!u`W52I&-UT2WyH;wxnF&hx$j6z68D(Wwcgc=W6^WWxbaMr)t{ZQb#n~@zF=d-Y4~q)+(KCFq($6%NjY$+ z3f8{^5&(C2eVGX|LW$eg7MYFn9XfN<>FeKg;(m>-{Uyy0q*N-5(P;hva*b6h-*)dvzDP^ zNZ|$nMXo`Q^@ajT<7cSWta-0i!WJK^=bxt|4P&H^_a6k9QN2Ds+-KnA7OLI>DL7zd z+I(sUAgd=360(B!D`N)oyc4%6;(F#gtyM9_7qE2BdG*xsyx3 zz|}|+`vOzt7DL>NJ#^oKM+}d*%eVlAW#NAFnyxQYUN4s5{DT2a^N-`bTyst`k@Yqn z$OP{%*SdH2&?+fj2U-|seai|illtM8yt<))nhdKTJnvPV{hNJSZudv=&ha2tx7Ug* zciZ?lqEd`WDe?&j`(QoDOH%c?bvMNY_O#q_z(?Dd+{@5s70?4+YtVTNg9oTO>m#RX z?y55-Qt30tKW4~g9*4C;NsJqJrBOmvC2n7o88h0*X1LA$N<={KLtchm0XC@GjN8hv z*-D0?$x;+Ea+za~{*Bv%14a6u7|Cwg^An~y3E(zwFRu8gnrsIpter(QA- zZlQeEth(S@P2R2XlxY3|ZbUaWzVe)5+SE!hWy>jsR(z$roo*Imr#<;eqWWS zv)?YbGt32OBaEu%(Nm-_@);6o`5EaIIti}Q4Ut1|ry?m@R)?vXPF%nE=Ykj=z2%(H z0xV3!&RhN-qlz3B-`wi9ML0g_63Xq$$D7A%;J3RRat`o#ZDTLf6p4fSPr05}zbz?8 z-}>PqhbQa)tj5h9X@HPG_Zd`}52;@RnbX9(Nu7$M?*Som^-6uYGZkTHu%a zPee5?2sYds1@k5wyoGVkjfuFww!hssMr-CeH$d5+$wYd?AE(Y5;OLv*SZlH6HwHJZ zMs4JqgddAonDvic*r9W?#DUcEDrQ_K?zcoXFON6gQ15SYOb{dZ5D_}ZuaEW03+d$Z z{Um%rz;{TneAd>(m0CWyy!iSnQ{tqz0JygyqM}&W7DXp>5z8m!P1HSit+ZoC9>^n4 z?z~Iuw(iuEyFc>QrbMCS8vC<)B_#V^??sa`fnj>%%yHb2R#;cshoVH}ogImZC#G;% zLdXQnu=RD%2>--(tgjsF-0toVt*s&u%BZpu@xU^&E=2RS2*lpk`TJ1GQ(@6IIp1vI z-AUy9EsU?(2&naz-uXk^Emf5tgCo9#;Cdx%L#FBUcXFzmt9p9WGo>$09N0q>`Vb^r zt#xMN?zs=Wj(Vnv57j9}o;!@njXZp3lIZ|0;JwKg{wz)U#ginUYe4V=qW%u&`h`J` zKfw8nTE}i*x$=jWSDn)yKYe=doYe^0DQHwZ^LD$wfk>#}OJDeY{iXxt`0t-#q~hY_ z^Ycl1sYtIA9u;CUevfp02fmHGJ(1713Tqa2c6Odsy5+_UndXPXozlt;cF5g4bB{(> zX(h2A_4f;xs~L|!YW5*im#at)N+kiZCP_rY6JBcd=DXbvPQTjTx67=&!QPwnmVzUE zLYU!hSI^=cvQ;l5qsZ!1=2#a)VfRb!7CrCmYa)Bi5NIo?3akDZV=CclZ*QSxC+YxU zdp$01zTJWIt0JBu&Fkw~a5Ky;_FBn(J_oPFMh#BAPdxVJnKysp{M^KkJ$jwL0|9BN z{lO1)KQ7$sO&uHznz!$L(V`~71NW)F{??meu6FQh02{$o+mVuZU*yr-Lj}fGJs#Q> zhg!~&y5xH7PW1EPS<0+eiZ5peoCjCmd>gFZdIZ*2MPcN1tD~v(DF#BJ3yQ)8KJ;@{ z#MNm1PueF7Calj@D2I?ZhGZ_oEwT>!^>s2hU$KVQ@%y$Y(4{8h{v3{-bq9gxrH-YK zzYk6Rjyxxp**CB=a%Z^E-B58YHFJv|(|IzB{gq&U~iz@ zhX*|dKGP=#-KK|@T&P-~;g>#ZLshw1FX__IX*ifH1|6Lc@!tpC>^Bb}Gy zK_Q!2ezFRP2fmdy z!-(vCA>|{iKdrP%!JBIq;zB^-$pt8E_o2(=N2k5Xo4)l2&79y)sefYh#^>o<1ySK6 zr(DY~Oypp!xD3kaeOGqv>(WLMM*PCIcF19o8dzU01O1{r3hL1HpwyAus_Mh;SMr{3 z&Uo%(szu(BB35mGZ^rK)@3gs_1_6{qkVmSH%VCXI-yFpCs~ev_e)xRYmWd`b;X54q zkE3&sXY&8w_`NY_k{nW!N~Jke7CASHaw=*=)}n*cm>f5UWR#Aik8)T!MKY5^%p4Xb zorICI%}`?HFqN|8^m~7Q{vjSb^sxKBU+?R6UDwkATq=((GW^ms3uNs*`tf@=aGA8b zHgjS+lwZk%Hi0)u0iGdPCSfq7hQCYPrLU6MLJWnetN3T$V3s>P@vdSOYY!Wj=BQfe zszUdZWrpSa9=_D(IdFXxlV*nlB_0r2{^x@NxP?G*$j4S2Y|3JWq)8DI!2h+!vU^kF zVM~+egjj!Qj(ezEpU}&EnZ|sOp?#1@t|cmAV>ufW7AdryNeJZ*=AFmzm?N2cq62u1 z)`}oWRZkHH;2uA-EK(<(dctH8Y~GOG_GlA%Hl7_B=fiWB|HOoF`OBkz|6*cOt8crv z7@ZfLtxb)tY+TzLzrK5})+!E2QF9en*I9X2f016talSu`N)dnk{biifNkjWCOO&ta zn{!doqn7388|SxbwM89a{phXm8e9Kc+#D0u_-BrHb#dzFW-s2voyBXMKPv-=!_MdF zHcifmnqx%5$yZjQtA9GL?BNe?0RIlKYPuFGy!uP{b7B8z-sJc~jaOXVneyZ5GxC;` z4=kIPz1EMc9gm->5SAOacF+tDbCOT;sP<`gSnOF9$sb#=&ISE2?ef7$0H1w04n2Gk zoI_1~oJ)N_arz7*;`gX6fPR4Rs0oK=`9$1KGQUKYEvp4i5UOxAlwanl2xhcyxQdGE zX-FK%pAf{QC-rDmVwAy$l&S}(k#0e-A0Hyzs}YJ^2_9Ylz4>``yU?|e%|x3LHlVfI z48Tjy<^@>n$8fjsG!G2p)4!tc$-s+``^fUY8ORfLJ5G;hZ>^lw9Ac*9hnYFg=7yc& zv*qHI2pw?!_cJa19%`K0#3YumbFqhmc$QG{9Y0a_(UZJ!;Y&wHtZ0ONI=>hl(Unjw z_%J(4h#iDY^9O(q)S#cAv0KcK$CUOvwfO{Z;I~Zgz#hr;YI>{CvqBEgj`xQW6Euy= zJt^72O4iP|dK!=0o61VNr5#H0#4yUj&wAE8BW&g_g1}Xl`XE2$%oaXh2I38fgQGL5 zFp>Q(@hRvL43;H4IbEcmZ*a?7wjZelm3sHh78obpey%@m#0tEr|D1pb>0XpLY0kCU zw4*L940?Q{JS*e242yrTd-5GD;v-!Hde`7U_Ca6b)ldi*W%~nII4$Y!0s{9VLmvyn zHsIA?N^r|ha=<1tguAp`Y^H)-qe0 z*guyrGaDW|_;$B2IGS*I$K0lZQVJ{fBo^1nRB`2N{-=-jbWl;)+a`3)^Hcd4iI1dn z(zRnaZKHU@>f-(3IpN&v=hj*4Z$%}8uXttImR)`Wa-vUy-#?AzEM{M6x9u|j9CNP9 zg0VFwbZu&W#cWk)VX3xh_+CYGQ~cV!!R;$Q)YQk~SN`_DPu)NGhcx;se!X8!u=aKG z=l9|(<+WhudVgfyeLcN-jl1wZ9t1T6^J|<`@Er15{W8#l=3UyM2~1r)@7|Ux2pZwbZ{pfGQ8oqNU1Jl6+75-8|TK zDX6rNgCY9*rfJ)sC~*Gu;izO?c_x3k!QjcchsPV3ui{>1VIOK|x<3p&P)`yylxJpU zE;p%r7{7A@>i6F)N~}C@-NE5BTcdY z0P>;sCEw?E&S~1`(P>QkzOSbfwA*O1NNsx`U#RB(<2}-&hR&XwzGw&Rco`QZy%lys zi3UeFaBt@Cru^^XrGT1oRp1(vOU#7E`lv$;4EO1x7mK~o zj}WWb6FkhnKnW{N%NTvlz&kj6AJ(3OyX4!>4KrUL-mvRjtX2g@*PQc@ZAt~ zT1KCXCCYx&VmzOe@FuCbTjGt*6OVJt(%Z01Nr?GhHMXex5M`ql(foPei-r**Rz2Yz zhVMp{rJ;INA&j1eEB>UlZ5p#C(=*eqw*OcW!rLK0sl8Xc#|^P_F0sr|%fJPEc$*1IkO0&`L1G1^^b=Q&7TAKndp@ZN_jAOr-IS{d-to>4N&57?unZ86)p%;VdVim2LE zER>E$Qg|*)lq>*GUb9%F>nGB~+29DCdY7r&9|{-zY?pj|I)kX8MP)K~{a1NGtVPpF zGSw$a*jvY+ndAaF;vAkm7}uC*o@qyX?HPJrb0I3gt5zumb$)fx@2cVA z(!Z!S-Zc~clZl1a;{6t*;}*GHqg~e)g%2y5|Nf}n&pA9=PtAN^|9N$l8#DI%T2s9y z`^n)e=QXc*0Yq-x+Syz0pS-V#6Pd67O1<`5IJbAc>F4(i@jo82Fa25@rPi|)6<&4; z=%Vl874F-Q#E&gMYpUzN;M_wZZv(`s>w=NS6PK!Er$K3Q&>bk2OGo3_5o8wKj3ujG z-!%gifaynqXv0emXV{23gp|S(L_JLun|6+9>59)FM9<)yOXf%N{~`Qeb7Cy(MbQqjQjfZaiM{0y%Od+ zN=%N^aT2!8C|A2xJHRO|?dx4opC>-rWWk-XeynX=3Gz=!@E4B6t8>pUz&o7MW*WE( zDAdcrKLzhf-6>RfrhA$kvc6BfUwbeTHr;X5Iqfov{!n{|eYtX0bca(W^=x!>G%PL4 z5h#2y+INy=>dWs*-LOxCIxPCm(d{YP_OGEShXU9M-*@>JDS>boY>u|0-}6AFGmo{+ zswDSpu%?gF+VY=k2LSm`;dst_t(vlkv=W3K+uVz}_2|9*Wk_*Vh{8nbdcwd}%LS%3 z2=TP6sGLg&7b4_vogH>r9P*c?0mHlkLL%X4Ld#r_&VS(!L|0_sOcir?6>;0S!?trm zCh@(j&he=TBbWR*7=!hMOFEq|cYv$vUs}zAYkjL!`Fz{ER=S%=A|sx8Quxf{y{cg` zo?8H8+LZh%m}B4oRxi3X+%N?#@b(1o+?d>kA{68%CJG13tK=Vp~;5IKqU zxm|-IueGn8fBr4uvd*QheoY!%TN8{e3k6q3W1O2-rb|U$aVw=`t0`mu0>_rWK5UBq z+R2OV__H|2s~=taxnX@JZ$&uYy!4KIO(a_TcHFCReLAi=YQ1{-klJM*(ddtdznIl%=gbTA=LvUBbh|N$yZ<8^isJUZEHvn^ zOnSgXK!|dt;ZJ8U67{bm8dE=eRF3c<3A6+lg1L{W+4?&mUrSyz^3;M z>>+#Z`Auc9X<2)o2AF4pIF=J76lQ3)&oG?&wOiXWr=?Ad?CXosAB@a7*4V?m{Kx{=sP?6_s9~SU$;mQw4GfnA8Qxl9FK$q#@pPqq&$316n?h(_v0$HSmn=MdIB`ne$cUV*O(G4uud}#b5r04?R)-IdzWRzATUz1o zO`Tg<`q}#Z+OMg(_E!;Jf=xBTbE#cxliOFnNgnz4OVHfVh4PA9TItW+J{mvEt4&>A zS~+hOozleqq|ni=6^VDypsoo}$$p4|Uj zk*OHoS5Ab1g+CMcoArpgba^y)0ItYm(ud(_UJ5eyNq6o*H>qNnzcm5i@Z#`L=4cYa zvtI^XC@gOf(AwZE-f6X->O1Y5E+()8aZ=s?yDxi_Be`&799;6cBJ;W+`#et-(h6#? z?wjU+x>wuHSQqIsAK zIzkw13);@7JNk0k%nwX2EoFKVaj1XTO%Uos-n#+oeER-l$|%+c6<$+ew&?VHx4mXM z>;NwXsAch=s-qW71leLKu>8~Ix|Uja`!``D#=l=Yt~%twBwo0e;-FgTdf`!ozW)C3 zPsGxrBm9|$#Ss*WyBIC}6#lPns)5Q(r4GhEjQ$n&clOqiX|O!%`ZHc#D=79_8=q{s zCjw?4>c}gh&}Y$#Ix1>Zsf1TF4%Wv1QrjN?yO;Ij=hS(yWm@vvKk-PAw?2OBT&nz$ z<*M&XA@NH>J}dRr)t{eR(?<^rY5EnBd31o%lTAX{gv~+ zN1_*${;R?rrk30E#^$+Fi}o4*R{X}?3*Z0+NaZ(0E%FTW7*FP#@;P}IoT1QQXE+CS zhife7Z9OBep(a&D+@HXhgB{|6Cp`4X01nyRePYl9c7v0Q8XavG`sw867N0wFxnf$D z5w%GNDE#2`gN|f1#o?X|hzRra#wARBob~i@LvTz%pwhp;*ikn5tPEaNQ6~00Z|X5$ z)8DZ=*mRnSyi8^hHWcgUrZH137MCy^gyna*!8m;_*&u9^w%|P8Ge0=Z5}Ks7FinVs z>@X-2%rR&4kEIYlca0a3TosBO^=PborHN?IFuLk&YmN{CzO!gk>KwNlD3KM@{se(yNkVEsB6XxFaMj38Bq|1F_{@iIc6DSx4TD*ac zjBi1*4Ch4NK-%R%9N3e(<(HYAXW`Vcx=E%4cL?(APhcXuH6gfv0>00D##kCSb@1}J z;W9`)Noa`3GeZwCjluF5hb|_@rqNj3t*2|tNMu;#M(+(^#k`eh9z}*xnNzfEBI{^- z)Evx2pa}B-j7Db|x{OydXHYN4oO5%%iw?Ml*bILGx@8PLCMcuP&qOECC4+FiFUpd&K&B!J+6GhCZiX1`~4hkaPi$d&TrrWw@ZX$^!|<eb44v(Wx8)yb!H)xRgE*N5o6t_q0#Q z09`>BKRZ+Q*rzG)$QFJy75Vs1{dctY^Xz$U#&VO_R1q%5i7fg#KD;t1c?}dWtVvB| zhkGQ1o8{+5Y(N{nVOGw$54O%cJRW7*>mRZnpdoruOC@kSZPu-im{ae^RTA1e-k=66;`9u$*J!zm0RTrt~GXn(kmUiKiLD<8_Q>I z-pg(zw;9Rq1B2|Afk>FTmhByWk7{>dtxFApk~H+4+jsettMUcM!#AIooeNQZ%fE~CX#%es7o9SB)dk=Lo?2WHe4}?h=P<=zR7@697mlxUH~Po z+_P4Hrej3^P+AxcJ50poI8uZ*hQgsjoe@# z#2o;Mqy7L{M!zeMnM5pwJNA3yh`RaVF{Lg1GTBNyN4jr+@l0##Ezhu_kKJZ^fk$^V z<=Q|*oK&SgaH%rocjS(4Va}4ccy3hLc0EPrF^Y>l8^jb7;OG&-W=Na`_#3+en*xkX98pQcU|IX=2nOnqA^3d9~qY9 zgR!?wK`ULgWYj2`*Y;vBlB8dHsSr+db|k|fbw_gG(;fV@|As9O9m0SApphGF4TG~U zewVUE^YNfuURA4}3vXlI;=&oWJB))q!VbB3oKyVvPMpG&Jq%s17lViuL7_K1UGUa9F)=)eo0(7fO6=HThreUS<^)r9-=meoH|SwT8AQ zV*R;2Em)tY`iD;79Aweeya6urM4IrEGWYeSd{A{hcLuBS!SuM@LH)o@;1#-4gQ*Yq z=(FK5iCuWDyZOrhwerKG%R1^A+w$(F%1Bo+2*q7k5S+xgeeKiG6^SG4oRVSYGo00fD87+7orkp+hoiOVC1X}aia zdQ@`{zpbzyrbZcJmU5fm1l&cQ78Dd)i!7Hb>*$mhhpA8){X*l6s@Ptr&8L=u2ymCo zuw&x+OmY9s5WNRMxp=JC?ZfzFhR5RqFeh?i4M1=-`$Vt!kWVm}d@kpZ_L|30*8XNnTW2=Z^h*H@`A9 zcJ)_U=9N{y^{#c$i}H5{cYeMpdSTB9u{bF2ZSVOu=4E-kRNWnAzNDpJtE-<&1!GIR zF-Ku4@V>2;ZfFk=k6Zoo{OX@}(f;t&AMcM;y`nA&*1t%0#WlS;w|;#dKu<^GSH7gi z>sfhqJxp~;YWO4EQ}kaIq8wQ51n)Nt?(&Qz^Mq*^MA}Eibh`~Cy(Pf_k+Ng=)D31j zQ9lam1-gDiDg45N80gufxWosF_jcRG@c+3|eTq+~iI3X<2n~MM^KGxQ) zmge5EwTyfuM2i5^lWg}+1cjK!M1L1b$N~cbEw~iUAj%>cJ&kH??;S;O1_G+lrv%)~ zI-O8NfYpgk_P7=8g){VVj{~`$Tm#u>NP_h5$gLv=`k!a%W%>@RGD^!MRT(DnN_#P9-mANqcEsFi+_=aM0N^obYkzPeVs+whmdz9odyU$H=mbDr$Ip|0f+5T zbrsEmh-eUQa}6=jGt!oY_%@0*HEdAI0y*{ATzd+WfHUSBVs$DJ&vcG*FHJMRlP0?00}BO?@u@u2Bj7-??Eg&fyYN2qWF@Z(ubI_7!TWyxK0@Py!KWZbKLk|g@=cBw1ZI&_y-fcZYgC}mr|KDwxz z(h2{@3`(1|!e)_LrF2zmSpGOa=3D+^!}BuWyCal1sM+-P6eKW{_L%bie$HJ0X ztibUbt{kg=cu#}lF#dL8P1)JY#ATUbzL|r5WxDDx68{>m%nf!6$}G?UldG^C&Snyw z^}5Pm+mI8=6?-zm-p#6dsM50e(kw^EnV5s^a4sp6m6Pi{6?E)4hlwTKLnL@#s7-of zbF;eZpqBG1E>d4G@`<_W9ZEXzpH^Y<++wbUs@FXHCl?;hABm_y9nEc&uHCzTX|cV1 z@nQ6`keAmqEEp^nwCc|1b!oE@;GR>jxA@^rc#QNJa_@3AdF=1+t%GXKE7Lyz+V9avjxQ1gGVyR7GY_2|4wt@W6eguKJz_S5j z<|nv{o>cvwsQG;!I-4O?u-r?p-U_D>7({C#FxY4G6R|KI7K=E6b)zh5>}74WxyMKm zTh!Pk16)$m4c@Sz)CT3-x#4>+JU#AxIs-GDGytn$DzYpqCBI3523vpogbA5X^d#fr zY5VW0E+p$9-Q2_zwTKMkeFQ?0^L7nYPhBL=yZ94HKf)8kO$;T2_FRD0Jr%I%%_K?> zsXo}F8Ikb&(F=$B4HQ;j4YSZa47tU(`Msga&;e1{` z-2HlIDW>+7-Me_s`LW-dht2QWR-KTAXz304Lf;3FM$X(e_5@RoLW|PP^pu1_iKi1_ z6jf~FvXNEgi|L(O#}D05dW0CVbBCR?Jvk}f26@hPKw643PAt!-B=!4SBkkoc4E5D% z$=BVJ(bc&G*}CpQD}^9J4rjdv)F5o&bPhEv7AC7I&b^lgy};1wKy#p+zkxi&fKT%) zP?w5neRRSb(0Y&{EblbEHOj}rEY0p8;F+V!PRw(XtzQ5z`7{bsmTt8xCh$6yyLF49 zk8tZ^AJ zmV5S}iJ)mdcA2F4C$nOI#=;N5@}HF-*;iJY*Z5;=oy}|QdDm78*Yt+sEMvdi{m4o1 za9^Cb-n@J({_pqVyxc1bO9o2eUIwK$69e4Kx2P|L`^VN-EKsPt`Q^D=+jCQ|99G&2 zDP@repT;q4{~Fw9ymw3HQznjm?g-eHQmTZkcAQ{*xo_yCFZ+hpDmK!DY`yqUDcw1a@ljEcoaMO( zlB7^JYk9$Z6~#Ty=D3E{5@7dwhVfY+L&>%jh5Rc-T?l&5V$EnFZ@`qe%tTno|436P zv5{Cm2}BsVLQ}I3B)PTZ&HoRN@V4 zm^5@%5Lz{KOtC@eXXQ#{oOB3g<#qP=*C4_E#Z7!X2#HCI#6wc9ieV4RT78t>4?h^v{~LmS6SKfa(Y%7seeXmRyHVc!hAxg)Mb!bv~i2yN)HKbZ9A$pZ>qjbw?M#4G^RzFv}~z5xQz^WEKYRYK;b4+Uw2vn|zX z&Yb~b@7vnXM+Idei|dljX6YvBNBOM`AE-b%wKrtmM}kKAzJ^)P-@Gcz+5@UGATzO2OrQ;=;D`|s88+S`h2e_{WRn zcXynbXR)**&JToV0a)_aYmFaLn-{Aq1RUes#;s_Yut{CUz+ji z64sg?No6?-g|!PmnXa4*o zFiid@G(WLL&c{c)R-qK%xUC;E)8A}ukmuRpP{$|^aoS0gwJ#ve8 zsIN9nMe&c;w?$Kw;ZN| zC<>&&(?NqpTaP!0=rkYz$_GM)i|9BulO0NtaY1f)Kz!R*(EhQKwF!Hi^8BMNXudpf zS2h$fX)irZcr||UdjzNCWB3T4Pqwqhm0>A-2pUq#e$ZH3^_5-Kd+vewZ!=W3LxqH< zciEv=2gAr?SGtx)%7WN%@$FGp)Em^e(eK7_aQ#Bikxm^|OE6sloi0TaMpNEbMDQdKoAAl1s{& z&#J8-{<}AjEX;M*Q8sPYcQAD#?J9TeiR z#Hmpb5yZ)IG`5@_;`ddd=Nr392#OipIrN?MONf_cJ6a(l_SNjz{X7#^XUDIdr&Z($ zXUyGX(MJ!KJEcIF@aNMKa`Y?0om~fJW}e>{6PEyWiMo<#lDpz&`K^h6OuObb?W6l( z_Kd?XYQGN+q3X+owwiCy)O}WfB4EFuV0A(m-QBE&1v$|1`+CB!>GHzH()(w=xV4smSStVLj$b=HFZ7wX z*%%3Cg#)AJw>%lA`{#thpD>RWlCHp3T;d)zit@)+-$?cJQ&6fWr1!ub!zd1XydQl$ zdl>(M=B^N=TU0##93qs(a7vKq4c!J%rySHx3*p|No>isc_W0{(NmOAEp=RUAt{=r% zw;K+7KmW+Ob(T™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%0D}ZGKUZy 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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/js/download.js b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/js/download.js new file mode 100644 index 000000000..19e9bf2ce --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/js/download.js @@ -0,0 +1,191 @@ +/* + * 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. + */ + +var modalPopup = ".wr-modalpopup"; +var modalPopupContainer = modalPopup + " .modalpopup-container"; +var modalPopupContent = modalPopup + " .modalpopup-content"; +var body = "body"; + +/* + * set popup maximum height function. + */ +function setPopupMaxHeight() { + $(modalPopupContent).css('max-height', ($(body).height() - ($(body).height() / 100 * 30))); + $(modalPopupContainer).css('margin-top', (-($(modalPopupContainer).height() / 2))); +} + +/* + * show popup function. + */ +function showPopup() { + $(modalPopup).show(); + setPopupMaxHeight(); + $('#downloadForm').validate({ + rules: { + deviceName: { + minlength: 4, + required: true + } + }, + highlight: function (element) { + $(element).closest('.control-group').removeClass('success').addClass('error'); + }, + success: function (element) { + $(element).closest('.control-group').removeClass('error').addClass('success'); + $('label[for=deviceName]').remove(); + } + }); + var deviceType = ""; + $('.deviceType').each(function () { + if (this.value != "") { + deviceType = this.value; + } + }); + if (deviceType == 'digitaldisplay'){ + $('.sketchType').remove(); + $('input[name="sketchType"][value="digitaldisplay"]').prop('checked', true); + $("label[for='digitaldisplay']").text("Simple Agent"); + }else{ + $('.sketchTypes').remove(); + } +} + +/* + * hide popup function. + */ +function hidePopup() { + $('label[for=deviceName]').remove(); + $('.control-group').removeClass('success').removeClass('error'); + $(modalPopupContent).html(''); + $(modalPopup).hide(); +} + +/* + * DOM ready functions. + */ +$(document).ready(function () { + attachEvents(); +}); + +function attachEvents() { + /** + * Following click function would execute + * when a user clicks on "Download" link + * on Device Management page in WSO2 DC Console. + */ + $("a.download-link").click(function () { + var sketchType = $(this).data("sketchtype"); + var deviceType = $(this).data("devicetype"); + var downloadDeviceAPI = "/devicemgt/api/devices/sketch/generate_link"; + var payload = {"sketchType": sketchType, "deviceType": deviceType}; + $(modalPopupContent).html($('#download-device-modal-content').html()); + showPopup(); + var deviceName; + $("a#download-device-download-link").click(function () { + $('.new-device-name').each(function () { + if (this.value != "") { + deviceName = this.value; + } + }); + $('label[for=deviceName]').remove(); + if (deviceName && deviceName.length >= 4) { + payload.deviceName = deviceName; + invokerUtil.post( + downloadDeviceAPI, + payload, + function (data, textStatus, jqxhr) { + doAction(data); + }, + function (data) { + doAction(data); + } + ); + }else if(deviceName){ + $('.controls').append(''); + $('.control-group').removeClass('success').addClass('error'); + } else { + $('.controls').append(''); + $('.control-group').removeClass('success').addClass('error'); + } + }); + + $("a#download-device-cancel-link").click(function () { + hidePopup(); + }); + + }); +} + +function downloadAgent() { + var deviceName; + $('.new-device-name').each(function () { + if (this.value != "") { + deviceName = this.value; + } + }); + var deviceNameFormat = /^[^~?!#$:;%^*`+={}\[\]\\()|<>,'"]{1,30}$/; + if (deviceName && deviceNameFormat.test(deviceName)) { + $('#downloadForm').submit(); + hidePopup(); + $(modalPopupContent).html($('#device-agent-downloading-content').html()); + showPopup(); + setTimeout(function () { + hidePopup(); + }, 1000); + }else { + $("#invalid-username-error-msg span").text("Invalid device name"); + $("#invalid-username-error-msg").removeClass("hidden"); + } +} + +function doAction(data) { + //if it is saml redirection response + if (data.status == null) { + document.write(data); + } + + if (data.status == "200") { + $(modalPopupContent).html($('#download-device-modal-content-links').html()); + $("input#download-device-url").val(data.responseText); + $("input#download-device-url").focus(function () { + $(this).select(); + }); + showPopup(); + } else if (data.status == "401") { + $(modalPopupContent).html($('#device-401-content').html()); + $("#device-401-link").click(function () { + window.location = "/devicemgt/login"; + }); + showPopup(); + } else if (data == "403") { + $(modalPopupContent).html($('#device-403-content').html()); + $("#device-403-link").click(function () { + window.location = "/devicemgt/login"; + }); + showPopup(); + } else { + $(modalPopupContent).html($('#device-unexpected-error-content').html()); + $("a#device-unexpected-error-link").click(function () { + hidePopup(); + }); + } +} \ No newline at end of file diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/js/jquery.validate.js b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/js/jquery.validate.js new file mode 100644 index 000000000..fe7ecf07a --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/public/js/jquery.validate.js @@ -0,0 +1,1227 @@ +/* + * 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. + */ + +(function($) { + +$.extend($.fn, { + // http://docs.jquery.com/Plugins/Validation/validate + validate: function( options ) { + + // if nothing is selected, return nothing; can't chain anyway + if ( !this.length ) { + if ( options && options.debug && window.console ) { + console.warn( "Nothing selected, can't validate, returning nothing." ); + } + return; + } + + // check if a validator for this form was already created + var validator = $.data( this[0], "validator" ); + if ( validator ) { + return validator; + } + + // Add novalidate tag if HTML5. + this.attr( "novalidate", "novalidate" ); + + validator = new $.validator( options, this[0] ); + $.data( this[0], "validator", validator ); + + if ( validator.settings.onsubmit ) { + + this.validateDelegate( ":submit", "click", function( event ) { + if ( validator.settings.submitHandler ) { + validator.submitButton = event.target; + } + // allow suppressing validation by adding a cancel class to the submit button + if ( $(event.target).hasClass("cancel") ) { + validator.cancelSubmit = true; + } + }); + + // validate the form on submit + this.submit( function( event ) { + if ( validator.settings.debug ) { + // prevent form submit to be able to see console output + event.preventDefault(); + } + function handle() { + var hidden; + if ( validator.settings.submitHandler ) { + if ( validator.submitButton ) { + // insert a hidden input as a replacement for the missing submit button + hidden = $("").attr("name", validator.submitButton.name).val(validator.submitButton.value).appendTo(validator.currentForm); + } + validator.settings.submitHandler.call( validator, validator.currentForm, event ); + if ( validator.submitButton ) { + // and clean up afterwards; thanks to no-block-scope, hidden can be referenced + hidden.remove(); + } + return false; + } + return true; + } + + // prevent submit for invalid forms or custom submit handlers + if ( validator.cancelSubmit ) { + validator.cancelSubmit = false; + return handle(); + } + if ( validator.form() ) { + if ( validator.pendingRequest ) { + validator.formSubmitted = true; + return false; + } + return handle(); + } else { + validator.focusInvalid(); + return false; + } + }); + } + + return validator; + }, + // http://docs.jquery.com/Plugins/Validation/valid + valid: function() { + if ( $(this[0]).is("form")) { + return this.validate().form(); + } else { + var valid = true; + var validator = $(this[0].form).validate(); + this.each(function() { + valid &= validator.element(this); + }); + return valid; + } + }, + // attributes: space seperated list of attributes to retrieve and remove + removeAttrs: function( attributes ) { + var result = {}, + $element = this; + $.each(attributes.split(/\s/), function( index, value ) { + result[value] = $element.attr(value); + $element.removeAttr(value); + }); + return result; + }, + // http://docs.jquery.com/Plugins/Validation/rules + rules: function( command, argument ) { + var element = this[0]; + + if ( command ) { + var settings = $.data(element.form, "validator").settings; + var staticRules = settings.rules; + var existingRules = $.validator.staticRules(element); + switch(command) { + case "add": + $.extend(existingRules, $.validator.normalizeRule(argument)); + staticRules[element.name] = existingRules; + if ( argument.messages ) { + settings.messages[element.name] = $.extend( settings.messages[element.name], argument.messages ); + } + break; + case "remove": + if ( !argument ) { + delete staticRules[element.name]; + return existingRules; + } + var filtered = {}; + $.each(argument.split(/\s/), function( index, method ) { + filtered[method] = existingRules[method]; + delete existingRules[method]; + }); + return filtered; + } + } + + var data = $.validator.normalizeRules( + $.extend( + {}, + $.validator.classRules(element), + $.validator.attributeRules(element), + $.validator.dataRules(element), + $.validator.staticRules(element) + ), element); + + // make sure required is at front + if ( data.required ) { + var param = data.required; + delete data.required; + data = $.extend({required: param}, data); + } + + return data; + } +}); + +// Custom selectors +$.extend($.expr[":"], { + // http://docs.jquery.com/Plugins/Validation/blank + blank: function( a ) { return !$.trim("" + a.value); }, + // http://docs.jquery.com/Plugins/Validation/filled + filled: function( a ) { return !!$.trim("" + a.value); }, + // http://docs.jquery.com/Plugins/Validation/unchecked + unchecked: function( a ) { return !a.checked; } +}); + +// constructor for validator +$.validator = function( options, form ) { + this.settings = $.extend( true, {}, $.validator.defaults, options ); + this.currentForm = form; + this.init(); +}; + +$.validator.format = function( source, params ) { + if ( arguments.length === 1 ) { + return function() { + var args = $.makeArray(arguments); + args.unshift(source); + return $.validator.format.apply( this, args ); + }; + } + if ( arguments.length > 2 && params.constructor !== Array ) { + params = $.makeArray(arguments).slice(1); + } + if ( params.constructor !== Array ) { + params = [ params ]; + } + $.each(params, function( i, n ) { + source = source.replace( new RegExp("\\{" + i + "\\}", "g"), function() { + return n; + }); + }); + return source; +}; + +$.extend($.validator, { + + defaults: { + messages: {}, + groups: {}, + rules: {}, + errorClass: "error", + validClass: "valid", + errorElement: "label", + focusInvalid: true, + errorContainer: $([]), + errorLabelContainer: $([]), + onsubmit: true, + ignore: ":hidden", + ignoreTitle: false, + onfocusin: function( element, event ) { + this.lastActive = element; + + // hide error label and remove error class on focus if enabled + if ( this.settings.focusCleanup && !this.blockFocusCleanup ) { + if ( this.settings.unhighlight ) { + this.settings.unhighlight.call( this, element, this.settings.errorClass, this.settings.validClass ); + } + this.addWrapper(this.errorsFor(element)).hide(); + } + }, + onfocusout: function( element, event ) { + if ( !this.checkable(element) && (element.name in this.submitted || !this.optional(element)) ) { + this.element(element); + } + }, + onkeyup: function( element, event ) { + if ( event.which === 9 && this.elementValue(element) === "" ) { + return; + } else if ( element.name in this.submitted || element === this.lastElement ) { + this.element(element); + } + }, + onclick: function( element, event ) { + // click on selects, radiobuttons and checkboxes + if ( element.name in this.submitted ) { + this.element(element); + } + // or option elements, check parent select in that case + else if ( element.parentNode.name in this.submitted ) { + this.element(element.parentNode); + } + }, + highlight: function( element, errorClass, validClass ) { + if ( element.type === "radio" ) { + this.findByName(element.name).addClass(errorClass).removeClass(validClass); + } else { + $(element).addClass(errorClass).removeClass(validClass); + } + }, + unhighlight: function( element, errorClass, validClass ) { + if ( element.type === "radio" ) { + this.findByName(element.name).removeClass(errorClass).addClass(validClass); + } else { + $(element).removeClass(errorClass).addClass(validClass); + } + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/setDefaults + setDefaults: function( settings ) { + $.extend( $.validator.defaults, settings ); + }, + + messages: { + required: "This field is required.", + remote: "Please fix this field.", + email: "Please enter a valid email address.", + url: "Please enter a valid URL.", + date: "Please enter a valid date.", + dateISO: "Please enter a valid date (ISO).", + number: "Please enter a valid number.", + digits: "Please enter only digits.", + creditcard: "Please enter a valid credit card number.", + equalTo: "Please enter the same value again.", + maxlength: $.validator.format("Please enter no more than {0} characters."), + minlength: $.validator.format("Please enter at least {0} characters."), + rangelength: $.validator.format("Please enter a value between {0} and {1} characters long."), + range: $.validator.format("Please enter a value between {0} and {1}."), + max: $.validator.format("Please enter a value less than or equal to {0}."), + min: $.validator.format("Please enter a value greater than or equal to {0}.") + }, + + autoCreateRanges: false, + + prototype: { + + init: function() { + this.labelContainer = $(this.settings.errorLabelContainer); + this.errorContext = this.labelContainer.length && this.labelContainer || $(this.currentForm); + this.containers = $(this.settings.errorContainer).add( this.settings.errorLabelContainer ); + this.submitted = {}; + this.valueCache = {}; + this.pendingRequest = 0; + this.pending = {}; + this.invalid = {}; + this.reset(); + + var groups = (this.groups = {}); + $.each(this.settings.groups, function( key, value ) { + if ( typeof value === "string" ) { + value = value.split(/\s/); + } + $.each(value, function( index, name ) { + groups[name] = key; + }); + }); + var rules = this.settings.rules; + $.each(rules, function( key, value ) { + rules[key] = $.validator.normalizeRule(value); + }); + + function delegate(event) { + var validator = $.data(this[0].form, "validator"), + eventType = "on" + event.type.replace(/^validate/, ""); + if ( validator.settings[eventType] ) { + validator.settings[eventType].call(validator, this[0], event); + } + } + $(this.currentForm) + .validateDelegate(":text, [type='password'], [type='file'], select, textarea, " + + "[type='number'], [type='search'] ,[type='tel'], [type='url'], " + + "[type='email'], [type='datetime'], [type='date'], [type='month'], " + + "[type='week'], [type='time'], [type='datetime-local'], " + + "[type='range'], [type='color'] ", + "focusin focusout keyup", delegate) + .validateDelegate("[type='radio'], [type='checkbox'], select, option", "click", delegate); + + if ( this.settings.invalidHandler ) { + $(this.currentForm).bind("invalid-form.validate", this.settings.invalidHandler); + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/form + form: function() { + this.checkForm(); + $.extend(this.submitted, this.errorMap); + this.invalid = $.extend({}, this.errorMap); + if ( !this.valid() ) { + $(this.currentForm).triggerHandler("invalid-form", [this]); + } + this.showErrors(); + return this.valid(); + }, + + checkForm: function() { + this.prepareForm(); + for ( var i = 0, elements = (this.currentElements = this.elements()); elements[i]; i++ ) { + this.check( elements[i] ); + } + return this.valid(); + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/element + element: function( element ) { + element = this.validationTargetFor( this.clean( element ) ); + this.lastElement = element; + this.prepareElement( element ); + this.currentElements = $(element); + var result = this.check( element ) !== false; + if ( result ) { + delete this.invalid[element.name]; + } else { + this.invalid[element.name] = true; + } + if ( !this.numberOfInvalids() ) { + // Hide error containers on last error + this.toHide = this.toHide.add( this.containers ); + } + this.showErrors(); + return result; + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/showErrors + showErrors: function( errors ) { + if ( errors ) { + // add items to error list and map + $.extend( this.errorMap, errors ); + this.errorList = []; + for ( var name in errors ) { + this.errorList.push({ + message: errors[name], + element: this.findByName(name)[0] + }); + } + // remove items from success list + this.successList = $.grep( this.successList, function( element ) { + return !(element.name in errors); + }); + } + if ( this.settings.showErrors ) { + this.settings.showErrors.call( this, this.errorMap, this.errorList ); + } else { + this.defaultShowErrors(); + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/resetForm + resetForm: function() { + if ( $.fn.resetForm ) { + $(this.currentForm).resetForm(); + } + this.submitted = {}; + this.lastElement = null; + this.prepareForm(); + this.hideErrors(); + this.elements().removeClass( this.settings.errorClass ).removeData( "previousValue" ); + }, + + numberOfInvalids: function() { + return this.objectLength(this.invalid); + }, + + objectLength: function( obj ) { + var count = 0; + for ( var i in obj ) { + count++; + } + return count; + }, + + hideErrors: function() { + this.addWrapper( this.toHide ).hide(); + }, + + valid: function() { + return this.size() === 0; + }, + + size: function() { + return this.errorList.length; + }, + + focusInvalid: function() { + if ( this.settings.focusInvalid ) { + try { + $(this.findLastActive() || this.errorList.length && this.errorList[0].element || []) + .filter(":visible") + .focus() + // manually trigger focusin event; without it, focusin handler isn't called, findLastActive won't have anything to find + .trigger("focusin"); + } catch(e) { + // ignore IE throwing errors when focusing hidden elements + } + } + }, + + findLastActive: function() { + var lastActive = this.lastActive; + return lastActive && $.grep(this.errorList, function( n ) { + return n.element.name === lastActive.name; + }).length === 1 && lastActive; + }, + + elements: function() { + var validator = this, + rulesCache = {}; + + // select all valid inputs inside the form (no submit or reset buttons) + return $(this.currentForm) + .find("input, select, textarea") + .not(":submit, :reset, :image, [disabled]") + .not( this.settings.ignore ) + .filter(function() { + if ( !this.name ) { + if ( window.console ) { + console.error( "%o has no name assigned", this ); + } + throw new Error( "Failed to validate, found an element with no name assigned. See console for element reference." ); + } + + // select only the first element for each name, and only those with rules specified + if ( this.name in rulesCache || !validator.objectLength($(this).rules()) ) { + return false; + } + + rulesCache[this.name] = true; + return true; + }); + }, + + clean: function( selector ) { + return $(selector)[0]; + }, + + errors: function() { + var errorClass = this.settings.errorClass.replace(" ", "."); + return $(this.settings.errorElement + "." + errorClass, this.errorContext); + }, + + reset: function() { + this.successList = []; + this.errorList = []; + this.errorMap = {}; + this.toShow = $([]); + this.toHide = $([]); + this.currentElements = $([]); + }, + + prepareForm: function() { + this.reset(); + this.toHide = this.errors().add( this.containers ); + }, + + prepareElement: function( element ) { + this.reset(); + this.toHide = this.errorsFor(element); + }, + + elementValue: function( element ) { + var type = $(element).attr("type"), + val = $(element).val(); + + if ( type === "radio" || type === "checkbox" ) { + return $("input[name='" + $(element).attr("name") + "']:checked").val(); + } + + if ( typeof val === "string" ) { + return val.replace(/\r/g, ""); + } + return val; + }, + + check: function( element ) { + element = this.validationTargetFor( this.clean( element ) ); + + var rules = $(element).rules(); + var dependencyMismatch = false; + var val = this.elementValue(element); + var result; + + for (var method in rules ) { + var rule = { method: method, parameters: rules[method] }; + try { + + result = $.validator.methods[method].call( this, val, element, rule.parameters ); + + // if a method indicates that the field is optional and therefore valid, + // don't mark it as valid when there are no other rules + if ( result === "dependency-mismatch" ) { + dependencyMismatch = true; + continue; + } + dependencyMismatch = false; + + if ( result === "pending" ) { + this.toHide = this.toHide.not( this.errorsFor(element) ); + return; + } + + if ( !result ) { + this.formatAndAdd( element, rule ); + return false; + } + } catch(e) { + if ( this.settings.debug && window.console ) { + console.log( "Exception occured when checking element " + element.id + ", check the '" + rule.method + "' method.", e ); + } + throw e; + } + } + if ( dependencyMismatch ) { + return; + } + if ( this.objectLength(rules) ) { + this.successList.push(element); + } + return true; + }, + + // return the custom message for the given element and validation method + // specified in the element's HTML5 data attribute + customDataMessage: function( element, method ) { + return $(element).data("msg-" + method.toLowerCase()) || (element.attributes && $(element).attr("data-msg-" + method.toLowerCase())); + }, + + // return the custom message for the given element name and validation method + customMessage: function( name, method ) { + var m = this.settings.messages[name]; + return m && (m.constructor === String ? m : m[method]); + }, + + // return the first defined argument, allowing empty strings + findDefined: function() { + for(var i = 0; i < arguments.length; i++) { + if ( arguments[i] !== undefined ) { + return arguments[i]; + } + } + return undefined; + }, + + defaultMessage: function( element, method ) { + return this.findDefined( + this.customMessage( element.name, method ), + this.customDataMessage( element, method ), + // title is never undefined, so handle empty string as undefined + !this.settings.ignoreTitle && element.title || undefined, + $.validator.messages[method], + "Warning: No message defined for " + element.name + "" + ); + }, + + formatAndAdd: function( element, rule ) { + var message = this.defaultMessage( element, rule.method ), + theregex = /\$?\{(\d+)\}/g; + if ( typeof message === "function" ) { + message = message.call(this, rule.parameters, element); + } else if (theregex.test(message)) { + message = $.validator.format(message.replace(theregex, "{$1}"), rule.parameters); + } + this.errorList.push({ + message: message, + element: element + }); + + this.errorMap[element.name] = message; + this.submitted[element.name] = message; + }, + + addWrapper: function( toToggle ) { + if ( this.settings.wrapper ) { + toToggle = toToggle.add( toToggle.parent( this.settings.wrapper ) ); + } + return toToggle; + }, + + defaultShowErrors: function() { + var i, elements; + for ( i = 0; this.errorList[i]; i++ ) { + var error = this.errorList[i]; + if ( this.settings.highlight ) { + this.settings.highlight.call( this, error.element, this.settings.errorClass, this.settings.validClass ); + } + this.showLabel( error.element, error.message ); + } + if ( this.errorList.length ) { + this.toShow = this.toShow.add( this.containers ); + } + if ( this.settings.success ) { + for ( i = 0; this.successList[i]; i++ ) { + this.showLabel( this.successList[i] ); + } + } + if ( this.settings.unhighlight ) { + for ( i = 0, elements = this.validElements(); elements[i]; i++ ) { + this.settings.unhighlight.call( this, elements[i], this.settings.errorClass, this.settings.validClass ); + } + } + this.toHide = this.toHide.not( this.toShow ); + this.hideErrors(); + this.addWrapper( this.toShow ).show(); + }, + + validElements: function() { + return this.currentElements.not(this.invalidElements()); + }, + + invalidElements: function() { + return $(this.errorList).map(function() { + return this.element; + }); + }, + + showLabel: function( element, message ) { + var label = this.errorsFor( element ); + if ( label.length ) { + // refresh error/success class + label.removeClass( this.settings.validClass ).addClass( this.settings.errorClass ); + + // check if we have a generated label, replace the message then + if ( label.attr("generated") ) { + label.html(message); + } + } else { + // create label + label = $("<" + this.settings.errorElement + "/>") + .attr({"for": this.idOrName(element), generated: true}) + .addClass(this.settings.errorClass) + .html(message || ""); + if ( this.settings.wrapper ) { + // make sure the element is visible, even in IE + // actually showing the wrapped element is handled elsewhere + label = label.hide().show().wrap("<" + this.settings.wrapper + "/>").parent(); + } + if ( !this.labelContainer.append(label).length ) { + if ( this.settings.errorPlacement ) { + this.settings.errorPlacement(label, $(element) ); + } else { + label.insertAfter(element); + } + } + } + if ( !message && this.settings.success ) { + label.text(""); + if ( typeof this.settings.success === "string" ) { + label.addClass( this.settings.success ); + } else { + this.settings.success( label, element ); + } + } + this.toShow = this.toShow.add(label); + }, + + errorsFor: function( element ) { + var name = this.idOrName(element); + return this.errors().filter(function() { + return $(this).attr("for") === name; + }); + }, + + idOrName: function( element ) { + return this.groups[element.name] || (this.checkable(element) ? element.name : element.id || element.name); + }, + + validationTargetFor: function( element ) { + // if radio/checkbox, validate first element in group instead + if ( this.checkable(element) ) { + element = this.findByName( element.name ).not(this.settings.ignore)[0]; + } + return element; + }, + + checkable: function( element ) { + return (/radio|checkbox/i).test(element.type); + }, + + findByName: function( name ) { + return $(this.currentForm).find("[name='" + name + "']"); + }, + + getLength: function( value, element ) { + switch( element.nodeName.toLowerCase() ) { + case "select": + return $("option:selected", element).length; + case "input": + if ( this.checkable( element) ) { + return this.findByName(element.name).filter(":checked").length; + } + } + return value.length; + }, + + depend: function( param, element ) { + return this.dependTypes[typeof param] ? this.dependTypes[typeof param](param, element) : true; + }, + + dependTypes: { + "boolean": function( param, element ) { + return param; + }, + "string": function( param, element ) { + return !!$(param, element.form).length; + }, + "function": function( param, element ) { + return param(element); + } + }, + + optional: function( element ) { + var val = this.elementValue(element); + return !$.validator.methods.required.call(this, val, element) && "dependency-mismatch"; + }, + + startRequest: function( element ) { + if ( !this.pending[element.name] ) { + this.pendingRequest++; + this.pending[element.name] = true; + } + }, + + stopRequest: function( element, valid ) { + this.pendingRequest--; + // sometimes synchronization fails, make sure pendingRequest is never < 0 + if ( this.pendingRequest < 0 ) { + this.pendingRequest = 0; + } + delete this.pending[element.name]; + if ( valid && this.pendingRequest === 0 && this.formSubmitted && this.form() ) { + $(this.currentForm).submit(); + this.formSubmitted = false; + } else if (!valid && this.pendingRequest === 0 && this.formSubmitted) { + $(this.currentForm).triggerHandler("invalid-form", [this]); + this.formSubmitted = false; + } + }, + + previousValue: function( element ) { + return $.data(element, "previousValue") || $.data(element, "previousValue", { + old: null, + valid: true, + message: this.defaultMessage( element, "remote" ) + }); + } + + }, + + classRuleSettings: { + required: {required: true}, + email: {email: true}, + url: {url: true}, + date: {date: true}, + dateISO: {dateISO: true}, + number: {number: true}, + digits: {digits: true}, + creditcard: {creditcard: true} + }, + + addClassRules: function( className, rules ) { + if ( className.constructor === String ) { + this.classRuleSettings[className] = rules; + } else { + $.extend(this.classRuleSettings, className); + } + }, + + classRules: function( element ) { + var rules = {}; + var classes = $(element).attr("class"); + if ( classes ) { + $.each(classes.split(" "), function() { + if ( this in $.validator.classRuleSettings ) { + $.extend(rules, $.validator.classRuleSettings[this]); + } + }); + } + return rules; + }, + + attributeRules: function( element ) { + var rules = {}; + var $element = $(element); + + for (var method in $.validator.methods) { + var value; + + // support for in both html5 and older browsers + if ( method === "required" ) { + value = $element.get(0).getAttribute(method); + // Some browsers return an empty string for the required attribute + // and non-HTML5 browsers might have required="" markup + if ( value === "" ) { + value = true; + } + // force non-HTML5 browsers to return bool + value = !!value; + } else { + value = $element.attr(method); + } + + if ( value ) { + rules[method] = value; + } else if ( $element[0].getAttribute("type") === method ) { + rules[method] = true; + } + } + + // maxlength may be returned as -1, 2147483647 (IE) and 524288 (safari) for text inputs + if ( rules.maxlength && /-1|2147483647|524288/.test(rules.maxlength) ) { + delete rules.maxlength; + } + + return rules; + }, + + dataRules: function( element ) { + var method, value, + rules = {}, $element = $(element); + for (method in $.validator.methods) { + value = $element.data("rule-" + method.toLowerCase()); + if ( value !== undefined ) { + rules[method] = value; + } + } + return rules; + }, + + staticRules: function( element ) { + var rules = {}; + var validator = $.data(element.form, "validator"); + if ( validator.settings.rules ) { + rules = $.validator.normalizeRule(validator.settings.rules[element.name]) || {}; + } + return rules; + }, + + normalizeRules: function( rules, element ) { + // handle dependency check + $.each(rules, function( prop, val ) { + // ignore rule when param is explicitly false, eg. required:false + if ( val === false ) { + delete rules[prop]; + return; + } + if ( val.param || val.depends ) { + var keepRule = true; + switch (typeof val.depends) { + case "string": + keepRule = !!$(val.depends, element.form).length; + break; + case "function": + keepRule = val.depends.call(element, element); + break; + } + if ( keepRule ) { + rules[prop] = val.param !== undefined ? val.param : true; + } else { + delete rules[prop]; + } + } + }); + + // evaluate parameters + $.each(rules, function( rule, parameter ) { + rules[rule] = $.isFunction(parameter) ? parameter(element) : parameter; + }); + + // clean number parameters + $.each(["minlength", "maxlength", "min", "max"], function() { + if ( rules[this] ) { + rules[this] = Number(rules[this]); + } + }); + $.each(["rangelength", "range"], function() { + var parts; + if ( rules[this] ) { + if ( $.isArray(rules[this]) ) { + rules[this] = [Number(rules[this][0]), Number(rules[this][1])]; + } else if ( typeof rules[this] === "string" ) { + parts = rules[this].split(/[\s,]+/); + rules[this] = [Number(parts[0]), Number(parts[1])]; + } + } + }); + + if ( $.validator.autoCreateRanges ) { + // auto-create ranges + if ( rules.min && rules.max ) { + rules.range = [rules.min, rules.max]; + delete rules.min; + delete rules.max; + } + if ( rules.minlength && rules.maxlength ) { + rules.rangelength = [rules.minlength, rules.maxlength]; + delete rules.minlength; + delete rules.maxlength; + } + } + + return rules; + }, + + // Converts a simple string to a {string: true} rule, e.g., "required" to {required:true} + normalizeRule: function( data ) { + if ( typeof data === "string" ) { + var transformed = {}; + $.each(data.split(/\s/), function() { + transformed[this] = true; + }); + data = transformed; + } + return data; + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/addMethod + addMethod: function( name, method, message ) { + $.validator.methods[name] = method; + $.validator.messages[name] = message !== undefined ? message : $.validator.messages[name]; + if ( method.length < 3 ) { + $.validator.addClassRules(name, $.validator.normalizeRule(name)); + } + }, + + methods: { + + // http://docs.jquery.com/Plugins/Validation/Methods/required + required: function( value, element, param ) { + // check if dependency is met + if ( !this.depend(param, element) ) { + return "dependency-mismatch"; + } + if ( element.nodeName.toLowerCase() === "select" ) { + // could be an array for select-multiple or a string, both are fine this way + var val = $(element).val(); + return val && val.length > 0; + } + if ( this.checkable(element) ) { + return this.getLength(value, element) > 0; + } + return $.trim(value).length > 0; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/remote + remote: function( value, element, param ) { + if ( this.optional(element) ) { + return "dependency-mismatch"; + } + + var previous = this.previousValue(element); + if (!this.settings.messages[element.name] ) { + this.settings.messages[element.name] = {}; + } + previous.originalMessage = this.settings.messages[element.name].remote; + this.settings.messages[element.name].remote = previous.message; + + param = typeof param === "string" && {url:param} || param; + + if ( previous.old === value ) { + return previous.valid; + } + + previous.old = value; + var validator = this; + this.startRequest(element); + var data = {}; + data[element.name] = value; + $.ajax($.extend(true, { + url: param, + mode: "abort", + port: "validate" + element.name, + dataType: "json", + data: data, + success: function( response ) { + validator.settings.messages[element.name].remote = previous.originalMessage; + var valid = response === true || response === "true"; + if ( valid ) { + var submitted = validator.formSubmitted; + validator.prepareElement(element); + validator.formSubmitted = submitted; + validator.successList.push(element); + delete validator.invalid[element.name]; + validator.showErrors(); + } else { + var errors = {}; + var message = response || validator.defaultMessage( element, "remote" ); + errors[element.name] = previous.message = $.isFunction(message) ? message(value) : message; + validator.invalid[element.name] = true; + validator.showErrors(errors); + } + previous.valid = valid; + validator.stopRequest(element, valid); + } + }, param)); + return "pending"; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/minlength + minlength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || length >= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/maxlength + maxlength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || length <= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/rangelength + rangelength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || ( length >= param[0] && length <= param[1] ); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/min + min: function( value, element, param ) { + return this.optional(element) || value >= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/max + max: function( value, element, param ) { + return this.optional(element) || value <= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/range + range: function( value, element, param ) { + return this.optional(element) || ( value >= param[0] && value <= param[1] ); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/email + email: function( value, element ) { + // contributed by Scott Gonzalez: http://projects.scottsplayground.com/email_address_validation/ + return this.optional(element) || /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/url + url: function( value, element ) { + // contributed by Scott Gonzalez: http://projects.scottsplayground.com/iri/ + return this.optional(element) || /^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/date + date: function( value, element ) { + return this.optional(element) || !/Invalid|NaN/.test(new Date(value).toString()); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/dateISO + dateISO: function( value, element ) { + return this.optional(element) || /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/number + number: function( value, element ) { + return this.optional(element) || /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/digits + digits: function( value, element ) { + return this.optional(element) || /^\d+$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/creditcard + // based on http://en.wikipedia.org/wiki/Luhn + creditcard: function( value, element ) { + if ( this.optional(element) ) { + return "dependency-mismatch"; + } + // accept only spaces, digits and dashes + if ( /[^0-9 \-]+/.test(value) ) { + return false; + } + var nCheck = 0, + nDigit = 0, + bEven = false; + + value = value.replace(/\D/g, ""); + + for (var n = value.length - 1; n >= 0; n--) { + var cDigit = value.charAt(n); + nDigit = parseInt(cDigit, 10); + if ( bEven ) { + if ( (nDigit *= 2) > 9 ) { + nDigit -= 9; + } + } + nCheck += nDigit; + bEven = !bEven; + } + + return (nCheck % 10) === 0; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/equalTo + equalTo: function( value, element, param ) { + // bind to the blur event of the target in order to revalidate whenever the target field is updated + // TODO find a way to bind the event just once, avoiding the unbind-rebind overhead + var target = $(param); + if ( this.settings.onfocusout ) { + target.unbind(".validate-equalTo").bind("blur.validate-equalTo", function() { + $(element).valid(); + }); + } + return value === target.val(); + } + + } + +}); + +// deprecated, use $.validator.format instead +$.format = $.validator.format; + +}(jQuery)); + +// ajax mode: abort +// usage: $.ajax({ mode: "abort"[, port: "uniqueport"]}); +// if mode:"abort" is used, the previous request on that port (port can be undefined) is aborted via XMLHttpRequest.abort() +(function($) { + var pendingRequests = {}; + // Use a prefilter if available (1.5+) + if ( $.ajaxPrefilter ) { + $.ajaxPrefilter(function( settings, _, xhr ) { + var port = settings.port; + if ( settings.mode === "abort" ) { + if ( pendingRequests[port] ) { + pendingRequests[port].abort(); + } + pendingRequests[port] = xhr; + } + }); + } else { + // Proxy ajax + var ajax = $.ajax; + $.ajax = function( settings ) { + var mode = ( "mode" in settings ? settings : $.ajaxSettings ).mode, + port = ( "port" in settings ? settings : $.ajaxSettings ).port; + if ( mode === "abort" ) { + if ( pendingRequests[port] ) { + pendingRequests[port].abort(); + } + return (pendingRequests[port] = ajax.apply(this, arguments)); + } + return ajax.apply(this, arguments); + }; + } +}(jQuery)); + +// provides delegate(type: String, delegate: Selector, handler: Callback) plugin for easier event delegation +// handler is only called when $(event.target).is(delegate), in the scope of the jquery-object for event.target +(function($) { + $.extend($.fn, { + validateDelegate: function( delegate, type, handler ) { + return this.bind(type, function( event ) { + var target = $(event.target); + if ( target.is(delegate) ) { + return handler.apply(target, arguments); + } + }); + } + }); +}(jQuery)); diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/type-view.hbs b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/type-view.hbs new file mode 100644 index 000000000..c516ec2b6 --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/type-view.hbs @@ -0,0 +1,292 @@ +

      + +
      + +
      + +
      + +
      + +
      +

      What it Does

      +
      +

      Connect a Digital Message Display to WSO2 IoT Server and manage + it.

      +
      +

      What You Need

      +
      +
        +
      • + ITEM 01 +    Raspberry Pi with SD Card(Internet Enabled [Wifi or Ethernet]). +
      • +
      • + ITEM 02 +    A Digital Display with HDMI Cable. +
      • +
      • + STEP 03 +    Proceed to [Prepare] section. +
      +
      + + View API
        + + + Download Agent + +
      + +
      +
      + +
      + +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +

      +
      +
      +

      Prepare

      +
      +
        +
      • + 01 +    Connect a monitor to your RaspberryPi via HDMI cable. +
      • +
      • + 02 +    Configure RaspberryPi to connect to the Internet. +
      • +
      • + 03 +    Go ahead and [Download Agent] the Digital Display Agent. +
      • +
      • + 04 +    Unzip downloaded agent. +
      • +
      • + 05 +    Start Agent by running installpackages.sh followed by wso2Agent.sh +
      • +
      +
      +
      +
      +

      Digital-Display Setup

      +
      +

      Click on the image to zoom

      +
      + + + +
      +
      +
      + +
      +

      Try Out

      +
      +
        +
      • + 01 +    You can view all your connected devices at + [Device Management] page. +
      • +
      • + 02 +    Select one of connected devices and check for available control + operations. +
      +
      +

      Click on the image to zoom

      +
      + + + +
      +
      + + +{{#zone "bottomJs"}} + {{js "/js/download.js"}} + {{js "/js/jquery.validate.js"}} +{{/zone}} diff --git a/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/type-view.json b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/type-view.json new file mode 100644 index 000000000..9eecd8f5b --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/org.wso2.carbon.device.mgt.iot.digitaldisplay.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.digital_display.type-view/type-view.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0" +} \ No newline at end of file diff --git a/components/iot-plugins/digital-display-plugin/pom.xml b/components/iot-plugins/digital-display-plugin/pom.xml new file mode 100644 index 000000000..ff50040e3 --- /dev/null +++ b/components/iot-plugins/digital-display-plugin/pom.xml @@ -0,0 +1,61 @@ + + + + + + + org.wso2.carbon.devicemgt-plugins + iot-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + digital-display-plugin + pom + WSO2 Carbon - Arduino Plugin + http://wso2.org + + + org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api + org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api + org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin + org.wso2.carbon.device.mgt.iot.digitaldisplay.ui + + + + + + + org.apache.felix + maven-scr-plugin + 1.7.2 + + + generate-scr-scrdescriptor + + scr + + + + + + + + diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/pom.xml b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/pom.xml new file mode 100644 index 000000000..5f70578fb --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/pom.xml @@ -0,0 +1,254 @@ + + + + drone-analyzer-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api + war + WSO2 Carbon - IoT Server Drone Analyzer Controller API + http://maven.apache.org + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.core + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.analytics + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.certificate.mgt.core + provided + + + commons-codec.wso2 + commons-codec + + + + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + provided + + + org.apache.cxf + cxf-rt-frontend-jaxrs + provided + + + org.apache.cxf + cxf-rt-transports-http + provided + + + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + provided + + + + + org.springframework + spring-context + + + + + org.apache.httpcomponents + httpasyncclient + 4.1 + provided + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot + provided + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin + provided + + + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-jaxrs + + + javax + javaee-web-api + provided + + + javax.ws.rs + jsr311-api + provided + + + commons-httpclient.wso2 + commons-httpclient + provided + + + + org.wso2.carbon + org.wso2.carbon.utils + provided + + + org.bouncycastle.wso2 + bcprov-jdk15on + + + org.wso2.carbon + org.wso2.carbon.user.api + + + org.wso2.carbon + org.wso2.carbon.queuing + + + org.wso2.carbon + org.wso2.carbon.base + + + org.apache.axis2.wso2 + axis2 + + + org.igniterealtime.smack.wso2 + smack + + + org.igniterealtime.smack.wso2 + smackx + + + jaxen + jaxen + + + commons-fileupload.wso2 + commons-fileupload + + + org.apache.ant.wso2 + ant + + + org.apache.ant.wso2 + ant + + + commons-httpclient.wso2 + commons-httpclient + + + org.eclipse.equinox + javax.servlet + + + org.wso2.carbon + org.wso2.carbon.registry.api + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.extensions + provided + + + + commons-codec.wso2 + commons-codec + + + + org.igniterealtime.smack.wso2 + smack + provided + + + org.igniterealtime.smack.wso2 + smackx + provided + + + org.apache.commons + commons-collections4 + 4.0 + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.annotations + provided + + + + + + + + + + maven-compiler-plugin + + UTF-8 + ${wso2.maven.compiler.source} + ${wso2.maven.compiler.target} + + + + maven-war-plugin + + src/main/webapp/WEB-INF/web.xml + drone_analyzer + + + + + diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/DroneControllerService.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/DroneControllerService.java new file mode 100644 index 000000000..17ea3c5d0 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/DroneControllerService.java @@ -0,0 +1,82 @@ +/* + * 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.device.mgt.iot.droneanalyzer.controller.api.impl; + +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.apimgt.annotations.api.API; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.extensions.feature.mgt.annotations.DeviceType; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api.impl.util.DroneAnalyzerServiceUtils; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.controller.DroneController; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.controller.impl.DroneControllerImpl; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import java.util.concurrent.ConcurrentHashMap; + +@API( name="drone_analyzer", version="1.0.0", context="/drone_analyzer", tags = {"drone_analyzer"}) +@DeviceType( value = "drone_analyzer") +public class DroneControllerService { + + private static org.apache.commons.logging.Log log = LogFactory.getLog(DroneControllerService.class); + @Context //injected response proxy supporting multiple thread + private HttpServletResponse response; + private ConcurrentHashMap deviceToIpMap = new ConcurrentHashMap<>(); + private DroneController droneController = new DroneControllerImpl(); + + + @Path("controller/register/{deviceId}/{ip}/{port}") + @POST + public Response registerDeviceIP(@PathParam("deviceId") String deviceId, @PathParam("ip") String deviceIP, + @PathParam("port") String devicePort, @Context HttpServletResponse response) { + try { + String result; + String deviceHttpEndpoint = deviceIP + ":" + devicePort; + deviceToIpMap.put(deviceId, deviceHttpEndpoint); + result = "Device-IP Registered"; + response.setStatus(Response.Status.OK.getStatusCode()); + if (log.isDebugEnabled()) { + log.debug(result); + } + return Response.ok(Response.Status.OK.getStatusCode()).build(); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("controller/device/{deviceId}/send_command") + @POST + /*@Feature( code="send_command", name="Send Command", type="operation", + description="Send Commands to Drone")*/ + public Response droneController(@PathParam("deviceId") String deviceId, + @FormParam("action") String action, @FormParam("duration") String duration, + @FormParam("speed") String speed){ + try { + DroneAnalyzerServiceUtils.sendControlCommand(droneController, deviceId, action, Double.valueOf(speed), + Double.valueOf(duration)); + return Response.status(Response.Status.ACCEPTED).build(); + } catch (DeviceManagementException e) { + log.error("Drone command didn't success. Try again, \n"+ e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/DroneRealTimeService.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/DroneRealTimeService.java new file mode 100644 index 000000000..c0c85bc94 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/DroneRealTimeService.java @@ -0,0 +1,124 @@ +/* + * 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.device.mgt.iot.droneanalyzer.controller.api.impl; + + +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.iot.controlqueue.xmpp.XmppConfig; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api.impl.transport.DroneAnalyzerXMPPConnector; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api.impl.trasformer.MessageTransformer; +import org.wso2.carbon.device.mgt.iot.service.IoTServerStartupListener; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.constants.DroneConstants; +import javax.websocket.OnClose; +import javax.websocket.OnError; +import javax.websocket.OnMessage; +import javax.websocket.OnOpen; +import javax.websocket.Session; +import javax.websocket.server.ServerEndpoint; +import java.io.IOException; + +@ServerEndpoint("/datastream/drone_status") +public class DroneRealTimeService { + + private static org.apache.commons.logging.Log log = LogFactory.getLog(DroneRealTimeService.class); + private MessageTransformer messageController; + private DroneAnalyzerXMPPConnector xmppConnector; + + public DroneRealTimeService() { + Runnable connector = new Runnable() { + public void run() { + if (waitForServerStartup()) { + return; + } + messageController = new MessageTransformer(); + xmppConnector = new DroneAnalyzerXMPPConnector(messageController); + if (XmppConfig.getInstance().isEnabled()){ + xmppConnector.connect(); + } else { + log.warn("XMPP disabled in 'devicemgt-config.xml'. Hence, DroneAnalyzerXMPPConnector not started."); + } + } + }; + Thread connectorThread = new Thread(connector); + connectorThread.setDaemon(true); + connectorThread.start(); + } + + private boolean waitForServerStartup() { + while (!IoTServerStartupListener.isServerReady()) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + return true; + } + } + return false; + } + + @OnOpen + public void onOpen(Session session){ + log.info(session.getId() + " has opened a connection"); + try { + session.getBasicRemote().sendText("Connection Established"); + } catch (IOException e) { + log.error( e.getMessage()+"\n"+ e); + } + } + + @OnMessage + public void onMessage(String message, Session session){ + while (true) { + try { + if (messageController != null && !messageController.isEmptyQueue()) { + String messageControllerMessage = messageController.getMessage(); + session.getBasicRemote().sendText(messageControllerMessage); + break; + } + Thread.sleep(DroneConstants.MINIMUM_TIME_DURATION); + } catch (IOException | InterruptedException ex) { + log.error(ex.getMessage(), ex); + break; + } + } + } + + @OnClose + public void onClose(Session session){ + try { + xmppConnector.disconnect(); + log.info("XMPP connection is disconnected"); + } + catch (Exception e) { + log.error(e.getMessage() + "\n" + e); + } + log.info("Session " + session.getId() + " has ended"); + } + + @OnError + public void onError(Session session, Throwable t) { + try { + session.getBasicRemote().sendText("Connection closed"); + xmppConnector.disconnect(); + log.info("XMPP connection is disconnected"); + } catch (Exception e) { + log.error(e.getMessage()+"\n"+ e); + } + log.info("Session " + session.getId() + " has ended"); + } + +} \ No newline at end of file diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/exception/DroneAnalyzerException.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/exception/DroneAnalyzerException.java new file mode 100644 index 000000000..7421c5932 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/exception/DroneAnalyzerException.java @@ -0,0 +1,31 @@ +/* + * 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.device.mgt.iot.droneanalyzer.controller.api.impl.exception; + +public class DroneAnalyzerException extends Exception { + private static final long serialVersionUID = 118512086958330189L; + + public DroneAnalyzerException(String errorMessage) { + super(errorMessage); + } + + public DroneAnalyzerException(String errorMessage, Throwable throwable) { + super(errorMessage, throwable); + } +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/transport/DroneAnalyzerXMPPConnector.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/transport/DroneAnalyzerXMPPConnector.java new file mode 100644 index 000000000..a4130c06b --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/transport/DroneAnalyzerXMPPConnector.java @@ -0,0 +1,160 @@ +/* + * 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.device.mgt.iot.droneanalyzer.controller.api.impl.transport; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jivesoftware.smack.packet.Message; +import org.wso2.carbon.device.mgt.iot.controlqueue.xmpp.XmppConfig; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api.impl.trasformer.MessageTransformer; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.constants.DroneConstants; +import org.wso2.carbon.device.mgt.iot.transport.TransportHandlerException; +import org.wso2.carbon.device.mgt.iot.transport.xmpp.XMPPTransportHandler; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class DroneAnalyzerXMPPConnector extends XMPPTransportHandler { + private static Log log = LogFactory.getLog(DroneAnalyzerXMPPConnector.class); + + private static String xmppServerIP; + private static String xmppAdminUsername; + private static String xmppAdminPassword; + private static String xmppAdminAccountJID; + private MessageTransformer messageTransformer; + private ScheduledFuture connectorServiceHandler; + private ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(); + + public DroneAnalyzerXMPPConnector(MessageTransformer messageTransformer) { + super(XmppConfig.getInstance().getXmppServerIP(), XmppConfig.getInstance().getSERVER_CONNECTION_PORT()); + this.messageTransformer = messageTransformer; + } + + @Override + public void connect() { + Runnable connector = new Runnable() { + @Override + public void run() { + if (!isConnected()) { + try { + initConnector(); + connectToServer(); + loginToServer(xmppAdminUsername, xmppAdminPassword, null); + setFilterOnReceiver(xmppAdminAccountJID); + } catch (TransportHandlerException e) { + if (log.isDebugEnabled()) { + log.warn("Connection/Login to XMPP server at: " + server + " as " + + xmppAdminUsername + " failed for device-type [" + + DroneConstants.DEVICE_TYPE + "].", e); + } + } + } + } + }; + connectorServiceHandler = service.scheduleAtFixedRate(connector, 0, timeoutInterval, TimeUnit.MILLISECONDS); + } + + public void initConnector() { + xmppServerIP = XmppConfig.getInstance().getXmppServerIP(); + xmppAdminUsername = XmppConfig.getInstance().getXmppUsername(); + xmppAdminPassword = XmppConfig.getInstance().getXmppPassword(); + xmppAdminAccountJID = xmppAdminUsername + "@" + xmppServerIP; + } + + @Override + public void processIncomingMessage(Message message) throws TransportHandlerException { + try { + String from = message.getFrom(); + String inboundMessage = message.getBody(); + int indexOfSlash = from.indexOf("/"); + if (indexOfSlash == 0) { + if (log.isDebugEnabled()) { + log.debug("Required resource not available."); + } + } else { + String resource = from.substring(indexOfSlash + 1, from.length()); + if ((inboundMessage != null) && (resource.equals(DroneConstants.MESSAGE_RESOURCE))) { + messageTransformer.messageTranslater(inboundMessage); + } else { + if (log.isDebugEnabled()) { + log.debug("Message is empty or it is not belongs to " + xmppAdminUsername); + } + } + } + } catch (ArrayIndexOutOfBoundsException e) { + log.error("Wrong message format: input message", e); + } catch (RuntimeException e) { + log.error("Unexpected error has been occurred, ", e); + } + } + + @Override + public void publishDeviceData(String... publishData) throws TransportHandlerException { + String xmppJID = publishData[0]; + String xmppMessage = publishData[1]; + String xmppSubject = publishData[2]; + sendXMPPMessage(xmppJID, xmppMessage, xmppSubject); + } + + public void disconnect() { + Runnable stopConnection = new Runnable() { + public void run() { + while (isConnected()) { + connectorServiceHandler.cancel(true); + closeConnection(); + if (log.isDebugEnabled()) { + log.warn("Unable to 'STOP' connection to XMPP server at: " + server + + " for user - " + xmppAdminUsername); + } + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException e1) { + log.error("XMPP-Terminator: Thread Sleep Interrupt Exception for " + + DroneConstants.DEVICE_TYPE + " type.", e1); + } + } + } + }; + Thread terminatorThread = new Thread(stopConnection); + terminatorThread.setDaemon(true); + terminatorThread.start(); + } + + @Override + public void processIncomingMessage() throws TransportHandlerException { + + } + + @Override + public void processIncomingMessage(Message message, String... messageParams) throws TransportHandlerException { + + } + + @Override + public void publishDeviceData() throws TransportHandlerException { + + } + + @Override + public void publishDeviceData(Message publishData) throws TransportHandlerException { + + } +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/trasformer/MessageTransformer.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/trasformer/MessageTransformer.java new file mode 100644 index 000000000..74d080771 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/trasformer/MessageTransformer.java @@ -0,0 +1,134 @@ +/* + * 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.device.mgt.iot.droneanalyzer.controller.api.impl.trasformer; + + +import org.apache.commons.collections4.queue.CircularFifoQueue; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.codehaus.jackson.JsonNode; +import org.codehaus.jackson.JsonProcessingException; +import org.codehaus.jackson.map.ObjectMapper; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.constants.DroneConstants; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.constants.MessageConfig; + +import java.io.IOException; + + +public class MessageTransformer { + + private Log log = LogFactory.getLog(MessageTransformer.class); + private CircularFifoQueue sharedQueue; + + private String outboundMessageFormatForSimulator = "{\"quatanium_val\":[%f, %f, %f, %f]," + + "\"basicParam\":{\"velocity\":[%f, %f, %f], \"global_location\":[%f, %f, %f]},\"battery_level\":%f, \"device_type\":\"SIMULATOR\"}"; + private String outboundMessageFormatForIrisDrone = "{\"quatanium_val\":[%f, %f, %f]," + + "\"basicParam\":{\"velocity\":[%f, %f, %f], \"global_location\":[%f, %f, %f]},\"battery_level\":%f," + + "\"device_type\":\"IRIS_DRONE\"}"; + + public MessageTransformer(){ + sharedQueue = new CircularFifoQueue(DroneConstants.MAXIMUM_BUFFERE_SIZE_OF_SHARED_QUEUE); + } + + private void messageTranslaterForSimulator(JsonNode inbound_message){ + JsonNode node = inbound_message; + String outboundMessage; + try { + JsonNode velocity = node.get(MessageConfig.OUT_BASIC_PARAM_VAL).get(MessageConfig.OUT_BASIC_PARAM_VELOCITY); + JsonNode globalLocation = node.get(MessageConfig.OUT_BASIC_PARAM_VAL).get( + MessageConfig.OUT_BASIC_PARAM_GLOBAL_LOCATION); + JsonNode quataniumVals = node.get(MessageConfig.OUT_QUATANNIM_VAL); + JsonNode batteryLevel = node.get(MessageConfig.OUT_BATTERY_LEVEL); + outboundMessage = String.format(outboundMessageFormatForSimulator, sTd(quataniumVals.get(0)), + sTd(quataniumVals.get(1)), sTd(quataniumVals.get(2)), sTd(quataniumVals.get(0)), + sTd(velocity.get(0)), sTd(velocity.get(1)), sTd(velocity.get(2)), sTd(globalLocation.get(0)), + sTd(globalLocation.get(1)), sTd(globalLocation.get(2)), sTd(batteryLevel)); + sharedQueue.add(outboundMessage); + } catch (Exception e) { + log.error(e.getMessage()+",\n"+ e); + } + } + + private void messageTranslaterForIRISDrone(JsonNode inbound_message){ + JsonNode node = inbound_message; + String outboundMessage; + try { + + JsonNode velocity = node.get(MessageConfig.OUT_BASIC_PARAM_VAL).get(MessageConfig.OUT_BASIC_PARAM_VELOCITY); + JsonNode globalLocation = node.get(MessageConfig.OUT_BASIC_PARAM_VAL).get( + MessageConfig.OUT_BASIC_PARAM_GLOBAL_LOCATION); + JsonNode quataniumVals = node.get(MessageConfig.OUT_QUATANNIM_VAL); + JsonNode batteryLevel = node.get(MessageConfig.OUT_BATTERY_LEVEL); + outboundMessage = String.format(outboundMessageFormatForIrisDrone, sTd(quataniumVals.get(0)), + sTd(quataniumVals.get(1)), sTd(quataniumVals.get(2)), sTd(velocity.get(0)), + sTd(velocity.get(1)), sTd(velocity.get(2)), sTd(globalLocation.get(0)), + sTd(globalLocation.get(1)), sTd(globalLocation.get(2)), sTd(batteryLevel)); + sharedQueue.add(outboundMessage); + + }catch (Exception e) { + log.error(e.getMessage()+",\n"+ e); + } + } + + public void messageTranslater(String inbound_message){ + JsonNode actualMessage; + ObjectMapper objectMapper = new ObjectMapper(); + try { + actualMessage = objectMapper.readValue(inbound_message, JsonNode.class); + JsonNode deviceType = actualMessage.get(MessageConfig.IN_DEVICE_TYPE); + switch (deviceType.getTextValue()) { + case MessageConfig.IN_IRIS_DRONE: + messageTranslaterForIRISDrone(actualMessage); + break; + case MessageConfig.IN_SIMULATOR: + messageTranslaterForSimulator(actualMessage); + break; + default: + if(log.isDebugEnabled()){ + log.debug("Wrong message format"); + } + } + } catch (JsonProcessingException e) { + log.error("Incoming message might be corrupted, "+ e); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + + private double sTd(JsonNode s) + { + return Double.parseDouble(s.toString()); + } + + public String getMessage() { + try{ + if(sharedQueue.isEmpty() || sharedQueue == null){ + return ""; + } + return sharedQueue.remove(); + }catch(Exception e) { + log.error("There is no more messages to send or internal server error has been occurred, \n"+ e ); + return ""; + } + } + + public boolean isEmptyQueue(){ + return sharedQueue != null? sharedQueue.isEmpty():false; + } + +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/util/DroneAnalyzerServiceUtils.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/util/DroneAnalyzerServiceUtils.java new file mode 100644 index 000000000..0b6a3abff --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/controller/api/impl/util/DroneAnalyzerServiceUtils.java @@ -0,0 +1,96 @@ + +/* + * 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.device.mgt.iot.droneanalyzer.controller.api.impl.util; + +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.iot.controlqueue.xmpp.XmppConfig; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api.impl.transport.DroneAnalyzerXMPPConnector; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.constants.DroneConstants; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.controller.DroneController; +import org.wso2.carbon.device.mgt.iot.transport.TransportHandlerException; + +import java.io.File; + +public class DroneAnalyzerServiceUtils { + + private static final String SUPER_TENANT = "carbon.super"; + private static org.apache.commons.logging.Log log = LogFactory.getLog(DroneAnalyzerServiceUtils.class); + + public static void sendCommandViaXMPP(String deviceOwner, String deviceId, String resource, + String state, DroneAnalyzerXMPPConnector droneXMPPConnector) + throws DeviceManagementException, TransportHandlerException { + + String xmppServerDomain = XmppConfig.getInstance().getXmppEndpoint(); + int indexOfChar = xmppServerDomain.lastIndexOf(File.separator); + if (indexOfChar != -1) { + xmppServerDomain = xmppServerDomain.substring((indexOfChar + 1), xmppServerDomain.length()); + } + indexOfChar = xmppServerDomain.indexOf(":"); + if (indexOfChar != -1) { + xmppServerDomain = xmppServerDomain.substring(0, indexOfChar); + } + String clientToConnect = deviceId + "@" + xmppServerDomain + File.separator + deviceOwner; + String message = resource.replace("/", "") + ":" + state; + droneXMPPConnector.publishDeviceData(clientToConnect, message, "CONTROL-REQUEST"); + } + + public static boolean sendControlCommand(DroneController controller, String deviceId, String action, + double speed, double duration) + throws DeviceManagementException { + boolean controlState = false; + try{ + switch (action){ + case DroneConstants.TAKE_OFF: + controlState = controller.takeoff(); + break; + case DroneConstants.LAND: + controlState = controller.land(); + break; + case DroneConstants.BACK: + controlState = controller.back(speed, duration); + break; + case DroneConstants.CLOCK_WISE: + controlState = controller.clockwise(speed, duration); + break; + case DroneConstants.COUNTER_CLOCKWISE: + controlState = controller.conterClockwise(speed, duration); + break; + case DroneConstants.DOWN: + controlState = controller.down(speed, duration); + break; + case DroneConstants.FRONT: + controlState = controller.back(speed, duration); + break; + case DroneConstants.FORWARD: + controlState = controller.clockwise(speed, duration); + break; + case DroneConstants.UP: + controlState = controller.up(speed, duration); + break; + default: + log.error("Invalid command"); + break; + } + }catch(Exception e){ + log.error(e.getMessage()+ "\n"+ e); + } + return controlState; + } +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/webapp/META-INF/webapp-classloading.xml b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/webapp/META-INF/webapp-classloading.xml new file mode 100644 index 000000000..fa4461919 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/webapp/META-INF/webapp-classloading.xml @@ -0,0 +1,33 @@ + + + + + + + + + false + + + CXF,Carbon + diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/webapp/WEB-INF/cxf-servlet.xml b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/webapp/WEB-INF/cxf-servlet.xml new file mode 100644 index 000000000..cbaf62e25 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/webapp/WEB-INF/cxf-servlet.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/webapp/WEB-INF/web.xml b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..2a9646df6 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,49 @@ + + + WSO2 IoT Server + WSO2 IoT Server + + CXFServlet + org.apache.cxf.transport.servlet.CXFServlet + 1 + + + CXFServlet + /* + + + + isAdminService + false + + + doAuthentication + false + + + + + managed-api-enabled + true + + + managed-api-owner + admin + + + managed-api-context-template + /drone_analyzer/{version} + + + managed-api-application + drone_analyzer + + + managed-api-isSecured + true + + diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/pom.xml b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/pom.xml new file mode 100644 index 000000000..34bf4cacd --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/pom.xml @@ -0,0 +1,261 @@ + + + + drone-analyzer-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api + war + WSO2 Carbon - IoT Server Drone Analyzer Manager API + http://maven.apache.org + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.core + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.analytics + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.certificate.mgt.core + provided + + + commons-codec.wso2 + commons-codec + + + + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + provided + + + org.apache.cxf + cxf-rt-frontend-jaxrs + provided + + + org.apache.cxf + cxf-rt-transports-http + provided + + + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + provided + + + + + org.springframework + spring-context + + + + + org.apache.httpcomponents + httpasyncclient + 4.1 + provided + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot + provided + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin + provided + + + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-jaxrs + + + javax + javaee-web-api + provided + + + javax.ws.rs + jsr311-api + provided + + + commons-httpclient.wso2 + commons-httpclient + provided + + + + org.wso2.carbon + org.wso2.carbon.utils + provided + + + org.bouncycastle.wso2 + bcprov-jdk15on + + + org.wso2.carbon + org.wso2.carbon.user.api + + + org.wso2.carbon + org.wso2.carbon.queuing + + + org.wso2.carbon + org.wso2.carbon.base + + + org.apache.axis2.wso2 + axis2 + + + org.igniterealtime.smack.wso2 + smack + + + org.igniterealtime.smack.wso2 + smackx + + + jaxen + jaxen + + + commons-fileupload.wso2 + commons-fileupload + + + org.apache.ant.wso2 + ant + + + org.apache.ant.wso2 + ant + + + commons-httpclient.wso2 + commons-httpclient + + + org.eclipse.equinox + javax.servlet + + + org.wso2.carbon + org.wso2.carbon.registry.api + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.webapp.publisher + provided + + + + commons-codec.wso2 + commons-codec + + + + org.igniterealtime.smack.wso2 + smack + provided + + + org.igniterealtime.smack.wso2 + smackx + provided + + + org.apache.commons + commons-collections4 + 4.0 + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.annotations + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.jwt.client.extension + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.application.extension + provided + + + + + + + + maven-compiler-plugin + + UTF-8 + ${wso2.maven.compiler.source} + ${wso2.maven.compiler.target} + + + + maven-war-plugin + + src/main/webapp/WEB-INF/web.xml + drone_analyzer_mgt + + + + + diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/manager/api/impl/DroneManagerService.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/manager/api/impl/DroneManagerService.java new file mode 100644 index 000000000..149c9070e --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/manager/api/impl/DroneManagerService.java @@ -0,0 +1,319 @@ +/* + * 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.device.mgt.iot.droneanalyzer.manager.api.impl; + +import org.apache.commons.logging.LogFactory; +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.common.Device; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.EnrolmentInfo; +import org.wso2.carbon.device.mgt.iot.controlqueue.xmpp.XmppAccount; +import org.wso2.carbon.device.mgt.iot.controlqueue.xmpp.XmppConfig; +import org.wso2.carbon.device.mgt.iot.controlqueue.xmpp.XmppServerClient; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api.impl.util.APIUtil; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.constants.DroneConstants; +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.device.mgt.jwt.client.extension.JWTClient; +import org.wso2.carbon.device.mgt.jwt.client.extension.JWTClientManager; +import org.wso2.carbon.device.mgt.jwt.client.extension.dto.AccessTokenInfo; +import org.wso2.carbon.device.mgt.jwt.client.extension.exception.JWTClientException; +import org.wso2.carbon.user.api.UserStoreException; + +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +public class DroneManagerService { + + private static org.apache.commons.logging.Log log = LogFactory.getLog(DroneManagerService.class); + @Context //injected response proxy supporting multiple thread + private HttpServletResponse response; + private static final String KEY_TYPE = "PRODUCTION"; + private static ApiApplicationKey apiApplicationKey; + + @Path("manager/device/register") + @POST + public boolean register(@QueryParam("deviceId") String deviceId, @QueryParam("name") String name) { + try { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(DroneConstants.DEVICE_TYPE); + if (APIUtil.getDeviceManagementService().isEnrolled(deviceIdentifier)) { + response.setStatus(Response.Status.CONFLICT.getStatusCode()); + return false; + } + Device device = new Device(); + device.setDeviceIdentifier(deviceId); + EnrolmentInfo enrolmentInfo = new EnrolmentInfo(); + enrolmentInfo.setDateOfEnrolment(new Date().getTime()); + enrolmentInfo.setDateOfLastUpdate(new Date().getTime()); + enrolmentInfo.setStatus(EnrolmentInfo.Status.ACTIVE); + enrolmentInfo.setOwnership(EnrolmentInfo.OwnerShip.BYOD); + device.setName(name); + device.setType(DroneConstants.DEVICE_TYPE); + enrolmentInfo.setOwner(APIUtil.getAuthenticatedUser()); + device.setEnrolmentInfo(enrolmentInfo); + boolean added = APIUtil.getDeviceManagementService().enrollDevice(device); + if (added) { + response.setStatus(Response.Status.OK.getStatusCode()); + } else { + response.setStatus(Response.Status.NOT_ACCEPTABLE.getStatusCode()); + } + return added; + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/device/{device_id}") + @DELETE + public void removeDevice(@PathParam("device_id") String deviceId, @Context HttpServletResponse response) { + try { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(DroneConstants.DEVICE_TYPE); + boolean removed = APIUtil.getDeviceManagementService().disenrollDevice(deviceIdentifier); + if (removed) { + response.setStatus(Response.Status.OK.getStatusCode()); + } else { + response.setStatus(Response.Status.NOT_ACCEPTABLE.getStatusCode()); + + } + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/device/{device_id}") + @PUT + public boolean updateDevice(@PathParam("device_id") String deviceId, @QueryParam("name") String name, + @Context HttpServletResponse response) { + try { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(DroneConstants.DEVICE_TYPE); + Device device = APIUtil.getDeviceManagementService().getDevice(deviceIdentifier); + device.setDeviceIdentifier(deviceId); + device.getEnrolmentInfo().setDateOfLastUpdate(new Date().getTime()); + device.setName(name); + device.setType(DroneConstants.DEVICE_TYPE); + boolean updated = APIUtil.getDeviceManagementService().modifyEnrollment(device); + if (updated) { + response.setStatus(Response.Status.OK.getStatusCode()); + } else { + response.setStatus(Response.Status.NOT_ACCEPTABLE.getStatusCode()); + } + return updated; + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/device/{device_id}") + @GET + @Consumes("application/json") + @Produces("application/json") + public Device getDevice(@PathParam("device_id") String deviceId) { + try { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(DroneConstants.DEVICE_TYPE); + return APIUtil.getDeviceManagementService().getDevice(deviceIdentifier); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/devices") + @GET + @Consumes("application/json") + @Produces("application/json") + public Device[] getDroneDevices() { + try { + List userDevices = APIUtil.getDeviceManagementService().getDevicesOfUser(APIUtil.getAuthenticatedUser()); + ArrayList userDevicesforDrone = new ArrayList<>(); + for (Device device : userDevices) { + if (device.getType().equals(DroneConstants.DEVICE_TYPE) && + device.getEnrolmentInfo().getStatus().equals( + EnrolmentInfo.Status.ACTIVE)) { + userDevicesforDrone.add(device); + } + } + return userDevicesforDrone.toArray(new Device[]{}); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + + + @Path("manager/device/{sketch_type}/download") + @GET + @Produces("application/octet-stream") + public Response downloadSketch(@QueryParam("deviceName") String deviceName, + @PathParam("sketch_type") String sketchType) { + try { + //create new device id + String deviceId = shortUUID(); + //create token + String token = UUID.randomUUID().toString(); + String refreshToken = UUID.randomUUID().toString(); + //adding registering data + boolean status = register(deviceId, deviceName); + if (!status) { + return Response.status(500).entity( + "Error occurred while registering the device with " + "id: " + deviceId + + " owner:" + APIUtil.getAuthenticatedUser()).build(); + + } + ZipUtil ziputil = new ZipUtil(); + ZipArchive zipFile; + try { + zipFile = ziputil.createZipFile(APIUtil.getAuthenticatedUser(), APIUtil.getTenantDomainOftheUser(), + sketchType, deviceId, deviceName, token, refreshToken); + } catch (DeviceManagementException ex) { + return Response.status(500).entity("Error occurred while creating zip file").build(); + } + Response.ResponseBuilder rb = Response.ok(zipFile.getZipFile()); + rb.header("Content-Disposition", "attachment; filename=\"" + zipFile.getFileName() + "\""); + return rb.build(); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/device/{sketch_type}/generate_link") + @GET + public Response generateSketchLink(@QueryParam("deviceName") String deviceName, + @PathParam("sketch_type") String sketchType) { + try { + ZipArchive zipFile = createDownloadFile(deviceName, sketchType); + Response.ResponseBuilder rb = Response.ok(zipFile.getDeviceId()); + return rb.build(); + } catch (IllegalArgumentException ex) { + return Response.status(400).entity(ex.getMessage()).build(); + } catch (DeviceManagementException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (JWTClientException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (DeviceControllerException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (APIManagerException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (UserStoreException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + private ZipArchive createDownloadFile(String deviceName, String sketchType) + throws DeviceManagementException, JWTClientException, APIManagerException, DeviceControllerException, + UserStoreException { + //create new device id + String deviceId = shortUUID(); + if (apiApplicationKey == null) { + String applicationUsername = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUserRealm().getRealmConfiguration() + .getAdminUserName(); + APIManagementProviderService apiManagementProviderService = APIUtil.getAPIManagementProviderService(); + String[] tags = {DroneConstants.DEVICE_TYPE}; + apiApplicationKey = apiManagementProviderService.generateAndRetrieveApplicationKeys( + DroneConstants.DEVICE_TYPE, tags, KEY_TYPE, applicationUsername, true); + } + JWTClient jwtClient = JWTClientManager.getInstance().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(); + //adding registering data + XmppAccount newXmppAccount = new XmppAccount(); + newXmppAccount.setAccountName(APIUtil.getAuthenticatedUser() + "_" + deviceId); + newXmppAccount.setUsername(deviceId); + newXmppAccount.setPassword(accessToken); + newXmppAccount.setEmail(deviceId + "@wso2.com"); + XmppServerClient xmppServerClient = new XmppServerClient(); + xmppServerClient.initControlQueue(); + boolean status; + if (XmppConfig.getInstance().isEnabled()) { + status = xmppServerClient.createXMPPAccount(newXmppAccount); + if (!status) { + String msg = "XMPP Account was not created for device - " + deviceId + " of owner - " + + APIUtil.getAuthenticatedUser() + ".XMPP might have been disabled in org.wso2.carbon.device.mgt.iot.common.config.server.configs"; + log.warn(msg); + throw new DeviceManagementException(msg); + } + } + //Register the device with CDMF + status = register(deviceId, deviceName); + if (!status) { + String msg = "Error occurred while registering the device with " + "id: " + deviceId + + " owner:" + APIUtil.getAuthenticatedUser(); + throw new DeviceManagementException(msg); + } + ZipUtil ziputil = new ZipUtil(); + ZipArchive zipFile = ziputil.createZipFile(APIUtil.getAuthenticatedUser(), APIUtil.getTenantDomainOftheUser(), + sketchType, deviceId, deviceName, accessToken, refreshToken); + zipFile.setDeviceId(deviceId); + return zipFile; + } + + private static String shortUUID() { + UUID uuid = UUID.randomUUID(); + long l = ByteBuffer.wrap(uuid.toString().getBytes(StandardCharsets.UTF_8)).getLong(); + return Long.toString(l, Character.MAX_RADIX); + } +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/manager/api/impl/util/APIUtil.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/manager/api/impl/util/APIUtil.java new file mode 100644 index 000000000..e9980fa5a --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/manager/api/impl/util/APIUtil.java @@ -0,0 +1,55 @@ +package org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api.impl.util; + +import org.apache.commons.logging.Log; +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; + +/** + * This class provides utility functions used by REST-API. + */ +public class APIUtil { + + private static Log log = LogFactory.getLog(APIUtil.class); + + public static String getAuthenticatedUser() { + PrivilegedCarbonContext threadLocalCarbonContext = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + String username = threadLocalCarbonContext.getUsername(); + String tenantDomain = threadLocalCarbonContext.getTenantDomain(); + if (username.endsWith(tenantDomain)) { + return username.substring(0, username.lastIndexOf("@")); + } + 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 = + (DeviceManagementProviderService) ctx.getOSGiService(DeviceManagementProviderService.class, null); + if (deviceManagementProviderService == null) { + String msg = "Device Management service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return deviceManagementProviderService; + } + + 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; + } +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/webapp/META-INF/webapp-classloading.xml b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/webapp/META-INF/webapp-classloading.xml new file mode 100644 index 000000000..fa4461919 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/webapp/META-INF/webapp-classloading.xml @@ -0,0 +1,33 @@ + + + + + + + + + false + + + CXF,Carbon + diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/webapp/WEB-INF/cxf-servlet.xml b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/webapp/WEB-INF/cxf-servlet.xml new file mode 100644 index 000000000..3a8849969 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/webapp/WEB-INF/cxf-servlet.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/webapp/WEB-INF/web.xml b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..6947c58e1 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,62 @@ + + + WSO2 IoT Server + WSO2 IoT Server + + + CXFServlet + org.apache.cxf.transport.servlet.CXFServlet + 1 + + + + + CXFServlet + /* + + + + isAdminService + false + + + doAuthentication + true + + + + + managed-api-enabled + false + + + managed-api-owner + admin + + + managed-api-context-template + /drone_analyzer/{version} + + + managed-api-application + drone_analyzer + + + managed-api-isSecured + true + + + + + + + + + + + + diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/pom.xml b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/pom.xml new file mode 100644 index 000000000..a3482fca4 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/pom.xml @@ -0,0 +1,108 @@ + + + + drone-analyzer-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin + bundle + WSO2 Carbon - IoT Server Drone Analyzer Device-Type Plugin + WSO2 Carbon - IoT Server Drone Analyzer Device-Type Plugin Implementation + http://maven.apache.org + + + UTF-8 + + + + + + org.apache.felix + maven-scr-plugin + + + maven-compiler-plugin + + 1.7 + 1.7 + + 2.3.2 + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + ${project.artifactId} + ${project.artifactId} + ${carbon.devicemgt.plugins.version} + IoT Server Impl Bundle + org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.internal + + org.osgi.framework, + org.osgi.service.component, + org.apache.commons.logging, + javax.xml.bind.*;resolution:=optional, + javax.naming;resolution:=optional, + javax.sql;resolution:=optional, + javax.xml.bind.annotation.*;resolution:=optional, + javax.xml.parsers;resolution:=optional, + javax.net;resolution:=optional, + javax.net.ssl;resolution:=optional, + org.w3c.dom;resolution:=optional, + org.wso2.carbon.device.mgt.common.*, + org.wso2.carbon.device.mgt.common, + org.wso2.carbon.context.*, + org.wso2.carbon.ndatasource.core, + org.wso2.carbon.device.mgt.iot.*, + org.wso2.carbon.device.mgt.extensions.feature.mgt.*, + org.wso2.carbon.utils.* + + + !org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.internal, + org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.* + + + + + + + + + + org.eclipse.osgi + org.eclipse.osgi + + + org.eclipse.osgi + org.eclipse.osgi.services + + + org.wso2.carbon + org.wso2.carbon.logging + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + + + org.wso2.carbon + org.wso2.carbon.ndatasource.core + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.extensions + + + org.wso2.carbon + org.wso2.carbon.utils + + + diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/constants/DroneConstants.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/constants/DroneConstants.java new file mode 100644 index 000000000..03b950f10 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/constants/DroneConstants.java @@ -0,0 +1,41 @@ +/* + * 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.device.mgt.iot.droneanalyzer.plugin.constants; + +public class DroneConstants { + + public final static String DEVICE_PLUGIN_DEVICE_NAME = "DEVICE_NAME"; + public final static String DEVICE_PLUGIN_DEVICE_ID = "DRONE_DEVICE_ID"; + public final static String MESSAGE_RESOURCE = "drone_current_status"; + public static final String DEVICE_TYPE = "drone_analyzer"; + public static final int MINIMUM_TIME_DURATION = 150; + public static final int MAXIMUM_BUFFERE_SIZE_OF_SHARED_QUEUE = 10; + public static final String DEVICE_ID = "drone_type1"; + public final static String TAKE_OFF = "takeoff"; + public final static String LAND = "land"; + public final static String UP = "up"; + public final static String DOWN = "down"; + public final static String BACK = "back"; + public final static String FORWARD = "forward"; + public final static String FRONT = "front"; + public final static String CLOCK_WISE = "clockwise"; + public final static String COUNTER_CLOCKWISE = "counterClockwise"; + public static final String DATA_SOURCE_NAME = "jdbc/DroneAnalyzerDM_DB"; + +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/constants/MessageConfig.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/constants/MessageConfig.java new file mode 100644 index 000000000..d6d64b359 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/constants/MessageConfig.java @@ -0,0 +1,40 @@ +/* + * 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.device.mgt.iot.droneanalyzer.plugin.constants; + +public class MessageConfig { + + public static final String IN_QUATANNIM_VAL = "quatanium_val"; + public static final String IN_DEVICE_TYPE = "device_type"; + public static final String IN_IRIS_DRONE = "IRIS_DRONE"; + public static final String IN_SIMULATOR = "SIMULATOR"; + public static final String OUT_QUATANNIM_VAL = "quatanium_val"; + public static final String IN_ACCELETOMETER_VAL = "accelerometer"; + public static final String OUT_ACCELETOMETER_VAL = "accelerometer"; + public static final String IN_GYROSCOPE_VAL = "gyroscope"; + public static final String OUT_GYROSCOPE_VAL = "gyroscope"; + public static final String IN_MAGNETOMETER_VAL = "magnetometer"; + public static final String OUT_MAGNETOMETER_VAL = "magnetometer"; + public static final String IN_BASIC_PARAM_VAL = "basicParam"; + public static final String OUT_BASIC_PARAM_VAL = "basicParam"; + public static final String OUT_BASIC_PARAM_VELOCITY = "velocity"; + public static final String OUT_BASIC_PARAM_GLOBAL_LOCATION = "global_location"; + public static final String OUT_BATTERY_LEVEL = "battery_level"; + +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/controller/DroneController.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/controller/DroneController.java new file mode 100644 index 000000000..0bf48f4ea --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/controller/DroneController.java @@ -0,0 +1,32 @@ +/* + * 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.device.mgt.iot.droneanalyzer.plugin.controller; + +public interface DroneController { + boolean takeoff(); + boolean land(); + boolean up(double speed, double duration); + boolean down(double speed, double duration); + boolean left(double speed, double duration); + boolean right(double speed, double duration); + boolean front(double speed, double duration); + boolean back(double speed, double duration); + boolean clockwise(double speed, double duration); + boolean conterClockwise(double speed, double duration); +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/controller/impl/DroneControllerImpl.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/controller/impl/DroneControllerImpl.java new file mode 100644 index 000000000..b9fd5e073 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/controller/impl/DroneControllerImpl.java @@ -0,0 +1,73 @@ +/* + * 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.device.mgt.iot.droneanalyzer.plugin.controller.impl; + +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.controller.DroneController; + +public class DroneControllerImpl implements DroneController{ + + @Override + public boolean takeoff() { + return false; + } + + @Override + public boolean land() { + return false; + } + + @Override + public boolean up(double speed, double duration) { + return false; + } + + @Override + public boolean down(double speed, double duration) { + return false; + } + + @Override + public boolean left(double speed, double duration) { + return false; + } + + @Override + public boolean right(double speed, double duration) { + return false; + } + + @Override + public boolean front(double speed, double duration) { + return false; + } + + @Override + public boolean back(double speed, double duration) { + return false; + } + + @Override + public boolean clockwise(double speed, double duration) { + return false; + } + + @Override + public boolean conterClockwise(double speed, double duration) { + return false; + } +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/exception/DroneAnalyzerDeviceMgtPluginException.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/exception/DroneAnalyzerDeviceMgtPluginException.java new file mode 100644 index 000000000..e3d118ef6 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/exception/DroneAnalyzerDeviceMgtPluginException.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.device.mgt.iot.droneanalyzer.plugin.exception; + + +public class DroneAnalyzerDeviceMgtPluginException extends Exception{ + + private String errorMessage; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public DroneAnalyzerDeviceMgtPluginException(String msg, Exception nestedEx) { + super(msg, nestedEx); + setErrorMessage(msg); + } + + public DroneAnalyzerDeviceMgtPluginException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public DroneAnalyzerDeviceMgtPluginException(String msg) { + super(msg); + setErrorMessage(msg); + } + + public DroneAnalyzerDeviceMgtPluginException() { + super(); + } + + public DroneAnalyzerDeviceMgtPluginException(Throwable cause) { + super(cause); + } + +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/DroneAnalyzerManager.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/DroneAnalyzerManager.java new file mode 100644 index 000000000..a88018885 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/DroneAnalyzerManager.java @@ -0,0 +1,250 @@ +/* + * 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.device.mgt.iot.droneanalyzer.plugin.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.*; +import org.wso2.carbon.device.mgt.common.configuration.mgt.TenantConfiguration; +import org.wso2.carbon.device.mgt.common.license.mgt.License; +import org.wso2.carbon.device.mgt.common.license.mgt.LicenseManagementException; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.exception.DroneAnalyzerDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.impl.dao.DroneAnalyzerDAO; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.impl.feature.DroneAnalyzerFeatureManager; +import java.util.List; + +/** + * This represents the Drone Analyzer service implementation of DeviceManagerService. + */ +public class DroneAnalyzerManager implements DeviceManager { + + private static final DroneAnalyzerDAO droneAnalyzerDAO = new DroneAnalyzerDAO(); + private static final Log log = LogFactory.getLog(DroneAnalyzerManager.class); + private FeatureManager droneFeatureManager = new DroneAnalyzerFeatureManager(); + @Override + public FeatureManager getFeatureManager() { + return droneFeatureManager; + } + + @Override + public boolean saveConfiguration(TenantConfiguration tenantConfiguration) + throws DeviceManagementException { + //TODO implement this + return false; + } + + @Override + public TenantConfiguration getConfiguration() throws DeviceManagementException { + //TODO implement this + return null; + } + + @Override + public boolean enrollDevice(Device device) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Enrolling a new drone device : " + device.getDeviceIdentifier()); + } + DroneAnalyzerDAO.beginTransaction(); + status = droneAnalyzerDAO.getDeviceDAO().addDevice(device); + DroneAnalyzerDAO.commitTransaction(); + } catch (DroneAnalyzerDeviceMgtPluginException e) { + try { + DroneAnalyzerDAO.rollbackTransaction(); + } catch (DroneAnalyzerDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the device enrol transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while enrolling the drone device : " + device.getDeviceIdentifier(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean modifyEnrollment(Device device) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Modifying the Virtual Firealarm device enrollment data"); + } + DroneAnalyzerDAO.beginTransaction(); + status = droneAnalyzerDAO.getDeviceDAO().updateDevice(device); + DroneAnalyzerDAO.commitTransaction(); + } catch (DroneAnalyzerDeviceMgtPluginException e) { + try { + DroneAnalyzerDAO.rollbackTransaction(); + } catch (DroneAnalyzerDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the update device transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while updating the enrollment of the drone device : " + + device.getDeviceIdentifier(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean disenrollDevice(DeviceIdentifier deviceId) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Dis-enrolling drone device : " + deviceId); + } + DroneAnalyzerDAO.beginTransaction(); + status = droneAnalyzerDAO.getDeviceDAO().deleteIotDevice(deviceId.getId()); + DroneAnalyzerDAO.commitTransaction(); + } catch (DroneAnalyzerDeviceMgtPluginException e) { + try { + DroneAnalyzerDAO.rollbackTransaction(); + } catch (DroneAnalyzerDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the device dis enrol transaction :" + deviceId.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while removing the drone device : " + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean isEnrolled(DeviceIdentifier deviceId) throws DeviceManagementException { + boolean isEnrolled = false; + try { + if (log.isDebugEnabled()) { + log.debug("Checking the enrollment of Drone device : " + deviceId.getId()); + } + Device iotDevice = droneAnalyzerDAO.getDeviceDAO().getDevice(deviceId.getId()); + if (iotDevice != null) { + isEnrolled = true; + } + } catch (DroneAnalyzerDeviceMgtPluginException e) { + String msg = "Error while checking the enrollment status of Drone device : " + + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return isEnrolled; + } + + @Override + public boolean isActive(DeviceIdentifier deviceId) throws DeviceManagementException { + return true; + } + + @Override + public boolean setActive(DeviceIdentifier deviceId, boolean status) + throws DeviceManagementException { + return true; + } + + @Override + public Device getDevice(DeviceIdentifier deviceId) throws DeviceManagementException { + Device device; + try { + if (log.isDebugEnabled()) { + log.debug("Getting the details of Drone device : " + deviceId.getId()); + } + device = droneAnalyzerDAO.getDeviceDAO().getDevice(deviceId.getId()); + } catch (DroneAnalyzerDeviceMgtPluginException e) { + String msg = "Error while fetching the Drone device : " + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return device; + } + + @Override + public boolean setOwnership(DeviceIdentifier deviceId, String ownershipType) + throws DeviceManagementException { + return true; + } + + public boolean isClaimable(DeviceIdentifier deviceIdentifier) throws DeviceManagementException { + return false; + } + + @Override + public boolean setStatus(DeviceIdentifier deviceId, String currentOwner, + EnrolmentInfo.Status status) throws DeviceManagementException { + return false; + } + + @Override + public License getLicense(String s) throws LicenseManagementException { + return null; + } + + @Override + public void addLicense(License license) throws LicenseManagementException { + + } + + @Override + public boolean updateDeviceInfo(DeviceIdentifier deviceIdentifier, Device device) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug( + "updating the details of Drone device : " + deviceIdentifier); + } + DroneAnalyzerDAO.beginTransaction(); + status = droneAnalyzerDAO.getDeviceDAO().updateDevice(device); + DroneAnalyzerDAO.commitTransaction(); + } catch (DroneAnalyzerDeviceMgtPluginException e) { + try { + DroneAnalyzerDAO.rollbackTransaction(); + } catch (DroneAnalyzerDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the update device info transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = + "Error while updating the Drone device : " + deviceIdentifier; + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public List getAllDevices() throws DeviceManagementException { + List devices = null; + try { + if (log.isDebugEnabled()) { + log.debug("Fetching the details of all Drone devices"); + } + devices = droneAnalyzerDAO.getDeviceDAO().getAllDevices(); + } catch (DroneAnalyzerDeviceMgtPluginException e) { + String msg = "Error while fetching all Drone devices."; + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return devices; + } + + @Override + public boolean requireDeviceAuthorization() { + return true; + } +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/DroneAnalyzerManagerService.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/DroneAnalyzerManagerService.java new file mode 100644 index 000000000..5af1a9008 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/DroneAnalyzerManagerService.java @@ -0,0 +1,106 @@ +/* + * 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.device.mgt.iot.droneanalyzer.plugin.impl; + +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.DeviceManager; +import org.wso2.carbon.device.mgt.common.app.mgt.Application; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManagementException; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManager; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.constants.DroneConstants; + +import java.util.List; + + +public class DroneAnalyzerManagerService implements DeviceManagementService { + private DeviceManager deviceManager; + @Override + public String getType() { + return DroneConstants.DEVICE_TYPE; + } + + @Override + public void init() throws DeviceManagementException { + this.deviceManager = new DroneAnalyzerManager(); + } + + @Override + public DeviceManager getDeviceManager() { + return deviceManager; + } + + @Override + public ApplicationManager getApplicationManager() { + return null; + } + + @Override + public void notifyOperationToDevices(Operation operation, List list) + throws DeviceManagementException { + + } + + @Override + public Application[] getApplications(String s, int i, int i1) throws ApplicationManagementException { + return new Application[0]; + } + + @Override + public void updateApplicationStatus(DeviceIdentifier deviceIdentifier, Application application, String s) + throws ApplicationManagementException { + + } + + @Override + public String getApplicationStatus(DeviceIdentifier deviceIdentifier, Application application) + throws ApplicationManagementException { + return null; + } + + @Override + public void installApplicationForDevices(Operation operation, List list) + throws ApplicationManagementException { + + } + + @Override + public void installApplicationForUsers(Operation operation, List list) + throws ApplicationManagementException { + + } + + @Override + public void installApplicationForUserRoles(Operation operation, List list) + throws ApplicationManagementException { + + } + + @Override + public String getProviderTenantDomain() { + return "carbon.super"; + } + + @Override + public boolean isSharedWithAllTenants() { + return true; + } + +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/dao/DroneAnalyzerDAO.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/dao/DroneAnalyzerDAO.java new file mode 100644 index 000000000..26896bc77 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/dao/DroneAnalyzerDAO.java @@ -0,0 +1,126 @@ +/* + * 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.device.mgt.iot.droneanalyzer.plugin.impl.dao; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.constants.DroneConstants; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.exception.DroneAnalyzerDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.impl.dao.impl.DroneAnalyzerDeviceDAOImpl; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + + +public class DroneAnalyzerDAO { + + private static final Log log = LogFactory.getLog(DroneAnalyzerDAO.class); + static DataSource dataSource; + private static ThreadLocal currentConnection = new ThreadLocal(); + + public DroneAnalyzerDAO() { + initDroneAnalyzerDAO(); + } + + public static void initDroneAnalyzerDAO() { + try { + Context ctx = new InitialContext(); + dataSource = (DataSource) ctx.lookup(DroneConstants.DATA_SOURCE_NAME); + } catch (NamingException e) { + log.error("Error while looking up the data source: " + DroneConstants.DATA_SOURCE_NAME); + } + } + + public DroneAnalyzerDeviceDAOImpl getDeviceDAO() { + return new DroneAnalyzerDeviceDAOImpl(); + } + + public static void beginTransaction() throws DroneAnalyzerDeviceMgtPluginException { + try { + Connection conn = dataSource.getConnection(); + conn.setAutoCommit(false); + currentConnection.set(conn); + } catch (SQLException e) { + throw new DroneAnalyzerDeviceMgtPluginException("Error occurred while retrieving datasource connection", e); + } + } + + public static Connection getConnection() throws DroneAnalyzerDeviceMgtPluginException { + + if (currentConnection != null && currentConnection.get() == null) { + try { + currentConnection.set(dataSource.getConnection()); + } catch (SQLException e) { + throw new DroneAnalyzerDeviceMgtPluginException("Error occurred while retrieving data source connection", e); + } + } + return currentConnection.get(); + } + + public static void commitTransaction() throws DroneAnalyzerDeviceMgtPluginException { + try { + Connection conn = currentConnection.get(); + if (conn != null) { + conn.commit(); + } else { + if (log.isDebugEnabled()) { + log.debug("Datasource connection associated with the current thread is null, hence commit " + + "has not been attempted"); + } + } + } catch (SQLException e) { + throw new DroneAnalyzerDeviceMgtPluginException("Error occurred while committing the transaction", e); + } finally { + closeConnection(); + } + } + + public static void closeConnection() throws DroneAnalyzerDeviceMgtPluginException { + Connection con = currentConnection.get(); + if (con != null) { + try { + con.close(); + } catch (SQLException e) { + log.error("Error occurred while close the connection"); + } + } + currentConnection.remove(); + } + + public static void rollbackTransaction() throws DroneAnalyzerDeviceMgtPluginException { + try { + Connection conn = currentConnection.get(); + if (conn != null) { + conn.rollback(); + } else { + if (log.isDebugEnabled()) { + log.debug("Datasource connection associated with the current thread is null, hence rollback " + + "has not been attempted"); + } + } + } catch (SQLException e) { + throw new DroneAnalyzerDeviceMgtPluginException("Error occurred while rollback the transaction", e); + } finally { + closeConnection(); + } + } +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/dao/impl/DroneAnalyzerDeviceDAOImpl.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/dao/impl/DroneAnalyzerDeviceDAOImpl.java new file mode 100644 index 000000000..35ce12860 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/dao/impl/DroneAnalyzerDeviceDAOImpl.java @@ -0,0 +1,194 @@ +/* + * 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.device.mgt.iot.droneanalyzer.plugin.impl.dao.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.constants.DroneConstants; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.exception.DroneAnalyzerDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.impl.dao.DroneAnalyzerDAO; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.impl.util.DroneAnalyzerUtils; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +/** + * Implements CRUD for drone analyzer. + */ +public class DroneAnalyzerDeviceDAOImpl { + + private static final Log log = LogFactory.getLog(DroneAnalyzerDeviceDAOImpl.class); + + public Device getDevice(String deviceId) throws DroneAnalyzerDeviceMgtPluginException { + Connection conn = null; + PreparedStatement stmt = null; + Device device = null; + ResultSet resultSet = null; + try { + conn = DroneAnalyzerDAO.getConnection(); + if(conn == null){ + log.error("Database connection hasn't been created"); + } + String selectDBQuery = + "SELECT DRONE_DEVICE_ID, DEVICE_NAME FROM DRONE_DEVICE WHERE DRONE_DEVICE_ID = ?"; + stmt = conn.prepareStatement(selectDBQuery); + stmt.setString(1, deviceId); + resultSet = stmt.executeQuery(); + + if (resultSet.next()) { + device = new Device(); + if (log.isDebugEnabled()) { + log.debug("Drone device " + deviceId + " data has been fetched from " + + "Drone database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while fetching drone device : '" + deviceId + "'"; + log.error(msg, e); + throw new DroneAnalyzerDeviceMgtPluginException(msg, e); + } finally { + DroneAnalyzerUtils.cleanupResources(stmt, resultSet); + DroneAnalyzerDAO.closeConnection(); + } + return device; + } + + public boolean addDevice(Device device) throws DroneAnalyzerDeviceMgtPluginException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = DroneAnalyzerDAO.getConnection(); + String createDBQuery = + "INSERT INTO DRONE_DEVICE(DRONE_DEVICE_ID, DEVICE_NAME) VALUES (?, ?)"; + stmt = conn.prepareStatement(createDBQuery); + stmt.setString(1, device.getDeviceIdentifier()); + stmt.setString(2, device.getName()); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("drone device " + device.getDeviceIdentifier() + " data has been" + + " added to the drone database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while adding the drone device '" + + device.getDeviceIdentifier() + "' to the drone db."; + log.error(msg, e); + throw new DroneAnalyzerDeviceMgtPluginException(msg, e); + } finally { + DroneAnalyzerUtils.cleanupResources(stmt, null); + } + return status; + } + + public boolean updateDevice(Device device) throws DroneAnalyzerDeviceMgtPluginException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = DroneAnalyzerDAO.getConnection(); + String updateDBQuery = + "UPDATE DRONE_DEVICE SET DEVICE_NAME = ? WHERE DRONE_DEVICE_ID = ?"; + stmt = conn.prepareStatement(updateDBQuery); + stmt.setString(1, device.getName()); + stmt.setString(2, device.getDeviceIdentifier()); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Drone device " + device.getDeviceIdentifier() + " data has been" + + " modified."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while modifying the Drone device '" + device.getDeviceIdentifier() + "' data."; + log.error(msg, e); + throw new DroneAnalyzerDeviceMgtPluginException(msg, e); + } finally { + DroneAnalyzerUtils.cleanupResources(stmt, null); + } + return status; + } + + public boolean deleteIotDevice(String iotDeviceId) throws DroneAnalyzerDeviceMgtPluginException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = DroneAnalyzerDAO.getConnection(); + System.out.println("delete device "); + String deleteDBQuery = + "DELETE FROM DRONE_DEVICE WHERE DRONE_DEVICE_ID = ?"; + stmt = conn.prepareStatement(deleteDBQuery); + stmt.setString(1, iotDeviceId); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Drone device " + iotDeviceId + " data has deleted" + + " from the drone database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while deleting drone device " + iotDeviceId; + log.error(msg, e); + throw new DroneAnalyzerDeviceMgtPluginException(msg, e); + } finally { + DroneAnalyzerUtils.cleanupResources(stmt, null); + } + return status; + } + + public List getAllDevices() throws DroneAnalyzerDeviceMgtPluginException { + Connection conn = null; + PreparedStatement stmt = null; + ResultSet resultSet = null; + Device iotDevice; + List iotDevices = new ArrayList(); + try { + conn = DroneAnalyzerDAO.getConnection(); + String selectDBQuery = + "SELECT DRONE_DEVICE_ID, DEVICE_NAME " + + "FROM DRONE_DEVICE"; + stmt = conn.prepareStatement(selectDBQuery); + resultSet = stmt.executeQuery(); + while (resultSet.next()) { + iotDevice = new Device(); + iotDevice.setDeviceIdentifier(resultSet.getString(DroneConstants.DEVICE_PLUGIN_DEVICE_ID)); + iotDevice.setName(resultSet.getString(DroneConstants.DEVICE_PLUGIN_DEVICE_NAME)); + } + if (log.isDebugEnabled()) { + log.debug("All drone device details have fetched from drone database."); + } + return iotDevices; + } catch (SQLException e) { + String msg = "Error occurred while fetching all drone device data'"; + log.error(msg, e); + throw new DroneAnalyzerDeviceMgtPluginException(msg, e); + } finally { + DroneAnalyzerUtils.cleanupResources(stmt, resultSet); + DroneAnalyzerDAO.closeConnection(); + } + } +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/feature/DroneAnalyzerFeatureManager.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/feature/DroneAnalyzerFeatureManager.java new file mode 100644 index 000000000..6a9127cfd --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/feature/DroneAnalyzerFeatureManager.java @@ -0,0 +1,58 @@ +/* + * 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.device.mgt.iot.droneanalyzer.plugin.impl.feature; + +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.Feature; +import org.wso2.carbon.device.mgt.common.FeatureManager; +import org.wso2.carbon.device.mgt.extensions.feature.mgt.GenericFeatureManager; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.constants.DroneConstants; + +import java.util.List; + +public class DroneAnalyzerFeatureManager implements FeatureManager { + @Override + public boolean addFeature(Feature feature) throws DeviceManagementException { + return false; + } + + @Override + public boolean addFeatures(List features) throws DeviceManagementException { + return false; + } + + @Override + public Feature getFeature(String name) throws DeviceManagementException { + GenericFeatureManager genericFeatureManager = GenericFeatureManager.getInstance(); + return genericFeatureManager.getFeature(DroneConstants.DEVICE_TYPE, name); + } + + @Override + public List getFeatures() throws DeviceManagementException { + GenericFeatureManager genericFeatureManager = GenericFeatureManager.getInstance(); + return genericFeatureManager.getFeatures(DroneConstants.DEVICE_TYPE); + } + + @Override + public boolean removeFeature(String name) throws DeviceManagementException { + return false; + } + + @Override + public boolean addSupportedFeaturesToDB() throws DeviceManagementException { + return false; + } +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/util/DeviceSchemaInitializer.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/util/DeviceSchemaInitializer.java new file mode 100644 index 000000000..e96b4e0b1 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/util/DeviceSchemaInitializer.java @@ -0,0 +1,50 @@ +/* + * 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.device.mgt.iot.droneanalyzer.plugin.impl.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.utils.CarbonUtils; +import org.wso2.carbon.utils.dbcreator.DatabaseCreator; + +import javax.sql.DataSource; +import java.io.File; + +/** + * Provides methods for initializing the database script. + */ +public class DeviceSchemaInitializer extends DatabaseCreator{ + + private static final Log log = LogFactory.getLog(DeviceSchemaInitializer.class); + private static final String setupSQLScriptBaseLocation = CarbonUtils.getCarbonHome() + File.separator + "dbscripts" + + File.separator + "cdm" + File.separator + "plugins" + File.separator; + + public DeviceSchemaInitializer(DataSource dataSource) { + super(dataSource); + } + + @Override + protected String getDbScriptLocation(String databaseType) { + String scriptName = databaseType + ".sql"; + if (log.isDebugEnabled()) { + log.debug("Loading database script from :" + scriptName); + } + return setupSQLScriptBaseLocation.replaceFirst("DBTYPE", databaseType) + scriptName; + } +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/util/DroneAnalyzerUtils.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/util/DroneAnalyzerUtils.java new file mode 100644 index 000000000..e65c0f0c4 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/impl/util/DroneAnalyzerUtils.java @@ -0,0 +1,108 @@ +/* + * 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.device.mgt.iot.droneanalyzer.plugin.impl.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.constants.DroneConstants; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.exception.DroneAnalyzerDeviceMgtPluginException; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +/** + * Contains utility methods used by Drone Analyzer plugin. + */ +public class DroneAnalyzerUtils { + private static Log log = LogFactory.getLog(DroneAnalyzerUtils.class); + + public static String getDeviceProperty(List deviceProperties, String propertyKey) { + String deviceProperty = ""; + for(Device.Property property :deviceProperties){ + if(propertyKey.equals(property.getName())){ + deviceProperty = property.getValue(); + } + } + return deviceProperty; + } + + public static Device.Property getProperty(String property, String value) { + if (property != null) { + Device.Property prop = new Device.Property(); + prop.setName(property); + prop.setValue(value); + return prop; + } + return null; + } + + public static void cleanupResources(Connection conn, PreparedStatement stmt, ResultSet rs) { + if (rs != null) { + try { + rs.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing result set", e); + } + } + if (stmt != null) { + try { + stmt.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing prepared statement", e); + } + } + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing database connection", e); + } + } + } + + public static void cleanupResources(PreparedStatement stmt, ResultSet rs) { + cleanupResources(null, stmt, rs); + } + + /** + * Creates the device management schema. + */ + public static void setupDeviceManagementSchema() throws DroneAnalyzerDeviceMgtPluginException { + try { + Context ctx = new InitialContext(); + DataSource dataSource = (DataSource) ctx.lookup(DroneConstants.DATA_SOURCE_NAME); + DeviceSchemaInitializer initializer = + new DeviceSchemaInitializer(dataSource); + log.info("Initializing device management repository database schema"); + initializer.createRegistryDatabase(); + + } catch (NamingException e) { + log.error("Error while looking up the data source: " + DroneConstants.DATA_SOURCE_NAME); + } catch (Exception e) { + throw new DroneAnalyzerDeviceMgtPluginException("Error occurred while initializing Iot Device " + + "Management database schema", e); + } + } +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/internal/DroneAnalyzerManagementServiceComponent.java b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/internal/DroneAnalyzerManagementServiceComponent.java new file mode 100644 index 000000000..01e648773 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin/src/main/java/org/wso2/carbon/device/mgt/iot/droneanalyzer/plugin/internal/DroneAnalyzerManagementServiceComponent.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.device.mgt.iot.droneanalyzer.plugin.internal; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.ComponentContext; +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.exception.DroneAnalyzerDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.impl.DroneAnalyzerManagerService; +import org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.impl.util.DroneAnalyzerUtils; + +/** + * @scr.component name="org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin.internal + * .DroneAnalyzerManagementServiceComponent" + * immediate="true" + */ +public class DroneAnalyzerManagementServiceComponent { + private ServiceRegistration firealarmServiceRegRef; + + private static final Log log = LogFactory.getLog( + DroneAnalyzerManagementServiceComponent.class); + + protected void activate(ComponentContext ctx) { + if (log.isDebugEnabled()) { + log.debug("Activating Drone Analyzer Management Service Component"); + } + try { + BundleContext bundleContext = ctx.getBundleContext(); + firealarmServiceRegRef = + bundleContext.registerService(DeviceManagementService.class.getName(), + new DroneAnalyzerManagerService(), null); + String setupOption = System.getProperty("setup"); + if (setupOption != null) { + if (log.isDebugEnabled()) { + log.debug( + "-Dsetup is enabled. Iot Device management repository schema initialization is about " + + "to begin"); + } + try { + DroneAnalyzerUtils.setupDeviceManagementSchema(); + } catch (DroneAnalyzerDeviceMgtPluginException e) { + log.error("Exception occurred while initializing device management database schema", e); + } + } + if (log.isDebugEnabled()) { + log.debug( + "Drone Analyzer Device Management Service Component has been successfully activated"); + } + } catch (Throwable e) { + log.error("Error occurred while activating Drone Analyzer Device Management Service Component", e); + } + } + + protected void deactivate(ComponentContext ctx) { + if (log.isDebugEnabled()) { + log.debug("De-activating Drone Analyzer Device Management Service Component"); + } + try { + if (firealarmServiceRegRef != null) { + firealarmServiceRegRef.unregister(); + } + if (log.isDebugEnabled()) { + log.debug("Drone Analyzer Device Management Service Component has been successfully de-activated"); + } + } catch (Throwable e) { + log.error( + "Error occurred while de-activating Drone Analyzer Device Management bundle",e); + } + } +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/pom.xml b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/pom.xml new file mode 100644 index 000000000..5b7426489 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/pom.xml @@ -0,0 +1,62 @@ + + + + + + + + drone-analyzer-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.droneanalyzer.ui + WSO2 Carbon - IoT Server Drone Analyzer UI + pom + + + + + maven-assembly-plugin + 2.5.5 + + ${project.artifactId}-${carbon.device.mgt.version} + false + + src/assembly/src.xml + + + + + create-archive + package + + single + + + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/assembly/src.xml b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/assembly/src.xml new file mode 100644 index 000000000..2797034e0 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/assembly/src.xml @@ -0,0 +1,36 @@ + + + + src + + zip + + false + ${basedir}/src + + + ${basedir}/src/main/resources/jaggeryapps/devicemgt + / + true + + + \ No newline at end of file diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.device-view/device-view.hbs b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.device-view/device-view.hbs new file mode 100644 index 000000000..1d32daed4 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.device-view/device-view.hbs @@ -0,0 +1,52 @@ +{{#zone "device-thumbnail"}} + +{{/zone}} +{{#zone "device-opetations"}} +
      + {{unit "iot.unit.device.operation-bar" device=device}} +
      +
      +
      Device Statistics
      + {{unit "iot.unit.device.droneanalyzer.statistics" device=device}} +
      + +{{/zone}} +{{#zone "device-detail-properties"}} +
      +
      + +
      +
      + +
      +
      +
      Operations Log
      +
      + +
      +
      + Not available yet +
      +
      +
      +
      +
      +
      +
      +
      +{{/zone}} \ No newline at end of file diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.device-view/device-view.js b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.device-view/device-view.js new file mode 100644 index 000000000..1fcbacc5b --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.device-view/device-view.js @@ -0,0 +1,34 @@ +/* + * 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. + */ +function onRequest(context) { + var log = new Log("device-view.js"); + var deviceType = context.uriParams.deviceType; + var deviceId = request.getParameter("id"); + + if (deviceType != null && deviceType != undefined && deviceId != null && deviceId != undefined) { + var deviceModule = require("/app/modules/device.js").deviceModule; + var device = deviceModule.viewDevice(deviceType, deviceId); + + if (device && device.status != "error") { + return {"device": device}; + } else { + response.sendError(404, "Device Id " + deviceId + "of type " + deviceType + " cannot be found!"); + exit(); + } + } +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.device-view/device-view.json b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.device-view/device-view.json new file mode 100644 index 000000000..9eecd8f5b --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.device-view/device-view.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0" +} \ No newline at end of file diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.device-view/public/images/drone-icon.png b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.device-view/public/images/drone-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0c7744ff42d16a9761778c98749aa50ea93e5cb4 GIT binary patch literal 46940 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

      J&TxbYUA`g6Fvzap%(O*Fj*K~ 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

      J&TxbYUA`g6Fvzap%(O*Fj*K~ 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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/public/images/drone-icon.png b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/public/images/drone-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0c7744ff42d16a9761778c98749aa50ea93e5cb4 GIT binary patch literal 46940 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

      J&TxbYUA`g6Fvzap%(O*Fj*K~ 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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/public/js/download.js b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/public/js/download.js new file mode 100644 index 000000000..0c22e5099 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/public/js/download.js @@ -0,0 +1,184 @@ +/* + * 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. + */ + +var modalPopup = ".wr-modalpopup"; +var modalPopupContainer = modalPopup + " .modalpopup-container"; +var modalPopupContent = modalPopup + " .modalpopup-content"; +var body = "body"; + +/* + * set popup maximum height function. + */ +function setPopupMaxHeight() { + $(modalPopupContent).css('max-height', ($(body).height() - ($(body).height() / 100 * 30))); + $(modalPopupContainer).css('margin-top', (-($(modalPopupContainer).height() / 2))); +} + +/* + * show popup function. + */ +function showPopup() { + $(modalPopup).show(); + setPopupMaxHeight(); + $('#downloadForm').validate({ + rules: { + deviceName: { + minlength: 4, + required: true + } + }, + highlight: function (element) { + $(element).closest('.control-group').removeClass('success').addClass('error'); + }, + success: function (element) { + $(element).closest('.control-group').removeClass('error').addClass('success'); + $('label[for=deviceName]').remove(); + } + }); + var deviceType = ""; + $('.deviceType').each(function () { + if (this.value != "") { + deviceType = this.value; + } + }); +} + +/* + * hide popup function. + */ +function hidePopup() { + $('label[for=deviceName]').remove(); + $('.control-group').removeClass('success').removeClass('error'); + $(modalPopupContent).html(''); + $(modalPopup).hide(); +} + +/* + * DOM ready functions. + */ +$(document).ready(function () { + attachEvents(); +}); + +function attachEvents() { + /** + * Following click function would execute + * when a user clicks on "Download" link + * on Device Management page in WSO2 DC Console. + */ + $("a.download-link").click(function () { + var sketchType = $(this).data("sketchtype"); + var deviceType = $(this).data("devicetype"); + var downloadDeviceAPI = "/devicemgt/api/devices/sketch/generate_link"; + var payload = {"sketchType": sketchType, "deviceType": deviceType}; + $(modalPopupContent).html($('#download-device-modal-content').html()); + showPopup(); + var deviceName; + $("a#download-device-download-link").click(function () { + $('.new-device-name').each(function () { + if (this.value != "") { + deviceName = this.value; + } + }); + $('label[for=deviceName]').remove(); + if (deviceName && deviceName.length >= 4) { + payload.deviceName = deviceName; + invokerUtil.post( + downloadDeviceAPI, + payload, + function (data, textStatus, jqxhr) { + doAction(data); + }, + function (data) { + doAction(data); + } + ); + }else if(deviceName){ + $('.controls').append(''); + $('.control-group').removeClass('success').addClass('error'); + } else { + $('.controls').append(''); + $('.control-group').removeClass('success').addClass('error'); + } + }); + + $("a#download-device-cancel-link").click(function () { + hidePopup(); + }); + + }); +} + +function downloadAgent() { + var deviceName; + $('.new-device-name').each(function () { + if (this.value != "") { + deviceName = this.value; + } + }); + var deviceNameFormat = /^[^~?!#$:;%^*`+={}\[\]\\()|<>,'"]{1,30}$/; + if (deviceName && deviceNameFormat.test(deviceName)) { + $('#downloadForm').submit(); + hidePopup(); + $(modalPopupContent).html($('#device-agent-downloading-content').html()); + showPopup(); + setTimeout(function () { + hidePopup(); + }, 1000); + }else { + $("#invalid-username-error-msg span").text("Invalid device name"); + $("#invalid-username-error-msg").removeClass("hidden"); + } +} + +function doAction(data) { + //if it is saml redirection response + if (data.status == null) { + document.write(data); + } + + if (data.status == "200") { + $(modalPopupContent).html($('#download-device-modal-content-links').html()); + $("input#download-device-url").val(data.responseText); + $("input#download-device-url").focus(function () { + $(this).select(); + }); + showPopup(); + } else if (data.status == "401") { + $(modalPopupContent).html($('#device-401-content').html()); + $("#device-401-link").click(function () { + window.location = "/devicemgt/login"; + }); + showPopup(); + } else if (data == "403") { + $(modalPopupContent).html($('#device-403-content').html()); + $("#device-403-link").click(function () { + window.location = "/devicemgt/login"; + }); + showPopup(); + } else { + $(modalPopupContent).html($('#device-unexpected-error-content').html()); + $("a#device-unexpected-error-link").click(function () { + hidePopup(); + }); + } +} \ No newline at end of file diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/public/js/jquery.validate.js b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/public/js/jquery.validate.js new file mode 100644 index 000000000..fe7ecf07a --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/public/js/jquery.validate.js @@ -0,0 +1,1227 @@ +/* + * 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. + */ + +(function($) { + +$.extend($.fn, { + // http://docs.jquery.com/Plugins/Validation/validate + validate: function( options ) { + + // if nothing is selected, return nothing; can't chain anyway + if ( !this.length ) { + if ( options && options.debug && window.console ) { + console.warn( "Nothing selected, can't validate, returning nothing." ); + } + return; + } + + // check if a validator for this form was already created + var validator = $.data( this[0], "validator" ); + if ( validator ) { + return validator; + } + + // Add novalidate tag if HTML5. + this.attr( "novalidate", "novalidate" ); + + validator = new $.validator( options, this[0] ); + $.data( this[0], "validator", validator ); + + if ( validator.settings.onsubmit ) { + + this.validateDelegate( ":submit", "click", function( event ) { + if ( validator.settings.submitHandler ) { + validator.submitButton = event.target; + } + // allow suppressing validation by adding a cancel class to the submit button + if ( $(event.target).hasClass("cancel") ) { + validator.cancelSubmit = true; + } + }); + + // validate the form on submit + this.submit( function( event ) { + if ( validator.settings.debug ) { + // prevent form submit to be able to see console output + event.preventDefault(); + } + function handle() { + var hidden; + if ( validator.settings.submitHandler ) { + if ( validator.submitButton ) { + // insert a hidden input as a replacement for the missing submit button + hidden = $("").attr("name", validator.submitButton.name).val(validator.submitButton.value).appendTo(validator.currentForm); + } + validator.settings.submitHandler.call( validator, validator.currentForm, event ); + if ( validator.submitButton ) { + // and clean up afterwards; thanks to no-block-scope, hidden can be referenced + hidden.remove(); + } + return false; + } + return true; + } + + // prevent submit for invalid forms or custom submit handlers + if ( validator.cancelSubmit ) { + validator.cancelSubmit = false; + return handle(); + } + if ( validator.form() ) { + if ( validator.pendingRequest ) { + validator.formSubmitted = true; + return false; + } + return handle(); + } else { + validator.focusInvalid(); + return false; + } + }); + } + + return validator; + }, + // http://docs.jquery.com/Plugins/Validation/valid + valid: function() { + if ( $(this[0]).is("form")) { + return this.validate().form(); + } else { + var valid = true; + var validator = $(this[0].form).validate(); + this.each(function() { + valid &= validator.element(this); + }); + return valid; + } + }, + // attributes: space seperated list of attributes to retrieve and remove + removeAttrs: function( attributes ) { + var result = {}, + $element = this; + $.each(attributes.split(/\s/), function( index, value ) { + result[value] = $element.attr(value); + $element.removeAttr(value); + }); + return result; + }, + // http://docs.jquery.com/Plugins/Validation/rules + rules: function( command, argument ) { + var element = this[0]; + + if ( command ) { + var settings = $.data(element.form, "validator").settings; + var staticRules = settings.rules; + var existingRules = $.validator.staticRules(element); + switch(command) { + case "add": + $.extend(existingRules, $.validator.normalizeRule(argument)); + staticRules[element.name] = existingRules; + if ( argument.messages ) { + settings.messages[element.name] = $.extend( settings.messages[element.name], argument.messages ); + } + break; + case "remove": + if ( !argument ) { + delete staticRules[element.name]; + return existingRules; + } + var filtered = {}; + $.each(argument.split(/\s/), function( index, method ) { + filtered[method] = existingRules[method]; + delete existingRules[method]; + }); + return filtered; + } + } + + var data = $.validator.normalizeRules( + $.extend( + {}, + $.validator.classRules(element), + $.validator.attributeRules(element), + $.validator.dataRules(element), + $.validator.staticRules(element) + ), element); + + // make sure required is at front + if ( data.required ) { + var param = data.required; + delete data.required; + data = $.extend({required: param}, data); + } + + return data; + } +}); + +// Custom selectors +$.extend($.expr[":"], { + // http://docs.jquery.com/Plugins/Validation/blank + blank: function( a ) { return !$.trim("" + a.value); }, + // http://docs.jquery.com/Plugins/Validation/filled + filled: function( a ) { return !!$.trim("" + a.value); }, + // http://docs.jquery.com/Plugins/Validation/unchecked + unchecked: function( a ) { return !a.checked; } +}); + +// constructor for validator +$.validator = function( options, form ) { + this.settings = $.extend( true, {}, $.validator.defaults, options ); + this.currentForm = form; + this.init(); +}; + +$.validator.format = function( source, params ) { + if ( arguments.length === 1 ) { + return function() { + var args = $.makeArray(arguments); + args.unshift(source); + return $.validator.format.apply( this, args ); + }; + } + if ( arguments.length > 2 && params.constructor !== Array ) { + params = $.makeArray(arguments).slice(1); + } + if ( params.constructor !== Array ) { + params = [ params ]; + } + $.each(params, function( i, n ) { + source = source.replace( new RegExp("\\{" + i + "\\}", "g"), function() { + return n; + }); + }); + return source; +}; + +$.extend($.validator, { + + defaults: { + messages: {}, + groups: {}, + rules: {}, + errorClass: "error", + validClass: "valid", + errorElement: "label", + focusInvalid: true, + errorContainer: $([]), + errorLabelContainer: $([]), + onsubmit: true, + ignore: ":hidden", + ignoreTitle: false, + onfocusin: function( element, event ) { + this.lastActive = element; + + // hide error label and remove error class on focus if enabled + if ( this.settings.focusCleanup && !this.blockFocusCleanup ) { + if ( this.settings.unhighlight ) { + this.settings.unhighlight.call( this, element, this.settings.errorClass, this.settings.validClass ); + } + this.addWrapper(this.errorsFor(element)).hide(); + } + }, + onfocusout: function( element, event ) { + if ( !this.checkable(element) && (element.name in this.submitted || !this.optional(element)) ) { + this.element(element); + } + }, + onkeyup: function( element, event ) { + if ( event.which === 9 && this.elementValue(element) === "" ) { + return; + } else if ( element.name in this.submitted || element === this.lastElement ) { + this.element(element); + } + }, + onclick: function( element, event ) { + // click on selects, radiobuttons and checkboxes + if ( element.name in this.submitted ) { + this.element(element); + } + // or option elements, check parent select in that case + else if ( element.parentNode.name in this.submitted ) { + this.element(element.parentNode); + } + }, + highlight: function( element, errorClass, validClass ) { + if ( element.type === "radio" ) { + this.findByName(element.name).addClass(errorClass).removeClass(validClass); + } else { + $(element).addClass(errorClass).removeClass(validClass); + } + }, + unhighlight: function( element, errorClass, validClass ) { + if ( element.type === "radio" ) { + this.findByName(element.name).removeClass(errorClass).addClass(validClass); + } else { + $(element).removeClass(errorClass).addClass(validClass); + } + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/setDefaults + setDefaults: function( settings ) { + $.extend( $.validator.defaults, settings ); + }, + + messages: { + required: "This field is required.", + remote: "Please fix this field.", + email: "Please enter a valid email address.", + url: "Please enter a valid URL.", + date: "Please enter a valid date.", + dateISO: "Please enter a valid date (ISO).", + number: "Please enter a valid number.", + digits: "Please enter only digits.", + creditcard: "Please enter a valid credit card number.", + equalTo: "Please enter the same value again.", + maxlength: $.validator.format("Please enter no more than {0} characters."), + minlength: $.validator.format("Please enter at least {0} characters."), + rangelength: $.validator.format("Please enter a value between {0} and {1} characters long."), + range: $.validator.format("Please enter a value between {0} and {1}."), + max: $.validator.format("Please enter a value less than or equal to {0}."), + min: $.validator.format("Please enter a value greater than or equal to {0}.") + }, + + autoCreateRanges: false, + + prototype: { + + init: function() { + this.labelContainer = $(this.settings.errorLabelContainer); + this.errorContext = this.labelContainer.length && this.labelContainer || $(this.currentForm); + this.containers = $(this.settings.errorContainer).add( this.settings.errorLabelContainer ); + this.submitted = {}; + this.valueCache = {}; + this.pendingRequest = 0; + this.pending = {}; + this.invalid = {}; + this.reset(); + + var groups = (this.groups = {}); + $.each(this.settings.groups, function( key, value ) { + if ( typeof value === "string" ) { + value = value.split(/\s/); + } + $.each(value, function( index, name ) { + groups[name] = key; + }); + }); + var rules = this.settings.rules; + $.each(rules, function( key, value ) { + rules[key] = $.validator.normalizeRule(value); + }); + + function delegate(event) { + var validator = $.data(this[0].form, "validator"), + eventType = "on" + event.type.replace(/^validate/, ""); + if ( validator.settings[eventType] ) { + validator.settings[eventType].call(validator, this[0], event); + } + } + $(this.currentForm) + .validateDelegate(":text, [type='password'], [type='file'], select, textarea, " + + "[type='number'], [type='search'] ,[type='tel'], [type='url'], " + + "[type='email'], [type='datetime'], [type='date'], [type='month'], " + + "[type='week'], [type='time'], [type='datetime-local'], " + + "[type='range'], [type='color'] ", + "focusin focusout keyup", delegate) + .validateDelegate("[type='radio'], [type='checkbox'], select, option", "click", delegate); + + if ( this.settings.invalidHandler ) { + $(this.currentForm).bind("invalid-form.validate", this.settings.invalidHandler); + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/form + form: function() { + this.checkForm(); + $.extend(this.submitted, this.errorMap); + this.invalid = $.extend({}, this.errorMap); + if ( !this.valid() ) { + $(this.currentForm).triggerHandler("invalid-form", [this]); + } + this.showErrors(); + return this.valid(); + }, + + checkForm: function() { + this.prepareForm(); + for ( var i = 0, elements = (this.currentElements = this.elements()); elements[i]; i++ ) { + this.check( elements[i] ); + } + return this.valid(); + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/element + element: function( element ) { + element = this.validationTargetFor( this.clean( element ) ); + this.lastElement = element; + this.prepareElement( element ); + this.currentElements = $(element); + var result = this.check( element ) !== false; + if ( result ) { + delete this.invalid[element.name]; + } else { + this.invalid[element.name] = true; + } + if ( !this.numberOfInvalids() ) { + // Hide error containers on last error + this.toHide = this.toHide.add( this.containers ); + } + this.showErrors(); + return result; + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/showErrors + showErrors: function( errors ) { + if ( errors ) { + // add items to error list and map + $.extend( this.errorMap, errors ); + this.errorList = []; + for ( var name in errors ) { + this.errorList.push({ + message: errors[name], + element: this.findByName(name)[0] + }); + } + // remove items from success list + this.successList = $.grep( this.successList, function( element ) { + return !(element.name in errors); + }); + } + if ( this.settings.showErrors ) { + this.settings.showErrors.call( this, this.errorMap, this.errorList ); + } else { + this.defaultShowErrors(); + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/resetForm + resetForm: function() { + if ( $.fn.resetForm ) { + $(this.currentForm).resetForm(); + } + this.submitted = {}; + this.lastElement = null; + this.prepareForm(); + this.hideErrors(); + this.elements().removeClass( this.settings.errorClass ).removeData( "previousValue" ); + }, + + numberOfInvalids: function() { + return this.objectLength(this.invalid); + }, + + objectLength: function( obj ) { + var count = 0; + for ( var i in obj ) { + count++; + } + return count; + }, + + hideErrors: function() { + this.addWrapper( this.toHide ).hide(); + }, + + valid: function() { + return this.size() === 0; + }, + + size: function() { + return this.errorList.length; + }, + + focusInvalid: function() { + if ( this.settings.focusInvalid ) { + try { + $(this.findLastActive() || this.errorList.length && this.errorList[0].element || []) + .filter(":visible") + .focus() + // manually trigger focusin event; without it, focusin handler isn't called, findLastActive won't have anything to find + .trigger("focusin"); + } catch(e) { + // ignore IE throwing errors when focusing hidden elements + } + } + }, + + findLastActive: function() { + var lastActive = this.lastActive; + return lastActive && $.grep(this.errorList, function( n ) { + return n.element.name === lastActive.name; + }).length === 1 && lastActive; + }, + + elements: function() { + var validator = this, + rulesCache = {}; + + // select all valid inputs inside the form (no submit or reset buttons) + return $(this.currentForm) + .find("input, select, textarea") + .not(":submit, :reset, :image, [disabled]") + .not( this.settings.ignore ) + .filter(function() { + if ( !this.name ) { + if ( window.console ) { + console.error( "%o has no name assigned", this ); + } + throw new Error( "Failed to validate, found an element with no name assigned. See console for element reference." ); + } + + // select only the first element for each name, and only those with rules specified + if ( this.name in rulesCache || !validator.objectLength($(this).rules()) ) { + return false; + } + + rulesCache[this.name] = true; + return true; + }); + }, + + clean: function( selector ) { + return $(selector)[0]; + }, + + errors: function() { + var errorClass = this.settings.errorClass.replace(" ", "."); + return $(this.settings.errorElement + "." + errorClass, this.errorContext); + }, + + reset: function() { + this.successList = []; + this.errorList = []; + this.errorMap = {}; + this.toShow = $([]); + this.toHide = $([]); + this.currentElements = $([]); + }, + + prepareForm: function() { + this.reset(); + this.toHide = this.errors().add( this.containers ); + }, + + prepareElement: function( element ) { + this.reset(); + this.toHide = this.errorsFor(element); + }, + + elementValue: function( element ) { + var type = $(element).attr("type"), + val = $(element).val(); + + if ( type === "radio" || type === "checkbox" ) { + return $("input[name='" + $(element).attr("name") + "']:checked").val(); + } + + if ( typeof val === "string" ) { + return val.replace(/\r/g, ""); + } + return val; + }, + + check: function( element ) { + element = this.validationTargetFor( this.clean( element ) ); + + var rules = $(element).rules(); + var dependencyMismatch = false; + var val = this.elementValue(element); + var result; + + for (var method in rules ) { + var rule = { method: method, parameters: rules[method] }; + try { + + result = $.validator.methods[method].call( this, val, element, rule.parameters ); + + // if a method indicates that the field is optional and therefore valid, + // don't mark it as valid when there are no other rules + if ( result === "dependency-mismatch" ) { + dependencyMismatch = true; + continue; + } + dependencyMismatch = false; + + if ( result === "pending" ) { + this.toHide = this.toHide.not( this.errorsFor(element) ); + return; + } + + if ( !result ) { + this.formatAndAdd( element, rule ); + return false; + } + } catch(e) { + if ( this.settings.debug && window.console ) { + console.log( "Exception occured when checking element " + element.id + ", check the '" + rule.method + "' method.", e ); + } + throw e; + } + } + if ( dependencyMismatch ) { + return; + } + if ( this.objectLength(rules) ) { + this.successList.push(element); + } + return true; + }, + + // return the custom message for the given element and validation method + // specified in the element's HTML5 data attribute + customDataMessage: function( element, method ) { + return $(element).data("msg-" + method.toLowerCase()) || (element.attributes && $(element).attr("data-msg-" + method.toLowerCase())); + }, + + // return the custom message for the given element name and validation method + customMessage: function( name, method ) { + var m = this.settings.messages[name]; + return m && (m.constructor === String ? m : m[method]); + }, + + // return the first defined argument, allowing empty strings + findDefined: function() { + for(var i = 0; i < arguments.length; i++) { + if ( arguments[i] !== undefined ) { + return arguments[i]; + } + } + return undefined; + }, + + defaultMessage: function( element, method ) { + return this.findDefined( + this.customMessage( element.name, method ), + this.customDataMessage( element, method ), + // title is never undefined, so handle empty string as undefined + !this.settings.ignoreTitle && element.title || undefined, + $.validator.messages[method], + "Warning: No message defined for " + element.name + "" + ); + }, + + formatAndAdd: function( element, rule ) { + var message = this.defaultMessage( element, rule.method ), + theregex = /\$?\{(\d+)\}/g; + if ( typeof message === "function" ) { + message = message.call(this, rule.parameters, element); + } else if (theregex.test(message)) { + message = $.validator.format(message.replace(theregex, "{$1}"), rule.parameters); + } + this.errorList.push({ + message: message, + element: element + }); + + this.errorMap[element.name] = message; + this.submitted[element.name] = message; + }, + + addWrapper: function( toToggle ) { + if ( this.settings.wrapper ) { + toToggle = toToggle.add( toToggle.parent( this.settings.wrapper ) ); + } + return toToggle; + }, + + defaultShowErrors: function() { + var i, elements; + for ( i = 0; this.errorList[i]; i++ ) { + var error = this.errorList[i]; + if ( this.settings.highlight ) { + this.settings.highlight.call( this, error.element, this.settings.errorClass, this.settings.validClass ); + } + this.showLabel( error.element, error.message ); + } + if ( this.errorList.length ) { + this.toShow = this.toShow.add( this.containers ); + } + if ( this.settings.success ) { + for ( i = 0; this.successList[i]; i++ ) { + this.showLabel( this.successList[i] ); + } + } + if ( this.settings.unhighlight ) { + for ( i = 0, elements = this.validElements(); elements[i]; i++ ) { + this.settings.unhighlight.call( this, elements[i], this.settings.errorClass, this.settings.validClass ); + } + } + this.toHide = this.toHide.not( this.toShow ); + this.hideErrors(); + this.addWrapper( this.toShow ).show(); + }, + + validElements: function() { + return this.currentElements.not(this.invalidElements()); + }, + + invalidElements: function() { + return $(this.errorList).map(function() { + return this.element; + }); + }, + + showLabel: function( element, message ) { + var label = this.errorsFor( element ); + if ( label.length ) { + // refresh error/success class + label.removeClass( this.settings.validClass ).addClass( this.settings.errorClass ); + + // check if we have a generated label, replace the message then + if ( label.attr("generated") ) { + label.html(message); + } + } else { + // create label + label = $("<" + this.settings.errorElement + "/>") + .attr({"for": this.idOrName(element), generated: true}) + .addClass(this.settings.errorClass) + .html(message || ""); + if ( this.settings.wrapper ) { + // make sure the element is visible, even in IE + // actually showing the wrapped element is handled elsewhere + label = label.hide().show().wrap("<" + this.settings.wrapper + "/>").parent(); + } + if ( !this.labelContainer.append(label).length ) { + if ( this.settings.errorPlacement ) { + this.settings.errorPlacement(label, $(element) ); + } else { + label.insertAfter(element); + } + } + } + if ( !message && this.settings.success ) { + label.text(""); + if ( typeof this.settings.success === "string" ) { + label.addClass( this.settings.success ); + } else { + this.settings.success( label, element ); + } + } + this.toShow = this.toShow.add(label); + }, + + errorsFor: function( element ) { + var name = this.idOrName(element); + return this.errors().filter(function() { + return $(this).attr("for") === name; + }); + }, + + idOrName: function( element ) { + return this.groups[element.name] || (this.checkable(element) ? element.name : element.id || element.name); + }, + + validationTargetFor: function( element ) { + // if radio/checkbox, validate first element in group instead + if ( this.checkable(element) ) { + element = this.findByName( element.name ).not(this.settings.ignore)[0]; + } + return element; + }, + + checkable: function( element ) { + return (/radio|checkbox/i).test(element.type); + }, + + findByName: function( name ) { + return $(this.currentForm).find("[name='" + name + "']"); + }, + + getLength: function( value, element ) { + switch( element.nodeName.toLowerCase() ) { + case "select": + return $("option:selected", element).length; + case "input": + if ( this.checkable( element) ) { + return this.findByName(element.name).filter(":checked").length; + } + } + return value.length; + }, + + depend: function( param, element ) { + return this.dependTypes[typeof param] ? this.dependTypes[typeof param](param, element) : true; + }, + + dependTypes: { + "boolean": function( param, element ) { + return param; + }, + "string": function( param, element ) { + return !!$(param, element.form).length; + }, + "function": function( param, element ) { + return param(element); + } + }, + + optional: function( element ) { + var val = this.elementValue(element); + return !$.validator.methods.required.call(this, val, element) && "dependency-mismatch"; + }, + + startRequest: function( element ) { + if ( !this.pending[element.name] ) { + this.pendingRequest++; + this.pending[element.name] = true; + } + }, + + stopRequest: function( element, valid ) { + this.pendingRequest--; + // sometimes synchronization fails, make sure pendingRequest is never < 0 + if ( this.pendingRequest < 0 ) { + this.pendingRequest = 0; + } + delete this.pending[element.name]; + if ( valid && this.pendingRequest === 0 && this.formSubmitted && this.form() ) { + $(this.currentForm).submit(); + this.formSubmitted = false; + } else if (!valid && this.pendingRequest === 0 && this.formSubmitted) { + $(this.currentForm).triggerHandler("invalid-form", [this]); + this.formSubmitted = false; + } + }, + + previousValue: function( element ) { + return $.data(element, "previousValue") || $.data(element, "previousValue", { + old: null, + valid: true, + message: this.defaultMessage( element, "remote" ) + }); + } + + }, + + classRuleSettings: { + required: {required: true}, + email: {email: true}, + url: {url: true}, + date: {date: true}, + dateISO: {dateISO: true}, + number: {number: true}, + digits: {digits: true}, + creditcard: {creditcard: true} + }, + + addClassRules: function( className, rules ) { + if ( className.constructor === String ) { + this.classRuleSettings[className] = rules; + } else { + $.extend(this.classRuleSettings, className); + } + }, + + classRules: function( element ) { + var rules = {}; + var classes = $(element).attr("class"); + if ( classes ) { + $.each(classes.split(" "), function() { + if ( this in $.validator.classRuleSettings ) { + $.extend(rules, $.validator.classRuleSettings[this]); + } + }); + } + return rules; + }, + + attributeRules: function( element ) { + var rules = {}; + var $element = $(element); + + for (var method in $.validator.methods) { + var value; + + // support for in both html5 and older browsers + if ( method === "required" ) { + value = $element.get(0).getAttribute(method); + // Some browsers return an empty string for the required attribute + // and non-HTML5 browsers might have required="" markup + if ( value === "" ) { + value = true; + } + // force non-HTML5 browsers to return bool + value = !!value; + } else { + value = $element.attr(method); + } + + if ( value ) { + rules[method] = value; + } else if ( $element[0].getAttribute("type") === method ) { + rules[method] = true; + } + } + + // maxlength may be returned as -1, 2147483647 (IE) and 524288 (safari) for text inputs + if ( rules.maxlength && /-1|2147483647|524288/.test(rules.maxlength) ) { + delete rules.maxlength; + } + + return rules; + }, + + dataRules: function( element ) { + var method, value, + rules = {}, $element = $(element); + for (method in $.validator.methods) { + value = $element.data("rule-" + method.toLowerCase()); + if ( value !== undefined ) { + rules[method] = value; + } + } + return rules; + }, + + staticRules: function( element ) { + var rules = {}; + var validator = $.data(element.form, "validator"); + if ( validator.settings.rules ) { + rules = $.validator.normalizeRule(validator.settings.rules[element.name]) || {}; + } + return rules; + }, + + normalizeRules: function( rules, element ) { + // handle dependency check + $.each(rules, function( prop, val ) { + // ignore rule when param is explicitly false, eg. required:false + if ( val === false ) { + delete rules[prop]; + return; + } + if ( val.param || val.depends ) { + var keepRule = true; + switch (typeof val.depends) { + case "string": + keepRule = !!$(val.depends, element.form).length; + break; + case "function": + keepRule = val.depends.call(element, element); + break; + } + if ( keepRule ) { + rules[prop] = val.param !== undefined ? val.param : true; + } else { + delete rules[prop]; + } + } + }); + + // evaluate parameters + $.each(rules, function( rule, parameter ) { + rules[rule] = $.isFunction(parameter) ? parameter(element) : parameter; + }); + + // clean number parameters + $.each(["minlength", "maxlength", "min", "max"], function() { + if ( rules[this] ) { + rules[this] = Number(rules[this]); + } + }); + $.each(["rangelength", "range"], function() { + var parts; + if ( rules[this] ) { + if ( $.isArray(rules[this]) ) { + rules[this] = [Number(rules[this][0]), Number(rules[this][1])]; + } else if ( typeof rules[this] === "string" ) { + parts = rules[this].split(/[\s,]+/); + rules[this] = [Number(parts[0]), Number(parts[1])]; + } + } + }); + + if ( $.validator.autoCreateRanges ) { + // auto-create ranges + if ( rules.min && rules.max ) { + rules.range = [rules.min, rules.max]; + delete rules.min; + delete rules.max; + } + if ( rules.minlength && rules.maxlength ) { + rules.rangelength = [rules.minlength, rules.maxlength]; + delete rules.minlength; + delete rules.maxlength; + } + } + + return rules; + }, + + // Converts a simple string to a {string: true} rule, e.g., "required" to {required:true} + normalizeRule: function( data ) { + if ( typeof data === "string" ) { + var transformed = {}; + $.each(data.split(/\s/), function() { + transformed[this] = true; + }); + data = transformed; + } + return data; + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/addMethod + addMethod: function( name, method, message ) { + $.validator.methods[name] = method; + $.validator.messages[name] = message !== undefined ? message : $.validator.messages[name]; + if ( method.length < 3 ) { + $.validator.addClassRules(name, $.validator.normalizeRule(name)); + } + }, + + methods: { + + // http://docs.jquery.com/Plugins/Validation/Methods/required + required: function( value, element, param ) { + // check if dependency is met + if ( !this.depend(param, element) ) { + return "dependency-mismatch"; + } + if ( element.nodeName.toLowerCase() === "select" ) { + // could be an array for select-multiple or a string, both are fine this way + var val = $(element).val(); + return val && val.length > 0; + } + if ( this.checkable(element) ) { + return this.getLength(value, element) > 0; + } + return $.trim(value).length > 0; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/remote + remote: function( value, element, param ) { + if ( this.optional(element) ) { + return "dependency-mismatch"; + } + + var previous = this.previousValue(element); + if (!this.settings.messages[element.name] ) { + this.settings.messages[element.name] = {}; + } + previous.originalMessage = this.settings.messages[element.name].remote; + this.settings.messages[element.name].remote = previous.message; + + param = typeof param === "string" && {url:param} || param; + + if ( previous.old === value ) { + return previous.valid; + } + + previous.old = value; + var validator = this; + this.startRequest(element); + var data = {}; + data[element.name] = value; + $.ajax($.extend(true, { + url: param, + mode: "abort", + port: "validate" + element.name, + dataType: "json", + data: data, + success: function( response ) { + validator.settings.messages[element.name].remote = previous.originalMessage; + var valid = response === true || response === "true"; + if ( valid ) { + var submitted = validator.formSubmitted; + validator.prepareElement(element); + validator.formSubmitted = submitted; + validator.successList.push(element); + delete validator.invalid[element.name]; + validator.showErrors(); + } else { + var errors = {}; + var message = response || validator.defaultMessage( element, "remote" ); + errors[element.name] = previous.message = $.isFunction(message) ? message(value) : message; + validator.invalid[element.name] = true; + validator.showErrors(errors); + } + previous.valid = valid; + validator.stopRequest(element, valid); + } + }, param)); + return "pending"; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/minlength + minlength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || length >= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/maxlength + maxlength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || length <= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/rangelength + rangelength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || ( length >= param[0] && length <= param[1] ); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/min + min: function( value, element, param ) { + return this.optional(element) || value >= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/max + max: function( value, element, param ) { + return this.optional(element) || value <= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/range + range: function( value, element, param ) { + return this.optional(element) || ( value >= param[0] && value <= param[1] ); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/email + email: function( value, element ) { + // contributed by Scott Gonzalez: http://projects.scottsplayground.com/email_address_validation/ + return this.optional(element) || /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/url + url: function( value, element ) { + // contributed by Scott Gonzalez: http://projects.scottsplayground.com/iri/ + return this.optional(element) || /^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/date + date: function( value, element ) { + return this.optional(element) || !/Invalid|NaN/.test(new Date(value).toString()); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/dateISO + dateISO: function( value, element ) { + return this.optional(element) || /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/number + number: function( value, element ) { + return this.optional(element) || /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/digits + digits: function( value, element ) { + return this.optional(element) || /^\d+$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/creditcard + // based on http://en.wikipedia.org/wiki/Luhn + creditcard: function( value, element ) { + if ( this.optional(element) ) { + return "dependency-mismatch"; + } + // accept only spaces, digits and dashes + if ( /[^0-9 \-]+/.test(value) ) { + return false; + } + var nCheck = 0, + nDigit = 0, + bEven = false; + + value = value.replace(/\D/g, ""); + + for (var n = value.length - 1; n >= 0; n--) { + var cDigit = value.charAt(n); + nDigit = parseInt(cDigit, 10); + if ( bEven ) { + if ( (nDigit *= 2) > 9 ) { + nDigit -= 9; + } + } + nCheck += nDigit; + bEven = !bEven; + } + + return (nCheck % 10) === 0; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/equalTo + equalTo: function( value, element, param ) { + // bind to the blur event of the target in order to revalidate whenever the target field is updated + // TODO find a way to bind the event just once, avoiding the unbind-rebind overhead + var target = $(param); + if ( this.settings.onfocusout ) { + target.unbind(".validate-equalTo").bind("blur.validate-equalTo", function() { + $(element).valid(); + }); + } + return value === target.val(); + } + + } + +}); + +// deprecated, use $.validator.format instead +$.format = $.validator.format; + +}(jQuery)); + +// ajax mode: abort +// usage: $.ajax({ mode: "abort"[, port: "uniqueport"]}); +// if mode:"abort" is used, the previous request on that port (port can be undefined) is aborted via XMLHttpRequest.abort() +(function($) { + var pendingRequests = {}; + // Use a prefilter if available (1.5+) + if ( $.ajaxPrefilter ) { + $.ajaxPrefilter(function( settings, _, xhr ) { + var port = settings.port; + if ( settings.mode === "abort" ) { + if ( pendingRequests[port] ) { + pendingRequests[port].abort(); + } + pendingRequests[port] = xhr; + } + }); + } else { + // Proxy ajax + var ajax = $.ajax; + $.ajax = function( settings ) { + var mode = ( "mode" in settings ? settings : $.ajaxSettings ).mode, + port = ( "port" in settings ? settings : $.ajaxSettings ).port; + if ( mode === "abort" ) { + if ( pendingRequests[port] ) { + pendingRequests[port].abort(); + } + return (pendingRequests[port] = ajax.apply(this, arguments)); + } + return ajax.apply(this, arguments); + }; + } +}(jQuery)); + +// provides delegate(type: String, delegate: Selector, handler: Callback) plugin for easier event delegation +// handler is only called when $(event.target).is(delegate), in the scope of the jquery-object for event.target +(function($) { + $.extend($.fn, { + validateDelegate: function( delegate, type, handler ) { + return this.bind(type, function( event ) { + var target = $(event.target); + if ( target.is(delegate) ) { + return handler.apply(target, arguments); + } + }); + } + }); +}(jQuery)); diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/type-view.hbs b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/type-view.hbs new file mode 100644 index 000000000..d02b3354f --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/type-view.hbs @@ -0,0 +1,315 @@ +

      +
      + +
      +
      + +
      +
      +

      What it Does

      +
      +

      Connect an + [IRIS+] Drone to + WSO2 IoT Server and visualize statistics. +

      +
      + +

      What You Need

      +
      +
        +
      • + ITEM 01 +    IRIS+ Drone. +
      • +
      • + ITEM 02 +    USB to Micro USB cable or Telemetry Radio receiver. +
      • +
      • + STEP 03 +    Proceed to [Prepare] section. +
      +
      + View API
        + + Download Agent + +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +

      +
      +
      +

      Prepare

      +
      +
        +
      • + 01 +    Connect your IRIS+ Drone to your computer using either USB to Micro + USB cable or Telemetry Radio receiver. +
      • +
      • + 02 +    Click on [Download Agent] button above to get IRIS+ Drone agent. +
      • +
      • + 03 +    Once you have downloaded the agent please run + "[startService.sh]" script with root privileges. +
      • +
      • + 04 +    Then you will be prompted to enter time interval (in seconds) between + successive Data-Pushes to the XMPP server, connection target and communication speed. So + below table will help you to find what is the correct connection target and + communication speed. +
      • +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        +
        CONNECTION-TYPE
        +
        +
        CONNECTION-STRING
        +
        Linux computer connected to the vehicle via USB e.g. /dev/ttyUSB0
        Linux computer connected to the drone via Serial port e.g. /dev/ttyAMA0 (also set baud=57600)
        OSX computer connected to the drone via USBe.g. dev/cu.usbmodem1
        Windows computer connected to the drone via USB (in this case on COM14)e.g. com14
        Windows computer connected to the drone using a 3DR Telemetry Radio on + COM14 + e.g. com14 (also set baud=57600)
        +
        +
      +
      +
      +
      +

      IRIS+ Drone Connected to a computer

      +
      +

      Click on the image to zoom

      +
      + + + +
      +
      +
      +
      +

      Try Out

      +
      +
        +
      • + 01 +    You can view all your connected devices at + [Device Management] page. +
      • +
      • + 02 +    Select one of connected devices and view stats which are published by + the device. +
      • +
      +
      +

      Click on the image to zoom

      +
      + + + +
      +
      + +{{#zone "topCss"}} + +{{/zone}} + +{{#zone "bottomJs"}} + {{js "/js/download.js"}} + {{js "/js/jquery.validate.js"}} +{{/zone}} \ No newline at end of file diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/type-view.json b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/type-view.json new file mode 100644 index 000000000..9eecd8f5b --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.drone_analyzer.type-view/type-view.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0" +} \ No newline at end of file diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/css/main-app.css b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/css/main-app.css new file mode 100644 index 000000000..9f7521295 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/css/main-app.css @@ -0,0 +1,220 @@ +/* + * 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. + */ +.box { + margin-top: 10px; + margin-bottom: 10px; +} + +.box-inner { + border: 1px solid #DEDEDE; + border-radius: 3px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + box-shadow: 0 0 10px rgba(189, 189, 189, 0.4); + -webkit-box-shadow: 0 0 10px rgba(189, 189, 189, 0.4); + -moz-box-shadow: 0 0 10px rgba(189, 189, 189, 0.4); +} + +.box-header { + border: none; + padding-top: 5px; + border-bottom: 1px solid #DEDEDE; + border-radius: 3px 3px 0 0; + -webkit-border-radius: 3px 3px 0 0; + -moz-border-radius: 3px 3px 0 0; + height: 35px; + min-height: 35px !important; + margin-bottom: 0; + font-weight: bold; + font-size: 16px; + background: -moz-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(0, 0, 0, 0.1) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(100%, rgba(0, 0, 0, 0.1))); + background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(0, 0, 0, 0.1) 100%); + background: -o-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(0, 0, 0, 0.1) 100%); + background: -ms-linear-gradient(top, rgba(255, 255, 255, 0) 0%, rgba(0, 0, 0, 0.1) 100%); + background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, rgba(0, 0, 0, 0.1) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffffff', endColorstr='#1a000000', GradientType=0); + +} + +.box-header h2 { + font-size: 15px; + width: auto; + clear: none; + float: left; + line-height: 25px; + white-space: nowrap; + font-weight: bold; + margin-top: 0; + margin-bottom: 0; +} + +.box-header h3 { + font-size: 13px; + width: auto; + clear: none; + float: left; + line-height: 25px; + white-space: nowrap; +} + +.box-header h2 > i { + margin-top: 1px; +} + +.box-icon { + float: right; +} + +.box-icon a { + clear: none; + float: left; + margin: 0 2px; + height: 20px; + width: 5px; + margin-top: 1px; +} + +.box-icon a i { + margin-left: -6px; + top: -1px; +} + +.box-content { + padding: 10px; +} + +.btn-round { + border-radius: 40px; + -webkit-border-radius: 40px; + -moz-border-radius: 40px; + font-size: 12px; + padding-top: 4px; +} + +.navbar-brand { + font-family: 'Shojumaru', cursive, Arial, serif; + letter-spacing: 2px; + text-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5); + width: 183px; + font-size: 17px; +} + +.navbar-brand img { + float: left; + height: 20px; + width: 20px; + margin-right: 5px; +} + +.navbar-brand span { + float: left; +} + +.navbar-search { + margin-left: 10px; + margin-top: 7px; +} + +.navbar-inner { + padding-top: 5px; + padding-bottom: 5px; + line-height: 30px; + height: 60px; +} + +.navbar-inner .btn-group { + margin: 7px 5px 0 5px; +} + +.bs-icons li { + list-style: none; +} + +.box-content .nav-tabs { + margin-right: -10px; + margin-left: -10px; +} + +.box-content.buttons { + min-height: 297px; +} + +.sidebar-nav .nav-header { + display: block; + padding: 3px 15px; + font-size: 11px; + font-weight: bold; + line-height: 18px; + color: #999999; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-transform: uppercase; +} + +.navbar { + border-radius: 0; +} +.circle { + background: none repeat scroll 0 0 #191919; + border-radius: 50px; + height: 50px; + padding: 10px; + width: 50px; + color: #fff; +} + +.padding-top-double { + padding-top: 20px; +} +.padding-double { + padding: 20px; +} +.grey { + color: #333; +} +hr { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #7f7f7f; + margin: 1em 0; + padding: 0; + opacity: 0.2; +} +.light-grey { + color: #7c7c7c; +} +.uppercase { + text-transform: uppercase; +} +.grey-bg { + background-color: #f6f4f4; +} + +path { + stroke: black; + stroke-width: 2; + fill: none; +} + +.axis path, .axis line { + fill: none; + stroke: #ccc; + stroke-width: 2; + shape-rendering: crispEdges; +} diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/images/drone_position_controller/background_drone.png b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/images/drone_position_controller/background_drone.png new file mode 100644 index 0000000000000000000000000000000000000000..c3ce7dc40d87da479b11193a13a91c2f68363547 GIT binary patch literal 26787 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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/images/drone_position_controller/pitch_drone.png b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/images/drone_position_controller/pitch_drone.png new file mode 100644 index 0000000000000000000000000000000000000000..84afb8f1888ebec0dbd5cb4b8c3bde86c3bc05de GIT binary patch literal 20046 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 0 ) { + + this.dispatchEvent( changeEvent ); + + lastPosition.copy( this.object.position ); + + } + + }; + + + function getAutoRotationAngle() { + + return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; + + } + + function getZoomScale() { + + return Math.pow( 0.95, scope.zoomSpeed ); + + } + + function onMouseDown( event ) { + + if ( scope.enabled === false ) { return; } + event.preventDefault(); + + if ( event.button === 0 ) { + if ( scope.noRotate === true ) { return; } + + state = STATE.ROTATE; + + rotateStart.set( event.clientX, event.clientY ); + + } else if ( event.button === 1 ) { + if ( scope.noZoom === true ) { return; } + + state = STATE.DOLLY; + + dollyStart.set( event.clientX, event.clientY ); + + } else if ( event.button === 2 ) { + if ( scope.noPan === true ) { return; } + + state = STATE.PAN; + + panStart.set( event.clientX, event.clientY ); + + } + + document.addEventListener( 'mousemove', onMouseMove, false ); + document.addEventListener( 'mouseup', onMouseUp, false ); + + } + + function onMouseMove( event ) { + + if ( scope.enabled === false ) { return; } + + event.preventDefault(); + + if ( state === STATE.ROTATE ) { + if ( scope.noRotate === true ) { return; } + + rotateEnd.set( event.clientX, event.clientY ); + rotateDelta.subVectors( rotateEnd, rotateStart ); + + // rotating across whole screen goes 360 degrees around + scope.rotateLeft( 2 * Math.PI * rotateDelta.x / scope.domElement.width * scope.rotateSpeed ); + // rotating up and down along whole screen attempts to go 360, but limited to 180 + scope.rotateUp( 2 * Math.PI * rotateDelta.y / scope.domElement.height * scope.rotateSpeed ); + + rotateStart.copy( rotateEnd ); + + } else if ( state === STATE.DOLLY ) { + if ( scope.noZoom === true ) { return; } + + dollyEnd.set( event.clientX, event.clientY ); + dollyDelta.subVectors( dollyEnd, dollyStart ); + + if ( dollyDelta.y > 0 ) { + + scope.dollyIn(); + + } else { + + scope.dollyOut(); + + } + + dollyStart.copy( dollyEnd ); + + } else if ( state === STATE.PAN ) { + if ( scope.noPan === true ) { return; } + + panEnd.set( event.clientX, event.clientY ); + panDelta.subVectors( panEnd, panStart ); + + scope.pan( panDelta ); + + panStart.copy( panEnd ); + + } + + } + + function onMouseUp( /* event */ ) { + + if ( scope.enabled === false ) { return; } + + document.removeEventListener( 'mousemove', onMouseMove, false ); + document.removeEventListener( 'mouseup', onMouseUp, false ); + + state = STATE.NONE; + + } + + function onMouseWheel( event ) { + // this is needed when the program is inside an iframe + // to prevent scrolling the whole page + event.preventDefault(); + if ( scope.enabled === false ) { return; } + if ( scope.noZoom === true ) { return; } + + var delta = 0; + + if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9 + + delta = event.wheelDelta; + + } else if ( event.detail ) { // Firefox + + delta = - event.detail; + + } + + if ( delta > 0 ) { + + scope.dollyOut(); + + } else { + + scope.dollyIn(); + + } + + } + + function onKeyDown( event ) { + + if ( scope.enabled === false ) { return; } + if ( scope.noKeys === true ) { return; } + if ( scope.noPan === true ) { return; } + + // pan a pixel - I guess for precise positioning? + switch ( event.keyCode ) { + + case scope.keys.UP: + scope.pan( new THREE.Vector2( 0, 1 ) ); + break; + case scope.keys.BOTTOM: + scope.pan( new THREE.Vector2( 0, -1 ) ); + break; + case scope.keys.LEFT: + scope.pan( new THREE.Vector2( 1, 0 ) ); + break; + case scope.keys.RIGHT: + scope.pan( new THREE.Vector2( -1, 0 ) ); + break; + } + + } + + function touchstart( event ) { + + if ( scope.enabled === false ) { return; } + + switch ( event.touches.length ) { + + case 1: // one-fingered touch: rotate + if ( scope.noRotate === true ) { return; } + + state = STATE.TOUCH_ROTATE; + + rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + break; + + case 2: // two-fingered touch: dolly + if ( scope.noZoom === true ) { return; } + + state = STATE.TOUCH_DOLLY; + + var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + var distance = Math.sqrt( dx * dx + dy * dy ); + dollyStart.set( 0, distance ); + break; + + case 3: // three-fingered touch: pan + if ( scope.noPan === true ) { return; } + + state = STATE.TOUCH_PAN; + + panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + break; + + default: + state = STATE.NONE; + + } + } + + function touchmove( event ) { + + if ( scope.enabled === false ) { return; } + + event.preventDefault(); + event.stopPropagation(); + + switch ( event.touches.length ) { + + case 1: // one-fingered touch: rotate + if ( scope.noRotate === true ) { return; } + if ( state !== STATE.TOUCH_ROTATE ) { return; } + + rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + rotateDelta.subVectors( rotateEnd, rotateStart ); + + // rotating across whole screen goes 360 degrees around + scope.rotateLeft( 2 * Math.PI * rotateDelta.x / scope.domElement.width * scope.rotateSpeed ); + // rotating up and down along whole screen attempts to go 360, but limited to 180 + scope.rotateUp( 2 * Math.PI * rotateDelta.y / scope.domElement.height * scope.rotateSpeed ); + + rotateStart.copy( rotateEnd ); + break; + + case 2: // two-fingered touch: dolly + if ( scope.noZoom === true ) { return; } + if ( state !== STATE.TOUCH_DOLLY ) { return; } + + var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; + var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; + var distance = Math.sqrt( dx * dx + dy * dy ); + + dollyEnd.set( 0, distance ); + dollyDelta.subVectors( dollyEnd, dollyStart ); + + if ( dollyDelta.y > 0 ) { + + scope.dollyOut(); + + } else { + + scope.dollyIn(); + + } + + dollyStart.copy( dollyEnd ); + break; + + case 3: // three-fingered touch: pan + if ( scope.noPan === true ) { return; } + if ( state !== STATE.TOUCH_PAN ) { return; } + + panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); + panDelta.subVectors( panEnd, panStart ); + + scope.pan( panDelta ); + + panStart.copy( panEnd ); + break; + + default: + state = STATE.NONE; + + } + + } + + function touchend( /* event */ ) { + + if ( scope.enabled === false ) { return; } + + state = STATE.NONE; + } + + this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); + this.domElement.addEventListener( 'mousedown', onMouseDown, false ); + this.domElement.addEventListener( 'mousewheel', onMouseWheel, false ); + this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox + + this.domElement.addEventListener( 'keydown', onKeyDown, false ); + + this.domElement.addEventListener( 'touchstart', touchstart, false ); + this.domElement.addEventListener( 'touchend', touchend, false ); + this.domElement.addEventListener( 'touchmove', touchmove, false ); + +}; diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/3dobject_controller/three.min.js b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/3dobject_controller/three.min.js new file mode 100644 index 000000000..f088be7af --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/3dobject_controller/three.min.js @@ -0,0 +1,724 @@ +/* + * 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. + */ + +// three.js - http://github.com/mrdoob/three.js +'use strict';var THREE=THREE||{REVISION:"56"};self.console=self.console||{info:function(){},log:function(){},debug:function(){},warn:function(){},error:function(){}};self.Int32Array=self.Int32Array||Array;self.Float32Array=self.Float32Array||Array;String.prototype.trim=String.prototype.trim||function(){return this.replace(/^\s+|\s+$/g,"")}; +THREE.extend=function(a,b){if(Object.keys)for(var c=Object.keys(b),d=0,e=c.length;d>16&255)/255;this.g=(a>>8&255)/255;this.b=(a&255)/255;return this},setRGB:function(a,b,c){this.r=a;this.g=b;this.b=c;return this},setHSV:function(a,b,c){console.log("DEPRECATED: Color's .setHSV() will be removed. Use .setHSL( h, s, l ) instead.");return this.setHSL(a,b*c/(1>(a=(2-b)*c)?a:2-a),a/2)},setHSL:function(a, +b,c){if(0===b)this.r=this.g=this.b=c;else{var d=function(a,b,c){0>c&&(c+=1);1c?b:c<2/3?a+6*(b-a)*(2/3-c):a},b=0.5>=c?c*(1+b):c+b-c*b,c=2*c-b;this.r=d(c,b,a+1/3);this.g=d(c,b,a);this.b=d(c,b,a-1/3)}return this},setStyle:function(a){if(/^rgb\((\d+),(\d+),(\d+)\)$/i.test(a))return a=/^rgb\((\d+),(\d+),(\d+)\)$/i.exec(a),this.r=Math.min(255,parseInt(a[1],10))/255,this.g=Math.min(255,parseInt(a[2],10))/255,this.b=Math.min(255,parseInt(a[3],10))/255,this;if(/^rgb\((\d+)\%,(\d+)\%,(\d+)\%\)$/i.test(a))return a= +/^rgb\((\d+)\%,(\d+)\%,(\d+)\%\)$/i.exec(a),this.r=Math.min(100,parseInt(a[1],10))/100,this.g=Math.min(100,parseInt(a[2],10))/100,this.b=Math.min(100,parseInt(a[3],10))/100,this;if(/^\#([0-9a-f]{6})$/i.test(a))return a=/^\#([0-9a-f]{6})$/i.exec(a),this.setHex(parseInt(a[1],16)),this;if(/^\#([0-9a-f])([0-9a-f])([0-9a-f])$/i.test(a))return a=/^\#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(a),this.setHex(parseInt(a[1]+a[1]+a[2]+a[2]+a[3]+a[3],16)),this;if(/^(\w+)$/i.test(a))return this.setHex(THREE.ColorKeywords[a]), +this},copy:function(a){this.r=a.r;this.g=a.g;this.b=a.b;return this},copyGammaToLinear:function(a){this.r=a.r*a.r;this.g=a.g*a.g;this.b=a.b*a.b;return this},copyLinearToGamma:function(a){this.r=Math.sqrt(a.r);this.g=Math.sqrt(a.g);this.b=Math.sqrt(a.b);return this},convertGammaToLinear:function(){var a=this.r,b=this.g,c=this.b;this.r=a*a;this.g=b*b;this.b=c*c;return this},convertLinearToGamma:function(){this.r=Math.sqrt(this.r);this.g=Math.sqrt(this.g);this.b=Math.sqrt(this.b);return this},getHex:function(){return 255* +this.r<<16^255*this.g<<8^255*this.b<<0},getHexString:function(){return("000000"+this.getHex().toString(16)).slice(-6)},getHSL:function(){var a={h:0,s:0,l:0};return function(){var b=this.r,c=this.g,d=this.b,e=Math.max(b,c,d),f=Math.min(b,c,d),g,h=(f+e)/2;if(f===e)f=g=0;else{var i=e-f,f=0.5>=h?i/(e+f):i/(2-e-f);switch(e){case b:g=(c-d)/i+(cf&&c>b?(c=2*Math.sqrt(1+c-f-b),this.w=(i-g)/c,this.x=0.25*c,this.y=(a+e)/c,this.z=(d+h)/c):f>b?(c=2*Math.sqrt(1+f-c-b),this.w=(d-h)/c,this.x=(a+e)/c,this.y=0.25*c,this.z=(g+i)/c):(c=2*Math.sqrt(1+b-c-f),this.w=(e-a)/c,this.x=(d+h)/c,this.y=(g+i)/c,this.z=0.25*c);return this},inverse:function(){this.conjugate().normalize(); +return this},conjugate:function(){this.x*=-1;this.y*=-1;this.z*=-1;return this},lengthSq:function(){return this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w},length:function(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w)},normalize:function(){var a=this.length();0===a?(this.z=this.y=this.x=0,this.w=1):(a=1/a,this.x*=a,this.y*=a,this.z*=a,this.w*=a);return this},multiply:function(a,b){return void 0!==b?(console.warn("DEPRECATED: Quaternion's .multiply() now only accepts one argument. Use .multiplyQuaternions( a, b ) instead."), +this.multiplyQuaternions(a,b)):this.multiplyQuaternions(this,a)},multiplyQuaternions:function(a,b){var c=a.x,d=a.y,e=a.z,f=a.w,g=b.x,h=b.y,i=b.z,k=b.w;this.x=c*k+f*g+d*i-e*h;this.y=d*k+f*h+e*g-c*i;this.z=e*k+f*i+c*h-d*g;this.w=f*k-c*g-d*h-e*i;return this},multiplyVector3:function(a){console.warn("DEPRECATED: Quaternion's .multiplyVector3() has been removed. Use is now vector.applyQuaternion( quaternion ) instead.");return a.applyQuaternion(this)},slerp:function(a,b){var c=this.x,d=this.y,e=this.z, +f=this.w,g=f*a.w+c*a.x+d*a.y+e*a.z;0>g?(this.w=-a.w,this.x=-a.x,this.y=-a.y,this.z=-a.z,g=-g):this.copy(a);if(1<=g)return this.w=f,this.x=c,this.y=d,this.z=e,this;var h=Math.acos(g),i=Math.sqrt(1-g*g);if(0.001>Math.abs(i))return this.w=0.5*(f+this.w),this.x=0.5*(c+this.x),this.y=0.5*(d+this.y),this.z=0.5*(e+this.z),this;g=Math.sin((1-b)*h)/i;h=Math.sin(b*h)/i;this.w=f*g+this.w*h;this.x=c*g+this.x*h;this.y=d*g+this.y*h;this.z=e*g+this.z*h;return this},equals:function(a){return a.x===this.x&&a.y=== +this.y&&a.z===this.z&&a.w===this.w},clone:function(){return new THREE.Quaternion(this.x,this.y,this.z,this.w)}});THREE.Quaternion.slerp=function(a,b,c,d){return c.copy(a).slerp(b,d)};THREE.Vector2=function(a,b){this.x=a||0;this.y=b||0}; +THREE.extend(THREE.Vector2.prototype,{set:function(a,b){this.x=a;this.y=b;return this},setX:function(a){this.x=a;return this},setY:function(a){this.y=a;return this},setComponent:function(a,b){switch(a){case 0:this.x=b;break;case 1:this.y=b;break;default:throw Error("index is out of range: "+a);}},getComponent:function(a){switch(a){case 0:return this.x;case 1:return this.y;default:throw Error("index is out of range: "+a);}},copy:function(a){this.x=a.x;this.y=a.y;return this},add:function(a,b){if(void 0!== +b)return console.warn("DEPRECATED: Vector2's .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(a,b);this.x+=a.x;this.y+=a.y;return this},addVectors:function(a,b){this.x=a.x+b.x;this.y=a.y+b.y;return this},addScalar:function(a){this.x+=a;this.y+=a;return this},sub:function(a,b){if(void 0!==b)return console.warn("DEPRECATED: Vector2's .sub() now only accepts one argument. Use .subVectors( a, b ) instead."),this.subVectors(a,b);this.x-=a.x;this.y-=a.y;return this}, +subVectors:function(a,b){this.x=a.x-b.x;this.y=a.y-b.y;return this},multiplyScalar:function(a){this.x*=a;this.y*=a;return this},divideScalar:function(a){0!==a?(this.x/=a,this.y/=a):this.set(0,0);return this},min:function(a){this.x>a.x&&(this.x=a.x);this.y>a.y&&(this.y=a.y);return this},max:function(a){this.xb.x&&(this.x=b.x);this.yb.y&&(this.y=b.y);return this},negate:function(){return this.multiplyScalar(-1)}, +dot:function(a){return this.x*a.x+this.y*a.y},lengthSq:function(){return this.x*this.x+this.y*this.y},length:function(){return Math.sqrt(this.x*this.x+this.y*this.y)},normalize:function(){return this.divideScalar(this.length())},distanceTo:function(a){return Math.sqrt(this.distanceToSquared(a))},distanceToSquared:function(a){var b=this.x-a.x,a=this.y-a.y;return b*b+a*a},setLength:function(a){var b=this.length();0!==b&&a!==b&&this.multiplyScalar(a/b);return this},lerp:function(a,b){this.x+=(a.x-this.x)* +b;this.y+=(a.y-this.y)*b;return this},equals:function(a){return a.x===this.x&&a.y===this.y},toArray:function(){return[this.x,this.y]},clone:function(){return new THREE.Vector2(this.x,this.y)}});THREE.Vector3=function(a,b,c){this.x=a||0;this.y=b||0;this.z=c||0}; +THREE.extend(THREE.Vector3.prototype,{set:function(a,b,c){this.x=a;this.y=b;this.z=c;return this},setX:function(a){this.x=a;return this},setY:function(a){this.y=a;return this},setZ:function(a){this.z=a;return this},setComponent:function(a,b){switch(a){case 0:this.x=b;break;case 1:this.y=b;break;case 2:this.z=b;break;default:throw Error("index is out of range: "+a);}},getComponent:function(a){switch(a){case 0:return this.x;case 1:return this.y;case 2:return this.z;default:throw Error("index is out of range: "+ +a);}},copy:function(a){this.x=a.x;this.y=a.y;this.z=a.z;return this},add:function(a,b){if(void 0!==b)return console.warn("DEPRECATED: Vector3's .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(a,b);this.x+=a.x;this.y+=a.y;this.z+=a.z;return this},addScalar:function(a){this.x+=a;this.y+=a;this.z+=a;return this},addVectors:function(a,b){this.x=a.x+b.x;this.y=a.y+b.y;this.z=a.z+b.z;return this},sub:function(a,b){if(void 0!==b)return console.warn("DEPRECATED: Vector3's .sub() now only accepts one argument. Use .subVectors( a, b ) instead."), +this.subVectors(a,b);this.x-=a.x;this.y-=a.y;this.z-=a.z;return this},subVectors:function(a,b){this.x=a.x-b.x;this.y=a.y-b.y;this.z=a.z-b.z;return this},multiply:function(a,b){if(void 0!==b)return console.warn("DEPRECATED: Vector3's .multiply() now only accepts one argument. Use .multiplyVectors( a, b ) instead."),this.multiplyVectors(a,b);this.x*=a.x;this.y*=a.y;this.z*=a.z;return this},multiplyScalar:function(a){this.x*=a;this.y*=a;this.z*=a;return this},multiplyVectors:function(a,b){this.x=a.x* +b.x;this.y=a.y*b.y;this.z=a.z*b.z;return this},applyMatrix3:function(a){var b=this.x,c=this.y,d=this.z,a=a.elements;this.x=a[0]*b+a[3]*c+a[6]*d;this.y=a[1]*b+a[4]*c+a[7]*d;this.z=a[2]*b+a[5]*c+a[8]*d;return this},applyMatrix4:function(a){var b=this.x,c=this.y,d=this.z,a=a.elements;this.x=a[0]*b+a[4]*c+a[8]*d+a[12];this.y=a[1]*b+a[5]*c+a[9]*d+a[13];this.z=a[2]*b+a[6]*c+a[10]*d+a[14];return this},applyProjection:function(a){var b=this.x,c=this.y,d=this.z,a=a.elements,e=1/(a[3]*b+a[7]*c+a[11]*d+a[15]); +this.x=(a[0]*b+a[4]*c+a[8]*d+a[12])*e;this.y=(a[1]*b+a[5]*c+a[9]*d+a[13])*e;this.z=(a[2]*b+a[6]*c+a[10]*d+a[14])*e;return this},applyQuaternion:function(a){var b=this.x,c=this.y,d=this.z,e=a.x,f=a.y,g=a.z,a=a.w,h=a*b+f*d-g*c,i=a*c+g*b-e*d,k=a*d+e*c-f*b,b=-e*b-f*c-g*d;this.x=h*a+b*-e+i*-g-k*-f;this.y=i*a+b*-f+k*-e-h*-g;this.z=k*a+b*-g+h*-f-i*-e;return this},applyEuler:function(){var a=new THREE.Quaternion;return function(b,c){var d=a.setFromEuler(b,c);this.applyQuaternion(d);return this}}(),applyAxisAngle:function(){var a= +new THREE.Quaternion;return function(b,c){var d=a.setFromAxisAngle(b,c);this.applyQuaternion(d);return this}}(),transformDirection:function(a){var b=this.x,c=this.y,d=this.z,a=a.elements;this.x=a[0]*b+a[4]*c+a[8]*d;this.y=a[1]*b+a[5]*c+a[9]*d;this.z=a[2]*b+a[6]*c+a[10]*d;this.normalize();return this},divide:function(a){this.x/=a.x;this.y/=a.y;this.z/=a.z;return this},divideScalar:function(a){0!==a?(this.x/=a,this.y/=a,this.z/=a):this.z=this.y=this.x=0;return this},min:function(a){this.x>a.x&&(this.x= +a.x);this.y>a.y&&(this.y=a.y);this.z>a.z&&(this.z=a.z);return this},max:function(a){this.xb.x&&(this.x=b.x);this.yb.y&&(this.y=b.y);this.zb.z&&(this.z=b.z);return this},negate:function(){return this.multiplyScalar(-1)},dot:function(a){return this.x*a.x+this.y*a.y+this.z*a.z},lengthSq:function(){return this.x*this.x+this.y* +this.y+this.z*this.z},length:function(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)},lengthManhattan:function(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)},normalize:function(){return this.divideScalar(this.length())},setLength:function(a){var b=this.length();0!==b&&a!==b&&this.multiplyScalar(a/b);return this},lerp:function(a,b){this.x+=(a.x-this.x)*b;this.y+=(a.y-this.y)*b;this.z+=(a.z-this.z)*b;return this},cross:function(a,b){if(void 0!==b)return console.warn("DEPRECATED: Vector3's .cross() now only accepts one argument. Use .crossVectors( a, b ) instead."), +this.crossVectors(a,b);var c=this.x,d=this.y,e=this.z;this.x=d*a.z-e*a.y;this.y=e*a.x-c*a.z;this.z=c*a.y-d*a.x;return this},crossVectors:function(a,b){this.x=a.y*b.z-a.z*b.y;this.y=a.z*b.x-a.x*b.z;this.z=a.x*b.y-a.y*b.x;return this},projectOnVector:function(){var a=new THREE.Vector3;return function(b){a.copy(b).normalize();b=this.dot(a);return this.copy(a).multiplyScalar(b)}}(),projectOnPlane:function(){var a=new THREE.Vector3;return function(b){a.copy(this).projectOnVector(b);return this.sub(a)}}(), +reflect:function(){var a=new THREE.Vector3;return function(b){a.copy(this).projectOnVector(b).multiplyScalar(2);return this.subVectors(a,this)}}(),angleTo:function(a){a=this.dot(a)/(this.length()*a.length());return Math.acos(THREE.Math.clamp(a,-1,1))},distanceTo:function(a){return Math.sqrt(this.distanceToSquared(a))},distanceToSquared:function(a){var b=this.x-a.x,c=this.y-a.y,a=this.z-a.z;return b*b+c*c+a*a},getPositionFromMatrix:function(a){this.x=a.elements[12];this.y=a.elements[13];this.z=a.elements[14]; +return this},setEulerFromRotationMatrix:function(a,b){function c(a){return Math.min(Math.max(a,-1),1)}var d=a.elements,e=d[0],f=d[4],g=d[8],h=d[1],i=d[5],k=d[9],l=d[2],m=d[6],d=d[10];void 0===b||"XYZ"===b?(this.y=Math.asin(c(g)),0.99999>Math.abs(g)?(this.x=Math.atan2(-k,d),this.z=Math.atan2(-f,e)):(this.x=Math.atan2(m,i),this.z=0)):"YXZ"===b?(this.x=Math.asin(-c(k)),0.99999>Math.abs(k)?(this.y=Math.atan2(g,d),this.z=Math.atan2(h,i)):(this.y=Math.atan2(-l,e),this.z=0)):"ZXY"===b?(this.x=Math.asin(c(m)), +0.99999>Math.abs(m)?(this.y=Math.atan2(-l,d),this.z=Math.atan2(-f,i)):(this.y=0,this.z=Math.atan2(h,e))):"ZYX"===b?(this.y=Math.asin(-c(l)),0.99999>Math.abs(l)?(this.x=Math.atan2(m,d),this.z=Math.atan2(h,e)):(this.x=0,this.z=Math.atan2(-f,i))):"YZX"===b?(this.z=Math.asin(c(h)),0.99999>Math.abs(h)?(this.x=Math.atan2(-k,i),this.y=Math.atan2(-l,e)):(this.x=0,this.y=Math.atan2(g,d))):"XZY"===b&&(this.z=Math.asin(-c(f)),0.99999>Math.abs(f)?(this.x=Math.atan2(m,i),this.y=Math.atan2(g,e)):(this.x=Math.atan2(-k, +d),this.y=0));return this},setEulerFromQuaternion:function(a,b){function c(a){return Math.min(Math.max(a,-1),1)}var d=a.x*a.x,e=a.y*a.y,f=a.z*a.z,g=a.w*a.w;void 0===b||"XYZ"===b?(this.x=Math.atan2(2*(a.x*a.w-a.y*a.z),g-d-e+f),this.y=Math.asin(c(2*(a.x*a.z+a.y*a.w))),this.z=Math.atan2(2*(a.z*a.w-a.x*a.y),g+d-e-f)):"YXZ"===b?(this.x=Math.asin(c(2*(a.x*a.w-a.y*a.z))),this.y=Math.atan2(2*(a.x*a.z+a.y*a.w),g-d-e+f),this.z=Math.atan2(2*(a.x*a.y+a.z*a.w),g-d+e-f)):"ZXY"===b?(this.x=Math.asin(c(2*(a.x*a.w+ +a.y*a.z))),this.y=Math.atan2(2*(a.y*a.w-a.z*a.x),g-d-e+f),this.z=Math.atan2(2*(a.z*a.w-a.x*a.y),g-d+e-f)):"ZYX"===b?(this.x=Math.atan2(2*(a.x*a.w+a.z*a.y),g-d-e+f),this.y=Math.asin(c(2*(a.y*a.w-a.x*a.z))),this.z=Math.atan2(2*(a.x*a.y+a.z*a.w),g+d-e-f)):"YZX"===b?(this.x=Math.atan2(2*(a.x*a.w-a.z*a.y),g-d+e-f),this.y=Math.atan2(2*(a.y*a.w-a.x*a.z),g+d-e-f),this.z=Math.asin(c(2*(a.x*a.y+a.z*a.w)))):"XZY"===b&&(this.x=Math.atan2(2*(a.x*a.w+a.y*a.z),g-d+e-f),this.y=Math.atan2(2*(a.x*a.z+a.y*a.w),g+d- +e-f),this.z=Math.asin(c(2*(a.z*a.w-a.x*a.y))));return this},getScaleFromMatrix:function(a){var b=this.set(a.elements[0],a.elements[1],a.elements[2]).length(),c=this.set(a.elements[4],a.elements[5],a.elements[6]).length(),a=this.set(a.elements[8],a.elements[9],a.elements[10]).length();this.x=b;this.y=c;this.z=a;return this},equals:function(a){return a.x===this.x&&a.y===this.y&&a.z===this.z},toArray:function(){return[this.x,this.y,this.z]},clone:function(){return new THREE.Vector3(this.x,this.y,this.z)}});THREE.Vector4=function(a,b,c,d){this.x=a||0;this.y=b||0;this.z=c||0;this.w=void 0!==d?d:1}; +THREE.extend(THREE.Vector4.prototype,{set:function(a,b,c,d){this.x=a;this.y=b;this.z=c;this.w=d;return this},setX:function(a){this.x=a;return this},setY:function(a){this.y=a;return this},setZ:function(a){this.z=a;return this},setW:function(a){this.w=a;return this},setComponent:function(a,b){switch(a){case 0:this.x=b;break;case 1:this.y=b;break;case 2:this.z=b;break;case 3:this.w=b;break;default:throw Error("index is out of range: "+a);}},getComponent:function(a){switch(a){case 0:return this.x;case 1:return this.y; +case 2:return this.z;case 3:return this.w;default:throw Error("index is out of range: "+a);}},copy:function(a){this.x=a.x;this.y=a.y;this.z=a.z;this.w=void 0!==a.w?a.w:1;return this},add:function(a,b){if(void 0!==b)return console.warn("DEPRECATED: Vector4's .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(a,b);this.x+=a.x;this.y+=a.y;this.z+=a.z;this.w+=a.w;return this},addScalar:function(a){this.x+=a;this.y+=a;this.z+=a;this.w+=a;return this},addVectors:function(a, +b){this.x=a.x+b.x;this.y=a.y+b.y;this.z=a.z+b.z;this.w=a.w+b.w;return this},sub:function(a,b){if(void 0!==b)return console.warn("DEPRECATED: Vector4's .sub() now only accepts one argument. Use .subVectors( a, b ) instead."),this.subVectors(a,b);this.x-=a.x;this.y-=a.y;this.z-=a.z;this.w-=a.w;return this},subVectors:function(a,b){this.x=a.x-b.x;this.y=a.y-b.y;this.z=a.z-b.z;this.w=a.w-b.w;return this},multiplyScalar:function(a){this.x*=a;this.y*=a;this.z*=a;this.w*=a;return this},applyMatrix4:function(a){var b= +this.x,c=this.y,d=this.z,e=this.w,a=a.elements;this.x=a[0]*b+a[4]*c+a[8]*d+a[12]*e;this.y=a[1]*b+a[5]*c+a[9]*d+a[13]*e;this.z=a[2]*b+a[6]*c+a[10]*d+a[14]*e;this.w=a[3]*b+a[7]*c+a[11]*d+a[15]*e;return this},divideScalar:function(a){0!==a?(this.x/=a,this.y/=a,this.z/=a,this.w/=a):(this.z=this.y=this.x=0,this.w=1);return this},setAxisAngleFromQuaternion:function(a){this.w=2*Math.acos(a.w);var b=Math.sqrt(1-a.w*a.w);1E-4>b?(this.x=1,this.z=this.y=0):(this.x=a.x/b,this.y=a.y/b,this.z=a.z/b);return this}, +setAxisAngleFromRotationMatrix:function(a){var b,c,d,a=a.elements,e=a[0];d=a[4];var f=a[8],g=a[1],h=a[5],i=a[9];c=a[2];b=a[6];var k=a[10];if(0.01>Math.abs(d-g)&&0.01>Math.abs(f-c)&&0.01>Math.abs(i-b)){if(0.1>Math.abs(d+g)&&0.1>Math.abs(f+c)&&0.1>Math.abs(i+b)&&0.1>Math.abs(e+h+k-3))return this.set(1,0,0,0),this;a=Math.PI;e=(e+1)/2;h=(h+1)/2;k=(k+1)/2;d=(d+g)/4;f=(f+c)/4;i=(i+b)/4;e>h&&e>k?0.01>e?(b=0,d=c=0.707106781):(b=Math.sqrt(e),c=d/b,d=f/b):h>k?0.01>h?(b=0.707106781,c=0,d=0.707106781):(c=Math.sqrt(h), +b=d/c,d=i/c):0.01>k?(c=b=0.707106781,d=0):(d=Math.sqrt(k),b=f/d,c=i/d);this.set(b,c,d,a);return this}a=Math.sqrt((b-i)*(b-i)+(f-c)*(f-c)+(g-d)*(g-d));0.001>Math.abs(a)&&(a=1);this.x=(b-i)/a;this.y=(f-c)/a;this.z=(g-d)/a;this.w=Math.acos((e+h+k-1)/2);return this},min:function(a){this.x>a.x&&(this.x=a.x);this.y>a.y&&(this.y=a.y);this.z>a.z&&(this.z=a.z);this.w>a.w&&(this.w=a.w);return this},max:function(a){this.xb.x&&(this.x=b.x);this.yb.y&&(this.y=b.y);this.zb.z&&(this.z=b.z);this.wb.w&&(this.w=b.w);return this},negate:function(){return this.multiplyScalar(-1)},dot:function(a){return this.x*a.x+this.y*a.y+this.z*a.z+this.w*a.w},lengthSq:function(){return this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w},length:function(){return Math.sqrt(this.x*this.x+this.y*this.y+ +this.z*this.z+this.w*this.w)},lengthManhattan:function(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)+Math.abs(this.w)},normalize:function(){return this.divideScalar(this.length())},setLength:function(a){var b=this.length();0!==b&&a!==b&&this.multiplyScalar(a/b);return this},lerp:function(a,b){this.x+=(a.x-this.x)*b;this.y+=(a.y-this.y)*b;this.z+=(a.z-this.z)*b;this.w+=(a.w-this.w)*b;return this},equals:function(a){return a.x===this.x&&a.y===this.y&&a.z===this.z&&a.w===this.w},toArray:function(){return[this.x, +this.y,this.z,this.w]},clone:function(){return new THREE.Vector4(this.x,this.y,this.z,this.w)}});THREE.Line3=function(a,b){this.start=void 0!==a?a:new THREE.Vector3;this.end=void 0!==b?b:new THREE.Vector3}; +THREE.extend(THREE.Line3.prototype,{set:function(a,b){this.start.copy(a);this.end.copy(b);return this},copy:function(a){this.start.copy(a.start);this.end.copy(a.end);return this},center:function(a){return(a||new THREE.Vector3).addVectors(this.start,this.end).multiplyScalar(0.5)},delta:function(a){return(a||new THREE.Vector3).subVectors(this.end,this.start)},distanceSq:function(){return this.start.distanceToSquared(this.end)},distance:function(){return this.start.distanceTo(this.end)},at:function(a, +b){var c=b||new THREE.Vector3;return this.delta(c).multiplyScalar(a).add(this.start)},closestPointToPointParameter:function(){var a=new THREE.Vector3,b=new THREE.Vector3;return function(c,d){a.subVectors(c,this.start);b.subVectors(this.end,this.start);var e=b.dot(b),e=b.dot(a)/e;d&&(e=THREE.Math.clamp(e,0,1));return e}}(),closestPointToPoint:function(a,b,c){a=this.closestPointToPointParameter(a,b);c=c||new THREE.Vector3;return this.delta(c).multiplyScalar(a).add(this.start)},applyMatrix4:function(a){this.start.applyMatrix4(a); +this.end.applyMatrix4(a);return this},equals:function(a){return a.start.equals(this.start)&&a.end.equals(this.end)},clone:function(){return(new THREE.Line3).copy(this)}});THREE.Box2=function(a,b){this.min=void 0!==a?a:new THREE.Vector2(Infinity,Infinity);this.max=void 0!==b?b:new THREE.Vector2(-Infinity,-Infinity)}; +THREE.extend(THREE.Box2.prototype,{set:function(a,b){this.min.copy(a);this.max.copy(b);return this},setFromPoints:function(a){if(0this.max.x&&(this.max.x=b.x),b.ythis.max.y&&(this.max.y=b.y)}else this.makeEmpty();return this},setFromCenterAndSize:function(){var a=new THREE.Vector2;return function(b,c){var d=a.copy(c).multiplyScalar(0.5);this.min.copy(b).sub(d); +this.max.copy(b).add(d);return this}}(),copy:function(a){this.min.copy(a.min);this.max.copy(a.max);return this},makeEmpty:function(){this.min.x=this.min.y=Infinity;this.max.x=this.max.y=-Infinity;return this},empty:function(){return this.max.xthis.max.x||a.ythis.max.y?!1:!0},containsBox:function(a){return this.min.x<=a.min.x&&a.max.x<=this.max.x&&this.min.y<=a.min.y&&a.max.y<=this.max.y?!0:!1},getParameter:function(a){return new THREE.Vector2((a.x-this.min.x)/(this.max.x-this.min.x),(a.y-this.min.y)/ +(this.max.y-this.min.y))},isIntersectionBox:function(a){return a.max.xthis.max.x||a.max.ythis.max.y?!1:!0},clampPoint:function(a,b){return(b||new THREE.Vector2).copy(a).clamp(this.min,this.max)},distanceToPoint:function(){var a=new THREE.Vector2;return function(b){return a.copy(b).clamp(this.min,this.max).sub(b).length()}}(),intersect:function(a){this.min.max(a.min);this.max.min(a.max);return this},union:function(a){this.min.min(a.min);this.max.max(a.max); +return this},translate:function(a){this.min.add(a);this.max.add(a);return this},equals:function(a){return a.min.equals(this.min)&&a.max.equals(this.max)},clone:function(){return(new THREE.Box2).copy(this)}});THREE.Box3=function(a,b){this.min=void 0!==a?a:new THREE.Vector3(Infinity,Infinity,Infinity);this.max=void 0!==b?b:new THREE.Vector3(-Infinity,-Infinity,-Infinity)}; +THREE.extend(THREE.Box3.prototype,{set:function(a,b){this.min.copy(a);this.max.copy(b);return this},setFromPoints:function(a){if(0this.max.x&&(this.max.x=b.x),b.ythis.max.y&&(this.max.y=b.y),b.zthis.max.z&&(this.max.z=b.z)}else this.makeEmpty();return this},setFromCenterAndSize:function(){var a=new THREE.Vector3; +return function(b,c){var d=a.copy(c).multiplyScalar(0.5);this.min.copy(b).sub(d);this.max.copy(b).add(d);return this}}(),copy:function(a){this.min.copy(a.min);this.max.copy(a.max);return this},makeEmpty:function(){this.min.x=this.min.y=this.min.z=Infinity;this.max.x=this.max.y=this.max.z=-Infinity;return this},empty:function(){return this.max.xthis.max.x||a.ythis.max.y||a.zthis.max.z?!1:!0},containsBox:function(a){return this.min.x<=a.min.x&&a.max.x<= +this.max.x&&this.min.y<=a.min.y&&a.max.y<=this.max.y&&this.min.z<=a.min.z&&a.max.z<=this.max.z?!0:!1},getParameter:function(a){return new THREE.Vector3((a.x-this.min.x)/(this.max.x-this.min.x),(a.y-this.min.y)/(this.max.y-this.min.y),(a.z-this.min.z)/(this.max.z-this.min.z))},isIntersectionBox:function(a){return a.max.xthis.max.x||a.max.ythis.max.y||a.max.zthis.max.z?!1:!0},clampPoint:function(a,b){return(b||new THREE.Vector3).copy(a).clamp(this.min, +this.max)},distanceToPoint:function(){var a=new THREE.Vector3;return function(b){return a.copy(b).clamp(this.min,this.max).sub(b).length()}}(),getBoundingSphere:function(){var a=new THREE.Vector3;return function(b){b=b||new THREE.Sphere;b.center=this.center();b.radius=0.5*this.size(a).length();return b}}(),intersect:function(a){this.min.max(a.min);this.max.min(a.max);return this},union:function(a){this.min.min(a.min);this.max.max(a.max);return this},applyMatrix4:function(){var a=[new THREE.Vector3, +new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3];return function(b){a[0].set(this.min.x,this.min.y,this.min.z).applyMatrix4(b);a[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(b);a[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(b);a[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(b);a[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(b);a[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(b);a[6].set(this.max.x, +this.max.y,this.min.z).applyMatrix4(b);a[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(b);this.makeEmpty();this.setFromPoints(a);return this}}(),translate:function(a){this.min.add(a);this.max.add(a);return this},equals:function(a){return a.min.equals(this.min)&&a.max.equals(this.max)},clone:function(){return(new THREE.Box3).copy(this)}});THREE.Matrix3=function(a,b,c,d,e,f,g,h,i){this.elements=new Float32Array(9);this.set(void 0!==a?a:1,b||0,c||0,d||0,void 0!==e?e:1,f||0,g||0,h||0,void 0!==i?i:1)}; +THREE.extend(THREE.Matrix3.prototype,{set:function(a,b,c,d,e,f,g,h,i){var k=this.elements;k[0]=a;k[3]=b;k[6]=c;k[1]=d;k[4]=e;k[7]=f;k[2]=g;k[5]=h;k[8]=i;return this},identity:function(){this.set(1,0,0,0,1,0,0,0,1);return this},copy:function(a){a=a.elements;this.set(a[0],a[3],a[6],a[1],a[4],a[7],a[2],a[5],a[8]);return this},multiplyVector3:function(a){console.warn("DEPRECATED: Matrix3's .multiplyVector3() has been removed. Use vector.applyMatrix3( matrix ) instead.");return a.applyMatrix3(this)},multiplyVector3Array:function(){var a= +new THREE.Vector3;return function(b){for(var c=0,d=b.length;c=this.radius},containsPoint:function(a){return a.distanceToSquared(this.center)<=this.radius*this.radius},distanceToPoint:function(a){return a.distanceTo(this.center)- +this.radius},intersectsSphere:function(a){var b=this.radius+a.radius;return a.center.distanceToSquared(this.center)<=b*b},clampPoint:function(a,b){var c=this.center.distanceToSquared(a),d=b||new THREE.Vector3;d.copy(a);c>this.radius*this.radius&&(d.sub(this.center).normalize(),d.multiplyScalar(this.radius).add(this.center));return d},getBoundingBox:function(a){a=a||new THREE.Box3;a.set(this.center,this.center);a.expandByScalar(this.radius);return a},applyMatrix4:function(a){this.center.applyMatrix4(a); +this.radius*=a.getMaxScaleOnAxis();return this},translate:function(a){this.center.add(a);return this},equals:function(a){return a.center.equals(this.center)&&a.radius===this.radius},clone:function(){return(new THREE.Sphere).copy(this)}});THREE.Frustum=function(a,b,c,d,e,f){this.planes=[void 0!==a?a:new THREE.Plane,void 0!==b?b:new THREE.Plane,void 0!==c?c:new THREE.Plane,void 0!==d?d:new THREE.Plane,void 0!==e?e:new THREE.Plane,void 0!==f?f:new THREE.Plane]}; +THREE.extend(THREE.Frustum.prototype,{set:function(a,b,c,d,e,f){var g=this.planes;g[0].copy(a);g[1].copy(b);g[2].copy(c);g[3].copy(d);g[4].copy(e);g[5].copy(f);return this},copy:function(a){for(var b=this.planes,c=0;6>c;c++)b[c].copy(a.planes[c]);return this},setFromMatrix:function(a){var b=this.planes,c=a.elements,a=c[0],d=c[1],e=c[2],f=c[3],g=c[4],h=c[5],i=c[6],k=c[7],l=c[8],m=c[9],n=c[10],s=c[11],r=c[12],p=c[13],q=c[14],c=c[15];b[0].setComponents(f-a,k-g,s-l,c-r).normalize();b[1].setComponents(f+ +a,k+g,s+l,c+r).normalize();b[2].setComponents(f+d,k+h,s+m,c+p).normalize();b[3].setComponents(f-d,k-h,s-m,c-p).normalize();b[4].setComponents(f-e,k-i,s-n,c-q).normalize();b[5].setComponents(f+e,k+i,s+n,c+q).normalize();return this},intersectsObject:function(){var a=new THREE.Vector3;return function(b){var c=b.matrixWorld,d=this.planes,b=-b.geometry.boundingSphere.radius*c.getMaxScaleOnAxis();a.getPositionFromMatrix(c);for(c=0;6>c;c++)if(d[c].distanceToPoint(a)d;d++)if(b[d].distanceToPoint(c)c;c++)if(0>b[c].distanceToPoint(a))return!1;return!0},clone:function(){return(new THREE.Frustum).copy(this)}});THREE.Plane=function(a,b){this.normal=void 0!==a?a:new THREE.Vector3(1,0,0);this.constant=void 0!==b?b:0}; +THREE.extend(THREE.Plane.prototype,{set:function(a,b){this.normal.copy(a);this.constant=b;return this},setComponents:function(a,b,c,d){this.normal.set(a,b,c);this.constant=d;return this},setFromNormalAndCoplanarPoint:function(a,b){this.normal.copy(a);this.constant=-b.dot(this.normal);return this},setFromCoplanarPoints:function(){var a=new THREE.Vector3,b=new THREE.Vector3;return function(c,d,e){d=a.subVectors(e,d).cross(b.subVectors(c,d)).normalize();this.setFromNormalAndCoplanarPoint(d,c);return this}}(), +copy:function(a){this.normal.copy(a.normal);this.constant=a.constant;return this},normalize:function(){var a=1/this.normal.length();this.normal.multiplyScalar(a);this.constant*=a;return this},negate:function(){this.constant*=-1;this.normal.negate();return this},distanceToPoint:function(a){return this.normal.dot(a)+this.constant},distanceToSphere:function(a){return this.distanceToPoint(a.center)-a.radius},projectPoint:function(a,b){return this.orthoPoint(a,b).sub(a).negate()},orthoPoint:function(a, +b){var c=this.distanceToPoint(a);return(b||new THREE.Vector3).copy(this.normal).multiplyScalar(c)},isIntersectionLine:function(a){var b=this.distanceToPoint(a.start),a=this.distanceToPoint(a.end);return 0>b&&0a&&0f||1c?c:a},clampBottom:function(a,b){return a=c)return 1;a=(a-b)/(c-b);return a*a*(3-2*a)},smootherstep:function(a,b,c){if(a<=b)return 0;if(a>=c)return 1;a=(a-b)/(c-b);return a*a*a*(a*(6*a-15)+10)},random16:function(){return(65280*Math.random()+255*Math.random())/65535},randInt:function(a,b){return a+Math.floor(Math.random()*(b-a+1))},randFloat:function(a, +b){return a+Math.random()*(b-a)},randFloatSpread:function(a){return a*(0.5-Math.random())},sign:function(a){return 0>a?-1:0this.points.length-2?this.points.length-1:f+1;c[3]=f>this.points.length-3?this.points.length-1: +f+2;k=this.points[c[0]];l=this.points[c[1]];m=this.points[c[2]];n=this.points[c[3]];h=g*g;i=g*h;d.x=b(k.x,l.x,m.x,n.x,g,h,i);d.y=b(k.y,l.y,m.y,n.y,g,h,i);d.z=b(k.z,l.z,m.z,n.z,g,h,i);return d};this.getControlPointsArray=function(){var a,b,c=this.points.length,d=[];for(a=0;a=b.x+b.y}}(); +THREE.extend(THREE.Triangle.prototype,{constructor:THREE.Triangle,set:function(a,b,c){this.a.copy(a);this.b.copy(b);this.c.copy(c);return this},setFromPointsAndIndices:function(a,b,c,d){this.a.copy(a[b]);this.b.copy(a[c]);this.c.copy(a[d]);return this},copy:function(a){this.a.copy(a.a);this.b.copy(a.b);this.c.copy(a.c);return this},area:function(){var a=new THREE.Vector3,b=new THREE.Vector3;return function(){a.subVectors(this.c,this.b);b.subVectors(this.a,this.b);return 0.5*a.cross(b).length()}}(), +midpoint:function(a){return(a||new THREE.Vector3).addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)},normal:function(a){return THREE.Triangle.normal(this.a,this.b,this.c,a)},plane:function(a){return(a||new THREE.Plane).setFromCoplanarPoints(this.a,this.b,this.c)},barycoordFromPoint:function(a,b){return THREE.Triangle.barycoordFromPoint(a,this.a,this.b,this.c,b)},containsPoint:function(a){return THREE.Triangle.containsPoint(a,this.a,this.b,this.c)},equals:function(a){return a.a.equals(this.a)&& +a.b.equals(this.b)&&a.c.equals(this.c)},clone:function(){return(new THREE.Triangle).copy(this)}});THREE.Vertex=function(a){console.warn("THREE.Vertex has been DEPRECATED. Use THREE.Vector3 instead.");return a};THREE.UV=function(a,b){console.warn("THREE.UV has been DEPRECATED. Use THREE.Vector2 instead.");return new THREE.Vector2(a,b)};THREE.Clock=function(a){this.autoStart=void 0!==a?a:!0;this.elapsedTime=this.oldTime=this.startTime=0;this.running=!1}; +THREE.extend(THREE.Clock.prototype,{start:function(){this.oldTime=this.startTime=void 0!==window.performance&&void 0!==window.performance.now?window.performance.now():Date.now();this.running=!0},stop:function(){this.getElapsedTime();this.running=!1},getElapsedTime:function(){this.getDelta();return this.elapsedTime},getDelta:function(){var a=0;this.autoStart&&!this.running&&this.start();if(this.running){var b=void 0!==window.performance&&void 0!==window.performance.now?window.performance.now():Date.now(), +a=0.001*(b-this.oldTime);this.oldTime=b;this.elapsedTime+=a}return a}});THREE.EventDispatcher=function(){var a={};this.addEventListener=function(b,c){void 0===a[b]&&(a[b]=[]);-1===a[b].indexOf(c)&&a[b].push(c)};this.removeEventListener=function(b,c){var d=a[b].indexOf(c);-1!==d&&a[b].splice(d,1)};this.dispatchEvent=function(b){var c=a[b.type];if(void 0!==c){b.target=this;for(var d=0,e=c.length;dh.scale.x)return k;k.push({distance:i,point:h.position,face:null,object:h})}else if(h instanceof +a.Mesh){f.getPositionFromMatrix(h.matrixWorld);b.set(f,h.geometry.boundingSphere.radius*h.matrixWorld.getMaxScaleOnAxis());if(!i.ray.isIntersectionSphere(b))return k;var s=h.geometry,r=s.vertices,p=h.material instanceof a.MeshFaceMaterial,q=!0===p?h.material.materials:null,y=h.material.side,v,z,t,A=i.precision;h.matrixRotationWorld.extractRotation(h.matrixWorld);g.getInverse(h.matrixWorld);c.copy(i.ray).applyMatrix4(g);for(var I=0,C=s.faces.length;IG)){y=y.side;if(y!==a.DoubleSide&&(v=c.direction.dot(d.normal),!(y===a.FrontSide?0>v:0i.far)){e=c.at(G,e);if(x instanceof a.Face3){if(y=r[x.a],v=r[x.b],z=r[x.c],!a.Triangle.containsPoint(e,y,v,z))continue}else if(x instanceof a.Face4){if(y=r[x.a],v=r[x.b],z=r[x.c],t=r[x.d],!a.Triangle.containsPoint(e,y,v,t)&&!a.Triangle.containsPoint(e,v,z,t))continue}else throw Error("face type not supported"); +k.push({distance:G,point:i.ray.at(G),face:x,faceIndex:I,object:h})}}}}}},k=function(a,b,c){for(var a=a.getDescendants(),d=0,e=a.length;de&&0>f||0>g&&0>h)return!1;0>e?c=Math.max(c,e/(e-f)):0>f&&(d=Math.min(d,e/(e-f)));0>g?c=Math.max(c,g/(g-h)):0>h&&(d=Math.min(d,g/(g-h)));if(d< +c)return!1;a.lerp(b,c);b.lerp(a,1-d);return!0}var e,f,g=[],h=0,i,k,l=[],m=0,n,s,r=[],p=0,q,y=[],v=0,z,t,A=[],I=0,C,x,G=[],J=0,E={objects:[],sprites:[],lights:[],elements:[]},H=new THREE.Vector3,B=new THREE.Vector4,W=new THREE.Box3(new THREE.Vector3(-1,-1,-1),new THREE.Vector3(1,1,1)),F=new THREE.Box3,K=Array(3),L=Array(4),U=new THREE.Matrix4,fa=new THREE.Matrix4,Ca,$a=new THREE.Matrix4,M=new THREE.Matrix3,ca=new THREE.Matrix3,qa=new THREE.Vector3,ha=new THREE.Frustum,ra=new THREE.Vector4,N=new THREE.Vector4; +this.projectVector=function(a,b){b.matrixWorldInverse.getInverse(b.matrixWorld);fa.multiplyMatrices(b.projectionMatrix,b.matrixWorldInverse);return a.applyProjection(fa)};this.unprojectVector=function(a,b){b.projectionMatrixInverse.getInverse(b.projectionMatrix);fa.multiplyMatrices(b.matrixWorld,b.projectionMatrixInverse);return a.applyProjection(fa)};this.pickingRay=function(a,b){a.z=-1;var c=new THREE.Vector3(a.x,a.y,1);this.unprojectVector(a,b);this.unprojectVector(c,b);c.sub(a).normalize();return new THREE.Raycaster(a, +c)};this.projectScene=function(g,h,m,Pa){var ta=!1,ka,aa,pa,Y,da,la,Z,oa,gb,nb,ia,Wa,ab;x=t=q=s=0;E.elements.length=0;g.updateMatrixWorld();void 0===h.parent&&h.updateMatrixWorld();U.copy(h.matrixWorldInverse.getInverse(h.matrixWorld));fa.multiplyMatrices(h.projectionMatrix,U);ca.getInverse(U);ca.transpose();ha.setFromMatrix(fa);f=0;E.objects.length=0;E.sprites.length=0;E.lights.length=0;var Fa=function(b){for(var c=0,d=b.children.length;ci.positionScreen.x||1i.positionScreen.y||1i.positionScreen.z||1(Z.positionScreen.x-Y.positionScreen.x)*(da.positionScreen.y-Y.positionScreen.y)-(Z.positionScreen.y-Y.positionScreen.y)*(da.positionScreen.x-Y.positionScreen.x),la===THREE.DoubleSide||ta===(la===THREE.FrontSide))s===p?(ia=new THREE.RenderableFace3,r.push(ia),p++,s++,n=ia):n=r[s++],n.v1.copy(Y),n.v2.copy(da),n.v3.copy(Z);else continue; +else continue;else if(aa instanceof THREE.Face4)if(Y=l[aa.a],da=l[aa.b],Z=l[aa.c],ia=l[aa.d],L[0]=Y.positionScreen,L[1]=da.positionScreen,L[2]=Z.positionScreen,L[3]=ia.positionScreen,!0===Y.visible||!0===da.visible||!0===Z.visible||!0===ia.visible||W.isIntersectionBox(F.setFromPoints(L)))if(ta=0>(ia.positionScreen.x-Y.positionScreen.x)*(da.positionScreen.y-Y.positionScreen.y)-(ia.positionScreen.y-Y.positionScreen.y)*(da.positionScreen.x-Y.positionScreen.x)||0>(da.positionScreen.x-Z.positionScreen.x)* +(ia.positionScreen.y-Z.positionScreen.y)-(da.positionScreen.y-Z.positionScreen.y)*(ia.positionScreen.x-Z.positionScreen.x),la===THREE.DoubleSide||ta===(la===THREE.FrontSide)){if(q===v){var ub=new THREE.RenderableFace4;y.push(ub);v++;q++;n=ub}else n=y[q++];n.v1.copy(Y);n.v2.copy(da);n.v3.copy(Z);n.v4.copy(ia)}else continue;else continue;n.normalModel.copy(aa.normal);!1===ta&&(la===THREE.BackSide||la===THREE.DoubleSide)&&n.normalModel.negate();n.normalModel.applyMatrix3(M).normalize();n.normalModelView.copy(n.normalModel).applyMatrix3(ca); +n.centroidModel.copy(aa.centroid).applyMatrix4(Ca);Z=aa.vertexNormals;Y=0;for(da=Z.length;YB.z&&(x===J?(ta=new THREE.RenderableParticle,G.push(ta),J++,x++,C=ta):C=G[x++],C.object=oa,C.x=B.x/B.w,C.y=B.y/B.w,C.z=B.z,C.rotation=oa.rotation.z,C.scale.x=oa.scale.x*Math.abs(C.x-(B.x+h.projectionMatrix.elements[0])/ +(B.w+h.projectionMatrix.elements[12])),C.scale.y=oa.scale.y*Math.abs(C.y-(B.y+h.projectionMatrix.elements[5])/(B.w+h.projectionMatrix.elements[13])),C.material=oa.material,E.elements.push(C)));!0===Pa&&E.elements.sort(c);return E}};THREE.Face3=function(a,b,c,d,e,f){this.a=a;this.b=b;this.c=c;this.normal=d instanceof THREE.Vector3?d:new THREE.Vector3;this.vertexNormals=d instanceof Array?d:[];this.color=e instanceof THREE.Color?e:new THREE.Color;this.vertexColors=e instanceof Array?e:[];this.vertexTangents=[];this.materialIndex=void 0!==f?f:0;this.centroid=new THREE.Vector3}; +THREE.Face3.prototype={constructor:THREE.Face3,clone:function(){var a=new THREE.Face3(this.a,this.b,this.c);a.normal.copy(this.normal);a.color.copy(this.color);a.centroid.copy(this.centroid);a.materialIndex=this.materialIndex;var b,c;b=0;for(c=this.vertexNormals.length;be?-1:1,f.vertexTangents[d]=new THREE.Vector4(B.x,B.y,B.z,e)}this.hasTangents=!0},computeLineDistances:function(){for(var a=0,b=this.vertices,c=0,d=b.length;ci;i++)if(h[i]==h[(i+1)%3]){e.push(f);break}}else if(a instanceof THREE.Face4){a.a=c[a.a];a.b=c[a.b];a.c=c[a.c];a.d=c[a.d];h=[a.a,a.b,a.c,a.d];d=-1;for(i=0;4>i;i++)h[i]==h[(i+1)%4]&&(0<=d&&e.push(f),d=i);if(0<=d){h.splice(d,1);var l=new THREE.Face3(h[0],h[1],h[2],a.normal,a.color,a.materialIndex);h=0;for(i=this.faceVertexUvs.length;hb.max.x&&(b.max.x=c),db.max.y&&(b.max.y=d),eb.max.z&&(b.max.z=e)}if(void 0===a||0===a.length)this.boundingBox.min.set(0,0,0),this.boundingBox.max.set(0,0,0)},computeBoundingSphere:function(){null===this.boundingSphere&&(this.boundingSphere= +new THREE.Sphere);var a=this.attributes.position.array;if(a){for(var b,c=0,d,e,f=0,g=a.length;fc&&(c=b);this.boundingSphere.radius=Math.sqrt(c)}},computeVertexNormals:function(){if(this.attributes.position){var a,b,c,d;a=this.attributes.position.array.length;if(void 0===this.attributes.normal)this.attributes.normal={itemSize:3,array:new Float32Array(a),numItems:a};else{a=0;for(b=this.attributes.normal.array.length;aqa?-1:1;h[4*a]=U.x;h[4*a+1]=U.y;h[4*a+2]=U.z;h[4*a+3]=M}if(void 0===this.attributes.index||void 0===this.attributes.position|| +void 0===this.attributes.normal||void 0===this.attributes.uv)console.warn("Missing required attributes (index, position, normal or uv) in BufferGeometry.computeTangents()");else{var b=this.attributes.index.array,c=this.attributes.position.array,d=this.attributes.normal.array,e=this.attributes.uv.array,f=c.length/3;if(void 0===this.attributes.tangent){var g=4*f;this.attributes.tangent={itemSize:4,array:new Float32Array(g),numItems:g}}for(var h=this.attributes.tangent.array,i=[],k=[],g=0;ga.length?".":a.join("/"))+"/"},initMaterials:function(a,b){for(var c=[],d=0;da.opacity)i.transparent=a.transparent;void 0!==a.depthTest&&(i.depthTest=a.depthTest);void 0!==a.depthWrite&&(i.depthWrite=a.depthWrite);void 0!==a.visible&&(i.visible=a.visible);void 0!==a.flipSided&&(i.side=THREE.BackSide); +void 0!==a.doubleSided&&(i.side=THREE.DoubleSide);void 0!==a.wireframe&&(i.wireframe=a.wireframe);void 0!==a.vertexColors&&("face"===a.vertexColors?i.vertexColors=THREE.FaceColors:a.vertexColors&&(i.vertexColors=THREE.VertexColors));a.colorDiffuse?i.color=f(a.colorDiffuse):a.DbgColor&&(i.color=a.DbgColor);a.colorSpecular&&(i.specular=f(a.colorSpecular));a.colorAmbient&&(i.ambient=f(a.colorAmbient));a.transparency&&(i.opacity=a.transparency);a.specularCoef&&(i.shininess=a.specularCoef);a.mapDiffuse&& +b&&e(i,"map",a.mapDiffuse,a.mapDiffuseRepeat,a.mapDiffuseOffset,a.mapDiffuseWrap,a.mapDiffuseAnisotropy);a.mapLight&&b&&e(i,"lightMap",a.mapLight,a.mapLightRepeat,a.mapLightOffset,a.mapLightWrap,a.mapLightAnisotropy);a.mapBump&&b&&e(i,"bumpMap",a.mapBump,a.mapBumpRepeat,a.mapBumpOffset,a.mapBumpWrap,a.mapBumpAnisotropy);a.mapNormal&&b&&e(i,"normalMap",a.mapNormal,a.mapNormalRepeat,a.mapNormalOffset,a.mapNormalWrap,a.mapNormalAnisotropy);a.mapSpecular&&b&&e(i,"specularMap",a.mapSpecular,a.mapSpecularRepeat, +a.mapSpecularOffset,a.mapSpecularWrap,a.mapSpecularAnisotropy);a.mapBumpScale&&(i.bumpScale=a.mapBumpScale);a.mapNormal?(h=THREE.ShaderLib.normalmap,k=THREE.UniformsUtils.clone(h.uniforms),k.tNormal.value=i.normalMap,a.mapNormalFactor&&k.uNormalScale.value.set(a.mapNormalFactor,a.mapNormalFactor),i.map&&(k.tDiffuse.value=i.map,k.enableDiffuse.value=!0),i.specularMap&&(k.tSpecular.value=i.specularMap,k.enableSpecular.value=!0),i.lightMap&&(k.tAO.value=i.lightMap,k.enableAO.value=!0),k.uDiffuseColor.value.setHex(i.color), +k.uSpecularColor.value.setHex(i.specular),k.uAmbientColor.value.setHex(i.ambient),k.uShininess.value=i.shininess,void 0!==i.opacity&&(k.uOpacity.value=i.opacity),h=new THREE.ShaderMaterial({fragmentShader:h.fragmentShader,vertexShader:h.vertexShader,uniforms:k,lights:!0,fog:!0}),i.transparent&&(h.transparent=!0)):h=new THREE[h](i);void 0!==a.DbgName&&(h.name=a.DbgName);return h}};THREE.ImageLoader=function(){THREE.EventDispatcher.call(this);this.crossOrigin=null};THREE.ImageLoader.prototype={constructor:THREE.ImageLoader,load:function(a,b){var c=this;void 0===b&&(b=new Image);b.addEventListener("load",function(){c.dispatchEvent({type:"load",content:b})},!1);b.addEventListener("error",function(){c.dispatchEvent({type:"error",message:"Couldn't load URL ["+a+"]"})},!1);c.crossOrigin&&(b.crossOrigin=c.crossOrigin);b.src=a}};THREE.JSONLoader=function(a){THREE.Loader.call(this,a);this.withCredentials=!1};THREE.JSONLoader.prototype=Object.create(THREE.Loader.prototype);THREE.JSONLoader.prototype.load=function(a,b,c){c=c&&"string"===typeof c?c:this.extractUrlBase(a);this.onLoadStart();this.loadAjaxJSON(this,a,b,c)}; +THREE.JSONLoader.prototype.loadAjaxJSON=function(a,b,c,d,e){var f=new XMLHttpRequest,g=0;f.onreadystatechange=function(){if(f.readyState===f.DONE)if(200===f.status||0===f.status){if(f.responseText){var h=JSON.parse(f.responseText);a.createModel(h,c,d)}else console.warn("THREE.JSONLoader: ["+b+"] seems to be unreachable or file there is empty");a.onLoadComplete()}else console.error("THREE.JSONLoader: Couldn't load ["+b+"] ["+f.status+"]");else f.readyState===f.LOADING?e&&(0===g&&(g=f.getResponseHeader("Content-Length")), +e({total:g,loaded:f.responseText.length})):f.readyState===f.HEADERS_RECEIVED&&(g=f.getResponseHeader("Content-Length"))};f.open("GET",b,!0);f.withCredentials=this.withCredentials;f.send(null)}; +THREE.JSONLoader.prototype.createModel=function(a,b,c){var d=new THREE.Geometry,e=void 0!==a.scale?1/a.scale:1,f,g,h,i,k,l,m,n,s,r,p,q,y,v,z,t=a.faces;r=a.vertices;var A=a.normals,I=a.colors,C=0;for(f=0;fF.parameters.opacity&& +(F.parameters.transparent=!0);F.parameters.normalMap?(E=THREE.ShaderLib.normalmap,B=THREE.UniformsUtils.clone(E.uniforms),q=F.parameters.color,W=F.parameters.specular,p=F.parameters.ambient,H=F.parameters.shininess,B.tNormal.value=x.textures[F.parameters.normalMap],F.parameters.normalScale&&B.uNormalScale.value.set(F.parameters.normalScale[0],F.parameters.normalScale[1]),F.parameters.map&&(B.tDiffuse.value=F.parameters.map,B.enableDiffuse.value=!0),F.parameters.envMap&&(B.tCube.value=F.parameters.envMap, +B.enableReflection.value=!0,B.uReflectivity.value=F.parameters.reflectivity),F.parameters.lightMap&&(B.tAO.value=F.parameters.lightMap,B.enableAO.value=!0),F.parameters.specularMap&&(B.tSpecular.value=x.textures[F.parameters.specularMap],B.enableSpecular.value=!0),F.parameters.displacementMap&&(B.tDisplacement.value=x.textures[F.parameters.displacementMap],B.enableDisplacement.value=!0,B.uDisplacementBias.value=F.parameters.displacementBias,B.uDisplacementScale.value=F.parameters.displacementScale), +B.uDiffuseColor.value.setHex(q),B.uSpecularColor.value.setHex(W),B.uAmbientColor.value.setHex(p),B.uShininess.value=H,F.parameters.opacity&&(B.uOpacity.value=F.parameters.opacity),s=new THREE.ShaderMaterial({fragmentShader:E.fragmentShader,vertexShader:E.vertexShader,uniforms:B,lights:!0,fog:!0})):s=new THREE[F.type](F.parameters);x.materials[K]=s}for(K in J.materials)if(F=J.materials[K],F.parameters.materials){L=[];for(q=0;qh.end&&(h.end=e);b||(b=g)}}a.firstAnimation=b}; +THREE.MorphAnimMesh.prototype.setAnimationLabel=function(a,b,c){this.geometry.animations||(this.geometry.animations={});this.geometry.animations[a]={start:b,end:c}};THREE.MorphAnimMesh.prototype.playAnimation=function(a,b){var c=this.geometry.animations[a];c?(this.setFrameRange(c.start,c.end),this.duration=1E3*((c.end-c.start)/b),this.time=0):console.warn("animation["+a+"] undefined")}; +THREE.MorphAnimMesh.prototype.updateAnimation=function(a){var b=this.duration/this.length;this.time+=this.direction*a;if(this.mirroredLoop){if(this.time>this.duration||0>this.time)this.direction*=-1,this.time>this.duration&&(this.time=this.duration,this.directionBackwards=!0),0>this.time&&(this.time=0,this.directionBackwards=!1)}else this.time%=this.duration,0>this.time&&(this.time+=this.duration);a=this.startKeyframe+THREE.Math.clamp(Math.floor(this.time/b),0,this.length-1);a!==this.currentKeyframe&& +(this.morphTargetInfluences[this.lastKeyframe]=0,this.morphTargetInfluences[this.currentKeyframe]=1,this.morphTargetInfluences[a]=0,this.lastKeyframe=this.currentKeyframe,this.currentKeyframe=a);b=this.time%b/b;this.directionBackwards&&(b=1-b);this.morphTargetInfluences[this.currentKeyframe]=b;this.morphTargetInfluences[this.lastKeyframe]=1-b}; +THREE.MorphAnimMesh.prototype.clone=function(a){void 0===a&&(a=new THREE.MorphAnimMesh(this.geometry,this.material));a.duration=this.duration;a.mirroredLoop=this.mirroredLoop;a.time=this.time;a.lastKeyframe=this.lastKeyframe;a.currentKeyframe=this.currentKeyframe;a.direction=this.direction;a.directionBackwards=this.directionBackwards;THREE.Mesh.prototype.clone.call(this,a);return a};THREE.Ribbon=function(a,b){THREE.Object3D.call(this);this.geometry=a;this.material=b};THREE.Ribbon.prototype=Object.create(THREE.Object3D.prototype);THREE.Ribbon.prototype.clone=function(a){void 0===a&&(a=new THREE.Ribbon(this.geometry,this.material));THREE.Object3D.prototype.clone.call(this,a);return a};THREE.LOD=function(){THREE.Object3D.call(this);this.LODs=[]};THREE.LOD.prototype=Object.create(THREE.Object3D.prototype);THREE.LOD.prototype.addLevel=function(a,b){void 0===b&&(b=0);for(var b=Math.abs(b),c=0;c=this.LODs[b].visibleAtDistance)this.LODs[b-1].object3D.visible=!1,this.LODs[b].object3D.visible=!0;else break;for(;bI&&t.clearRect(Ka.min.x|0,Ka.min.y|0,Ka.max.x-Ka.min.x|0,Ka.max.y-Ka.min.y|0),0=j||(j*=f.intensity,c.add(gb.multiplyScalar(j)))}else f instanceof THREE.PointLight&&(g=bb.getPositionFromMatrix(f.matrixWorld),j=b.dot(bb.subVectors(g,a).normalize()),0>=j||(j*=0==f.distance?1:1-Math.min(a.distanceTo(g)/f.distance,1),0!=j&&(j*=f.intensity,c.add(gb.multiplyScalar(j)))))}}function x(a,d,e,f,g,j,h,i){l.info.render.vertices+= +3;l.info.render.faces++;b(i.opacity);c(i.blending);M=a.positionScreen.x;ca=a.positionScreen.y;qa=d.positionScreen.x;ha=d.positionScreen.y;ra=e.positionScreen.x;N=e.positionScreen.y;y(M,ca,qa,ha,ra,N);(i instanceof THREE.MeshLambertMaterial||i instanceof THREE.MeshPhongMaterial)&&null===i.map?(Z.copy(i.color),oa.copy(i.emissive),i.vertexColors===THREE.FaceColors&&Z.multiply(h.color),!0===gc?!1===i.wireframe&&i.shading==THREE.SmoothShading&&3==h.vertexNormalsLength?(pa.copy(vb),Y.copy(vb),da.copy(vb), +q(h.v1.positionWorld,h.vertexNormalsModel[0],pa),q(h.v2.positionWorld,h.vertexNormalsModel[1],Y),q(h.v3.positionWorld,h.vertexNormalsModel[2],da),pa.multiply(Z).add(oa),Y.multiply(Z).add(oa),da.multiply(Z).add(oa),la.addColors(Y,da).multiplyScalar(0.5),Fa=E(pa,Y,da,la),G(M,ca,qa,ha,ra,N,0,0,1,0,0,1,Fa)):(aa.copy(vb),q(h.centroidModel,h.normalModel,aa),aa.multiply(Z).add(oa),!0===i.wireframe?C(aa,i.wireframeLinewidth,i.wireframeLinecap,i.wireframeLinejoin):A(aa)):!0===i.wireframe?C(i.color,i.wireframeLinewidth, +i.wireframeLinecap,i.wireframeLinejoin):A(i.color)):i instanceof THREE.MeshBasicMaterial||i instanceof THREE.MeshLambertMaterial||i instanceof THREE.MeshPhongMaterial?null!==i.map?i.map.mapping instanceof THREE.UVMapping&&(Xa=h.uvs[0],F(M,ca,qa,ha,ra,N,Xa[f].x,Xa[f].y,Xa[g].x,Xa[g].y,Xa[j].x,Xa[j].y,i.map)):null!==i.envMap?i.envMap.mapping instanceof THREE.SphericalReflectionMapping&&(bb.copy(h.vertexNormalsModelView[f]),ub=0.5*bb.x+0.5,Ib=0.5*bb.y+0.5,bb.copy(h.vertexNormalsModelView[g]),Jb=0.5* +bb.x+0.5,fc=0.5*bb.y+0.5,bb.copy(h.vertexNormalsModelView[j]),Ab=0.5*bb.x+0.5,mc=0.5*bb.y+0.5,F(M,ca,qa,ha,ra,N,ub,Ib,Jb,fc,Ab,mc,i.envMap)):(aa.copy(i.color),i.vertexColors===THREE.FaceColors&&aa.multiply(h.color),!0===i.wireframe?C(aa,i.wireframeLinewidth,i.wireframeLinecap,i.wireframeLinejoin):A(aa)):i instanceof THREE.MeshDepthMaterial?(Wa=p.near,ab=p.far,pa.r=pa.g=pa.b=1-k(a.positionScreen.z*a.positionScreen.w,Wa,ab),Y.r=Y.g=Y.b=1-k(d.positionScreen.z*d.positionScreen.w,Wa,ab),da.r=da.g=da.b= +1-k(e.positionScreen.z*e.positionScreen.w,Wa,ab),la.addColors(Y,da).multiplyScalar(0.5),Fa=E(pa,Y,da,la),G(M,ca,qa,ha,ra,N,0,0,1,0,0,1,Fa)):i instanceof THREE.MeshNormalMaterial&&(i.shading==THREE.FlatShading?(a=h.normalModelView,aa.setRGB(a.x,a.y,a.z).multiplyScalar(0.5).addScalar(0.5),!0===i.wireframe?C(aa,i.wireframeLinewidth,i.wireframeLinecap,i.wireframeLinejoin):A(aa)):i.shading==THREE.SmoothShading&&(a=h.vertexNormalsModelView[f],pa.setRGB(a.x,a.y,a.z).multiplyScalar(0.5).addScalar(0.5),a= +h.vertexNormalsModelView[g],Y.setRGB(a.x,a.y,a.z).multiplyScalar(0.5).addScalar(0.5),a=h.vertexNormalsModelView[j],da.setRGB(a.x,a.y,a.z).multiplyScalar(0.5).addScalar(0.5),la.addColors(Y,da).multiplyScalar(0.5),Fa=E(pa,Y,da,la),G(M,ca,qa,ha,ra,N,0,0,1,0,0,1,Fa)))}function y(a,b,c,d,e,f){t.beginPath();t.moveTo(a,b);t.lineTo(c,d);t.lineTo(e,f);t.closePath()}function B(a,b,c,d,e,f,g,j){t.beginPath();t.moveTo(a,b);t.lineTo(c,d);t.lineTo(e,f);t.lineTo(g,j);t.closePath()}function C(a,b,c,j){d(b);e(c); +f(j);g(a.getStyle());t.stroke();Va.expandByScalar(2*b)}function A(a){h(a.getStyle());t.fill()}function F(a,b,c,d,e,f,g,j,i,wa,k,l,n){if(!(n instanceof THREE.DataTexture||void 0===n.image||0==n.image.width)){if(!0===n.needsUpdate){var m=n.wrapS==THREE.RepeatWrapping,hb=n.wrapT==THREE.RepeatWrapping;nb[n.id]=t.createPattern(n.image,!0===m&&!0===hb?"repeat":!0===m&&!1===hb?"repeat-x":!1===m&&!0===hb?"repeat-y":"no-repeat");n.needsUpdate=!1}void 0===nb[n.id]?h("rgba(0,0,0,1)"):h(nb[n.id]);var m=n.offset.x/ +n.repeat.x,hb=n.offset.y/n.repeat.y,p=n.image.width*n.repeat.x,q=n.image.height*n.repeat.y,g=(g+m)*p,j=(1-j+hb)*q,c=c-a,d=d-b,e=e-a,f=f-b,i=(i+m)*p-g,wa=(1-wa+hb)*q-j,k=(k+m)*p-g,l=(1-l+hb)*q-j,m=i*l-k*wa;0===m?(void 0===ia[n.id]&&(b=document.createElement("canvas"),b.width=n.image.width,b.height=n.image.height,b=b.getContext("2d"),b.drawImage(n.image,0,0),ia[n.id]=b.getImageData(0,0,n.image.width,n.image.height).data),b=ia[n.id],g=4*(Math.floor(g)+Math.floor(j)*n.image.width),aa.setRGB(b[g]/255, +b[g+1]/255,b[g+2]/255),A(aa)):(m=1/m,n=(l*c-wa*e)*m,wa=(l*d-wa*f)*m,c=(i*e-k*c)*m,d=(i*f-k*d)*m,a=a-n*g-c*j,g=b-wa*g-d*j,t.save(),t.transform(n,wa,c,d,a,g),t.fill(),t.restore())}}function G(a,b,c,d,e,f,g,j,i,h,wa,k,n){var l,m;l=n.width-1;m=n.height-1;g*=l;j*=m;c-=a;d-=b;e-=a;f-=b;i=i*l-g;h=h*m-j;wa=wa*l-g;k=k*m-j;m=1/(i*k-wa*h);l=(k*c-h*e)*m;h=(k*d-h*f)*m;c=(i*e-wa*c)*m;d=(i*f-wa*d)*m;a=a-l*g-c*j;b=b-h*g-d*j;t.save();t.transform(l,h,c,d,a,b);t.clip();t.drawImage(n,0,0);t.restore()}function E(a,b, +c,d){Ra[0]=255*a.r|0;Ra[1]=255*a.g|0;Ra[2]=255*a.b|0;Ra[4]=255*b.r|0;Ra[5]=255*b.g|0;Ra[6]=255*b.b|0;Ra[8]=255*c.r|0;Ra[9]=255*c.g|0;Ra[10]=255*c.b|0;Ra[12]=255*d.r|0;Ra[13]=255*d.g|0;Ra[14]=255*d.b|0;j.putImageData(yb,0,0);Sa.drawImage(xb,0,0);return cb}function I(a,b){var c=b.x-a.x,d=b.y-a.y,e=c*c+d*d;0!==e&&(e=1/Math.sqrt(e),c*=e,d*=e,b.x+=c,b.y+=d,a.x-=c,a.y-=d)}if(!1===p instanceof THREE.Camera)console.error("THREE.CanvasRenderer.render: camera is not an instance of THREE.Camera.");else{!0=== +this.autoClear&&this.clear();t.setTransform(1,0,0,-1,v,z);l.info.render.vertices=0;l.info.render.faces=0;m=r.projectScene(a,p,this.sortObjects,this.sortElements);n=m.elements;s=m.lights;gc=0>1,nd=H.height>>1,Cb=wa.scale.x*v,Db=wa.scale.y*z,Bb=Cb*md,hb=Db*nd,Va.min.set(X.x-Bb,X.y-hb),Va.max.set(X.x+Bb,X.y+hb),!1!==pb.isIntersectionBox(Va)&&(t.save(),t.translate(X.x,X.y),t.rotate(-wa.rotation),t.scale(Cb,-Db),t.translate(-md,-nd),t.drawImage(H,0,0),t.restore())):P instanceof THREE.ParticleCanvasMaterial&&(Bb=wa.scale.x*v,hb=wa.scale.y*z,Va.min.set(X.x-Bb,X.y-hb),Va.max.set(X.x+Bb,X.y+hb),!1!==pb.isIntersectionBox(Va)&&(g(P.color.getStyle()), +h(P.color.getStyle()),t.save(),t.translate(X.x,X.y),t.rotate(-wa.rotation),t.scale(Bb,hb),P.program(t),t.restore()))}else if(H instanceof THREE.RenderableLine)K=H.v1,L=H.v2,K.positionScreen.x*=v,K.positionScreen.y*=z,L.positionScreen.x*=v,L.positionScreen.y*=z,Va.setFromPoints([K.positionScreen,L.positionScreen]),!0===pb.isIntersectionBox(Va)&&(X=K,wa=L,b(P.opacity),c(P.blending),t.beginPath(),t.moveTo(X.positionScreen.x,X.positionScreen.y),t.lineTo(wa.positionScreen.x,wa.positionScreen.y),P instanceof +THREE.LineBasicMaterial?(d(P.linewidth),e(P.linecap),f(P.linejoin),g(P.color.getStyle()),i(null,null),t.stroke(),Va.expandByScalar(2*P.linewidth)):P instanceof THREE.LineDashedMaterial&&(d(P.linewidth),e(P.linecap),f(P.linejoin),g(P.color.getStyle()),i(P.dashSize,P.gapSize),t.stroke(),Va.expandByScalar(2*P.linewidth)));else if(H instanceof THREE.RenderableFace3){K=H.v1;L=H.v2;U=H.v3;if(-1>K.positionScreen.z||1L.positionScreen.z||1 +U.positionScreen.z||1K.positionScreen.z|| +1L.positionScreen.z||1U.positionScreen.z||1fa.positionScreen.z||1 0\nuniform vec3 directionalLightColor[ MAX_DIR_LIGHTS ];\nuniform vec3 directionalLightDirection[ MAX_DIR_LIGHTS ];\n#endif\n#if MAX_HEMI_LIGHTS > 0\nuniform vec3 hemisphereLightSkyColor[ MAX_HEMI_LIGHTS ];\nuniform vec3 hemisphereLightGroundColor[ MAX_HEMI_LIGHTS ];\nuniform vec3 hemisphereLightDirection[ MAX_HEMI_LIGHTS ];\n#endif\n#if MAX_POINT_LIGHTS > 0\nuniform vec3 pointLightColor[ MAX_POINT_LIGHTS ];\nuniform vec3 pointLightPosition[ MAX_POINT_LIGHTS ];\nuniform float pointLightDistance[ MAX_POINT_LIGHTS ];\n#endif\n#if MAX_SPOT_LIGHTS > 0\nuniform vec3 spotLightColor[ MAX_SPOT_LIGHTS ];\nuniform vec3 spotLightPosition[ MAX_SPOT_LIGHTS ];\nuniform vec3 spotLightDirection[ MAX_SPOT_LIGHTS ];\nuniform float spotLightDistance[ MAX_SPOT_LIGHTS ];\nuniform float spotLightAngleCos[ MAX_SPOT_LIGHTS ];\nuniform float spotLightExponent[ MAX_SPOT_LIGHTS ];\n#endif\n#ifdef WRAP_AROUND\nuniform vec3 wrapRGB;\n#endif", +lights_lambert_vertex:"vLightFront = vec3( 0.0 );\n#ifdef DOUBLE_SIDED\nvLightBack = vec3( 0.0 );\n#endif\ntransformedNormal = normalize( transformedNormal );\n#if MAX_DIR_LIGHTS > 0\nfor( int i = 0; i < MAX_DIR_LIGHTS; i ++ ) {\nvec4 lDirection = viewMatrix * vec4( directionalLightDirection[ i ], 0.0 );\nvec3 dirVector = normalize( lDirection.xyz );\nfloat dotProduct = dot( transformedNormal, dirVector );\nvec3 directionalLightWeighting = vec3( max( dotProduct, 0.0 ) );\n#ifdef DOUBLE_SIDED\nvec3 directionalLightWeightingBack = vec3( max( -dotProduct, 0.0 ) );\n#ifdef WRAP_AROUND\nvec3 directionalLightWeightingHalfBack = vec3( max( -0.5 * dotProduct + 0.5, 0.0 ) );\n#endif\n#endif\n#ifdef WRAP_AROUND\nvec3 directionalLightWeightingHalf = vec3( max( 0.5 * dotProduct + 0.5, 0.0 ) );\ndirectionalLightWeighting = mix( directionalLightWeighting, directionalLightWeightingHalf, wrapRGB );\n#ifdef DOUBLE_SIDED\ndirectionalLightWeightingBack = mix( directionalLightWeightingBack, directionalLightWeightingHalfBack, wrapRGB );\n#endif\n#endif\nvLightFront += directionalLightColor[ i ] * directionalLightWeighting;\n#ifdef DOUBLE_SIDED\nvLightBack += directionalLightColor[ i ] * directionalLightWeightingBack;\n#endif\n}\n#endif\n#if MAX_POINT_LIGHTS > 0\nfor( int i = 0; i < MAX_POINT_LIGHTS; i ++ ) {\nvec4 lPosition = viewMatrix * vec4( pointLightPosition[ i ], 1.0 );\nvec3 lVector = lPosition.xyz - mvPosition.xyz;\nfloat lDistance = 1.0;\nif ( pointLightDistance[ i ] > 0.0 )\nlDistance = 1.0 - min( ( length( lVector ) / pointLightDistance[ i ] ), 1.0 );\nlVector = normalize( lVector );\nfloat dotProduct = dot( transformedNormal, lVector );\nvec3 pointLightWeighting = vec3( max( dotProduct, 0.0 ) );\n#ifdef DOUBLE_SIDED\nvec3 pointLightWeightingBack = vec3( max( -dotProduct, 0.0 ) );\n#ifdef WRAP_AROUND\nvec3 pointLightWeightingHalfBack = vec3( max( -0.5 * dotProduct + 0.5, 0.0 ) );\n#endif\n#endif\n#ifdef WRAP_AROUND\nvec3 pointLightWeightingHalf = vec3( max( 0.5 * dotProduct + 0.5, 0.0 ) );\npointLightWeighting = mix( pointLightWeighting, pointLightWeightingHalf, wrapRGB );\n#ifdef DOUBLE_SIDED\npointLightWeightingBack = mix( pointLightWeightingBack, pointLightWeightingHalfBack, wrapRGB );\n#endif\n#endif\nvLightFront += pointLightColor[ i ] * pointLightWeighting * lDistance;\n#ifdef DOUBLE_SIDED\nvLightBack += pointLightColor[ i ] * pointLightWeightingBack * lDistance;\n#endif\n}\n#endif\n#if MAX_SPOT_LIGHTS > 0\nfor( int i = 0; i < MAX_SPOT_LIGHTS; i ++ ) {\nvec4 lPosition = viewMatrix * vec4( spotLightPosition[ i ], 1.0 );\nvec3 lVector = lPosition.xyz - mvPosition.xyz;\nfloat spotEffect = dot( spotLightDirection[ i ], normalize( spotLightPosition[ i ] - worldPosition.xyz ) );\nif ( spotEffect > spotLightAngleCos[ i ] ) {\nspotEffect = max( pow( spotEffect, spotLightExponent[ i ] ), 0.0 );\nfloat lDistance = 1.0;\nif ( spotLightDistance[ i ] > 0.0 )\nlDistance = 1.0 - min( ( length( lVector ) / spotLightDistance[ i ] ), 1.0 );\nlVector = normalize( lVector );\nfloat dotProduct = dot( transformedNormal, lVector );\nvec3 spotLightWeighting = vec3( max( dotProduct, 0.0 ) );\n#ifdef DOUBLE_SIDED\nvec3 spotLightWeightingBack = vec3( max( -dotProduct, 0.0 ) );\n#ifdef WRAP_AROUND\nvec3 spotLightWeightingHalfBack = vec3( max( -0.5 * dotProduct + 0.5, 0.0 ) );\n#endif\n#endif\n#ifdef WRAP_AROUND\nvec3 spotLightWeightingHalf = vec3( max( 0.5 * dotProduct + 0.5, 0.0 ) );\nspotLightWeighting = mix( spotLightWeighting, spotLightWeightingHalf, wrapRGB );\n#ifdef DOUBLE_SIDED\nspotLightWeightingBack = mix( spotLightWeightingBack, spotLightWeightingHalfBack, wrapRGB );\n#endif\n#endif\nvLightFront += spotLightColor[ i ] * spotLightWeighting * lDistance * spotEffect;\n#ifdef DOUBLE_SIDED\nvLightBack += spotLightColor[ i ] * spotLightWeightingBack * lDistance * spotEffect;\n#endif\n}\n}\n#endif\n#if MAX_HEMI_LIGHTS > 0\nfor( int i = 0; i < MAX_HEMI_LIGHTS; i ++ ) {\nvec4 lDirection = viewMatrix * vec4( hemisphereLightDirection[ i ], 0.0 );\nvec3 lVector = normalize( lDirection.xyz );\nfloat dotProduct = dot( transformedNormal, lVector );\nfloat hemiDiffuseWeight = 0.5 * dotProduct + 0.5;\nfloat hemiDiffuseWeightBack = -0.5 * dotProduct + 0.5;\nvLightFront += mix( hemisphereLightGroundColor[ i ], hemisphereLightSkyColor[ i ], hemiDiffuseWeight );\n#ifdef DOUBLE_SIDED\nvLightBack += mix( hemisphereLightGroundColor[ i ], hemisphereLightSkyColor[ i ], hemiDiffuseWeightBack );\n#endif\n}\n#endif\nvLightFront = vLightFront * diffuse + ambient * ambientLightColor + emissive;\n#ifdef DOUBLE_SIDED\nvLightBack = vLightBack * diffuse + ambient * ambientLightColor + emissive;\n#endif", +lights_phong_pars_vertex:"#ifndef PHONG_PER_PIXEL\n#if MAX_POINT_LIGHTS > 0\nuniform vec3 pointLightPosition[ MAX_POINT_LIGHTS ];\nuniform float pointLightDistance[ MAX_POINT_LIGHTS ];\nvarying vec4 vPointLight[ MAX_POINT_LIGHTS ];\n#endif\n#if MAX_SPOT_LIGHTS > 0\nuniform vec3 spotLightPosition[ MAX_SPOT_LIGHTS ];\nuniform float spotLightDistance[ MAX_SPOT_LIGHTS ];\nvarying vec4 vSpotLight[ MAX_SPOT_LIGHTS ];\n#endif\n#endif\n#if MAX_SPOT_LIGHTS > 0 || defined( USE_BUMPMAP )\nvarying vec3 vWorldPosition;\n#endif", +lights_phong_vertex:"#ifndef PHONG_PER_PIXEL\n#if MAX_POINT_LIGHTS > 0\nfor( int i = 0; i < MAX_POINT_LIGHTS; i ++ ) {\nvec4 lPosition = viewMatrix * vec4( pointLightPosition[ i ], 1.0 );\nvec3 lVector = lPosition.xyz - mvPosition.xyz;\nfloat lDistance = 1.0;\nif ( pointLightDistance[ i ] > 0.0 )\nlDistance = 1.0 - min( ( length( lVector ) / pointLightDistance[ i ] ), 1.0 );\nvPointLight[ i ] = vec4( lVector, lDistance );\n}\n#endif\n#if MAX_SPOT_LIGHTS > 0\nfor( int i = 0; i < MAX_SPOT_LIGHTS; i ++ ) {\nvec4 lPosition = viewMatrix * vec4( spotLightPosition[ i ], 1.0 );\nvec3 lVector = lPosition.xyz - mvPosition.xyz;\nfloat lDistance = 1.0;\nif ( spotLightDistance[ i ] > 0.0 )\nlDistance = 1.0 - min( ( length( lVector ) / spotLightDistance[ i ] ), 1.0 );\nvSpotLight[ i ] = vec4( lVector, lDistance );\n}\n#endif\n#endif\n#if MAX_SPOT_LIGHTS > 0 || defined( USE_BUMPMAP )\nvWorldPosition = worldPosition.xyz;\n#endif", +lights_phong_pars_fragment:"uniform vec3 ambientLightColor;\n#if MAX_DIR_LIGHTS > 0\nuniform vec3 directionalLightColor[ MAX_DIR_LIGHTS ];\nuniform vec3 directionalLightDirection[ MAX_DIR_LIGHTS ];\n#endif\n#if MAX_HEMI_LIGHTS > 0\nuniform vec3 hemisphereLightSkyColor[ MAX_HEMI_LIGHTS ];\nuniform vec3 hemisphereLightGroundColor[ MAX_HEMI_LIGHTS ];\nuniform vec3 hemisphereLightDirection[ MAX_HEMI_LIGHTS ];\n#endif\n#if MAX_POINT_LIGHTS > 0\nuniform vec3 pointLightColor[ MAX_POINT_LIGHTS ];\n#ifdef PHONG_PER_PIXEL\nuniform vec3 pointLightPosition[ MAX_POINT_LIGHTS ];\nuniform float pointLightDistance[ MAX_POINT_LIGHTS ];\n#else\nvarying vec4 vPointLight[ MAX_POINT_LIGHTS ];\n#endif\n#endif\n#if MAX_SPOT_LIGHTS > 0\nuniform vec3 spotLightColor[ MAX_SPOT_LIGHTS ];\nuniform vec3 spotLightPosition[ MAX_SPOT_LIGHTS ];\nuniform vec3 spotLightDirection[ MAX_SPOT_LIGHTS ];\nuniform float spotLightAngleCos[ MAX_SPOT_LIGHTS ];\nuniform float spotLightExponent[ MAX_SPOT_LIGHTS ];\n#ifdef PHONG_PER_PIXEL\nuniform float spotLightDistance[ MAX_SPOT_LIGHTS ];\n#else\nvarying vec4 vSpotLight[ MAX_SPOT_LIGHTS ];\n#endif\n#endif\n#if MAX_SPOT_LIGHTS > 0 || defined( USE_BUMPMAP )\nvarying vec3 vWorldPosition;\n#endif\n#ifdef WRAP_AROUND\nuniform vec3 wrapRGB;\n#endif\nvarying vec3 vViewPosition;\nvarying vec3 vNormal;", +lights_phong_fragment:"vec3 normal = normalize( vNormal );\nvec3 viewPosition = normalize( vViewPosition );\n#ifdef DOUBLE_SIDED\nnormal = normal * ( -1.0 + 2.0 * float( gl_FrontFacing ) );\n#endif\n#ifdef USE_NORMALMAP\nnormal = perturbNormal2Arb( -viewPosition, normal );\n#elif defined( USE_BUMPMAP )\nnormal = perturbNormalArb( -vViewPosition, normal, dHdxy_fwd() );\n#endif\n#if MAX_POINT_LIGHTS > 0\nvec3 pointDiffuse = vec3( 0.0 );\nvec3 pointSpecular = vec3( 0.0 );\nfor ( int i = 0; i < MAX_POINT_LIGHTS; i ++ ) {\n#ifdef PHONG_PER_PIXEL\nvec4 lPosition = viewMatrix * vec4( pointLightPosition[ i ], 1.0 );\nvec3 lVector = lPosition.xyz + vViewPosition.xyz;\nfloat lDistance = 1.0;\nif ( pointLightDistance[ i ] > 0.0 )\nlDistance = 1.0 - min( ( length( lVector ) / pointLightDistance[ i ] ), 1.0 );\nlVector = normalize( lVector );\n#else\nvec3 lVector = normalize( vPointLight[ i ].xyz );\nfloat lDistance = vPointLight[ i ].w;\n#endif\nfloat dotProduct = dot( normal, lVector );\n#ifdef WRAP_AROUND\nfloat pointDiffuseWeightFull = max( dotProduct, 0.0 );\nfloat pointDiffuseWeightHalf = max( 0.5 * dotProduct + 0.5, 0.0 );\nvec3 pointDiffuseWeight = mix( vec3 ( pointDiffuseWeightFull ), vec3( pointDiffuseWeightHalf ), wrapRGB );\n#else\nfloat pointDiffuseWeight = max( dotProduct, 0.0 );\n#endif\npointDiffuse += diffuse * pointLightColor[ i ] * pointDiffuseWeight * lDistance;\nvec3 pointHalfVector = normalize( lVector + viewPosition );\nfloat pointDotNormalHalf = max( dot( normal, pointHalfVector ), 0.0 );\nfloat pointSpecularWeight = specularStrength * max( pow( pointDotNormalHalf, shininess ), 0.0 );\n#ifdef PHYSICALLY_BASED_SHADING\nfloat specularNormalization = ( shininess + 2.0001 ) / 8.0;\nvec3 schlick = specular + vec3( 1.0 - specular ) * pow( 1.0 - dot( lVector, pointHalfVector ), 5.0 );\npointSpecular += schlick * pointLightColor[ i ] * pointSpecularWeight * pointDiffuseWeight * lDistance * specularNormalization;\n#else\npointSpecular += specular * pointLightColor[ i ] * pointSpecularWeight * pointDiffuseWeight * lDistance;\n#endif\n}\n#endif\n#if MAX_SPOT_LIGHTS > 0\nvec3 spotDiffuse = vec3( 0.0 );\nvec3 spotSpecular = vec3( 0.0 );\nfor ( int i = 0; i < MAX_SPOT_LIGHTS; i ++ ) {\n#ifdef PHONG_PER_PIXEL\nvec4 lPosition = viewMatrix * vec4( spotLightPosition[ i ], 1.0 );\nvec3 lVector = lPosition.xyz + vViewPosition.xyz;\nfloat lDistance = 1.0;\nif ( spotLightDistance[ i ] > 0.0 )\nlDistance = 1.0 - min( ( length( lVector ) / spotLightDistance[ i ] ), 1.0 );\nlVector = normalize( lVector );\n#else\nvec3 lVector = normalize( vSpotLight[ i ].xyz );\nfloat lDistance = vSpotLight[ i ].w;\n#endif\nfloat spotEffect = dot( spotLightDirection[ i ], normalize( spotLightPosition[ i ] - vWorldPosition ) );\nif ( spotEffect > spotLightAngleCos[ i ] ) {\nspotEffect = max( pow( spotEffect, spotLightExponent[ i ] ), 0.0 );\nfloat dotProduct = dot( normal, lVector );\n#ifdef WRAP_AROUND\nfloat spotDiffuseWeightFull = max( dotProduct, 0.0 );\nfloat spotDiffuseWeightHalf = max( 0.5 * dotProduct + 0.5, 0.0 );\nvec3 spotDiffuseWeight = mix( vec3 ( spotDiffuseWeightFull ), vec3( spotDiffuseWeightHalf ), wrapRGB );\n#else\nfloat spotDiffuseWeight = max( dotProduct, 0.0 );\n#endif\nspotDiffuse += diffuse * spotLightColor[ i ] * spotDiffuseWeight * lDistance * spotEffect;\nvec3 spotHalfVector = normalize( lVector + viewPosition );\nfloat spotDotNormalHalf = max( dot( normal, spotHalfVector ), 0.0 );\nfloat spotSpecularWeight = specularStrength * max( pow( spotDotNormalHalf, shininess ), 0.0 );\n#ifdef PHYSICALLY_BASED_SHADING\nfloat specularNormalization = ( shininess + 2.0001 ) / 8.0;\nvec3 schlick = specular + vec3( 1.0 - specular ) * pow( 1.0 - dot( lVector, spotHalfVector ), 5.0 );\nspotSpecular += schlick * spotLightColor[ i ] * spotSpecularWeight * spotDiffuseWeight * lDistance * specularNormalization * spotEffect;\n#else\nspotSpecular += specular * spotLightColor[ i ] * spotSpecularWeight * spotDiffuseWeight * lDistance * spotEffect;\n#endif\n}\n}\n#endif\n#if MAX_DIR_LIGHTS > 0\nvec3 dirDiffuse = vec3( 0.0 );\nvec3 dirSpecular = vec3( 0.0 );\nfor( int i = 0; i < MAX_DIR_LIGHTS; i ++ ) {\nvec4 lDirection = viewMatrix * vec4( directionalLightDirection[ i ], 0.0 );\nvec3 dirVector = normalize( lDirection.xyz );\nfloat dotProduct = dot( normal, dirVector );\n#ifdef WRAP_AROUND\nfloat dirDiffuseWeightFull = max( dotProduct, 0.0 );\nfloat dirDiffuseWeightHalf = max( 0.5 * dotProduct + 0.5, 0.0 );\nvec3 dirDiffuseWeight = mix( vec3( dirDiffuseWeightFull ), vec3( dirDiffuseWeightHalf ), wrapRGB );\n#else\nfloat dirDiffuseWeight = max( dotProduct, 0.0 );\n#endif\ndirDiffuse += diffuse * directionalLightColor[ i ] * dirDiffuseWeight;\nvec3 dirHalfVector = normalize( dirVector + viewPosition );\nfloat dirDotNormalHalf = max( dot( normal, dirHalfVector ), 0.0 );\nfloat dirSpecularWeight = specularStrength * max( pow( dirDotNormalHalf, shininess ), 0.0 );\n#ifdef PHYSICALLY_BASED_SHADING\nfloat specularNormalization = ( shininess + 2.0001 ) / 8.0;\nvec3 schlick = specular + vec3( 1.0 - specular ) * pow( 1.0 - dot( dirVector, dirHalfVector ), 5.0 );\ndirSpecular += schlick * directionalLightColor[ i ] * dirSpecularWeight * dirDiffuseWeight * specularNormalization;\n#else\ndirSpecular += specular * directionalLightColor[ i ] * dirSpecularWeight * dirDiffuseWeight;\n#endif\n}\n#endif\n#if MAX_HEMI_LIGHTS > 0\nvec3 hemiDiffuse = vec3( 0.0 );\nvec3 hemiSpecular = vec3( 0.0 );\nfor( int i = 0; i < MAX_HEMI_LIGHTS; i ++ ) {\nvec4 lDirection = viewMatrix * vec4( hemisphereLightDirection[ i ], 0.0 );\nvec3 lVector = normalize( lDirection.xyz );\nfloat dotProduct = dot( normal, lVector );\nfloat hemiDiffuseWeight = 0.5 * dotProduct + 0.5;\nvec3 hemiColor = mix( hemisphereLightGroundColor[ i ], hemisphereLightSkyColor[ i ], hemiDiffuseWeight );\nhemiDiffuse += diffuse * hemiColor;\nvec3 hemiHalfVectorSky = normalize( lVector + viewPosition );\nfloat hemiDotNormalHalfSky = 0.5 * dot( normal, hemiHalfVectorSky ) + 0.5;\nfloat hemiSpecularWeightSky = specularStrength * max( pow( hemiDotNormalHalfSky, shininess ), 0.0 );\nvec3 lVectorGround = -lVector;\nvec3 hemiHalfVectorGround = normalize( lVectorGround + viewPosition );\nfloat hemiDotNormalHalfGround = 0.5 * dot( normal, hemiHalfVectorGround ) + 0.5;\nfloat hemiSpecularWeightGround = specularStrength * max( pow( hemiDotNormalHalfGround, shininess ), 0.0 );\n#ifdef PHYSICALLY_BASED_SHADING\nfloat dotProductGround = dot( normal, lVectorGround );\nfloat specularNormalization = ( shininess + 2.0001 ) / 8.0;\nvec3 schlickSky = specular + vec3( 1.0 - specular ) * pow( 1.0 - dot( lVector, hemiHalfVectorSky ), 5.0 );\nvec3 schlickGround = specular + vec3( 1.0 - specular ) * pow( 1.0 - dot( lVectorGround, hemiHalfVectorGround ), 5.0 );\nhemiSpecular += hemiColor * specularNormalization * ( schlickSky * hemiSpecularWeightSky * max( dotProduct, 0.0 ) + schlickGround * hemiSpecularWeightGround * max( dotProductGround, 0.0 ) );\n#else\nhemiSpecular += specular * hemiColor * ( hemiSpecularWeightSky + hemiSpecularWeightGround ) * hemiDiffuseWeight;\n#endif\n}\n#endif\nvec3 totalDiffuse = vec3( 0.0 );\nvec3 totalSpecular = vec3( 0.0 );\n#if MAX_DIR_LIGHTS > 0\ntotalDiffuse += dirDiffuse;\ntotalSpecular += dirSpecular;\n#endif\n#if MAX_HEMI_LIGHTS > 0\ntotalDiffuse += hemiDiffuse;\ntotalSpecular += hemiSpecular;\n#endif\n#if MAX_POINT_LIGHTS > 0\ntotalDiffuse += pointDiffuse;\ntotalSpecular += pointSpecular;\n#endif\n#if MAX_SPOT_LIGHTS > 0\ntotalDiffuse += spotDiffuse;\ntotalSpecular += spotSpecular;\n#endif\n#ifdef METAL\ngl_FragColor.xyz = gl_FragColor.xyz * ( emissive + totalDiffuse + ambientLightColor * ambient + totalSpecular );\n#else\ngl_FragColor.xyz = gl_FragColor.xyz * ( emissive + totalDiffuse + ambientLightColor * ambient ) + totalSpecular;\n#endif", +color_pars_fragment:"#ifdef USE_COLOR\nvarying vec3 vColor;\n#endif",color_fragment:"#ifdef USE_COLOR\ngl_FragColor = gl_FragColor * vec4( vColor, opacity );\n#endif",color_pars_vertex:"#ifdef USE_COLOR\nvarying vec3 vColor;\n#endif",color_vertex:"#ifdef USE_COLOR\n#ifdef GAMMA_INPUT\nvColor = color * color;\n#else\nvColor = color;\n#endif\n#endif",skinning_pars_vertex:"#ifdef USE_SKINNING\n#ifdef BONE_TEXTURE\nuniform sampler2D boneTexture;\nmat4 getBoneMatrix( const in float i ) {\nfloat j = i * 4.0;\nfloat x = mod( j, N_BONE_PIXEL_X );\nfloat y = floor( j / N_BONE_PIXEL_X );\nconst float dx = 1.0 / N_BONE_PIXEL_X;\nconst float dy = 1.0 / N_BONE_PIXEL_Y;\ny = dy * ( y + 0.5 );\nvec4 v1 = texture2D( boneTexture, vec2( dx * ( x + 0.5 ), y ) );\nvec4 v2 = texture2D( boneTexture, vec2( dx * ( x + 1.5 ), y ) );\nvec4 v3 = texture2D( boneTexture, vec2( dx * ( x + 2.5 ), y ) );\nvec4 v4 = texture2D( boneTexture, vec2( dx * ( x + 3.5 ), y ) );\nmat4 bone = mat4( v1, v2, v3, v4 );\nreturn bone;\n}\n#else\nuniform mat4 boneGlobalMatrices[ MAX_BONES ];\nmat4 getBoneMatrix( const in float i ) {\nmat4 bone = boneGlobalMatrices[ int(i) ];\nreturn bone;\n}\n#endif\n#endif", +skinbase_vertex:"#ifdef USE_SKINNING\nmat4 boneMatX = getBoneMatrix( skinIndex.x );\nmat4 boneMatY = getBoneMatrix( skinIndex.y );\n#endif",skinning_vertex:"#ifdef USE_SKINNING\n#ifdef USE_MORPHTARGETS\nvec4 skinVertex = vec4( morphed, 1.0 );\n#else\nvec4 skinVertex = vec4( position, 1.0 );\n#endif\nvec4 skinned = boneMatX * skinVertex * skinWeight.x;\nskinned \t += boneMatY * skinVertex * skinWeight.y;\n#endif",morphtarget_pars_vertex:"#ifdef USE_MORPHTARGETS\n#ifndef USE_MORPHNORMALS\nuniform float morphTargetInfluences[ 8 ];\n#else\nuniform float morphTargetInfluences[ 4 ];\n#endif\n#endif", +morphtarget_vertex:"#ifdef USE_MORPHTARGETS\nvec3 morphed = vec3( 0.0 );\nmorphed += ( morphTarget0 - position ) * morphTargetInfluences[ 0 ];\nmorphed += ( morphTarget1 - position ) * morphTargetInfluences[ 1 ];\nmorphed += ( morphTarget2 - position ) * morphTargetInfluences[ 2 ];\nmorphed += ( morphTarget3 - position ) * morphTargetInfluences[ 3 ];\n#ifndef USE_MORPHNORMALS\nmorphed += ( morphTarget4 - position ) * morphTargetInfluences[ 4 ];\nmorphed += ( morphTarget5 - position ) * morphTargetInfluences[ 5 ];\nmorphed += ( morphTarget6 - position ) * morphTargetInfluences[ 6 ];\nmorphed += ( morphTarget7 - position ) * morphTargetInfluences[ 7 ];\n#endif\nmorphed += position;\n#endif", +default_vertex:"vec4 mvPosition;\n#ifdef USE_SKINNING\nmvPosition = modelViewMatrix * skinned;\n#endif\n#if !defined( USE_SKINNING ) && defined( USE_MORPHTARGETS )\nmvPosition = modelViewMatrix * vec4( morphed, 1.0 );\n#endif\n#if !defined( USE_SKINNING ) && ! defined( USE_MORPHTARGETS )\nmvPosition = modelViewMatrix * vec4( position, 1.0 );\n#endif\ngl_Position = projectionMatrix * mvPosition;",morphnormal_vertex:"#ifdef USE_MORPHNORMALS\nvec3 morphedNormal = vec3( 0.0 );\nmorphedNormal += ( morphNormal0 - normal ) * morphTargetInfluences[ 0 ];\nmorphedNormal += ( morphNormal1 - normal ) * morphTargetInfluences[ 1 ];\nmorphedNormal += ( morphNormal2 - normal ) * morphTargetInfluences[ 2 ];\nmorphedNormal += ( morphNormal3 - normal ) * morphTargetInfluences[ 3 ];\nmorphedNormal += normal;\n#endif", +skinnormal_vertex:"#ifdef USE_SKINNING\nmat4 skinMatrix = skinWeight.x * boneMatX;\nskinMatrix \t+= skinWeight.y * boneMatY;\n#ifdef USE_MORPHNORMALS\nvec4 skinnedNormal = skinMatrix * vec4( morphedNormal, 0.0 );\n#else\nvec4 skinnedNormal = skinMatrix * vec4( normal, 0.0 );\n#endif\n#endif",defaultnormal_vertex:"vec3 objectNormal;\n#ifdef USE_SKINNING\nobjectNormal = skinnedNormal.xyz;\n#endif\n#if !defined( USE_SKINNING ) && defined( USE_MORPHNORMALS )\nobjectNormal = morphedNormal;\n#endif\n#if !defined( USE_SKINNING ) && ! defined( USE_MORPHNORMALS )\nobjectNormal = normal;\n#endif\n#ifdef FLIP_SIDED\nobjectNormal = -objectNormal;\n#endif\nvec3 transformedNormal = normalMatrix * objectNormal;", +shadowmap_pars_fragment:"#ifdef USE_SHADOWMAP\nuniform sampler2D shadowMap[ MAX_SHADOWS ];\nuniform vec2 shadowMapSize[ MAX_SHADOWS ];\nuniform float shadowDarkness[ MAX_SHADOWS ];\nuniform float shadowBias[ MAX_SHADOWS ];\nvarying vec4 vShadowCoord[ MAX_SHADOWS ];\nfloat unpackDepth( const in vec4 rgba_depth ) {\nconst vec4 bit_shift = vec4( 1.0 / ( 256.0 * 256.0 * 256.0 ), 1.0 / ( 256.0 * 256.0 ), 1.0 / 256.0, 1.0 );\nfloat depth = dot( rgba_depth, bit_shift );\nreturn depth;\n}\n#endif",shadowmap_fragment:"#ifdef USE_SHADOWMAP\n#ifdef SHADOWMAP_DEBUG\nvec3 frustumColors[3];\nfrustumColors[0] = vec3( 1.0, 0.5, 0.0 );\nfrustumColors[1] = vec3( 0.0, 1.0, 0.8 );\nfrustumColors[2] = vec3( 0.0, 0.5, 1.0 );\n#endif\n#ifdef SHADOWMAP_CASCADE\nint inFrustumCount = 0;\n#endif\nfloat fDepth;\nvec3 shadowColor = vec3( 1.0 );\nfor( int i = 0; i < MAX_SHADOWS; i ++ ) {\nvec3 shadowCoord = vShadowCoord[ i ].xyz / vShadowCoord[ i ].w;\nbvec4 inFrustumVec = bvec4 ( shadowCoord.x >= 0.0, shadowCoord.x <= 1.0, shadowCoord.y >= 0.0, shadowCoord.y <= 1.0 );\nbool inFrustum = all( inFrustumVec );\n#ifdef SHADOWMAP_CASCADE\ninFrustumCount += int( inFrustum );\nbvec3 frustumTestVec = bvec3( inFrustum, inFrustumCount == 1, shadowCoord.z <= 1.0 );\n#else\nbvec2 frustumTestVec = bvec2( inFrustum, shadowCoord.z <= 1.0 );\n#endif\nbool frustumTest = all( frustumTestVec );\nif ( frustumTest ) {\nshadowCoord.z += shadowBias[ i ];\n#if defined( SHADOWMAP_TYPE_PCF )\nfloat shadow = 0.0;\nconst float shadowDelta = 1.0 / 9.0;\nfloat xPixelOffset = 1.0 / shadowMapSize[ i ].x;\nfloat yPixelOffset = 1.0 / shadowMapSize[ i ].y;\nfloat dx0 = -1.25 * xPixelOffset;\nfloat dy0 = -1.25 * yPixelOffset;\nfloat dx1 = 1.25 * xPixelOffset;\nfloat dy1 = 1.25 * yPixelOffset;\nfDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx0, dy0 ) ) );\nif ( fDepth < shadowCoord.z ) shadow += shadowDelta;\nfDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( 0.0, dy0 ) ) );\nif ( fDepth < shadowCoord.z ) shadow += shadowDelta;\nfDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx1, dy0 ) ) );\nif ( fDepth < shadowCoord.z ) shadow += shadowDelta;\nfDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx0, 0.0 ) ) );\nif ( fDepth < shadowCoord.z ) shadow += shadowDelta;\nfDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy ) );\nif ( fDepth < shadowCoord.z ) shadow += shadowDelta;\nfDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx1, 0.0 ) ) );\nif ( fDepth < shadowCoord.z ) shadow += shadowDelta;\nfDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx0, dy1 ) ) );\nif ( fDepth < shadowCoord.z ) shadow += shadowDelta;\nfDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( 0.0, dy1 ) ) );\nif ( fDepth < shadowCoord.z ) shadow += shadowDelta;\nfDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx1, dy1 ) ) );\nif ( fDepth < shadowCoord.z ) shadow += shadowDelta;\nshadowColor = shadowColor * vec3( ( 1.0 - shadowDarkness[ i ] * shadow ) );\n#elif defined( SHADOWMAP_TYPE_PCF_SOFT )\nfloat shadow = 0.0;\nfloat xPixelOffset = 1.0 / shadowMapSize[ i ].x;\nfloat yPixelOffset = 1.0 / shadowMapSize[ i ].y;\nfloat dx0 = -1.0 * xPixelOffset;\nfloat dy0 = -1.0 * yPixelOffset;\nfloat dx1 = 1.0 * xPixelOffset;\nfloat dy1 = 1.0 * yPixelOffset;\nmat3 shadowKernel;\nmat3 depthKernel;\ndepthKernel[0][0] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx0, dy0 ) ) );\nif ( depthKernel[0][0] < shadowCoord.z ) shadowKernel[0][0] = 0.25;\nelse shadowKernel[0][0] = 0.0;\ndepthKernel[0][1] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx0, 0.0 ) ) );\nif ( depthKernel[0][1] < shadowCoord.z ) shadowKernel[0][1] = 0.25;\nelse shadowKernel[0][1] = 0.0;\ndepthKernel[0][2] = unpackDepth( texture2D( shadowMap[ i], shadowCoord.xy + vec2( dx0, dy1 ) ) );\nif ( depthKernel[0][2] < shadowCoord.z ) shadowKernel[0][2] = 0.25;\nelse shadowKernel[0][2] = 0.0;\ndepthKernel[1][0] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( 0.0, dy0 ) ) );\nif ( depthKernel[1][0] < shadowCoord.z ) shadowKernel[1][0] = 0.25;\nelse shadowKernel[1][0] = 0.0;\ndepthKernel[1][1] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy ) );\nif ( depthKernel[1][1] < shadowCoord.z ) shadowKernel[1][1] = 0.25;\nelse shadowKernel[1][1] = 0.0;\ndepthKernel[1][2] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( 0.0, dy1 ) ) );\nif ( depthKernel[1][2] < shadowCoord.z ) shadowKernel[1][2] = 0.25;\nelse shadowKernel[1][2] = 0.0;\ndepthKernel[2][0] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx1, dy0 ) ) );\nif ( depthKernel[2][0] < shadowCoord.z ) shadowKernel[2][0] = 0.25;\nelse shadowKernel[2][0] = 0.0;\ndepthKernel[2][1] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx1, 0.0 ) ) );\nif ( depthKernel[2][1] < shadowCoord.z ) shadowKernel[2][1] = 0.25;\nelse shadowKernel[2][1] = 0.0;\ndepthKernel[2][2] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx1, dy1 ) ) );\nif ( depthKernel[2][2] < shadowCoord.z ) shadowKernel[2][2] = 0.25;\nelse shadowKernel[2][2] = 0.0;\nvec2 fractionalCoord = 1.0 - fract( shadowCoord.xy * shadowMapSize[i].xy );\nshadowKernel[0] = mix( shadowKernel[1], shadowKernel[0], fractionalCoord.x );\nshadowKernel[1] = mix( shadowKernel[2], shadowKernel[1], fractionalCoord.x );\nvec4 shadowValues;\nshadowValues.x = mix( shadowKernel[0][1], shadowKernel[0][0], fractionalCoord.y );\nshadowValues.y = mix( shadowKernel[0][2], shadowKernel[0][1], fractionalCoord.y );\nshadowValues.z = mix( shadowKernel[1][1], shadowKernel[1][0], fractionalCoord.y );\nshadowValues.w = mix( shadowKernel[1][2], shadowKernel[1][1], fractionalCoord.y );\nshadow = dot( shadowValues, vec4( 1.0 ) );\nshadowColor = shadowColor * vec3( ( 1.0 - shadowDarkness[ i ] * shadow ) );\n#else\nvec4 rgbaDepth = texture2D( shadowMap[ i ], shadowCoord.xy );\nfloat fDepth = unpackDepth( rgbaDepth );\nif ( fDepth < shadowCoord.z )\nshadowColor = shadowColor * vec3( 1.0 - shadowDarkness[ i ] );\n#endif\n}\n#ifdef SHADOWMAP_DEBUG\n#ifdef SHADOWMAP_CASCADE\nif ( inFrustum && inFrustumCount == 1 ) gl_FragColor.xyz *= frustumColors[ i ];\n#else\nif ( inFrustum ) gl_FragColor.xyz *= frustumColors[ i ];\n#endif\n#endif\n}\n#ifdef GAMMA_OUTPUT\nshadowColor *= shadowColor;\n#endif\ngl_FragColor.xyz = gl_FragColor.xyz * shadowColor;\n#endif", +shadowmap_pars_vertex:"#ifdef USE_SHADOWMAP\nvarying vec4 vShadowCoord[ MAX_SHADOWS ];\nuniform mat4 shadowMatrix[ MAX_SHADOWS ];\n#endif",shadowmap_vertex:"#ifdef USE_SHADOWMAP\nfor( int i = 0; i < MAX_SHADOWS; i ++ ) {\nvShadowCoord[ i ] = shadowMatrix[ i ] * worldPosition;\n}\n#endif",alphatest_fragment:"#ifdef ALPHATEST\nif ( gl_FragColor.a < ALPHATEST ) discard;\n#endif",linear_to_gamma_fragment:"#ifdef GAMMA_OUTPUT\ngl_FragColor.xyz = sqrt( gl_FragColor.xyz );\n#endif"}; +THREE.UniformsUtils={merge:function(a){var b,c,d,e={};for(b=0;b dashSize ) {\ndiscard;\n}\ngl_FragColor = vec4( diffuse, opacity );",THREE.ShaderChunk.color_fragment,THREE.ShaderChunk.fog_fragment,"}"].join("\n")},depth:{uniforms:{mNear:{type:"f",value:1},mFar:{type:"f",value:2E3},opacity:{type:"f", +value:1}},vertexShader:"void main() {\ngl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n}",fragmentShader:"uniform float mNear;\nuniform float mFar;\nuniform float opacity;\nvoid main() {\nfloat depth = gl_FragCoord.z / gl_FragCoord.w;\nfloat color = 1.0 - smoothstep( mNear, mFar, depth );\ngl_FragColor = vec4( vec3( color ), opacity );\n}"},normal:{uniforms:{opacity:{type:"f",value:1}},vertexShader:"varying vec3 vNormal;\nvoid main() {\nvec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );\nvNormal = normalize( normalMatrix * normal );\ngl_Position = projectionMatrix * mvPosition;\n}", +fragmentShader:"uniform float opacity;\nvarying vec3 vNormal;\nvoid main() {\ngl_FragColor = vec4( 0.5 * normalize( vNormal ) + 0.5, opacity );\n}"},normalmap:{uniforms:THREE.UniformsUtils.merge([THREE.UniformsLib.fog,THREE.UniformsLib.lights,THREE.UniformsLib.shadowmap,{enableAO:{type:"i",value:0},enableDiffuse:{type:"i",value:0},enableSpecular:{type:"i",value:0},enableReflection:{type:"i",value:0},enableDisplacement:{type:"i",value:0},tDisplacement:{type:"t",value:null},tDiffuse:{type:"t",value:null}, +tCube:{type:"t",value:null},tNormal:{type:"t",value:null},tSpecular:{type:"t",value:null},tAO:{type:"t",value:null},uNormalScale:{type:"v2",value:new THREE.Vector2(1,1)},uDisplacementBias:{type:"f",value:0},uDisplacementScale:{type:"f",value:1},uDiffuseColor:{type:"c",value:new THREE.Color(16777215)},uSpecularColor:{type:"c",value:new THREE.Color(1118481)},uAmbientColor:{type:"c",value:new THREE.Color(16777215)},uShininess:{type:"f",value:30},uOpacity:{type:"f",value:1},useRefract:{type:"i",value:0}, +uRefractionRatio:{type:"f",value:0.98},uReflectivity:{type:"f",value:0.5},uOffset:{type:"v2",value:new THREE.Vector2(0,0)},uRepeat:{type:"v2",value:new THREE.Vector2(1,1)},wrapRGB:{type:"v3",value:new THREE.Vector3(1,1,1)}}]),fragmentShader:["uniform vec3 uAmbientColor;\nuniform vec3 uDiffuseColor;\nuniform vec3 uSpecularColor;\nuniform float uShininess;\nuniform float uOpacity;\nuniform bool enableDiffuse;\nuniform bool enableSpecular;\nuniform bool enableAO;\nuniform bool enableReflection;\nuniform sampler2D tDiffuse;\nuniform sampler2D tNormal;\nuniform sampler2D tSpecular;\nuniform sampler2D tAO;\nuniform samplerCube tCube;\nuniform vec2 uNormalScale;\nuniform bool useRefract;\nuniform float uRefractionRatio;\nuniform float uReflectivity;\nvarying vec3 vTangent;\nvarying vec3 vBinormal;\nvarying vec3 vNormal;\nvarying vec2 vUv;\nuniform vec3 ambientLightColor;\n#if MAX_DIR_LIGHTS > 0\nuniform vec3 directionalLightColor[ MAX_DIR_LIGHTS ];\nuniform vec3 directionalLightDirection[ MAX_DIR_LIGHTS ];\n#endif\n#if MAX_HEMI_LIGHTS > 0\nuniform vec3 hemisphereLightSkyColor[ MAX_HEMI_LIGHTS ];\nuniform vec3 hemisphereLightGroundColor[ MAX_HEMI_LIGHTS ];\nuniform vec3 hemisphereLightDirection[ MAX_HEMI_LIGHTS ];\n#endif\n#if MAX_POINT_LIGHTS > 0\nuniform vec3 pointLightColor[ MAX_POINT_LIGHTS ];\nuniform vec3 pointLightPosition[ MAX_POINT_LIGHTS ];\nuniform float pointLightDistance[ MAX_POINT_LIGHTS ];\n#endif\n#if MAX_SPOT_LIGHTS > 0\nuniform vec3 spotLightColor[ MAX_SPOT_LIGHTS ];\nuniform vec3 spotLightPosition[ MAX_SPOT_LIGHTS ];\nuniform vec3 spotLightDirection[ MAX_SPOT_LIGHTS ];\nuniform float spotLightAngleCos[ MAX_SPOT_LIGHTS ];\nuniform float spotLightExponent[ MAX_SPOT_LIGHTS ];\nuniform float spotLightDistance[ MAX_SPOT_LIGHTS ];\n#endif\n#ifdef WRAP_AROUND\nuniform vec3 wrapRGB;\n#endif\nvarying vec3 vWorldPosition;\nvarying vec3 vViewPosition;", +THREE.ShaderChunk.shadowmap_pars_fragment,THREE.ShaderChunk.fog_pars_fragment,"void main() {\ngl_FragColor = vec4( vec3( 1.0 ), uOpacity );\nvec3 specularTex = vec3( 1.0 );\nvec3 normalTex = texture2D( tNormal, vUv ).xyz * 2.0 - 1.0;\nnormalTex.xy *= uNormalScale;\nnormalTex = normalize( normalTex );\nif( enableDiffuse ) {\n#ifdef GAMMA_INPUT\nvec4 texelColor = texture2D( tDiffuse, vUv );\ntexelColor.xyz *= texelColor.xyz;\ngl_FragColor = gl_FragColor * texelColor;\n#else\ngl_FragColor = gl_FragColor * texture2D( tDiffuse, vUv );\n#endif\n}\nif( enableAO ) {\n#ifdef GAMMA_INPUT\nvec4 aoColor = texture2D( tAO, vUv );\naoColor.xyz *= aoColor.xyz;\ngl_FragColor.xyz = gl_FragColor.xyz * aoColor.xyz;\n#else\ngl_FragColor.xyz = gl_FragColor.xyz * texture2D( tAO, vUv ).xyz;\n#endif\n}\nif( enableSpecular )\nspecularTex = texture2D( tSpecular, vUv ).xyz;\nmat3 tsb = mat3( normalize( vTangent ), normalize( vBinormal ), normalize( vNormal ) );\nvec3 finalNormal = tsb * normalTex;\n#ifdef FLIP_SIDED\nfinalNormal = -finalNormal;\n#endif\nvec3 normal = normalize( finalNormal );\nvec3 viewPosition = normalize( vViewPosition );\n#if MAX_POINT_LIGHTS > 0\nvec3 pointDiffuse = vec3( 0.0 );\nvec3 pointSpecular = vec3( 0.0 );\nfor ( int i = 0; i < MAX_POINT_LIGHTS; i ++ ) {\nvec4 lPosition = viewMatrix * vec4( pointLightPosition[ i ], 1.0 );\nvec3 pointVector = lPosition.xyz + vViewPosition.xyz;\nfloat pointDistance = 1.0;\nif ( pointLightDistance[ i ] > 0.0 )\npointDistance = 1.0 - min( ( length( pointVector ) / pointLightDistance[ i ] ), 1.0 );\npointVector = normalize( pointVector );\n#ifdef WRAP_AROUND\nfloat pointDiffuseWeightFull = max( dot( normal, pointVector ), 0.0 );\nfloat pointDiffuseWeightHalf = max( 0.5 * dot( normal, pointVector ) + 0.5, 0.0 );\nvec3 pointDiffuseWeight = mix( vec3 ( pointDiffuseWeightFull ), vec3( pointDiffuseWeightHalf ), wrapRGB );\n#else\nfloat pointDiffuseWeight = max( dot( normal, pointVector ), 0.0 );\n#endif\npointDiffuse += pointDistance * pointLightColor[ i ] * uDiffuseColor * pointDiffuseWeight;\nvec3 pointHalfVector = normalize( pointVector + viewPosition );\nfloat pointDotNormalHalf = max( dot( normal, pointHalfVector ), 0.0 );\nfloat pointSpecularWeight = specularTex.r * max( pow( pointDotNormalHalf, uShininess ), 0.0 );\n#ifdef PHYSICALLY_BASED_SHADING\nfloat specularNormalization = ( uShininess + 2.0001 ) / 8.0;\nvec3 schlick = uSpecularColor + vec3( 1.0 - uSpecularColor ) * pow( 1.0 - dot( pointVector, pointHalfVector ), 5.0 );\npointSpecular += schlick * pointLightColor[ i ] * pointSpecularWeight * pointDiffuseWeight * pointDistance * specularNormalization;\n#else\npointSpecular += pointDistance * pointLightColor[ i ] * uSpecularColor * pointSpecularWeight * pointDiffuseWeight;\n#endif\n}\n#endif\n#if MAX_SPOT_LIGHTS > 0\nvec3 spotDiffuse = vec3( 0.0 );\nvec3 spotSpecular = vec3( 0.0 );\nfor ( int i = 0; i < MAX_SPOT_LIGHTS; i ++ ) {\nvec4 lPosition = viewMatrix * vec4( spotLightPosition[ i ], 1.0 );\nvec3 spotVector = lPosition.xyz + vViewPosition.xyz;\nfloat spotDistance = 1.0;\nif ( spotLightDistance[ i ] > 0.0 )\nspotDistance = 1.0 - min( ( length( spotVector ) / spotLightDistance[ i ] ), 1.0 );\nspotVector = normalize( spotVector );\nfloat spotEffect = dot( spotLightDirection[ i ], normalize( spotLightPosition[ i ] - vWorldPosition ) );\nif ( spotEffect > spotLightAngleCos[ i ] ) {\nspotEffect = max( pow( spotEffect, spotLightExponent[ i ] ), 0.0 );\n#ifdef WRAP_AROUND\nfloat spotDiffuseWeightFull = max( dot( normal, spotVector ), 0.0 );\nfloat spotDiffuseWeightHalf = max( 0.5 * dot( normal, spotVector ) + 0.5, 0.0 );\nvec3 spotDiffuseWeight = mix( vec3 ( spotDiffuseWeightFull ), vec3( spotDiffuseWeightHalf ), wrapRGB );\n#else\nfloat spotDiffuseWeight = max( dot( normal, spotVector ), 0.0 );\n#endif\nspotDiffuse += spotDistance * spotLightColor[ i ] * uDiffuseColor * spotDiffuseWeight * spotEffect;\nvec3 spotHalfVector = normalize( spotVector + viewPosition );\nfloat spotDotNormalHalf = max( dot( normal, spotHalfVector ), 0.0 );\nfloat spotSpecularWeight = specularTex.r * max( pow( spotDotNormalHalf, uShininess ), 0.0 );\n#ifdef PHYSICALLY_BASED_SHADING\nfloat specularNormalization = ( uShininess + 2.0001 ) / 8.0;\nvec3 schlick = uSpecularColor + vec3( 1.0 - uSpecularColor ) * pow( 1.0 - dot( spotVector, spotHalfVector ), 5.0 );\nspotSpecular += schlick * spotLightColor[ i ] * spotSpecularWeight * spotDiffuseWeight * spotDistance * specularNormalization * spotEffect;\n#else\nspotSpecular += spotDistance * spotLightColor[ i ] * uSpecularColor * spotSpecularWeight * spotDiffuseWeight * spotEffect;\n#endif\n}\n}\n#endif\n#if MAX_DIR_LIGHTS > 0\nvec3 dirDiffuse = vec3( 0.0 );\nvec3 dirSpecular = vec3( 0.0 );\nfor( int i = 0; i < MAX_DIR_LIGHTS; i++ ) {\nvec4 lDirection = viewMatrix * vec4( directionalLightDirection[ i ], 0.0 );\nvec3 dirVector = normalize( lDirection.xyz );\n#ifdef WRAP_AROUND\nfloat directionalLightWeightingFull = max( dot( normal, dirVector ), 0.0 );\nfloat directionalLightWeightingHalf = max( 0.5 * dot( normal, dirVector ) + 0.5, 0.0 );\nvec3 dirDiffuseWeight = mix( vec3( directionalLightWeightingFull ), vec3( directionalLightWeightingHalf ), wrapRGB );\n#else\nfloat dirDiffuseWeight = max( dot( normal, dirVector ), 0.0 );\n#endif\ndirDiffuse += directionalLightColor[ i ] * uDiffuseColor * dirDiffuseWeight;\nvec3 dirHalfVector = normalize( dirVector + viewPosition );\nfloat dirDotNormalHalf = max( dot( normal, dirHalfVector ), 0.0 );\nfloat dirSpecularWeight = specularTex.r * max( pow( dirDotNormalHalf, uShininess ), 0.0 );\n#ifdef PHYSICALLY_BASED_SHADING\nfloat specularNormalization = ( uShininess + 2.0001 ) / 8.0;\nvec3 schlick = uSpecularColor + vec3( 1.0 - uSpecularColor ) * pow( 1.0 - dot( dirVector, dirHalfVector ), 5.0 );\ndirSpecular += schlick * directionalLightColor[ i ] * dirSpecularWeight * dirDiffuseWeight * specularNormalization;\n#else\ndirSpecular += directionalLightColor[ i ] * uSpecularColor * dirSpecularWeight * dirDiffuseWeight;\n#endif\n}\n#endif\n#if MAX_HEMI_LIGHTS > 0\nvec3 hemiDiffuse = vec3( 0.0 );\nvec3 hemiSpecular = vec3( 0.0 );\nfor( int i = 0; i < MAX_HEMI_LIGHTS; i ++ ) {\nvec4 lDirection = viewMatrix * vec4( hemisphereLightDirection[ i ], 0.0 );\nvec3 lVector = normalize( lDirection.xyz );\nfloat dotProduct = dot( normal, lVector );\nfloat hemiDiffuseWeight = 0.5 * dotProduct + 0.5;\nvec3 hemiColor = mix( hemisphereLightGroundColor[ i ], hemisphereLightSkyColor[ i ], hemiDiffuseWeight );\nhemiDiffuse += uDiffuseColor * hemiColor;\nvec3 hemiHalfVectorSky = normalize( lVector + viewPosition );\nfloat hemiDotNormalHalfSky = 0.5 * dot( normal, hemiHalfVectorSky ) + 0.5;\nfloat hemiSpecularWeightSky = specularTex.r * max( pow( hemiDotNormalHalfSky, uShininess ), 0.0 );\nvec3 lVectorGround = -lVector;\nvec3 hemiHalfVectorGround = normalize( lVectorGround + viewPosition );\nfloat hemiDotNormalHalfGround = 0.5 * dot( normal, hemiHalfVectorGround ) + 0.5;\nfloat hemiSpecularWeightGround = specularTex.r * max( pow( hemiDotNormalHalfGround, uShininess ), 0.0 );\n#ifdef PHYSICALLY_BASED_SHADING\nfloat dotProductGround = dot( normal, lVectorGround );\nfloat specularNormalization = ( uShininess + 2.0001 ) / 8.0;\nvec3 schlickSky = uSpecularColor + vec3( 1.0 - uSpecularColor ) * pow( 1.0 - dot( lVector, hemiHalfVectorSky ), 5.0 );\nvec3 schlickGround = uSpecularColor + vec3( 1.0 - uSpecularColor ) * pow( 1.0 - dot( lVectorGround, hemiHalfVectorGround ), 5.0 );\nhemiSpecular += hemiColor * specularNormalization * ( schlickSky * hemiSpecularWeightSky * max( dotProduct, 0.0 ) + schlickGround * hemiSpecularWeightGround * max( dotProductGround, 0.0 ) );\n#else\nhemiSpecular += uSpecularColor * hemiColor * ( hemiSpecularWeightSky + hemiSpecularWeightGround ) * hemiDiffuseWeight;\n#endif\n}\n#endif\nvec3 totalDiffuse = vec3( 0.0 );\nvec3 totalSpecular = vec3( 0.0 );\n#if MAX_DIR_LIGHTS > 0\ntotalDiffuse += dirDiffuse;\ntotalSpecular += dirSpecular;\n#endif\n#if MAX_HEMI_LIGHTS > 0\ntotalDiffuse += hemiDiffuse;\ntotalSpecular += hemiSpecular;\n#endif\n#if MAX_POINT_LIGHTS > 0\ntotalDiffuse += pointDiffuse;\ntotalSpecular += pointSpecular;\n#endif\n#if MAX_SPOT_LIGHTS > 0\ntotalDiffuse += spotDiffuse;\ntotalSpecular += spotSpecular;\n#endif\n#ifdef METAL\ngl_FragColor.xyz = gl_FragColor.xyz * ( totalDiffuse + ambientLightColor * uAmbientColor + totalSpecular );\n#else\ngl_FragColor.xyz = gl_FragColor.xyz * ( totalDiffuse + ambientLightColor * uAmbientColor ) + totalSpecular;\n#endif\nif ( enableReflection ) {\nvec3 vReflect;\nvec3 cameraToVertex = normalize( vWorldPosition - cameraPosition );\nif ( useRefract ) {\nvReflect = refract( cameraToVertex, normal, uRefractionRatio );\n} else {\nvReflect = reflect( cameraToVertex, normal );\n}\nvec4 cubeColor = textureCube( tCube, vec3( -vReflect.x, vReflect.yz ) );\n#ifdef GAMMA_INPUT\ncubeColor.xyz *= cubeColor.xyz;\n#endif\ngl_FragColor.xyz = mix( gl_FragColor.xyz, cubeColor.xyz, specularTex.r * uReflectivity );\n}", +THREE.ShaderChunk.shadowmap_fragment,THREE.ShaderChunk.linear_to_gamma_fragment,THREE.ShaderChunk.fog_fragment,"}"].join("\n"),vertexShader:["attribute vec4 tangent;\nuniform vec2 uOffset;\nuniform vec2 uRepeat;\nuniform bool enableDisplacement;\n#ifdef VERTEX_TEXTURES\nuniform sampler2D tDisplacement;\nuniform float uDisplacementScale;\nuniform float uDisplacementBias;\n#endif\nvarying vec3 vTangent;\nvarying vec3 vBinormal;\nvarying vec3 vNormal;\nvarying vec2 vUv;\nvarying vec3 vWorldPosition;\nvarying vec3 vViewPosition;", +THREE.ShaderChunk.skinning_pars_vertex,THREE.ShaderChunk.shadowmap_pars_vertex,"void main() {",THREE.ShaderChunk.skinbase_vertex,THREE.ShaderChunk.skinnormal_vertex,"#ifdef USE_SKINNING\nvNormal = normalize( normalMatrix * skinnedNormal.xyz );\nvec4 skinnedTangent = skinMatrix * vec4( tangent.xyz, 0.0 );\nvTangent = normalize( normalMatrix * skinnedTangent.xyz );\n#else\nvNormal = normalize( normalMatrix * normal );\nvTangent = normalize( normalMatrix * tangent.xyz );\n#endif\nvBinormal = normalize( cross( vNormal, vTangent ) * tangent.w );\nvUv = uv * uRepeat + uOffset;\nvec3 displacedPosition;\n#ifdef VERTEX_TEXTURES\nif ( enableDisplacement ) {\nvec3 dv = texture2D( tDisplacement, uv ).xyz;\nfloat df = uDisplacementScale * dv.x + uDisplacementBias;\ndisplacedPosition = position + normalize( normal ) * df;\n} else {\n#ifdef USE_SKINNING\nvec4 skinVertex = vec4( position, 1.0 );\nvec4 skinned = boneMatX * skinVertex * skinWeight.x;\nskinned \t += boneMatY * skinVertex * skinWeight.y;\ndisplacedPosition = skinned.xyz;\n#else\ndisplacedPosition = position;\n#endif\n}\n#else\n#ifdef USE_SKINNING\nvec4 skinVertex = vec4( position, 1.0 );\nvec4 skinned = boneMatX * skinVertex * skinWeight.x;\nskinned \t += boneMatY * skinVertex * skinWeight.y;\ndisplacedPosition = skinned.xyz;\n#else\ndisplacedPosition = position;\n#endif\n#endif\nvec4 mvPosition = modelViewMatrix * vec4( displacedPosition, 1.0 );\nvec4 worldPosition = modelMatrix * vec4( displacedPosition, 1.0 );\ngl_Position = projectionMatrix * mvPosition;\nvWorldPosition = worldPosition.xyz;\nvViewPosition = -mvPosition.xyz;\n#ifdef USE_SHADOWMAP\nfor( int i = 0; i < MAX_SHADOWS; i ++ ) {\nvShadowCoord[ i ] = shadowMatrix[ i ] * worldPosition;\n}\n#endif\n}"].join("\n")}, +cube:{uniforms:{tCube:{type:"t",value:null},tFlip:{type:"f",value:-1}},vertexShader:"varying vec3 vWorldPosition;\nvoid main() {\nvec4 worldPosition = modelMatrix * vec4( position, 1.0 );\nvWorldPosition = worldPosition.xyz;\ngl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n}",fragmentShader:"uniform samplerCube tCube;\nuniform float tFlip;\nvarying vec3 vWorldPosition;\nvoid main() {\ngl_FragColor = textureCube( tCube, vec3( tFlip * vWorldPosition.x, vWorldPosition.yz ) );\n}"}, +depthRGBA:{uniforms:{},vertexShader:[THREE.ShaderChunk.morphtarget_pars_vertex,THREE.ShaderChunk.skinning_pars_vertex,"void main() {",THREE.ShaderChunk.skinbase_vertex,THREE.ShaderChunk.morphtarget_vertex,THREE.ShaderChunk.skinning_vertex,THREE.ShaderChunk.default_vertex,"}"].join("\n"),fragmentShader:"vec4 pack_depth( const in float depth ) {\nconst vec4 bit_shift = vec4( 256.0 * 256.0 * 256.0, 256.0 * 256.0, 256.0, 1.0 );\nconst vec4 bit_mask = vec4( 0.0, 1.0 / 256.0, 1.0 / 256.0, 1.0 / 256.0 );\nvec4 res = fract( depth * bit_shift );\nres -= res.xxyz * bit_mask;\nreturn res;\n}\nvoid main() {\ngl_FragData[ 0 ] = pack_depth( gl_FragCoord.z );\n}"}};THREE.WebGLRenderer=function(a){function b(a){if(a.__webglCustomAttributesList)for(var b in a.__webglCustomAttributesList)j.deleteBuffer(a.__webglCustomAttributesList[b].buffer)}function c(a,b){var c=a.vertices.length,d=b.material;if(d.attributes){void 0===a.__webglCustomAttributesList&&(a.__webglCustomAttributesList=[]);for(var e in d.attributes){var f=d.attributes[e];if(!f.__webglInitialized||f.createUniqueBuffers){f.__webglInitialized=!0;var g=1;"v2"===f.type?g=2:"v3"===f.type?g=3:"v4"===f.type? +g=4:"c"===f.type&&(g=3);f.size=g;f.array=new Float32Array(c*g);f.buffer=j.createBuffer();f.buffer.belongsToAttribute=e;f.needsUpdate=!0}a.__webglCustomAttributesList.push(f)}}}function d(a,b){var c=b.geometry,d=a.faces3,h=a.faces4,i=3*d.length+4*h.length,k=1*d.length+2*h.length,h=3*d.length+4*h.length,d=e(b,a),n=g(d),l=f(d),m=d.vertexColors?d.vertexColors:!1;a.__vertexArray=new Float32Array(3*i);l&&(a.__normalArray=new Float32Array(3*i));c.hasTangents&&(a.__tangentArray=new Float32Array(4*i));m&& +(a.__colorArray=new Float32Array(3*i));if(n){if(0l;l++)N.autoScaleCubemaps&&!f?(m=k,r=l,t=c.image[l],y=gd,t.width<=y&&t.height<=y||(z=Math.max(t.width,t.height),v=Math.floor(t.width*y/z),y=Math.floor(t.height*y/z),z=document.createElement("canvas"),z.width=v,z.height=y,z.getContext("2d").drawImage(t,0,0,t.width,t.height,0,0,v,y),t=z),m[r]=t):k[l]=c.image[l];l=k[0];m=0===(l.width&l.width-1)&&0===(l.height&l.height-1);r=L(c.format); +t=L(c.type);W(j.TEXTURE_CUBE_MAP,c,m);for(l=0;6>l;l++)if(f){y=k[l].mipmaps;z=0;for(B=y.length;z=Mc&&console.warn("WebGLRenderer: trying to use "+ +a+" texture units while this GPU supports only "+Mc);Y+=1;return a}function C(a,b){a._modelViewMatrix.multiplyMatrices(b.matrixWorldInverse,a.matrixWorld);a._normalMatrix.getInverse(a._modelViewMatrix);a._normalMatrix.transpose()}function x(a,b,c,d){a[b]=c.r*c.r*d;a[b+1]=c.g*c.g*d;a[b+2]=c.b*c.b*d}function G(a,b,c,d){a[b]=c.r*d;a[b+1]=c.g*d;a[b+2]=c.b*d}function J(a){a!==ub&&(j.lineWidth(a),ub=a)}function E(a,b,c){ab!==a&&(a?j.enable(j.POLYGON_OFFSET_FILL):j.disable(j.POLYGON_OFFSET_FILL),ab=a);if(a&& +(Fa!==b||Xa!==c))j.polygonOffset(b,c),Fa=b,Xa=c}function H(a){for(var a=a.split("\n"),b=0,c=a.length;bb;b++)j.deleteFramebuffer(a.__webglFramebuffer[b]),j.deleteRenderbuffer(a.__webglRenderbuffer[b]); +else j.deleteFramebuffer(a.__webglFramebuffer),j.deleteRenderbuffer(a.__webglRenderbuffer);N.info.memory.textures--},X=function(a){a=a.target;a.removeEventListener("dispose",X);Pc(a)},Pc=function(a){var b=a.program;if(void 0!==b){a.program=void 0;var c,d,e=!1,a=0;for(c=Ma.length;ad.numSupportedMorphTargets?(i.sort(n),i.length=d.numSupportedMorphTargets):i.length>d.numSupportedMorphNormals?i.sort(n):0===i.length&&i.push([0,0]);for(h=0;hxa;xa++)nc=ab[xa],Fb[kb]=nc.x,Fb[kb+1]=nc.y,Fb[kb+2]=nc.z,kb+=3;else for(xa=0;3>xa;xa++)Fb[kb]=Xa.x,Fb[kb+1]=Xa.y,Fb[kb+2]=Xa.z,kb+=3;D=0;for(V=va.length;Dxa;xa++)nc= +ab[xa],Fb[kb]=nc.x,Fb[kb+1]=nc.y,Fb[kb+2]=nc.z,kb+=3;else for(xa=0;4>xa;xa++)Fb[kb]=Xa.x,Fb[kb+1]=Xa.y,Fb[kb+2]=Xa.z,kb+=3;j.bindBuffer(j.ARRAY_BUFFER,sa.__webglNormalBuffer);j.bufferData(j.ARRAY_BUFFER,Fb,Pa)}if(Cc&&pd&&bb){D=0;for(V=ua.length;Dxa;xa++)Ab=pb[xa],Dc[ic]=Ab.x,Dc[ic+1]=Ab.y,ic+=2;D=0;for(V=va.length;Dxa;xa++)Ab=pb[xa],Dc[ic]=Ab.x,Dc[ic+1]=Ab.y,ic+=2;0xa;xa++)yc=ub[xa],Ec[jc]=yc.x,Ec[jc+1]=yc.y,jc+=2;D=0;for(V=va.length;Dxa;xa++)yc=ub[xa],Ec[jc]=yc.x,Ec[jc+1]=yc.y,jc+=2;0f;f++){a.__webglFramebuffer[f]=j.createFramebuffer();a.__webglRenderbuffer[f]=j.createRenderbuffer();j.texImage2D(j.TEXTURE_CUBE_MAP_POSITIVE_X+f,0,d,a.width,a.height,0,d,e,null);var g=a,i=j.TEXTURE_CUBE_MAP_POSITIVE_X+f;j.bindFramebuffer(j.FRAMEBUFFER, +a.__webglFramebuffer[f]);j.framebufferTexture2D(j.FRAMEBUFFER,j.COLOR_ATTACHMENT0,i,g.__webglTexture,0);F(a.__webglRenderbuffer[f],a)}c&&j.generateMipmap(j.TEXTURE_CUBE_MAP)}else a.__webglFramebuffer=j.createFramebuffer(),a.__webglRenderbuffer=a.shareDepthFrom?a.shareDepthFrom.__webglRenderbuffer:j.createRenderbuffer(),j.bindTexture(j.TEXTURE_2D,a.__webglTexture),W(j.TEXTURE_2D,a,c),j.texImage2D(j.TEXTURE_2D,0,d,a.width,a.height,0,d,e,null),d=j.TEXTURE_2D,j.bindFramebuffer(j.FRAMEBUFFER,a.__webglFramebuffer), +j.framebufferTexture2D(j.FRAMEBUFFER,j.COLOR_ATTACHMENT0,d,a.__webglTexture,0),a.shareDepthFrom?a.depthBuffer&&!a.stencilBuffer?j.framebufferRenderbuffer(j.FRAMEBUFFER,j.DEPTH_ATTACHMENT,j.RENDERBUFFER,a.__webglRenderbuffer):a.depthBuffer&&a.stencilBuffer&&j.framebufferRenderbuffer(j.FRAMEBUFFER,j.DEPTH_STENCIL_ATTACHMENT,j.RENDERBUFFER,a.__webglRenderbuffer):F(a.__webglRenderbuffer,a),c&&j.generateMipmap(j.TEXTURE_2D);b?j.bindTexture(j.TEXTURE_CUBE_MAP,null):j.bindTexture(j.TEXTURE_2D,null);j.bindRenderbuffer(j.RENDERBUFFER, +null);j.bindFramebuffer(j.FRAMEBUFFER,null)}a?(b=b?a.__webglFramebuffer[a.activeCubeFace]:a.__webglFramebuffer,c=a.width,a=a.height,e=d=0):(b=null,c=fc,a=Ab,d=Ib,e=Jb);b!==Pa&&(j.bindFramebuffer(j.FRAMEBUFFER,b),j.viewport(d,e,c,a),Pa=b);mc=c;pb=a};this.shadowMapPlugin=new THREE.ShadowMapPlugin;this.addPrePlugin(this.shadowMapPlugin);this.addPostPlugin(new THREE.SpritePlugin);this.addPostPlugin(new THREE.LensFlarePlugin)};THREE.WebGLRenderTarget=function(a,b,c){THREE.EventDispatcher.call(this);this.width=a;this.height=b;c=c||{};this.wrapS=void 0!==c.wrapS?c.wrapS:THREE.ClampToEdgeWrapping;this.wrapT=void 0!==c.wrapT?c.wrapT:THREE.ClampToEdgeWrapping;this.magFilter=void 0!==c.magFilter?c.magFilter:THREE.LinearFilter;this.minFilter=void 0!==c.minFilter?c.minFilter:THREE.LinearMipMapLinearFilter;this.anisotropy=void 0!==c.anisotropy?c.anisotropy:1;this.offset=new THREE.Vector2(0,0);this.repeat=new THREE.Vector2(1,1); +this.format=void 0!==c.format?c.format:THREE.RGBAFormat;this.type=void 0!==c.type?c.type:THREE.UnsignedByteType;this.depthBuffer=void 0!==c.depthBuffer?c.depthBuffer:!0;this.stencilBuffer=void 0!==c.stencilBuffer?c.stencilBuffer:!0;this.generateMipmaps=!0;this.shareDepthFrom=null}; +THREE.WebGLRenderTarget.prototype.clone=function(){var a=new THREE.WebGLRenderTarget(this.width,this.height);a.wrapS=this.wrapS;a.wrapT=this.wrapT;a.magFilter=this.magFilter;a.minFilter=this.minFilter;a.anisotropy=this.anisotropy;a.offset.copy(this.offset);a.repeat.copy(this.repeat);a.format=this.format;a.type=this.type;a.depthBuffer=this.depthBuffer;a.stencilBuffer=this.stencilBuffer;a.generateMipmaps=this.generateMipmaps;a.shareDepthFrom=this.shareDepthFrom;return a}; +THREE.WebGLRenderTarget.prototype.dispose=function(){this.dispatchEvent({type:"dispose"})};THREE.WebGLRenderTargetCube=function(a,b,c){THREE.WebGLRenderTarget.call(this,a,b,c);this.activeCubeFace=0};THREE.WebGLRenderTargetCube.prototype=Object.create(THREE.WebGLRenderTarget.prototype);THREE.RenderableVertex=function(){this.positionWorld=new THREE.Vector3;this.positionScreen=new THREE.Vector4;this.visible=!0};THREE.RenderableVertex.prototype.copy=function(a){this.positionWorld.copy(a.positionWorld);this.positionScreen.copy(a.positionScreen)};THREE.RenderableFace3=function(){this.v1=new THREE.RenderableVertex;this.v2=new THREE.RenderableVertex;this.v3=new THREE.RenderableVertex;this.centroidModel=new THREE.Vector3;this.normalModel=new THREE.Vector3;this.normalModelView=new THREE.Vector3;this.vertexNormalsLength=0;this.vertexNormalsModel=[new THREE.Vector3,new THREE.Vector3,new THREE.Vector3];this.vertexNormalsModelView=[new THREE.Vector3,new THREE.Vector3,new THREE.Vector3];this.material=this.color=null;this.uvs=[[]];this.z=null};THREE.RenderableFace4=function(){this.v1=new THREE.RenderableVertex;this.v2=new THREE.RenderableVertex;this.v3=new THREE.RenderableVertex;this.v4=new THREE.RenderableVertex;this.centroidModel=new THREE.Vector3;this.normalModel=new THREE.Vector3;this.normalModelView=new THREE.Vector3;this.vertexNormalsLength=0;this.vertexNormalsModel=[new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3];this.vertexNormalsModelView=[new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3]; +this.material=this.color=null;this.uvs=[[]];this.z=null};THREE.RenderableObject=function(){this.z=this.object=null};THREE.RenderableParticle=function(){this.rotation=this.z=this.y=this.x=this.object=null;this.scale=new THREE.Vector2;this.material=null};THREE.RenderableLine=function(){this.z=null;this.v1=new THREE.RenderableVertex;this.v2=new THREE.RenderableVertex;this.material=null};THREE.GeometryUtils={merge:function(a,b){var c,d,e=a.vertices.length,f=b instanceof THREE.Mesh?b.geometry:b,g=a.vertices,h=f.vertices,i=a.faces,k=f.faces,l=a.faceVertexUvs[0],f=f.faceVertexUvs[0];b instanceof THREE.Mesh&&(b.matrixAutoUpdate&&b.updateMatrix(),c=b.matrix,d=new THREE.Matrix3,d.getInverse(c),d.transpose());for(var m=0,n=h.length;ma?b(c,e-1):k[e]>8&255,i>>16&255,i>>24&255)),d}d.mipmapCount=1;h[2]&131072&&!1!==b&&(d.mipmapCount=Math.max(1,h[7]));d.isCubemap=h[28]&512?!0:!1;d.width=h[4];d.height=h[3];for(var h=h[1]+4,f=d.width,g=d.height,i=d.isCubemap? +6:1,k=0;km-1?0:m-1,s=m+1>e-1?e-1:m+1,r=0>l-1?0:l-1,p=l+1>d-1?d-1:l+1,q=[],y=[0,0,h[4*(m*d+l)]/255*b];q.push([-1,0,h[4*(m*d+r)]/255*b]);q.push([-1,-1,h[4*(n*d+r)]/255*b]);q.push([0,-1,h[4*(n*d+l)]/255*b]);q.push([1,-1,h[4*(n*d+p)]/255*b]);q.push([1,0,h[4*(m*d+p)]/255*b]);q.push([1,1,h[4*(s*d+p)]/255*b]);q.push([0,1,h[4*(s*d+l)]/255*b]);q.push([-1,1,h[4*(s*d+r)]/255*b]);n=[];r=q.length;for(s=0;se)return null;var f=[],g=[],h=[],i,k,l;if(0=m--){console.log("Warning, unable to triangulate polygon!");break}i=k;e<=i&&(i=0);k=i+1;e<=k&&(k=0);l=k+1;e<=l&&(l=0);var n;a:{var s=n=void 0,r=void 0,p=void 0,q=void 0,y=void 0,v=void 0,z=void 0,t= +void 0,s=a[g[i]].x,r=a[g[i]].y,p=a[g[k]].x,q=a[g[k]].y,y=a[g[l]].x,v=a[g[l]].y;if(1E-10>(p-s)*(v-r)-(q-r)*(y-s))n=!1;else{var A=void 0,I=void 0,C=void 0,x=void 0,G=void 0,J=void 0,E=void 0,H=void 0,B=void 0,W=void 0,B=H=E=t=z=void 0,A=y-p,I=v-q,C=s-y,x=r-v,G=p-s,J=q-r;for(n=0;ni)g=d+1;else if(0b&&(b=0);1d.length-2?d.length-1:a+1;c[3]=a>d.length-3?d.length-1:a+2;b.x=THREE.Curve.Utils.interpolate(d[c[0]].x,d[c[1]].x,d[c[2]].x,d[c[3]].x,e);b.y=THREE.Curve.Utils.interpolate(d[c[0]].y,d[c[1]].y,d[c[2]].y,d[c[3]].y,e);return b}; +THREE.EllipseCurve=function(a,b,c,d,e,f,g){this.aX=a;this.aY=b;this.xRadius=c;this.yRadius=d;this.aStartAngle=e;this.aEndAngle=f;this.aClockwise=g};THREE.EllipseCurve.prototype=Object.create(THREE.Curve.prototype);THREE.EllipseCurve.prototype.getPoint=function(a){var b=this.aEndAngle-this.aStartAngle;this.aClockwise||(a=1-a);b=this.aStartAngle+a*b;a=this.aX+this.xRadius*Math.cos(b);b=this.aY+this.yRadius*Math.sin(b);return new THREE.Vector2(a,b)}; +THREE.ArcCurve=function(a,b,c,d,e,f){THREE.EllipseCurve.call(this,a,b,c,c,d,e,f)};THREE.ArcCurve.prototype=Object.create(THREE.EllipseCurve.prototype); +THREE.Curve.Utils={tangentQuadraticBezier:function(a,b,c,d){return 2*(1-a)*(c-b)+2*a*(d-c)},tangentCubicBezier:function(a,b,c,d,e){return-3*b*(1-a)*(1-a)+3*c*(1-a)*(1-a)-6*a*c*(1-a)+6*a*d*(1-a)-3*a*a*d+3*a*a*e},tangentSpline:function(a){return 6*a*a-6*a+(3*a*a-4*a+1)+(-6*a*a+6*a)+(3*a*a-2*a)},interpolate:function(a,b,c,d,e){var a=0.5*(c-a),d=0.5*(d-b),f=e*e;return(2*b-2*c+a+d)*e*f+(-3*b+3*c-2*a-d)*f+a*e+b}}; +THREE.Curve.create=function(a,b){a.prototype=Object.create(THREE.Curve.prototype);a.prototype.getPoint=b;return a};THREE.LineCurve3=THREE.Curve.create(function(a,b){this.v1=a;this.v2=b},function(a){var b=new THREE.Vector3;b.subVectors(this.v2,this.v1);b.multiplyScalar(a);b.add(this.v1);return b}); +THREE.QuadraticBezierCurve3=THREE.Curve.create(function(a,b,c){this.v0=a;this.v1=b;this.v2=c},function(a){var b,c;b=THREE.Shape.Utils.b2(a,this.v0.x,this.v1.x,this.v2.x);c=THREE.Shape.Utils.b2(a,this.v0.y,this.v1.y,this.v2.y);a=THREE.Shape.Utils.b2(a,this.v0.z,this.v1.z,this.v2.z);return new THREE.Vector3(b,c,a)}); +THREE.CubicBezierCurve3=THREE.Curve.create(function(a,b,c,d){this.v0=a;this.v1=b;this.v2=c;this.v3=d},function(a){var b,c;b=THREE.Shape.Utils.b3(a,this.v0.x,this.v1.x,this.v2.x,this.v3.x);c=THREE.Shape.Utils.b3(a,this.v0.y,this.v1.y,this.v2.y,this.v3.y);a=THREE.Shape.Utils.b3(a,this.v0.z,this.v1.z,this.v2.z,this.v3.z);return new THREE.Vector3(b,c,a)}); +THREE.SplineCurve3=THREE.Curve.create(function(a){this.points=void 0==a?[]:a},function(a){var b=new THREE.Vector3,c=[],d=this.points,e,a=(d.length-1)*a;e=Math.floor(a);a-=e;c[0]=0==e?e:e-1;c[1]=e;c[2]=e>d.length-2?d.length-1:e+1;c[3]=e>d.length-3?d.length-1:e+2;e=d[c[0]];var f=d[c[1]],g=d[c[2]],c=d[c[3]];b.x=THREE.Curve.Utils.interpolate(e.x,f.x,g.x,c.x,a);b.y=THREE.Curve.Utils.interpolate(e.y,f.y,g.y,c.y,a);b.z=THREE.Curve.Utils.interpolate(e.z,f.z,g.z,c.z,a);return b}); +THREE.ClosedSplineCurve3=THREE.Curve.create(function(a){this.points=void 0==a?[]:a},function(a){var b=new THREE.Vector3,c=[],d=this.points,e;e=(d.length-0)*a;a=Math.floor(e);e-=a;a+=0=b)return b=c[a]-b,a=this.curves[a],b=1-b/a.getLength(),a.getPointAt(b);a++}return null};THREE.CurvePath.prototype.getLength=function(){var a=this.getCurveLengths();return a[a.length-1]}; +THREE.CurvePath.prototype.getCurveLengths=function(){if(this.cacheLengths&&this.cacheLengths.length==this.curves.length)return this.cacheLengths;var a=[],b=0,c,d=this.curves.length;for(c=0;cb?b=h.x:h.xc?c=h.y:h.yd?d=h.z:h.zMath.abs(d.x-c[0].x)&&1E-10>Math.abs(d.y-c[0].y)&&c.splice(c.length-1,1);b&&c.push(c[0]);return c}; +THREE.Path.prototype.toShapes=function(){var a,b,c,d,e=[],f=new THREE.Path;a=0;for(b=this.actions.length;a +h&&(h+=c.length);h%=c.length;0>g&&(g+=k.length);g%=k.length;e=0<=h-1?h-1:c.length-1;f=0<=g-1?g-1:k.length-1;p=[k[g],c[h],c[e]];p=THREE.FontUtils.Triangulate.area(p);q=[k[g],k[f],c[h]];q=THREE.FontUtils.Triangulate.area(q);m+n>p+q&&(h=s,g=l,0>h&&(h+=c.length),h%=c.length,0>g&&(g+=k.length),g%=k.length,e=0<=h-1?h-1:c.length-1,f=0<=g-1?g-1:k.length-1);m=c.slice(0,h);n=c.slice(h);s=k.slice(g);l=k.slice(0,g);f=[k[g],k[f],c[h]];r.push([k[g],c[h],c[e]]);r.push(f);c=m.concat(s).concat(l).concat(n)}return{shape:c, +isolatedPts:r,allpoints:d}},triangulateShape:function(a,b){var c=THREE.Shape.Utils.removeHoles(a,b),d=c.allpoints,e=c.isolatedPts,c=THREE.FontUtils.Triangulate(c.shape,!1),f,g,h,i,k={};f=0;for(g=d.length;fd;d++)i=h[d].x+":"+h[d].y,i=k[i],void 0!==i&&(h[d]=i)}f=0;for(g=e.length;fd;d++)i=h[d].x+":"+h[d].y,i=k[i],void 0!==i&&(h[d]=i)}return c.concat(e)}, +isClockWise:function(a){return 0>THREE.FontUtils.Triangulate.area(a)},b2p0:function(a,b){var c=1-a;return c*c*b},b2p1:function(a,b){return 2*(1-a)*a*b},b2p2:function(a,b){return a*a*b},b2:function(a,b,c,d){return this.b2p0(a,b)+this.b2p1(a,c)+this.b2p2(a,d)},b3p0:function(a,b){var c=1-a;return c*c*c*b},b3p1:function(a,b){var c=1-a;return 3*c*c*a*b},b3p2:function(a,b){return 3*(1-a)*a*a*b},b3p3:function(a,b){return a*a*a*b},b3:function(a,b,c,d,e){return this.b3p0(a,b)+this.b3p1(a,c)+this.b3p2(a,d)+ +this.b3p3(a,e)}};THREE.AnimationHandler=function(){var a=[],b={},c={update:function(b){for(var c=0;ca.hierarchy[c].keys[d].time&& +(a.hierarchy[c].keys[d].time=0),void 0!==a.hierarchy[c].keys[d].rot&&!(a.hierarchy[c].keys[d].rot instanceof THREE.Quaternion)){var h=a.hierarchy[c].keys[d].rot;a.hierarchy[c].keys[d].rot=new THREE.Quaternion(h[0],h[1],h[2],h[3])}if(a.hierarchy[c].keys.length&&void 0!==a.hierarchy[c].keys[0].morphTargets){h={};for(d=0;ds;s++){c=b[s];g=i.prevKey[c];h=i.nextKey[c];if(h.time<=l){if(kd||1d?0:1;if("pos"===c)if(c=a.position,this.interpolationType===THREE.AnimationHandler.LINEAR)c.x=e[0]+(f[0]-e[0])*d,c.y=e[1]+(f[1]-e[1])*d,c.z=e[2]+ +(f[2]-e[2])*d;else{if(this.interpolationType===THREE.AnimationHandler.CATMULLROM||this.interpolationType===THREE.AnimationHandler.CATMULLROM_FORWARD)this.points[0]=this.getPrevKeyWith("pos",m,g.index-1).pos,this.points[1]=e,this.points[2]=f,this.points[3]=this.getNextKeyWith("pos",m,h.index+1).pos,d=0.33*d+0.33,e=this.interpolateCatmullRom(this.points,d),c.x=e[0],c.y=e[1],c.z=e[2],this.interpolationType===THREE.AnimationHandler.CATMULLROM_FORWARD&&(d=this.interpolateCatmullRom(this.points,1.01*d), +this.target.set(d[0],d[1],d[2]),this.target.sub(c),this.target.y=0,this.target.normalize(),d=Math.atan2(this.target.x,this.target.z),a.rotation.set(0,d,0))}else"rot"===c?THREE.Quaternion.slerp(e,f,a.quaternion,d):"scl"===c&&(c=a.scale,c.x=e[0]+(f[0]-e[0])*d,c.y=e[1]+(f[1]-e[1])*d,c.z=e[2]+(f[2]-e[2])*d)}}}}; +THREE.Animation.prototype.interpolateCatmullRom=function(a,b){var c=[],d=[],e,f,g,h,i,k;e=(a.length-1)*b;f=Math.floor(e);e-=f;c[0]=0===f?f:f-1;c[1]=f;c[2]=f>a.length-2?f:f+1;c[3]=f>a.length-3?f:f+2;f=a[c[0]];h=a[c[1]];i=a[c[2]];k=a[c[3]];c=e*e;g=e*c;d[0]=this.interpolate(f[0],h[0],i[0],k[0],e,c,g);d[1]=this.interpolate(f[1],h[1],i[1],k[1],e,c,g);d[2]=this.interpolate(f[2],h[2],i[2],k[2],e,c,g);return d}; +THREE.Animation.prototype.interpolate=function(a,b,c,d,e,f,g){a=0.5*(c-a);d=0.5*(d-b);return(2*(b-c)+a+d)*g+(-3*(b-c)-2*a-d)*f+a*e+b};THREE.Animation.prototype.getNextKeyWith=function(a,b,c){for(var d=this.data.hierarchy[b].keys,c=this.interpolationType===THREE.AnimationHandler.CATMULLROM||this.interpolationType===THREE.AnimationHandler.CATMULLROM_FORWARD?c=g?b.interpolate(c,g):b.interpolate(c,c.time)}this.data.hierarchy[a].node.updateMatrix();d.matrixWorldNeedsUpdate=!0}}if(this.JITCompile&&void 0===f[0][e]){this.hierarchy[0].updateMatrixWorld(!0);for(a=0;ag?(b=Math.atan2(b.y-a.y,b.x-a.x),a=Math.atan2(c.y-a.y,c.x-a.x),b>a&&(a+=2*Math.PI),c=(b+a)/2,a=-Math.cos(c),c=-Math.sin(c),new THREE.Vector2(a,c)):d.multiplyScalar(g).add(i).sub(a).clone()}function e(c,d){var e,f;for(M=c.length;0<=--M;){e=M;f=M-1;0>f&&(f=c.length-1);for(var g=0,i=s+2*l, +g=0;gMath.abs(c-i)?[new THREE.Vector2(b,1-e),new THREE.Vector2(d,1-f),new THREE.Vector2(k,1-g),new THREE.Vector2(m,1-a)]:[new THREE.Vector2(c,1-e),new THREE.Vector2(i,1-f),new THREE.Vector2(l,1-g),new THREE.Vector2(n,1-a)]}};THREE.ExtrudeGeometry.__v1=new THREE.Vector2;THREE.ExtrudeGeometry.__v2=new THREE.Vector2;THREE.ExtrudeGeometry.__v3=new THREE.Vector2;THREE.ExtrudeGeometry.__v4=new THREE.Vector2; +THREE.ExtrudeGeometry.__v5=new THREE.Vector2;THREE.ExtrudeGeometry.__v6=new THREE.Vector2;THREE.ShapeGeometry=function(a,b){THREE.Geometry.call(this);!1===a instanceof Array&&(a=[a]);this.shapebb=a[a.length-1].getBoundingBox();this.addShapeList(a,b);this.computeCentroids();this.computeFaceNormals()};THREE.ShapeGeometry.prototype=Object.create(THREE.Geometry.prototype);THREE.ShapeGeometry.prototype.addShapeList=function(a,b){for(var c=0,d=a.length;cd?(d=new THREE.Face3(a.index,b.index,c.index,[a.clone(),b.clone(),c.clone()]),d.centroid.add(a).add(b).add(c).divideScalar(3),d.normal=d.centroid.clone().normalize(),i.faces.push(d),d=Math.atan2(d.centroid.z,-d.centroid.x),i.faceVertexUvs[0].push([h(a.uv, +a,d),h(b.uv,b,d),h(c.uv,c,d)])):(d-=1,f(a,g(a,b),g(a,c),d),f(g(a,b),b,g(b,c),d),f(g(a,c),g(b,c),c,d),f(g(a,b),g(b,c),g(a,c),d))}function g(a,b){m[a.index]||(m[a.index]=[]);m[b.index]||(m[b.index]=[]);var c=m[a.index][b.index];void 0===c&&(m[a.index][b.index]=m[b.index][a.index]=c=e((new THREE.Vector3).addVectors(a,b).divideScalar(2)));return c}function h(a,b,c){0>c&&1===a.x&&(a=new THREE.Vector2(a.x-1,a.y));0===b.x&&0===b.z&&(a=new THREE.Vector2(c/2/Math.PI+0.5,a.y));return a}THREE.Geometry.call(this); +for(var c=c||1,d=d||0,i=this,k=0,l=a.length;k=l){for(k=0;3>k;k++){l=[i[k],i[(k+1)%3]];m=!0;for(n=0;nh;h++)void 0===f[g[h]]&&(f[g[h]]=e++,this.vertices.push(a[g[h]])),g[h]=f[g[h]]}for(d=0;db.y?this.rotation.set(Math.PI,0,0):(a=THREE.ArrowHelper.__v2.set(b.z,0,-b.x).normalize(),b=Math.acos(b.y),a=THREE.ArrowHelper.__q1.setFromAxisAngle(a,b),this.rotation.setEulerFromQuaternion(a,this.eulerOrder))}; +THREE.ArrowHelper.prototype.setLength=function(a){this.scale.set(a,a,a)};THREE.ArrowHelper.prototype.setColor=function(a){this.line.material.color.setHex(a);this.cone.material.color.setHex(a)};THREE.ArrowHelper.__v1=new THREE.Vector3;THREE.ArrowHelper.__v2=new THREE.Vector3;THREE.ArrowHelper.__q1=new THREE.Quaternion;THREE.CameraHelper=function(a){function b(a,b,d){c(a,d);c(b,d)}function c(a,b){d.geometry.vertices.push(new THREE.Vector3);d.geometry.colors.push(new THREE.Color(b));void 0===d.pointMap[a]&&(d.pointMap[a]=[]);d.pointMap[a].push(d.geometry.vertices.length-1)}THREE.Line.call(this);var d=this;this.geometry=new THREE.Geometry;this.material=new THREE.LineBasicMaterial({color:16777215,vertexColors:THREE.FaceColors});this.type=THREE.LinePieces;this.matrixWorld=a.matrixWorld;this.matrixAutoUpdate=!1;this.pointMap= +{};b("n1","n2",16755200);b("n2","n4",16755200);b("n4","n3",16755200);b("n3","n1",16755200);b("f1","f2",16755200);b("f2","f4",16755200);b("f4","f3",16755200);b("f3","f1",16755200);b("n1","f1",16755200);b("n2","f2",16755200);b("n3","f3",16755200);b("n4","f4",16755200);b("p","n1",16711680);b("p","n2",16711680);b("p","n3",16711680);b("p","n4",16711680);b("u1","u2",43775);b("u2","u3",43775);b("u3","u1",43775);b("c","t",16777215);b("p","c",3355443);b("cn1","cn2",3355443);b("cn3","cn4",3355443);b("cf1", +"cf2",3355443);b("cf3","cf4",3355443);this.camera=a;this.update(a)};THREE.CameraHelper.prototype=Object.create(THREE.Line.prototype); +THREE.CameraHelper.prototype.update=function(){function a(a,d,e,f){THREE.CameraHelper.__v.set(d,e,f);THREE.CameraHelper.__projector.unprojectVector(THREE.CameraHelper.__v,THREE.CameraHelper.__c);a=b.pointMap[a];if(void 0!==a){d=0;for(e=a.length;dh.end&&(h.end=f);c||(c=i)}}for(i in d)h=d[i],this.createAnimation(i,h.start,h.end,a);this.firstAnimation=c}; +THREE.MorphBlendMesh.prototype.setAnimationDirectionForward=function(a){if(a=this.animationsMap[a])a.direction=1,a.directionBackwards=!1};THREE.MorphBlendMesh.prototype.setAnimationDirectionBackward=function(a){if(a=this.animationsMap[a])a.direction=-1,a.directionBackwards=!0};THREE.MorphBlendMesh.prototype.setAnimationFPS=function(a,b){var c=this.animationsMap[a];c&&(c.fps=b,c.duration=(c.end-c.start)/c.fps)}; +THREE.MorphBlendMesh.prototype.setAnimationDuration=function(a,b){var c=this.animationsMap[a];c&&(c.duration=b,c.fps=(c.end-c.start)/c.duration)};THREE.MorphBlendMesh.prototype.setAnimationWeight=function(a,b){var c=this.animationsMap[a];c&&(c.weight=b)};THREE.MorphBlendMesh.prototype.setAnimationTime=function(a,b){var c=this.animationsMap[a];c&&(c.time=b)};THREE.MorphBlendMesh.prototype.getAnimationTime=function(a){var b=0;if(a=this.animationsMap[a])b=a.time;return b}; +THREE.MorphBlendMesh.prototype.getAnimationDuration=function(a){var b=-1;if(a=this.animationsMap[a])b=a.duration;return b};THREE.MorphBlendMesh.prototype.playAnimation=function(a){var b=this.animationsMap[a];b?(b.time=0,b.active=!0):console.warn("animation["+a+"] undefined")};THREE.MorphBlendMesh.prototype.stopAnimation=function(a){if(a=this.animationsMap[a])a.active=!1}; +THREE.MorphBlendMesh.prototype.update=function(a){for(var b=0,c=this.animationsList.length;bd.duration||0>d.time)d.direction*=-1,d.time>d.duration&&(d.time=d.duration,d.directionBackwards=!0),0>d.time&&(d.time=0,d.directionBackwards=!1)}else d.time%=d.duration,0>d.time&&(d.time+=d.duration);var f=d.startFrame+THREE.Math.clamp(Math.floor(d.time/e),0,d.length-1),g=d.weight; +f!==d.currentFrame&&(this.morphTargetInfluences[d.lastFrame]=0,this.morphTargetInfluences[d.currentFrame]=1*g,this.morphTargetInfluences[f]=0,d.lastFrame=d.currentFrame,d.currentFrame=f);e=d.time%e/e;d.directionBackwards&&(e=1-e);this.morphTargetInfluences[d.currentFrame]=e*g;this.morphTargetInfluences[d.lastFrame]=(1-e)*g}}};THREE.LensFlarePlugin=function(){function a(a,c){var d=b.createProgram(),e=b.createShader(b.FRAGMENT_SHADER),f=b.createShader(b.VERTEX_SHADER),g="precision "+c+" float;\n";b.shaderSource(e,g+a.fragmentShader);b.shaderSource(f,g+a.vertexShader);b.compileShader(e);b.compileShader(f);b.attachShader(d,e);b.attachShader(d,f);b.linkProgram(d);return d}var b,c,d,e,f,g,h,i,k,l,m,n,s;this.init=function(r){b=r.context;c=r;d=r.getPrecision();e=new Float32Array(16);f=new Uint16Array(6);r=0;e[r++]=-1;e[r++]=-1; +e[r++]=0;e[r++]=0;e[r++]=1;e[r++]=-1;e[r++]=1;e[r++]=0;e[r++]=1;e[r++]=1;e[r++]=1;e[r++]=1;e[r++]=-1;e[r++]=1;e[r++]=0;e[r++]=1;r=0;f[r++]=0;f[r++]=1;f[r++]=2;f[r++]=0;f[r++]=2;f[r++]=3;g=b.createBuffer();h=b.createBuffer();b.bindBuffer(b.ARRAY_BUFFER,g);b.bufferData(b.ARRAY_BUFFER,e,b.STATIC_DRAW);b.bindBuffer(b.ELEMENT_ARRAY_BUFFER,h);b.bufferData(b.ELEMENT_ARRAY_BUFFER,f,b.STATIC_DRAW);i=b.createTexture();k=b.createTexture();b.bindTexture(b.TEXTURE_2D,i);b.texImage2D(b.TEXTURE_2D,0,b.RGB,16,16, +0,b.RGB,b.UNSIGNED_BYTE,null);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_S,b.CLAMP_TO_EDGE);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_T,b.CLAMP_TO_EDGE);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MAG_FILTER,b.NEAREST);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MIN_FILTER,b.NEAREST);b.bindTexture(b.TEXTURE_2D,k);b.texImage2D(b.TEXTURE_2D,0,b.RGBA,16,16,0,b.RGBA,b.UNSIGNED_BYTE,null);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_S,b.CLAMP_TO_EDGE);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_T,b.CLAMP_TO_EDGE); +b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MAG_FILTER,b.NEAREST);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MIN_FILTER,b.NEAREST);0>=b.getParameter(b.MAX_VERTEX_TEXTURE_IMAGE_UNITS)?(l=!1,m=a(THREE.ShaderFlares.lensFlare,d)):(l=!0,m=a(THREE.ShaderFlares.lensFlareVertexTexture,d));n={};s={};n.vertex=b.getAttribLocation(m,"position");n.uv=b.getAttribLocation(m,"uv");s.renderType=b.getUniformLocation(m,"renderType");s.map=b.getUniformLocation(m,"map");s.occlusionMap=b.getUniformLocation(m,"occlusionMap");s.opacity= +b.getUniformLocation(m,"opacity");s.color=b.getUniformLocation(m,"color");s.scale=b.getUniformLocation(m,"scale");s.rotation=b.getUniformLocation(m,"rotation");s.screenPosition=b.getUniformLocation(m,"screenPosition")};this.render=function(a,d,e,f){var a=a.__webglFlares,v=a.length;if(v){var z=new THREE.Vector3,t=f/e,A=0.5*e,I=0.5*f,C=16/f,x=new THREE.Vector2(C*t,C),G=new THREE.Vector3(1,1,0),J=new THREE.Vector2(1,1),E=s,C=n;b.useProgram(m);b.enableVertexAttribArray(n.vertex);b.enableVertexAttribArray(n.uv); +b.uniform1i(E.occlusionMap,0);b.uniform1i(E.map,1);b.bindBuffer(b.ARRAY_BUFFER,g);b.vertexAttribPointer(C.vertex,2,b.FLOAT,!1,16,0);b.vertexAttribPointer(C.uv,2,b.FLOAT,!1,16,8);b.bindBuffer(b.ELEMENT_ARRAY_BUFFER,h);b.disable(b.CULL_FACE);b.depthMask(!1);var H,B,W,F,K;for(H=0;Hx;x++)t[x]=new THREE.Vector3,v[x]=new THREE.Vector3;t=A.shadowCascadeNearZ[z];A=A.shadowCascadeFarZ[z];v[0].set(-1,-1,t);v[1].set(1,-1,t);v[2].set(-1, +1,t);v[3].set(1,1,t);v[4].set(-1,-1,A);v[5].set(1,-1,A);v[6].set(-1,1,A);v[7].set(1,1,A);C.originalCamera=n;v=new THREE.Gyroscope;v.position=p.shadowCascadeOffset;v.add(C);v.add(C.target);n.add(v);p.shadowCascadeArray[y]=C;console.log("Created virtualLight",C)}z=p;t=y;A=z.shadowCascadeArray[t];A.position.copy(z.position);A.target.position.copy(z.target.position);A.lookAt(A.target);A.shadowCameraVisible=z.shadowCameraVisible;A.shadowDarkness=z.shadowDarkness;A.shadowBias=z.shadowCascadeBias[t];v=z.shadowCascadeNearZ[t]; +z=z.shadowCascadeFarZ[t];A=A.pointsFrustum;A[0].z=v;A[1].z=v;A[2].z=v;A[3].z=v;A[4].z=z;A[5].z=z;A[6].z=z;A[7].z=z;I[q]=C;q++}else I[q]=p,q++;s=0;for(r=I.length;sz;z++)t=A[z],t.copy(v[z]),THREE.ShadowMapPlugin.__projector.unprojectVector(t,y),t.applyMatrix4(q.matrixWorldInverse),t.xk.x&&(k.x=t.x),t.yk.y&&(k.y=t.y),t.zk.z&& +(k.z=t.z);q.left=i.x;q.right=k.x;q.top=k.y;q.bottom=i.y;q.updateProjectionMatrix()}q=p.shadowMap;v=p.shadowMatrix;y=p.shadowCamera;y.position.getPositionFromMatrix(p.matrixWorld);l.getPositionFromMatrix(p.target.matrixWorld);y.lookAt(l);y.updateMatrixWorld();y.matrixWorldInverse.getInverse(y.matrixWorld);p.cameraHelper&&(p.cameraHelper.visible=p.shadowCameraVisible);p.shadowCameraVisible&&p.cameraHelper.update();v.set(0.5,0,0,0.5,0,0.5,0,0.5,0,0,0.5,0.5,0,0,0,1);v.multiply(y.projectionMatrix);v.multiply(y.matrixWorldInverse); +h.multiplyMatrices(y.projectionMatrix,y.matrixWorldInverse);g.setFromMatrix(h);b.setRenderTarget(q);b.clear();A=m.__webglObjects;p=0;for(q=A.length;p 0 ) {\nfloat depth = gl_FragCoord.z / gl_FragCoord.w;\nfloat fogFactor = 0.0;\nif ( fogType == 1 ) {\nfogFactor = smoothstep( fogNear, fogFar, depth );\n} else {\nconst float LOG2 = 1.442695;\nfloat fogFactor = exp2( - fogDensity * fogDensity * depth * depth * LOG2 );\nfogFactor = 1.0 - clamp( fogFactor, 0.0, 1.0 );\n}\ngl_FragColor = mix( gl_FragColor, vec4( fogColor, gl_FragColor.w ), fogFactor );\n}\n}"}}; diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/common/ajax_handler.js b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/common/ajax_handler.js new file mode 100644 index 000000000..63d536fb6 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/common/ajax_handler.js @@ -0,0 +1,50 @@ +/* + * 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. + */ +var ajax_handler = function () { + var api = this; + api.response = "v"; + api.ajaxRequest = function (url, type, data, dataType, callback) { + var response; + $.ajax({ + url: url, + type: type, + dataType: dataType, + success: function (data, success) { + api.response = data; + console.log(" success " + JSON.stringify(success)); + console.log(" data " + JSON.stringify(data)); + callback(data, success); + }, + error: function (jqxhr, textStatus, error) { + var err = textStatus + ', ' + error; + console.log("Request Failed: " + err); + callback(data, error); + api.response = data; + }, + data: data + }); + return api.response; + }; + api.makeJSONObject = function () { + var object = {}; + for (var i = 0; i < arguments.length - 1; i = i + 2) { + object[arguments[i]] = arguments[i + 1]; + } + return object; + } +}; \ No newline at end of file diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/common/general_handler b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/common/general_handler new file mode 100644 index 000000000..2e8f4b172 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/common/general_handler @@ -0,0 +1,101 @@ +/* + * 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. + */ + +$('.btn-minimize').click(function (e) { + e.preventDefault(); + var $target = $(this).parent().parent().next('.box-content'); + if ($target.is(':visible')) { + $('i', $(this)).removeClass('glyphicon-chevron-up').addClass('glyphicon-chevron-down'); + checkAndDisable($(this).parent().attr('id')); + + } + else { + $('i', $(this)).removeClass('glyphicon-chevron-down').addClass('glyphicon-chevron-up'); + checkAndEnable($(this).parent().attr('id')); + } + $target.slideToggle(); +}); + +function checkAndEnable(id) { + + //console.log("enable: " + id); + if (id === "RealtimePlotting") { + config_api.modules_status.realtimePlotting = true; + } + else if (id === "SensorReadings") { + config_api.modules_status.sensorReadings = true; + } else if (id === "AngleOfRotation_2") { + config_api.modules_status.angleOfRotation_2 = true; + } else if (id === "AngleOfRotation_1") { + config_api.modules_status.angleOfRotation_1 = true; + } +} + +function checkAndDisable(id) { + //console.log("disable: " + id); + if (id === "RealtimePlotting") { + config_api.modules_status.realtimePlotting = false; + } + else if (id === "SensorReadings") { + config_api.modules_status.sensorReadings = false; + } else if (id === "AngleOfRotation_2") { + config_api.modules_status.angleOfRotation_2 = false; + } else if (id === "AngleOfRotation_1") { + config_api.modules_status.angleOfRotation_1 = false; + } +} + +function isJSON(data) { + try { + return JSON.parse(data); + } + catch (error) { + return null; + } +} + +function Queue() { + var a = [], b = ''; + this.enqueue = function (b) { + a.push([this.getLength() - 1 <= 0 ? 0 : this.getLength() - 1, b]); + }; + this.dequeue = function () { + if (0 != a.length) { + var c = a[b]; + 2 * ++b >= a.length && (a = a.slice(b), b = 0); + return c + } + }; + this.getLength = function () { + return a.length - b; + }; + this.isEmpty = function () { + return 0 == a.length; + }; + + this.peek = function () { + return 0 < a.length ? a[b] : void 0 + }; + this.getData = function () { + return a; + }; + this.make_fixed_size = function (start, end) { + a = a.slice(start, end); + } +} + diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/common/websocket_api b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/common/websocket_api new file mode 100644 index 000000000..6dc690c6c --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/common/websocket_api @@ -0,0 +1,60 @@ +/* + * 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. + */ + + +var webSocket_api = function () { + var api = this; + api.wekSocket; + api.message = function (holder) { + return $("#" + holder + ""); + }, + api.openSocket = function (url, callback) { + if (api.wekSocket !== undefined && api.wekSocket.readyState !== WebSocket.CLOSED) { + console.log("WebSocket is already opened."); + + } else { + api.wekSocket = new WebSocket(url); + } + api.wekSocket.onopen = function (event) { + if (event.data === undefined) + return; + api.writeResponse(event.data); + callback(event.data); + }; + api.wekSocket.onmessage = function (event) { + api.writeResponse(event.data); + }; + + api.wekSocket.onclose = function (event) { + api.writeResponse(event.data); + }; + }, + api.send = function (message) { + api.wekSocket.send(message); + }, + + api.closeSocket = function () { + api.wekSocket.close(); + }, + + api.writeResponse = function (arg) { + console.log(arg); + } +} + + diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/config/config.js b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/config/config.js new file mode 100644 index 000000000..fd234991c --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/config/config.js @@ -0,0 +1,37 @@ +/* + * 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. + */ + +var config_api = function () { + var config_api = this; + var context_controller = "/drone_analyzer/controller/send_command"; + config_api.config_3dobject_holder = "#virtualDrone"; + config_api.realtime_plotting_update_interval = 30; + config_api.realtime_plotting_totalPoints = 30; + config_api.realtime_plotting_data_window = {}; + config_api.effectController = {uy: 70.0, uz: 15.0, ux: 10.0, fx: 2.0, fz: 15.0, Tmax: 1}; + config_api.drone_control = context_controller; + config_api.drone_controlType = "POST"; + config_api.drone_controlDataType = "json"; + config_api.web_socket_endpoint = "/drone_analyzer/datastream/drone_status"; + config_api.modules_status = { + "realtimePlotting": false, + "sensorReadings": false, + "angleOfRotation_2": false, + "angleOfRotation_1": false + }; +}; \ No newline at end of file diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/d3.min.js b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/d3.min.js new file mode 100644 index 000000000..be855941e --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/d3.min.js @@ -0,0 +1,23 @@ +/* + * 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. + */ + +!function(){function n(n){return n&&(n.ownerDocument||n.document||n).documentElement}function t(n){return n&&(n.ownerDocument&&n.ownerDocument.defaultView||n.document&&n||n.defaultView)}function e(n,t){return t>n?-1:n>t?1:n>=t?0:NaN}function r(n){return null===n?NaN:+n}function u(n){return!isNaN(n)}function i(n){return{left:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n(t[i],e)<0?r=i+1:u=i}return r},right:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n(t[i],e)>0?u=i:r=i+1}return r}}}function a(n){return n.length}function o(n){for(var t=1;n*t%1;)t*=10;return t}function l(n,t){for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}function c(){this._=Object.create(null)}function s(n){return(n+="")===xa||n[0]===ba?ba+n:n}function f(n){return(n+="")[0]===ba?n.slice(1):n}function h(n){return s(n)in this._}function g(n){return(n=s(n))in this._&&delete this._[n]}function p(){var n=[];for(var t in this._)n.push(f(t));return n}function v(){var n=0;for(var t in this._)++n;return n}function d(){for(var n in this._)return!1;return!0}function m(){this._=Object.create(null)}function y(n){return n}function M(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function x(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.slice(1);for(var e=0,r=_a.length;r>e;++e){var u=_a[e]+t;if(u in n)return u}}function b(){}function _(){}function w(n){function t(){for(var t,r=e,u=-1,i=r.length;++ue;e++)for(var u,i=n[e],a=0,o=i.length;o>a;a++)(u=i[a])&&t(u,a,e);return n}function Z(n){return Sa(n,za),n}function V(n){var t,e;return function(r,u,i){var a,o=n[i].update,l=o.length;for(i!=e&&(e=i,t=0),u>=t&&(t=u+1);!(a=o[t])&&++t0&&(n=n.slice(0,o));var c=La.get(n);return c&&(n=c,l=B),o?t?u:r:t?b:i}function $(n,t){return function(e){var r=oa.event;oa.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{oa.event=r}}}function B(n,t){var e=$(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function W(e){var r=".dragsuppress-"+ ++Ta,u="click"+r,i=oa.select(t(e)).on("touchmove"+r,S).on("dragstart"+r,S).on("selectstart"+r,S);if(null==qa&&(qa="onselectstart"in e?!1:x(e.style,"userSelect")),qa){var a=n(e).style,o=a[qa];a[qa]="none"}return function(n){if(i.on(r,null),qa&&(a[qa]=o),n){var t=function(){i.on(u,null)};i.on(u,function(){S(),t()},!0),setTimeout(t,0)}}}function J(n,e){e.changedTouches&&(e=e.changedTouches[0]);var r=n.ownerSVGElement||n;if(r.createSVGPoint){var u=r.createSVGPoint();if(0>Ra){var i=t(n);if(i.scrollX||i.scrollY){r=oa.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var a=r[0][0].getScreenCTM();Ra=!(a.f||a.e),r.remove()}}return Ra?(u.x=e.pageX,u.y=e.pageY):(u.x=e.clientX,u.y=e.clientY),u=u.matrixTransform(n.getScreenCTM().inverse()),[u.x,u.y]}var o=n.getBoundingClientRect();return[e.clientX-o.left-n.clientLeft,e.clientY-o.top-n.clientTop]}function G(){return oa.event.changedTouches[0].identifier}function K(n){return n>0?1:0>n?-1:0}function Q(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function nn(n){return n>1?0:-1>n?ja:Math.acos(n)}function tn(n){return n>1?Ha:-1>n?-Ha:Math.asin(n)}function en(n){return((n=Math.exp(n))-1/n)/2}function rn(n){return((n=Math.exp(n))+1/n)/2}function un(n){return((n=Math.exp(2*n))-1)/(n+1)}function an(n){return(n=Math.sin(n/2))*n}function on(){}function ln(n,t,e){return this instanceof ln?(this.h=+n,this.s=+t,void(this.l=+e)):arguments.length<2?n instanceof ln?new ln(n.h,n.s,n.l):_n(""+n,wn,ln):new ln(n,t,e)}function cn(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?i+(a-i)*n/60:180>n?a:240>n?i+(a-i)*(240-n)/60:i}function u(n){return Math.round(255*r(n))}var i,a;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,a=.5>=e?e*(1+t):e+t-e*t,i=2*e-a,new yn(u(n+120),u(n),u(n-120))}function sn(n,t,e){return this instanceof sn?(this.h=+n,this.c=+t,void(this.l=+e)):arguments.length<2?n instanceof sn?new sn(n.h,n.c,n.l):n instanceof hn?pn(n.l,n.a,n.b):pn((n=Sn((n=oa.rgb(n)).r,n.g,n.b)).l,n.a,n.b):new sn(n,t,e)}function fn(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),new hn(e,Math.cos(n*=Oa)*t,Math.sin(n)*t)}function hn(n,t,e){return this instanceof hn?(this.l=+n,this.a=+t,void(this.b=+e)):arguments.length<2?n instanceof hn?new hn(n.l,n.a,n.b):n instanceof sn?fn(n.h,n.c,n.l):Sn((n=yn(n)).r,n.g,n.b):new hn(n,t,e)}function gn(n,t,e){var r=(n+16)/116,u=r+t/500,i=r-e/200;return u=vn(u)*Ka,r=vn(r)*Qa,i=vn(i)*no,new yn(mn(3.2404542*u-1.5371385*r-.4985314*i),mn(-.969266*u+1.8760108*r+.041556*i),mn(.0556434*u-.2040259*r+1.0572252*i))}function pn(n,t,e){return n>0?new sn(Math.atan2(e,t)*Ia,Math.sqrt(t*t+e*e),n):new sn(NaN,NaN,n)}function vn(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function dn(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function mn(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function yn(n,t,e){return this instanceof yn?(this.r=~~n,this.g=~~t,void(this.b=~~e)):arguments.length<2?n instanceof yn?new yn(n.r,n.g,n.b):_n(""+n,yn,cn):new yn(n,t,e)}function Mn(n){return new yn(n>>16,n>>8&255,255&n)}function xn(n){return Mn(n)+""}function bn(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function _n(n,t,e){var r,u,i,a=0,o=0,l=0;if(r=/([a-z]+)\((.*)\)/.exec(n=n.toLowerCase()))switch(u=r[2].split(","),r[1]){case"hsl":return e(parseFloat(u[0]),parseFloat(u[1])/100,parseFloat(u[2])/100);case"rgb":return t(Nn(u[0]),Nn(u[1]),Nn(u[2]))}return(i=ro.get(n))?t(i.r,i.g,i.b):(null==n||"#"!==n.charAt(0)||isNaN(i=parseInt(n.slice(1),16))||(4===n.length?(a=(3840&i)>>4,a=a>>4|a,o=240&i,o=o>>4|o,l=15&i,l=l<<4|l):7===n.length&&(a=(16711680&i)>>16,o=(65280&i)>>8,l=255&i)),t(a,o,l))}function wn(n,t,e){var r,u,i=Math.min(n/=255,t/=255,e/=255),a=Math.max(n,t,e),o=a-i,l=(a+i)/2;return o?(u=.5>l?o/(a+i):o/(2-a-i),r=n==a?(t-e)/o+(e>t?6:0):t==a?(e-n)/o+2:(n-t)/o+4,r*=60):(r=NaN,u=l>0&&1>l?0:r),new ln(r,u,l)}function Sn(n,t,e){n=kn(n),t=kn(t),e=kn(e);var r=dn((.4124564*n+.3575761*t+.1804375*e)/Ka),u=dn((.2126729*n+.7151522*t+.072175*e)/Qa),i=dn((.0193339*n+.119192*t+.9503041*e)/no);return hn(116*u-16,500*(r-u),200*(u-i))}function kn(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function Nn(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function En(n){return"function"==typeof n?n:function(){return n}}function An(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),Cn(t,e,n,r)}}function Cn(n,t,e,r){function u(){var n,t=l.status;if(!t&&Ln(l)||t>=200&&300>t||304===t){try{n=e.call(i,l)}catch(r){return void a.error.call(i,r)}a.load.call(i,n)}else a.error.call(i,l)}var i={},a=oa.dispatch("beforesend","progress","load","error"),o={},l=new XMLHttpRequest,c=null;return!this.XDomainRequest||"withCredentials"in l||!/^(http(s)?:)?\/\//.test(n)||(l=new XDomainRequest),"onload"in l?l.onload=l.onerror=u:l.onreadystatechange=function(){l.readyState>3&&u()},l.onprogress=function(n){var t=oa.event;oa.event=n;try{a.progress.call(i,l)}finally{oa.event=t}},i.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?o[n]:(null==t?delete o[n]:o[n]=t+"",i)},i.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",i):t},i.responseType=function(n){return arguments.length?(c=n,i):c},i.response=function(n){return e=n,i},["get","post"].forEach(function(n){i[n]=function(){return i.send.apply(i,[n].concat(ca(arguments)))}}),i.send=function(e,r,u){if(2===arguments.length&&"function"==typeof r&&(u=r,r=null),l.open(e,n,!0),null==t||"accept"in o||(o.accept=t+",*/*"),l.setRequestHeader)for(var s in o)l.setRequestHeader(s,o[s]);return null!=t&&l.overrideMimeType&&l.overrideMimeType(t),null!=c&&(l.responseType=c),null!=u&&i.on("error",u).on("load",function(n){u(null,n)}),a.beforesend.call(i,l),l.send(null==r?null:r),i},i.abort=function(){return l.abort(),i},oa.rebind(i,a,"on"),null==r?i:i.get(zn(r))}function zn(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function Ln(n){var t=n.responseType;return t&&"text"!==t?n.response:n.responseText}function qn(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var u=e+t,i={c:n,t:u,n:null};return io?io.n=i:uo=i,io=i,ao||(oo=clearTimeout(oo),ao=1,lo(Tn)),i}function Tn(){var n=Rn(),t=Dn()-n;t>24?(isFinite(t)&&(clearTimeout(oo),oo=setTimeout(Tn,t)),ao=0):(ao=1,lo(Tn))}function Rn(){for(var n=Date.now(),t=uo;t;)n>=t.t&&t.c(n-t.t)&&(t.c=null),t=t.n;return n}function Dn(){for(var n,t=uo,e=1/0;t;)t.c?(t.t8?function(n){return n/e}:function(n){return n*e},symbol:n}}function Un(n){var t=n.decimal,e=n.thousands,r=n.grouping,u=n.currency,i=r&&e?function(n,t){for(var u=n.length,i=[],a=0,o=r[0],l=0;u>0&&o>0&&(l+o+1>t&&(o=Math.max(1,t-l)),i.push(n.substring(u-=o,u+o)),!((l+=o+1)>t));)o=r[a=(a+1)%r.length];return i.reverse().join(e)}:y;return function(n){var e=so.exec(n),r=e[1]||" ",a=e[2]||">",o=e[3]||"-",l=e[4]||"",c=e[5],s=+e[6],f=e[7],h=e[8],g=e[9],p=1,v="",d="",m=!1,y=!0;switch(h&&(h=+h.substring(1)),(c||"0"===r&&"="===a)&&(c=r="0",a="="),g){case"n":f=!0,g="g";break;case"%":p=100,d="%",g="f";break;case"p":p=100,d="%",g="r";break;case"b":case"o":case"x":case"X":"#"===l&&(v="0"+g.toLowerCase());case"c":y=!1;case"d":m=!0,h=0;break;case"s":p=-1,g="r"}"$"===l&&(v=u[0],d=u[1]),"r"!=g||h||(g="g"),null!=h&&("g"==g?h=Math.max(1,Math.min(21,h)):("e"==g||"f"==g)&&(h=Math.max(0,Math.min(20,h)))),g=fo.get(g)||Fn;var M=c&&f;return function(n){var e=d;if(m&&n%1)return"";var u=0>n||0===n&&0>1/n?(n=-n,"-"):"-"===o?"":o;if(0>p){var l=oa.formatPrefix(n,h);n=l.scale(n),e=l.symbol+d}else n*=p;n=g(n,h);var x,b,_=n.lastIndexOf(".");if(0>_){var w=y?n.lastIndexOf("e"):-1;0>w?(x=n,b=""):(x=n.substring(0,w),b=n.substring(w))}else x=n.substring(0,_),b=t+n.substring(_+1);!c&&f&&(x=i(x,1/0));var S=v.length+x.length+b.length+(M?0:u.length),k=s>S?new Array(S=s-S+1).join(r):"";return M&&(x=i(k+x,k.length?s-b.length:1/0)),u+=v,n=x+b,("<"===a?u+n+k:">"===a?k+u+n:"^"===a?k.substring(0,S>>=1)+u+n+k.substring(S):u+(M?n:k+n))+e}}}function Fn(n){return n+""}function Hn(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function On(n,t,e){function r(t){var e=n(t),r=i(e,1);return r-t>t-e?e:r}function u(e){return t(e=n(new go(e-1)),1),e}function i(n,e){return t(n=new go(+n),e),n}function a(n,r,i){var a=u(n),o=[];if(i>1)for(;r>a;)e(a)%i||o.push(new Date(+a)),t(a,1);else for(;r>a;)o.push(new Date(+a)),t(a,1);return o}function o(n,t,e){try{go=Hn;var r=new Hn;return r._=n,a(r,t,e)}finally{go=Date}}n.floor=n,n.round=r,n.ceil=u,n.offset=i,n.range=a;var l=n.utc=In(n);return l.floor=l,l.round=In(r),l.ceil=In(u),l.offset=In(i),l.range=o,n}function In(n){return function(t,e){try{go=Hn;var r=new Hn;return r._=t,n(r,e)._}finally{go=Date}}}function Yn(n){function t(n){function t(t){for(var e,u,i,a=[],o=-1,l=0;++oo;){if(r>=c)return-1;if(u=t.charCodeAt(o++),37===u){if(a=t.charAt(o++),i=C[a in vo?t.charAt(o++):a],!i||(r=i(n,e,r))<0)return-1}else if(u!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){_.lastIndex=0;var r=_.exec(t.slice(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){x.lastIndex=0;var r=x.exec(t.slice(e));return r?(n.w=b.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){N.lastIndex=0;var r=N.exec(t.slice(e));return r?(n.m=E.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,e){S.lastIndex=0;var r=S.exec(t.slice(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,r){return e(n,A.c.toString(),t,r)}function l(n,t,r){return e(n,A.x.toString(),t,r)}function c(n,t,r){return e(n,A.X.toString(),t,r)}function s(n,t,e){var r=M.get(t.slice(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var f=n.dateTime,h=n.date,g=n.time,p=n.periods,v=n.days,d=n.shortDays,m=n.months,y=n.shortMonths;t.utc=function(n){function e(n){try{go=Hn;var t=new go;return t._=n,r(t)}finally{go=Date}}var r=t(n);return e.parse=function(n){try{go=Hn;var t=r.parse(n);return t&&t._}finally{go=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=ct;var M=oa.map(),x=Vn(v),b=Xn(v),_=Vn(d),w=Xn(d),S=Vn(m),k=Xn(m),N=Vn(y),E=Xn(y);p.forEach(function(n,t){M.set(n.toLowerCase(),t)});var A={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return y[n.getMonth()]},B:function(n){return m[n.getMonth()]},c:t(f),d:function(n,t){return Zn(n.getDate(),t,2)},e:function(n,t){return Zn(n.getDate(),t,2)},H:function(n,t){return Zn(n.getHours(),t,2)},I:function(n,t){return Zn(n.getHours()%12||12,t,2)},j:function(n,t){return Zn(1+ho.dayOfYear(n),t,3)},L:function(n,t){return Zn(n.getMilliseconds(),t,3)},m:function(n,t){return Zn(n.getMonth()+1,t,2)},M:function(n,t){return Zn(n.getMinutes(),t,2)},p:function(n){return p[+(n.getHours()>=12)]},S:function(n,t){return Zn(n.getSeconds(),t,2)},U:function(n,t){return Zn(ho.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Zn(ho.mondayOfYear(n),t,2)},x:t(h),X:t(g),y:function(n,t){return Zn(n.getFullYear()%100,t,2)},Y:function(n,t){return Zn(n.getFullYear()%1e4,t,4)},Z:ot,"%":function(){return"%"}},C={a:r,A:u,b:i,B:a,c:o,d:tt,e:tt,H:rt,I:rt,j:et,L:at,m:nt,M:ut,p:s,S:it,U:Bn,w:$n,W:Wn,x:l,X:c,y:Gn,Y:Jn,Z:Kn,"%":lt};return t}function Zn(n,t,e){var r=0>n?"-":"",u=(r?-n:n)+"",i=u.length;return r+(e>i?new Array(e-i+1).join(t)+u:u)}function Vn(n){return new RegExp("^(?:"+n.map(oa.requote).join("|")+")","i")}function Xn(n){for(var t=new c,e=-1,r=n.length;++e68?1900:2e3)}function nt(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function tt(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function et(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function rt(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function ut(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function it(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function at(n,t,e){mo.lastIndex=0;var r=mo.exec(t.slice(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function ot(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=Ma(t)/60|0,u=Ma(t)%60;return e+Zn(r,"0",2)+Zn(u,"0",2)}function lt(n,t,e){yo.lastIndex=0;var r=yo.exec(t.slice(e,e+1));return r?e+r[0].length:-1}function ct(n){for(var t=n.length,e=-1;++e=0?1:-1,o=a*e,l=Math.cos(t),c=Math.sin(t),s=i*c,f=u*l+s*Math.cos(o),h=s*a*Math.sin(o);So.add(Math.atan2(h,f)),r=n,u=l,i=c}var t,e,r,u,i;ko.point=function(a,o){ko.point=n,r=(t=a)*Oa,u=Math.cos(o=(e=o)*Oa/2+ja/4),i=Math.sin(o)},ko.lineEnd=function(){n(t,e)}}function dt(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function mt(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function yt(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function Mt(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function xt(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function bt(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function _t(n){return[Math.atan2(n[1],n[0]),tn(n[2])]}function wt(n,t){return Ma(n[0]-t[0])o;++o)u.point((e=n[o])[0],e[1]);return void u.lineEnd()}var l=new Tt(e,n,null,!0),c=new Tt(e,null,l,!1);l.o=c,i.push(l),a.push(c),l=new Tt(r,n,null,!1),c=new Tt(r,null,l,!0),l.o=c,i.push(l),a.push(c)}}),a.sort(t),qt(i),qt(a),i.length){for(var o=0,l=e,c=a.length;c>o;++o)a[o].e=l=!l;for(var s,f,h=i[0];;){for(var g=h,p=!0;g.v;)if((g=g.n)===h)return;s=g.z,u.lineStart();do{if(g.v=g.o.v=!0,g.e){if(p)for(var o=0,c=s.length;c>o;++o)u.point((f=s[o])[0],f[1]);else r(g.x,g.n.x,1,u);g=g.n}else{if(p){s=g.p.z;for(var o=s.length-1;o>=0;--o)u.point((f=s[o])[0],f[1])}else r(g.x,g.p.x,-1,u);g=g.p}g=g.o,s=g.z,p=!p}while(!g.v);u.lineEnd()}}}function qt(n){if(t=n.length){for(var t,e,r=0,u=n[0];++r0){for(b||(i.polygonStart(),b=!0),i.lineStart();++a1&&2&t&&e.push(e.pop().concat(e.shift())),g.push(e.filter(Dt))}var g,p,v,d=t(i),m=u.invert(r[0],r[1]),y={point:a,lineStart:l,lineEnd:c,polygonStart:function(){y.point=s,y.lineStart=f,y.lineEnd=h,g=[],p=[]},polygonEnd:function(){y.point=a,y.lineStart=l,y.lineEnd=c,g=oa.merge(g);var n=Ot(m,p);g.length?(b||(i.polygonStart(),b=!0),Lt(g,jt,n,e,i)):n&&(b||(i.polygonStart(),b=!0),i.lineStart(),e(null,null,1,i),i.lineEnd()),b&&(i.polygonEnd(),b=!1),g=p=null},sphere:function(){i.polygonStart(),i.lineStart(),e(null,null,1,i),i.lineEnd(),i.polygonEnd()}},M=Pt(),x=t(M),b=!1;return y}}function Dt(n){return n.length>1}function Pt(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:b,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function jt(n,t){return((n=n.x)[0]<0?n[1]-Ha-Da:Ha-n[1])-((t=t.x)[0]<0?t[1]-Ha-Da:Ha-t[1])}function Ut(n){var t,e=NaN,r=NaN,u=NaN;return{lineStart:function(){n.lineStart(),t=1},point:function(i,a){var o=i>0?ja:-ja,l=Ma(i-e);Ma(l-ja)0?Ha:-Ha),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(o,r),n.point(i,r),t=0):u!==o&&l>=ja&&(Ma(e-u)Da?Math.atan((Math.sin(t)*(i=Math.cos(r))*Math.sin(e)-Math.sin(r)*(u=Math.cos(t))*Math.sin(n))/(u*i*a)):(t+r)/2}function Ht(n,t,e,r){var u;if(null==n)u=e*Ha,r.point(-ja,u),r.point(0,u),r.point(ja,u),r.point(ja,0),r.point(ja,-u),r.point(0,-u),r.point(-ja,-u),r.point(-ja,0),r.point(-ja,u);else if(Ma(n[0]-t[0])>Da){var i=n[0]o;++o){var c=t[o],s=c.length;if(s)for(var f=c[0],h=f[0],g=f[1]/2+ja/4,p=Math.sin(g),v=Math.cos(g),d=1;;){d===s&&(d=0),n=c[d];var m=n[0],y=n[1]/2+ja/4,M=Math.sin(y),x=Math.cos(y),b=m-h,_=b>=0?1:-1,w=_*b,S=w>ja,k=p*M;if(So.add(Math.atan2(k*_*Math.sin(w),v*x+k*Math.cos(w))),i+=S?b+_*Ua:b,S^h>=e^m>=e){var N=yt(dt(f),dt(n));bt(N);var E=yt(u,N);bt(E);var A=(S^b>=0?-1:1)*tn(E[2]);(r>A||r===A&&(N[0]||N[1]))&&(a+=S^b>=0?1:-1)}if(!d++)break;h=m,p=M,v=x,f=n}}return(-Da>i||Da>i&&0>So)^1&a}function It(n){function t(n,t){return Math.cos(n)*Math.cos(t)>i}function e(n){var e,i,l,c,s;return{lineStart:function(){c=l=!1,s=1},point:function(f,h){var g,p=[f,h],v=t(f,h),d=a?v?0:u(f,h):v?u(f+(0>f?ja:-ja),h):0;if(!e&&(c=l=v)&&n.lineStart(),v!==l&&(g=r(e,p),(wt(e,g)||wt(p,g))&&(p[0]+=Da,p[1]+=Da,v=t(p[0],p[1]))),v!==l)s=0,v?(n.lineStart(),g=r(p,e),n.point(g[0],g[1])):(g=r(e,p),n.point(g[0],g[1]),n.lineEnd()),e=g;else if(o&&e&&a^v){var m;d&i||!(m=r(p,e,!0))||(s=0,a?(n.lineStart(),n.point(m[0][0],m[0][1]),n.point(m[1][0],m[1][1]),n.lineEnd()):(n.point(m[1][0],m[1][1]),n.lineEnd(),n.lineStart(),n.point(m[0][0],m[0][1])))}!v||e&&wt(e,p)||n.point(p[0],p[1]),e=p,l=v,i=d},lineEnd:function(){l&&n.lineEnd(),e=null},clean:function(){return s|(c&&l)<<1}}}function r(n,t,e){var r=dt(n),u=dt(t),a=[1,0,0],o=yt(r,u),l=mt(o,o),c=o[0],s=l-c*c;if(!s)return!e&&n;var f=i*l/s,h=-i*c/s,g=yt(a,o),p=xt(a,f),v=xt(o,h);Mt(p,v);var d=g,m=mt(p,d),y=mt(d,d),M=m*m-y*(mt(p,p)-1);if(!(0>M)){var x=Math.sqrt(M),b=xt(d,(-m-x)/y);if(Mt(b,p),b=_t(b),!e)return b;var _,w=n[0],S=t[0],k=n[1],N=t[1];w>S&&(_=w,w=S,S=_);var E=S-w,A=Ma(E-ja)E;if(!A&&k>N&&(_=k,k=N,N=_),C?A?k+N>0^b[1]<(Ma(b[0]-w)ja^(w<=b[0]&&b[0]<=S)){var z=xt(d,(-m+x)/y);return Mt(z,p),[b,_t(z)]}}}function u(t,e){var r=a?n:ja-n,u=0;return-r>t?u|=1:t>r&&(u|=2),-r>e?u|=4:e>r&&(u|=8),u}var i=Math.cos(n),a=i>0,o=Ma(i)>Da,l=ve(n,6*Oa);return Rt(t,e,l,a?[0,-n]:[-ja,n-ja])}function Yt(n,t,e,r){return function(u){var i,a=u.a,o=u.b,l=a.x,c=a.y,s=o.x,f=o.y,h=0,g=1,p=s-l,v=f-c;if(i=n-l,p||!(i>0)){if(i/=p,0>p){if(h>i)return;g>i&&(g=i)}else if(p>0){if(i>g)return;i>h&&(h=i)}if(i=e-l,p||!(0>i)){if(i/=p,0>p){if(i>g)return;i>h&&(h=i)}else if(p>0){if(h>i)return;g>i&&(g=i)}if(i=t-c,v||!(i>0)){if(i/=v,0>v){if(h>i)return;g>i&&(g=i)}else if(v>0){if(i>g)return;i>h&&(h=i)}if(i=r-c,v||!(0>i)){if(i/=v,0>v){if(i>g)return;i>h&&(h=i)}else if(v>0){if(h>i)return;g>i&&(g=i)}return h>0&&(u.a={x:l+h*p,y:c+h*v}),1>g&&(u.b={x:l+g*p,y:c+g*v}),u}}}}}}function Zt(n,t,e,r){function u(r,u){return Ma(r[0]-n)0?0:3:Ma(r[0]-e)0?2:1:Ma(r[1]-t)0?1:0:u>0?3:2}function i(n,t){return a(n.x,t.x)}function a(n,t){var e=u(n,1),r=u(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(o){function l(n){for(var t=0,e=d.length,r=n[1],u=0;e>u;++u)for(var i,a=1,o=d[u],l=o.length,c=o[0];l>a;++a)i=o[a],c[1]<=r?i[1]>r&&Q(c,i,n)>0&&++t:i[1]<=r&&Q(c,i,n)<0&&--t,c=i;return 0!==t}function c(i,o,l,c){var s=0,f=0;if(null==i||(s=u(i,l))!==(f=u(o,l))||a(i,o)<0^l>0){do c.point(0===s||3===s?n:e,s>1?r:t);while((s=(s+l+4)%4)!==f)}else c.point(o[0],o[1])}function s(u,i){return u>=n&&e>=u&&i>=t&&r>=i}function f(n,t){s(n,t)&&o.point(n,t)}function h(){C.point=p,d&&d.push(m=[]),S=!0,w=!1,b=_=NaN}function g(){v&&(p(y,M),x&&w&&E.rejoin(),v.push(E.buffer())),C.point=f,w&&o.lineEnd()}function p(n,t){n=Math.max(-Fo,Math.min(Fo,n)),t=Math.max(-Fo,Math.min(Fo,t));var e=s(n,t);if(d&&m.push([n,t]),S)y=n,M=t,x=e,S=!1,e&&(o.lineStart(),o.point(n,t));else if(e&&w)o.point(n,t);else{var r={a:{x:b,y:_},b:{x:n,y:t}};A(r)?(w||(o.lineStart(),o.point(r.a.x,r.a.y)),o.point(r.b.x,r.b.y),e||o.lineEnd(),k=!1):e&&(o.lineStart(),o.point(n,t),k=!1)}b=n,_=t,w=e}var v,d,m,y,M,x,b,_,w,S,k,N=o,E=Pt(),A=Yt(n,t,e,r),C={point:f,lineStart:h,lineEnd:g,polygonStart:function(){o=E,v=[],d=[],k=!0},polygonEnd:function(){o=N,v=oa.merge(v);var t=l([n,r]),e=k&&t,u=v.length;(e||u)&&(o.polygonStart(),e&&(o.lineStart(),c(null,null,1,o),o.lineEnd()),u&&Lt(v,i,t,c,o),o.polygonEnd()),v=d=m=null}};return C}}function Vt(n){var t=0,e=ja/3,r=oe(n),u=r(t,e);return u.parallels=function(n){return arguments.length?r(t=n[0]*ja/180,e=n[1]*ja/180):[t/ja*180,e/ja*180]},u}function Xt(n,t){function e(n,t){var e=Math.sqrt(i-2*u*Math.sin(t))/u;return[e*Math.sin(n*=u),a-e*Math.cos(n)]}var r=Math.sin(n),u=(r+Math.sin(t))/2,i=1+r*(2*u-r),a=Math.sqrt(i)/u;return e.invert=function(n,t){var e=a-t;return[Math.atan2(n,e)/u,tn((i-(n*n+e*e)*u*u)/(2*u))]},e}function $t(){function n(n,t){Oo+=u*n-r*t,r=n,u=t}var t,e,r,u;Xo.point=function(i,a){Xo.point=n,t=r=i,e=u=a},Xo.lineEnd=function(){n(t,e)}}function Bt(n,t){Io>n&&(Io=n),n>Zo&&(Zo=n),Yo>t&&(Yo=t),t>Vo&&(Vo=t)}function Wt(){function n(n,t){a.push("M",n,",",t,i)}function t(n,t){a.push("M",n,",",t),o.point=e}function e(n,t){a.push("L",n,",",t)}function r(){o.point=n}function u(){a.push("Z")}var i=Jt(4.5),a=[],o={point:n,lineStart:function(){o.point=t},lineEnd:r,polygonStart:function(){o.lineEnd=u},polygonEnd:function(){o.lineEnd=r,o.point=n},pointRadius:function(n){return i=Jt(n),o},result:function(){if(a.length){var n=a.join("");return a=[],n}}};return o}function Jt(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Gt(n,t){Ao+=n,Co+=t,++zo}function Kt(){function n(n,r){var u=n-t,i=r-e,a=Math.sqrt(u*u+i*i);Lo+=a*(t+n)/2,qo+=a*(e+r)/2,To+=a,Gt(t=n,e=r)}var t,e;Bo.point=function(r,u){Bo.point=n,Gt(t=r,e=u)}}function Qt(){Bo.point=Gt}function ne(){function n(n,t){var e=n-r,i=t-u,a=Math.sqrt(e*e+i*i);Lo+=a*(r+n)/2,qo+=a*(u+t)/2,To+=a,a=u*n-r*t,Ro+=a*(r+n),Do+=a*(u+t),Po+=3*a,Gt(r=n,u=t)}var t,e,r,u;Bo.point=function(i,a){Bo.point=n,Gt(t=r=i,e=u=a)},Bo.lineEnd=function(){n(t,e)}}function te(n){function t(t,e){n.moveTo(t+a,e),n.arc(t,e,a,0,Ua)}function e(t,e){n.moveTo(t,e),o.point=r}function r(t,e){n.lineTo(t,e)}function u(){o.point=t}function i(){n.closePath()}var a=4.5,o={point:t,lineStart:function(){o.point=e},lineEnd:u,polygonStart:function(){o.lineEnd=i},polygonEnd:function(){o.lineEnd=u,o.point=t},pointRadius:function(n){return a=n,o},result:b};return o}function ee(n){function t(n){return(o?r:e)(n)}function e(t){return ie(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){M=NaN,S.point=i,t.lineStart()}function i(e,r){var i=dt([e,r]),a=n(e,r);u(M,x,y,b,_,w,M=a[0],x=a[1],y=e,b=i[0],_=i[1],w=i[2],o,t),t.point(M,x)}function a(){S.point=e,t.lineEnd()}function l(){r(),S.point=c,S.lineEnd=s}function c(n,t){ +i(f=n,h=t),g=M,p=x,v=b,d=_,m=w,S.point=i}function s(){u(M,x,y,b,_,w,g,p,f,v,d,m,o,t),S.lineEnd=a,a()}var f,h,g,p,v,d,m,y,M,x,b,_,w,S={point:e,lineStart:r,lineEnd:a,polygonStart:function(){t.polygonStart(),S.lineStart=l},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function u(t,e,r,o,l,c,s,f,h,g,p,v,d,m){var y=s-t,M=f-e,x=y*y+M*M;if(x>4*i&&d--){var b=o+g,_=l+p,w=c+v,S=Math.sqrt(b*b+_*_+w*w),k=Math.asin(w/=S),N=Ma(Ma(w)-1)i||Ma((y*z+M*L)/x-.5)>.3||a>o*g+l*p+c*v)&&(u(t,e,r,o,l,c,A,C,N,b/=S,_/=S,w,d,m),m.point(A,C),u(A,C,N,b,_,w,s,f,h,g,p,v,d,m))}}var i=.5,a=Math.cos(30*Oa),o=16;return t.precision=function(n){return arguments.length?(o=(i=n*n)>0&&16,t):Math.sqrt(i)},t}function re(n){var t=ee(function(t,e){return n([t*Ia,e*Ia])});return function(n){return le(t(n))}}function ue(n){this.stream=n}function ie(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function ae(n){return oe(function(){return n})()}function oe(n){function t(n){return n=o(n[0]*Oa,n[1]*Oa),[n[0]*h+l,c-n[1]*h]}function e(n){return n=o.invert((n[0]-l)/h,(c-n[1])/h),n&&[n[0]*Ia,n[1]*Ia]}function r(){o=Ct(a=fe(m,M,x),i);var n=i(v,d);return l=g-n[0]*h,c=p+n[1]*h,u()}function u(){return s&&(s.valid=!1,s=null),t}var i,a,o,l,c,s,f=ee(function(n,t){return n=i(n,t),[n[0]*h+l,c-n[1]*h]}),h=150,g=480,p=250,v=0,d=0,m=0,M=0,x=0,b=Uo,_=y,w=null,S=null;return t.stream=function(n){return s&&(s.valid=!1),s=le(b(a,f(_(n)))),s.valid=!0,s},t.clipAngle=function(n){return arguments.length?(b=null==n?(w=n,Uo):It((w=+n)*Oa),u()):w},t.clipExtent=function(n){return arguments.length?(S=n,_=n?Zt(n[0][0],n[0][1],n[1][0],n[1][1]):y,u()):S},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(g=+n[0],p=+n[1],r()):[g,p]},t.center=function(n){return arguments.length?(v=n[0]%360*Oa,d=n[1]%360*Oa,r()):[v*Ia,d*Ia]},t.rotate=function(n){return arguments.length?(m=n[0]%360*Oa,M=n[1]%360*Oa,x=n.length>2?n[2]%360*Oa:0,r()):[m*Ia,M*Ia,x*Ia]},oa.rebind(t,f,"precision"),function(){return i=n.apply(this,arguments),t.invert=i.invert&&e,r()}}function le(n){return ie(n,function(t,e){n.point(t*Oa,e*Oa)})}function ce(n,t){return[n,t]}function se(n,t){return[n>ja?n-Ua:-ja>n?n+Ua:n,t]}function fe(n,t,e){return n?t||e?Ct(ge(n),pe(t,e)):ge(n):t||e?pe(t,e):se}function he(n){return function(t,e){return t+=n,[t>ja?t-Ua:-ja>t?t+Ua:t,e]}}function ge(n){var t=he(n);return t.invert=he(-n),t}function pe(n,t){function e(n,t){var e=Math.cos(t),o=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),s=c*r+o*u;return[Math.atan2(l*i-s*a,o*r-c*u),tn(s*i+l*a)]}var r=Math.cos(n),u=Math.sin(n),i=Math.cos(t),a=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),o=Math.cos(n)*e,l=Math.sin(n)*e,c=Math.sin(t),s=c*i-l*a;return[Math.atan2(l*i+c*a,o*r+s*u),tn(s*r-o*u)]},e}function ve(n,t){var e=Math.cos(n),r=Math.sin(n);return function(u,i,a,o){var l=a*t;null!=u?(u=de(e,u),i=de(e,i),(a>0?i>u:u>i)&&(u+=a*Ua)):(u=n+a*Ua,i=n-.5*l);for(var c,s=u;a>0?s>i:i>s;s-=l)o.point((c=_t([e,-r*Math.cos(s),-r*Math.sin(s)]))[0],c[1])}}function de(n,t){var e=dt(t);e[0]-=n,bt(e);var r=nn(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Da)%(2*Math.PI)}function me(n,t,e){var r=oa.range(n,t-Da,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function ye(n,t,e){var r=oa.range(n,t-Da,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function Me(n){return n.source}function xe(n){return n.target}function be(n,t,e,r){var u=Math.cos(t),i=Math.sin(t),a=Math.cos(r),o=Math.sin(r),l=u*Math.cos(n),c=u*Math.sin(n),s=a*Math.cos(e),f=a*Math.sin(e),h=2*Math.asin(Math.sqrt(an(r-t)+u*a*an(e-n))),g=1/Math.sin(h),p=h?function(n){var t=Math.sin(n*=h)*g,e=Math.sin(h-n)*g,r=e*l+t*s,u=e*c+t*f,a=e*i+t*o;return[Math.atan2(u,r)*Ia,Math.atan2(a,Math.sqrt(r*r+u*u))*Ia]}:function(){return[n*Ia,t*Ia]};return p.distance=h,p}function _e(){function n(n,u){var i=Math.sin(u*=Oa),a=Math.cos(u),o=Ma((n*=Oa)-t),l=Math.cos(o);Wo+=Math.atan2(Math.sqrt((o=a*Math.sin(o))*o+(o=r*i-e*a*l)*o),e*i+r*a*l),t=n,e=i,r=a}var t,e,r;Jo.point=function(u,i){t=u*Oa,e=Math.sin(i*=Oa),r=Math.cos(i),Jo.point=n},Jo.lineEnd=function(){Jo.point=Jo.lineEnd=b}}function we(n,t){function e(t,e){var r=Math.cos(t),u=Math.cos(e),i=n(r*u);return[i*u*Math.sin(t),i*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),u=t(r),i=Math.sin(u),a=Math.cos(u);return[Math.atan2(n*i,r*a),Math.asin(r&&e*i/r)]},e}function Se(n,t){function e(n,t){a>0?-Ha+Da>t&&(t=-Ha+Da):t>Ha-Da&&(t=Ha-Da);var e=a/Math.pow(u(t),i);return[e*Math.sin(i*n),a-e*Math.cos(i*n)]}var r=Math.cos(n),u=function(n){return Math.tan(ja/4+n/2)},i=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(u(t)/u(n)),a=r*Math.pow(u(n),i)/i;return i?(e.invert=function(n,t){var e=a-t,r=K(i)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/i,2*Math.atan(Math.pow(a/r,1/i))-Ha]},e):Ne}function ke(n,t){function e(n,t){var e=i-t;return[e*Math.sin(u*n),i-e*Math.cos(u*n)]}var r=Math.cos(n),u=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),i=r/u+n;return Ma(u)u;u++){for(;r>1&&Q(n[e[r-2]],n[e[r-1]],n[u])<=0;)--r;e[r++]=u}return e.slice(0,r)}function qe(n,t){return n[0]-t[0]||n[1]-t[1]}function Te(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Re(n,t,e,r){var u=n[0],i=e[0],a=t[0]-u,o=r[0]-i,l=n[1],c=e[1],s=t[1]-l,f=r[1]-c,h=(o*(l-c)-f*(u-i))/(f*a-o*s);return[u+h*a,l+h*s]}function De(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function Pe(){rr(this),this.edge=this.site=this.circle=null}function je(n){var t=ll.pop()||new Pe;return t.site=n,t}function Ue(n){Be(n),il.remove(n),ll.push(n),rr(n)}function Fe(n){var t=n.circle,e=t.x,r=t.cy,u={x:e,y:r},i=n.P,a=n.N,o=[n];Ue(n);for(var l=i;l.circle&&Ma(e-l.circle.x)s;++s)c=o[s],l=o[s-1],nr(c.edge,l.site,c.site,u);l=o[0],c=o[f-1],c.edge=Ke(l.site,c.site,null,u),$e(l),$e(c)}function He(n){for(var t,e,r,u,i=n.x,a=n.y,o=il._;o;)if(r=Oe(o,a)-i,r>Da)o=o.L;else{if(u=i-Ie(o,a),!(u>Da)){r>-Da?(t=o.P,e=o):u>-Da?(t=o,e=o.N):t=e=o;break}if(!o.R){t=o;break}o=o.R}var l=je(n);if(il.insert(t,l),t||e){if(t===e)return Be(t),e=je(t.site),il.insert(l,e),l.edge=e.edge=Ke(t.site,l.site),$e(t),void $e(e);if(!e)return void(l.edge=Ke(t.site,l.site));Be(t),Be(e);var c=t.site,s=c.x,f=c.y,h=n.x-s,g=n.y-f,p=e.site,v=p.x-s,d=p.y-f,m=2*(h*d-g*v),y=h*h+g*g,M=v*v+d*d,x={x:(d*y-g*M)/m+s,y:(h*M-v*y)/m+f};nr(e.edge,c,p,x),l.edge=Ke(c,n,null,x),e.edge=Ke(n,p,null,x),$e(t),$e(e)}}function Oe(n,t){var e=n.site,r=e.x,u=e.y,i=u-t;if(!i)return r;var a=n.P;if(!a)return-(1/0);e=a.site;var o=e.x,l=e.y,c=l-t;if(!c)return o;var s=o-r,f=1/i-1/c,h=s/c;return f?(-h+Math.sqrt(h*h-2*f*(s*s/(-2*c)-l+c/2+u-i/2)))/f+r:(r+o)/2}function Ie(n,t){var e=n.N;if(e)return Oe(e,t);var r=n.site;return r.y===t?r.x:1/0}function Ye(n){this.site=n,this.edges=[]}function Ze(n){for(var t,e,r,u,i,a,o,l,c,s,f=n[0][0],h=n[1][0],g=n[0][1],p=n[1][1],v=ul,d=v.length;d--;)if(i=v[d],i&&i.prepare())for(o=i.edges,l=o.length,a=0;l>a;)s=o[a].end(),r=s.x,u=s.y,c=o[++a%l].start(),t=c.x,e=c.y,(Ma(r-t)>Da||Ma(u-e)>Da)&&(o.splice(a,0,new tr(Qe(i.site,s,Ma(r-f)Da?{x:f,y:Ma(t-f)Da?{x:Ma(e-p)Da?{x:h,y:Ma(t-h)Da?{x:Ma(e-g)=-Pa)){var g=l*l+c*c,p=s*s+f*f,v=(f*g-c*p)/h,d=(l*p-s*g)/h,f=d+o,m=cl.pop()||new Xe;m.arc=n,m.site=u,m.x=v+a,m.y=f+Math.sqrt(v*v+d*d),m.cy=f,n.circle=m;for(var y=null,M=ol._;M;)if(m.yd||d>=o)return;if(h>p){if(i){if(i.y>=c)return}else i={x:d,y:l};e={x:d,y:c}}else{if(i){if(i.yr||r>1)if(h>p){if(i){if(i.y>=c)return}else i={x:(l-u)/r,y:l};e={x:(c-u)/r,y:c}}else{if(i){if(i.yg){if(i){if(i.x>=o)return}else i={x:a,y:r*a+u};e={x:o,y:r*o+u}}else{if(i){if(i.xi||f>a||r>h||u>g)){if(p=n.point){var p,v=t-n.x,d=e-n.y,m=v*v+d*d;if(l>m){var y=Math.sqrt(l=m);r=t-y,u=e-y,i=t+y,a=e+y,o=p}}for(var M=n.nodes,x=.5*(s+h),b=.5*(f+g),_=t>=x,w=e>=b,S=w<<1|_,k=S+4;k>S;++S)if(n=M[3&S])switch(3&S){case 0:c(n,s,f,x,b);break;case 1:c(n,x,f,h,b);break;case 2:c(n,s,b,x,g);break;case 3:c(n,x,b,h,g)}}}(n,r,u,i,a),o}function vr(n,t){n=oa.rgb(n),t=oa.rgb(t);var e=n.r,r=n.g,u=n.b,i=t.r-e,a=t.g-r,o=t.b-u;return function(n){return"#"+bn(Math.round(e+i*n))+bn(Math.round(r+a*n))+bn(Math.round(u+o*n))}}function dr(n,t){var e,r={},u={};for(e in n)e in t?r[e]=Mr(n[e],t[e]):u[e]=n[e];for(e in t)e in n||(u[e]=t[e]);return function(n){for(e in r)u[e]=r[e](n);return u}}function mr(n,t){return n=+n,t=+t,function(e){return n*(1-e)+t*e}}function yr(n,t){var e,r,u,i=fl.lastIndex=hl.lastIndex=0,a=-1,o=[],l=[];for(n+="",t+="";(e=fl.exec(n))&&(r=hl.exec(t));)(u=r.index)>i&&(u=t.slice(i,u),o[a]?o[a]+=u:o[++a]=u),(e=e[0])===(r=r[0])?o[a]?o[a]+=r:o[++a]=r:(o[++a]=null,l.push({i:a,x:mr(e,r)})),i=hl.lastIndex;return ir;++r)o[(e=l[r]).i]=e.x(n);return o.join("")})}function Mr(n,t){for(var e,r=oa.interpolators.length;--r>=0&&!(e=oa.interpolators[r](n,t)););return e}function xr(n,t){var e,r=[],u=[],i=n.length,a=t.length,o=Math.min(n.length,t.length);for(e=0;o>e;++e)r.push(Mr(n[e],t[e]));for(;i>e;++e)u[e]=n[e];for(;a>e;++e)u[e]=t[e];return function(n){for(e=0;o>e;++e)u[e]=r[e](n);return u}}function br(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function _r(n){return function(t){return 1-n(1-t)}}function wr(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function Sr(n){return n*n}function kr(n){return n*n*n}function Nr(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function Er(n){return function(t){return Math.pow(t,n)}}function Ar(n){return 1-Math.cos(n*Ha)}function Cr(n){return Math.pow(2,10*(n-1))}function zr(n){return 1-Math.sqrt(1-n*n)}function Lr(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/Ua*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*Ua/t)}}function qr(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function Tr(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Rr(n,t){n=oa.hcl(n),t=oa.hcl(t);var e=n.h,r=n.c,u=n.l,i=t.h-e,a=t.c-r,o=t.l-u;return isNaN(a)&&(a=0,r=isNaN(r)?t.c:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return fn(e+i*n,r+a*n,u+o*n)+""}}function Dr(n,t){n=oa.hsl(n),t=oa.hsl(t);var e=n.h,r=n.s,u=n.l,i=t.h-e,a=t.s-r,o=t.l-u;return isNaN(a)&&(a=0,r=isNaN(r)?t.s:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return cn(e+i*n,r+a*n,u+o*n)+""}}function Pr(n,t){n=oa.lab(n),t=oa.lab(t);var e=n.l,r=n.a,u=n.b,i=t.l-e,a=t.a-r,o=t.b-u;return function(n){return gn(e+i*n,r+a*n,u+o*n)+""}}function jr(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function Ur(n){var t=[n.a,n.b],e=[n.c,n.d],r=Hr(t),u=Fr(t,e),i=Hr(Or(e,t,-u))||0;t[0]*e[1]180?t+=360:t-n>180&&(n+=360),r.push({i:e.push(Ir(e)+"rotate(",null,")")-2,x:mr(n,t)})):t&&e.push(Ir(e)+"rotate("+t+")")}function Vr(n,t,e,r){n!==t?r.push({i:e.push(Ir(e)+"skewX(",null,")")-2,x:mr(n,t)}):t&&e.push(Ir(e)+"skewX("+t+")")}function Xr(n,t,e,r){if(n[0]!==t[0]||n[1]!==t[1]){var u=e.push(Ir(e)+"scale(",null,",",null,")");r.push({i:u-4,x:mr(n[0],t[0])},{i:u-2,x:mr(n[1],t[1])})}else(1!==t[0]||1!==t[1])&&e.push(Ir(e)+"scale("+t+")")}function $r(n,t){var e=[],r=[];return n=oa.transform(n),t=oa.transform(t),Yr(n.translate,t.translate,e,r),Zr(n.rotate,t.rotate,e,r),Vr(n.skew,t.skew,e,r),Xr(n.scale,t.scale,e,r),n=t=null,function(n){for(var t,u=-1,i=r.length;++u=0;)e.push(u[r])}function au(n,t){for(var e=[n],r=[];null!=(n=e.pop());)if(r.push(n),(i=n.children)&&(u=i.length))for(var u,i,a=-1;++ae;++e)(t=n[e][1])>u&&(r=e,u=t);return r}function mu(n){return n.reduce(yu,0)}function yu(n,t){return n+t[1]}function Mu(n,t){return xu(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function xu(n,t){for(var e=-1,r=+n[0],u=(n[1]-r)/t,i=[];++e<=t;)i[e]=u*e+r;return i}function bu(n){return[oa.min(n),oa.max(n)]}function _u(n,t){return n.value-t.value}function wu(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function Su(n,t){n._pack_next=t,t._pack_prev=n}function ku(n,t){var e=t.x-n.x,r=t.y-n.y,u=n.r+t.r;return.999*u*u>e*e+r*r}function Nu(n){function t(n){s=Math.min(n.x-n.r,s),f=Math.max(n.x+n.r,f),h=Math.min(n.y-n.r,h),g=Math.max(n.y+n.r,g)}if((e=n.children)&&(c=e.length)){var e,r,u,i,a,o,l,c,s=1/0,f=-(1/0),h=1/0,g=-(1/0);if(e.forEach(Eu),r=e[0],r.x=-r.r,r.y=0,t(r),c>1&&(u=e[1],u.x=u.r,u.y=0,t(u),c>2))for(i=e[2],zu(r,u,i),t(i),wu(r,i),r._pack_prev=i,wu(i,u),u=r._pack_next,a=3;c>a;a++){zu(r,u,i=e[a]);var p=0,v=1,d=1;for(o=u._pack_next;o!==u;o=o._pack_next,v++)if(ku(o,i)){p=1;break}if(1==p)for(l=r._pack_prev;l!==o._pack_prev&&!ku(l,i);l=l._pack_prev,d++);p?(d>v||v==d&&u.ra;a++)i=e[a],i.x-=m,i.y-=y,M=Math.max(M,i.r+Math.sqrt(i.x*i.x+i.y*i.y));n.r=M,e.forEach(Au)}}function Eu(n){n._pack_next=n._pack_prev=n}function Au(n){delete n._pack_next,delete n._pack_prev}function Cu(n,t,e,r){var u=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,u)for(var i=-1,a=u.length;++i=0;)t=u[i],t.z+=e,t.m+=e,e+=t.s+(r+=t.c)}function Pu(n,t,e){return n.a.parent===t.parent?n.a:e}function ju(n){return 1+oa.max(n,function(n){return n.y})}function Uu(n){return n.reduce(function(n,t){return n+t.x},0)/n.length}function Fu(n){var t=n.children;return t&&t.length?Fu(t[0]):n}function Hu(n){var t,e=n.children;return e&&(t=e.length)?Hu(e[t-1]):n}function Ou(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function Iu(n,t){var e=n.x+t[3],r=n.y+t[0],u=n.dx-t[1]-t[3],i=n.dy-t[0]-t[2];return 0>u&&(e+=u/2,u=0),0>i&&(r+=i/2,i=0),{x:e,y:r,dx:u,dy:i}}function Yu(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Zu(n){return n.rangeExtent?n.rangeExtent():Yu(n.range())}function Vu(n,t,e,r){var u=e(n[0],n[1]),i=r(t[0],t[1]);return function(n){return i(u(n))}}function Xu(n,t){var e,r=0,u=n.length-1,i=n[r],a=n[u];return i>a&&(e=r,r=u,u=e,e=i,i=a,a=e),n[r]=t.floor(i),n[u]=t.ceil(a),n}function $u(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:wl}function Bu(n,t,e,r){var u=[],i=[],a=0,o=Math.min(n.length,t.length)-1;for(n[o]2?Bu:Vu,l=r?Wr:Br;return a=u(n,t,l,e),o=u(t,n,l,Mr),i}function i(n){return a(n)}var a,o;return i.invert=function(n){return o(n)},i.domain=function(t){return arguments.length?(n=t.map(Number),u()):n},i.range=function(n){return arguments.length?(t=n,u()):t},i.rangeRound=function(n){return i.range(n).interpolate(jr)},i.clamp=function(n){return arguments.length?(r=n,u()):r},i.interpolate=function(n){return arguments.length?(e=n,u()):e},i.ticks=function(t){return Qu(n,t)},i.tickFormat=function(t,e){return ni(n,t,e)},i.nice=function(t){return Gu(n,t),u()},i.copy=function(){return Wu(n,t,e,r)},u()}function Ju(n,t){return oa.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Gu(n,t){return Xu(n,$u(Ku(n,t)[2]))}function Ku(n,t){null==t&&(t=10);var e=Yu(n),r=e[1]-e[0],u=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),i=t/r*u;return.15>=i?u*=10:.35>=i?u*=5:.75>=i&&(u*=2),e[0]=Math.ceil(e[0]/u)*u,e[1]=Math.floor(e[1]/u)*u+.5*u,e[2]=u,e}function Qu(n,t){return oa.range.apply(oa,Ku(n,t))}function ni(n,t,e){var r=Ku(n,t);if(e){var u=so.exec(e);if(u.shift(),"s"===u[8]){var i=oa.formatPrefix(Math.max(Ma(r[0]),Ma(r[1])));return u[7]||(u[7]="."+ti(i.scale(r[2]))),u[8]="f",e=oa.format(u.join("")),function(n){return e(i.scale(n))+i.symbol}}u[7]||(u[7]="."+ei(u[8],r)),e=u.join("")}else e=",."+ti(r[2])+"f";return oa.format(e)}function ti(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function ei(n,t){var e=ti(t[2]);return n in Sl?Math.abs(e-ti(Math.max(Ma(t[0]),Ma(t[1]))))+ +("e"!==n):e-2*("%"===n)}function ri(n,t,e,r){function u(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function i(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function a(t){return n(u(t))}return a.invert=function(t){return i(n.invert(t))},a.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(u)),a):r},a.base=function(e){return arguments.length?(t=+e,n.domain(r.map(u)),a):t},a.nice=function(){var t=Xu(r.map(u),e?Math:Nl);return n.domain(t),r=t.map(i),a},a.ticks=function(){var n=Yu(r),a=[],o=n[0],l=n[1],c=Math.floor(u(o)),s=Math.ceil(u(l)),f=t%1?2:t;if(isFinite(s-c)){if(e){for(;s>c;c++)for(var h=1;f>h;h++)a.push(i(c)*h);a.push(i(c))}else for(a.push(i(c));c++0;h--)a.push(i(c)*h);for(c=0;a[c]l;s--);a=a.slice(c,s)}return a},a.tickFormat=function(n,t){if(!arguments.length)return kl;arguments.length<2?t=kl:"function"!=typeof t&&(t=oa.format(t));var r,o=Math.max(.1,n/a.ticks().length),l=e?(r=1e-12,Math.ceil):(r=-1e-12,Math.floor);return function(n){return n/i(l(u(n)+r))<=o?t(n):""}},a.copy=function(){return ri(n.copy(),t,e,r)},Ju(a,n)}function ui(n,t,e){function r(t){return n(u(t))}var u=ii(t),i=ii(1/t);return r.invert=function(t){return i(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(u)),r):e},r.ticks=function(n){return Qu(e,n)},r.tickFormat=function(n,t){return ni(e,n,t)},r.nice=function(n){return r.domain(Gu(e,n))},r.exponent=function(a){return arguments.length?(u=ii(t=a),i=ii(1/t),n.domain(e.map(u)),r):t},r.copy=function(){return ui(n.copy(),t,e)},Ju(r,n)}function ii(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function ai(n,t){function e(e){return i[((u.get(e)||("range"===t.t?u.set(e,n.push(e)):NaN))-1)%i.length]}function r(t,e){return oa.range(n.length).map(function(n){return t+e*n})}var u,i,a;return e.domain=function(r){if(!arguments.length)return n;n=[],u=new c;for(var i,a=-1,o=r.length;++ae?[NaN,NaN]:[e>0?o[e-1]:n[0],et?NaN:t/i+n,[t,t+1/i]},r.copy=function(){return li(n,t,e)},u()}function ci(n,t){function e(e){return e>=e?t[oa.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return ci(n,t)},e}function si(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Qu(n,t)},t.tickFormat=function(t,e){return ni(n,t,e)},t.copy=function(){return si(n)},t}function fi(){return 0}function hi(n){return n.innerRadius}function gi(n){return n.outerRadius}function pi(n){return n.startAngle}function vi(n){return n.endAngle}function di(n){return n&&n.padAngle}function mi(n,t,e,r){return(n-e)*t-(t-r)*n>0?0:1}function yi(n,t,e,r,u){var i=n[0]-t[0],a=n[1]-t[1],o=(u?r:-r)/Math.sqrt(i*i+a*a),l=o*a,c=-o*i,s=n[0]+l,f=n[1]+c,h=t[0]+l,g=t[1]+c,p=(s+h)/2,v=(f+g)/2,d=h-s,m=g-f,y=d*d+m*m,M=e-r,x=s*g-h*f,b=(0>m?-1:1)*Math.sqrt(Math.max(0,M*M*y-x*x)),_=(x*m-d*b)/y,w=(-x*d-m*b)/y,S=(x*m+d*b)/y,k=(-x*d+m*b)/y,N=_-p,E=w-v,A=S-p,C=k-v;return N*N+E*E>A*A+C*C&&(_=S,w=k),[[_-l,w-c],[_*e/M,w*e/M]]}function Mi(n){function t(t){function a(){c.push("M",i(n(s),o))}for(var l,c=[],s=[],f=-1,h=t.length,g=En(e),p=En(r);++f1?n.join("L"):n+"Z"}function bi(n){return n.join("L")+"Z"}function _i(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t1&&u.push("H",r[0]),u.join("")}function wi(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t1){o=t[1],i=n[l],l++,r+="C"+(u[0]+a[0])+","+(u[1]+a[1])+","+(i[0]-o[0])+","+(i[1]-o[1])+","+i[0]+","+i[1];for(var c=2;c9&&(u=3*t/Math.sqrt(u),a[o]=u*e,a[o+1]=u*r));for(o=-1;++o<=l;)u=(n[Math.min(l,o+1)][0]-n[Math.max(0,o-1)][0])/(6*(1+a[o]*a[o])),i.push([u||0,a[o]*u||0]);return i}function Fi(n){return n.length<3?xi(n):n[0]+Ai(n,Ui(n))}function Hi(n){for(var t,e,r,u=-1,i=n.length;++u=t?a(n-t):void(s.c=a)}function a(e){var u=p.active,i=p[u];i&&(i.timer.c=null,i.timer.t=NaN,--p.count,delete p[u],i.event&&i.event.interrupt.call(n,n.__data__,i.index));for(var a in p)if(r>+a){var c=p[a];c.timer.c=null,c.timer.t=NaN,--p.count,delete p[a]}s.c=o,qn(function(){return s.c&&o(e||1)&&(s.c=null,s.t=NaN),1},0,l),p.active=r,v.event&&v.event.start.call(n,n.__data__,t),g=[],v.tween.forEach(function(e,r){(r=r.call(n,n.__data__,t))&&g.push(r)}),h=v.ease,f=v.duration}function o(u){for(var i=u/f,a=h(i),o=g.length;o>0;)g[--o].call(n,a);return i>=1?(v.event&&v.event.end.call(n,n.__data__,t),--p.count?delete p[r]:delete n[e],1):void 0}var l,s,f,h,g,p=n[e]||(n[e]={active:0,count:0}),v=p[r];v||(l=u.time,s=qn(i,0,l),v=p[r]={tween:new c,time:l,timer:s,delay:u.delay,duration:u.duration,ease:u.ease,index:t},u=null,++p.count)}function na(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate("+(isFinite(r)?r:e(n))+",0)"})}function ta(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate(0,"+(isFinite(r)?r:e(n))+")"})}function ea(n){return n.toISOString()}function ra(n,t,e){function r(t){return n(t)}function u(n,e){var r=n[1]-n[0],u=r/e,i=oa.bisect(Gl,u);return i==Gl.length?[t.year,Ku(n.map(function(n){return n/31536e6}),e)[2]]:i?t[u/Gl[i-1]1?{floor:function(t){for(;e(t=n.floor(t));)t=ua(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=ua(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Yu(r.domain()),i=null==n?u(e,10):"number"==typeof n?u(e,n):!n.range&&[{range:n},t];return i&&(n=i[0],t=i[1]),n.range(e[0],ua(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return ra(n.copy(),t,e)},Ju(r,n)}function ua(n){return new Date(n)}function ia(n){return JSON.parse(n.responseText)}function aa(n){var t=sa.createRange();return t.selectNode(sa.body),t.createContextualFragment(n.responseText)}var oa={version:"3.5.10"},la=[].slice,ca=function(n){return la.call(n)},sa=this.document;if(sa)try{ca(sa.documentElement.childNodes)[0].nodeType}catch(fa){ca=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}if(Date.now||(Date.now=function(){return+new Date}),sa)try{sa.createElement("DIV").style.setProperty("opacity",0,"")}catch(ha){var ga=this.Element.prototype,pa=ga.setAttribute,va=ga.setAttributeNS,da=this.CSSStyleDeclaration.prototype,ma=da.setProperty;ga.setAttribute=function(n,t){pa.call(this,n,t+"")},ga.setAttributeNS=function(n,t,e){va.call(this,n,t,e+"")},da.setProperty=function(n,t,e){ma.call(this,n,t+"",e)}}oa.ascending=e,oa.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:NaN},oa.min=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u=r){e=r;break}for(;++ur&&(e=r)}else{for(;++u=r){e=r;break}for(;++ur&&(e=r)}return e},oa.max=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u=r){e=r;break}for(;++ue&&(e=r)}else{for(;++u=r){e=r;break}for(;++ue&&(e=r)}return e},oa.extent=function(n,t){var e,r,u,i=-1,a=n.length;if(1===arguments.length){for(;++i=r){e=u=r;break}for(;++ir&&(e=r),r>u&&(u=r))}else{for(;++i=r){e=u=r;break}for(;++ir&&(e=r),r>u&&(u=r))}return[e,u]},oa.sum=function(n,t){var e,r=0,i=n.length,a=-1;if(1===arguments.length)for(;++a1?l/(s-1):void 0},oa.deviation=function(){var n=oa.variance.apply(this,arguments);return n?Math.sqrt(n):n};var ya=i(e);oa.bisectLeft=ya.left,oa.bisect=oa.bisectRight=ya.right,oa.bisector=function(n){return i(1===n.length?function(t,r){return e(n(t),r)}:n)},oa.shuffle=function(n,t,e){(i=arguments.length)<3&&(e=n.length,2>i&&(t=0));for(var r,u,i=e-t;i;)u=Math.random()*i--|0,r=n[i+t],n[i+t]=n[u+t],n[u+t]=r;return n},oa.permute=function(n,t){for(var e=t.length,r=new Array(e);e--;)r[e]=n[t[e]];return r},oa.pairs=function(n){for(var t,e=0,r=n.length-1,u=n[0],i=new Array(0>r?0:r);r>e;)i[e]=[t=u,u=n[++e]];return i},oa.zip=function(){if(!(r=arguments.length))return[];for(var n=-1,t=oa.min(arguments,a),e=new Array(t);++n=0;)for(r=n[u],t=r.length;--t>=0;)e[--a]=r[t];return e};var Ma=Math.abs;oa.range=function(n,t,e){if(arguments.length<3&&(e=1,arguments.length<2&&(t=n,n=0)),(t-n)/e===1/0)throw new Error("infinite range");var r,u=[],i=o(Ma(e)),a=-1;if(n*=i,t*=i,e*=i,0>e)for(;(r=n+e*++a)>t;)u.push(r/i);else for(;(r=n+e*++a)=i.length)return r?r.call(u,a):e?a.sort(e):a;for(var l,s,f,h,g=-1,p=a.length,v=i[o++],d=new c;++g=i.length)return n;var r=[],u=a[e++];return n.forEach(function(n,u){r.push({key:n,values:t(u,e)})}),u?r.sort(function(n,t){return u(n.key,t.key)}):r}var e,r,u={},i=[],a=[];return u.map=function(t,e){return n(e,t,0)},u.entries=function(e){return t(n(oa.map,e,0),0)},u.key=function(n){return i.push(n),u},u.sortKeys=function(n){return a[i.length-1]=n,u},u.sortValues=function(n){return e=n,u},u.rollup=function(n){return r=n,u},u},oa.set=function(n){var t=new m;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},l(m,{has:h,add:function(n){return this._[s(n+="")]=!0,n},remove:g,values:p,size:v,empty:d,forEach:function(n){for(var t in this._)n.call(this,f(t))}}),oa.behavior={},oa.rebind=function(n,t){for(var e,r=1,u=arguments.length;++r=0&&(r=n.slice(e+1),n=n.slice(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},oa.event=null,oa.requote=function(n){return n.replace(wa,"\\$&")};var wa=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,Sa={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},ka=function(n,t){return t.querySelector(n)},Na=function(n,t){return t.querySelectorAll(n)},Ea=function(n,t){var e=n.matches||n[x(n,"matchesSelector")];return(Ea=function(n,t){return e.call(n,t)})(n,t)};"function"==typeof Sizzle&&(ka=function(n,t){return Sizzle(n,t)[0]||null},Na=Sizzle,Ea=Sizzle.matchesSelector),oa.selection=function(){return oa.select(sa.documentElement)};var Aa=oa.selection.prototype=[];Aa.select=function(n){var t,e,r,u,i=[];n=A(n);for(var a=-1,o=this.length;++a=0&&"xmlns"!==(e=n.slice(0,t))&&(n=n.slice(t+1)),Ca.hasOwnProperty(e)?{space:Ca[e],local:n}:n}},Aa.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=oa.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(z(t,n[t]));return this}return this.each(z(n,t))},Aa.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=T(n)).length,u=-1;if(t=e.classList){for(;++uu){if("string"!=typeof n){2>u&&(e="");for(r in n)this.each(P(r,n[r],e));return this}if(2>u){var i=this.node();return t(i).getComputedStyle(i,null).getPropertyValue(n)}r=""}return this.each(P(n,e,r))},Aa.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(j(t,n[t]));return this}return this.each(j(n,t))},Aa.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},Aa.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},Aa.append=function(n){return n=U(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},Aa.insert=function(n,t){return n=U(n),t=A(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},Aa.remove=function(){return this.each(F)},Aa.data=function(n,t){function e(n,e){var r,u,i,a=n.length,f=e.length,h=Math.min(a,f),g=new Array(f),p=new Array(f),v=new Array(a);if(t){var d,m=new c,y=new Array(a);for(r=-1;++rr;++r)p[r]=H(e[r]);for(;a>r;++r)v[r]=n[r]}p.update=g,p.parentNode=g.parentNode=v.parentNode=n.parentNode,o.push(p),l.push(g),s.push(v)}var r,u,i=-1,a=this.length;if(!arguments.length){for(n=new Array(a=(r=this[0]).length);++ii;i++){u.push(t=[]),t.parentNode=(e=this[i]).parentNode;for(var o=0,l=e.length;l>o;o++)(r=e[o])&&n.call(r,r.__data__,o,i)&&t.push(r)}return E(u)},Aa.order=function(){for(var n=-1,t=this.length;++n=0;)(e=r[u])&&(i&&i!==e.nextSibling&&i.parentNode.insertBefore(e,i),i=e);return this},Aa.sort=function(n){n=I.apply(this,arguments);for(var t=-1,e=this.length;++tn;n++)for(var e=this[n],r=0,u=e.length;u>r;r++){var i=e[r];if(i)return i}return null},Aa.size=function(){var n=0;return Y(this,function(){++n}),n};var za=[];oa.selection.enter=Z,oa.selection.enter.prototype=za,za.append=Aa.append,za.empty=Aa.empty,za.node=Aa.node,za.call=Aa.call,za.size=Aa.size,za.select=function(n){for(var t,e,r,u,i,a=[],o=-1,l=this.length;++or){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(X(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(X(n,t,e))};var La=oa.map({mouseenter:"mouseover",mouseleave:"mouseout"});sa&&La.forEach(function(n){"on"+n in sa&&La.remove(n)});var qa,Ta=0;oa.mouse=function(n){return J(n,k())};var Ra=this.navigator&&/WebKit/.test(this.navigator.userAgent)?-1:0;oa.touch=function(n,t,e){if(arguments.length<3&&(e=t,t=k().changedTouches),t)for(var r,u=0,i=t.length;i>u;++u)if((r=t[u]).identifier===e)return J(n,r)},oa.behavior.drag=function(){function n(){this.on("mousedown.drag",i).on("touchstart.drag",a)}function e(n,t,e,i,a){return function(){function o(){var n,e,r=t(h,v);r&&(n=r[0]-M[0],e=r[1]-M[1],p|=n|e,M=r,g({type:"drag",x:r[0]+c[0],y:r[1]+c[1],dx:n,dy:e}))}function l(){t(h,v)&&(m.on(i+d,null).on(a+d,null),y(p),g({type:"dragend"}))}var c,s=this,f=oa.event.target,h=s.parentNode,g=r.of(s,arguments),p=0,v=n(),d=".drag"+(null==v?"":"-"+v),m=oa.select(e(f)).on(i+d,o).on(a+d,l),y=W(f),M=t(h,v);u?(c=u.apply(s,arguments),c=[c.x-M[0],c.y-M[1]]):c=[0,0],g({type:"dragstart"})}}var r=N(n,"drag","dragstart","dragend"),u=null,i=e(b,oa.mouse,t,"mousemove","mouseup"),a=e(G,oa.touch,y,"touchmove","touchend");return n.origin=function(t){return arguments.length?(u=t,n):u},oa.rebind(n,r,"on")},oa.touches=function(n,t){return arguments.length<2&&(t=k().touches),t?ca(t).map(function(t){var e=J(n,t);return e.identifier=t.identifier,e}):[]};var Da=1e-6,Pa=Da*Da,ja=Math.PI,Ua=2*ja,Fa=Ua-Da,Ha=ja/2,Oa=ja/180,Ia=180/ja,Ya=Math.SQRT2,Za=2,Va=4;oa.interpolateZoom=function(n,t){var e,r,u=n[0],i=n[1],a=n[2],o=t[0],l=t[1],c=t[2],s=o-u,f=l-i,h=s*s+f*f;if(Pa>h)r=Math.log(c/a)/Ya,e=function(n){return[u+n*s,i+n*f,a*Math.exp(Ya*n*r)]};else{var g=Math.sqrt(h),p=(c*c-a*a+Va*h)/(2*a*Za*g),v=(c*c-a*a-Va*h)/(2*c*Za*g),d=Math.log(Math.sqrt(p*p+1)-p),m=Math.log(Math.sqrt(v*v+1)-v);r=(m-d)/Ya,e=function(n){var t=n*r,e=rn(d),o=a/(Za*g)*(e*un(Ya*t+d)-en(d));return[u+o*s,i+o*f,a*e/rn(Ya*t+d)]}}return e.duration=1e3*r,e},oa.behavior.zoom=function(){function n(n){n.on(L,f).on($a+".zoom",g).on("dblclick.zoom",p).on(R,h)}function e(n){return[(n[0]-k.x)/k.k,(n[1]-k.y)/k.k]}function r(n){return[n[0]*k.k+k.x,n[1]*k.k+k.y]}function u(n){k.k=Math.max(A[0],Math.min(A[1],n))}function i(n,t){t=r(t),k.x+=n[0]-t[0],k.y+=n[1]-t[1]}function a(t,e,r,a){t.__chart__={x:k.x,y:k.y,k:k.k},u(Math.pow(2,a)),i(d=e,r),t=oa.select(t),C>0&&(t=t.transition().duration(C)),t.call(n.event)}function o(){b&&b.domain(x.range().map(function(n){return(n-k.x)/k.k}).map(x.invert)),w&&w.domain(_.range().map(function(n){return(n-k.y)/k.k}).map(_.invert))}function l(n){z++||n({type:"zoomstart"})}function c(n){o(),n({type:"zoom",scale:k.k,translate:[k.x,k.y]})}function s(n){--z||(n({type:"zoomend"}),d=null)}function f(){function n(){o=1,i(oa.mouse(u),h),c(a)}function r(){f.on(q,null).on(T,null),g(o),s(a)}var u=this,a=D.of(u,arguments),o=0,f=oa.select(t(u)).on(q,n).on(T,r),h=e(oa.mouse(u)),g=W(u);Ol.call(u),l(a)}function h(){function n(){var n=oa.touches(p);return g=k.k,n.forEach(function(n){n.identifier in d&&(d[n.identifier]=e(n))}),n}function t(){var t=oa.event.target;oa.select(t).on(x,r).on(b,o),_.push(t);for(var e=oa.event.changedTouches,u=0,i=e.length;i>u;++u)d[e[u].identifier]=null;var l=n(),c=Date.now();if(1===l.length){if(500>c-M){var s=l[0];a(p,s,d[s.identifier],Math.floor(Math.log(k.k)/Math.LN2)+1),S()}M=c}else if(l.length>1){var s=l[0],f=l[1],h=s[0]-f[0],g=s[1]-f[1];m=h*h+g*g}}function r(){var n,t,e,r,a=oa.touches(p);Ol.call(p);for(var o=0,l=a.length;l>o;++o,r=null)if(e=a[o],r=d[e.identifier]){if(t)break;n=e,t=r}if(r){var s=(s=e[0]-n[0])*s+(s=e[1]-n[1])*s,f=m&&Math.sqrt(s/m);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+r[0])/2,(t[1]+r[1])/2],u(f*g)}M=null,i(n,t),c(v)}function o(){if(oa.event.touches.length){for(var t=oa.event.changedTouches,e=0,r=t.length;r>e;++e)delete d[t[e].identifier];for(var u in d)return void n()}oa.selectAll(_).on(y,null),w.on(L,f).on(R,h),N(),s(v)}var g,p=this,v=D.of(p,arguments),d={},m=0,y=".zoom-"+oa.event.changedTouches[0].identifier,x="touchmove"+y,b="touchend"+y,_=[],w=oa.select(p),N=W(p);t(),l(v),w.on(L,null).on(R,t)}function g(){var n=D.of(this,arguments);y?clearTimeout(y):(Ol.call(this),v=e(d=m||oa.mouse(this)),l(n)),y=setTimeout(function(){y=null,s(n)},50),S(),u(Math.pow(2,.002*Xa())*k.k),i(d,v),c(n)}function p(){var n=oa.mouse(this),t=Math.log(k.k)/Math.LN2;a(this,n,e(n),oa.event.shiftKey?Math.ceil(t)-1:Math.floor(t)+1)}var v,d,m,y,M,x,b,_,w,k={x:0,y:0,k:1},E=[960,500],A=Ba,C=250,z=0,L="mousedown.zoom",q="mousemove.zoom",T="mouseup.zoom",R="touchstart.zoom",D=N(n,"zoomstart","zoom","zoomend");return $a||($a="onwheel"in sa?(Xa=function(){return-oa.event.deltaY*(oa.event.deltaMode?120:1)},"wheel"):"onmousewheel"in sa?(Xa=function(){return oa.event.wheelDelta},"mousewheel"):(Xa=function(){return-oa.event.detail},"MozMousePixelScroll")),n.event=function(n){n.each(function(){var n=D.of(this,arguments),t=k;Fl?oa.select(this).transition().each("start.zoom",function(){k=this.__chart__||{x:0,y:0,k:1},l(n)}).tween("zoom:zoom",function(){var e=E[0],r=E[1],u=d?d[0]:e/2,i=d?d[1]:r/2,a=oa.interpolateZoom([(u-k.x)/k.k,(i-k.y)/k.k,e/k.k],[(u-t.x)/t.k,(i-t.y)/t.k,e/t.k]);return function(t){var r=a(t),o=e/r[2];this.__chart__=k={x:u-r[0]*o,y:i-r[1]*o,k:o},c(n)}}).each("interrupt.zoom",function(){s(n)}).each("end.zoom",function(){s(n)}):(this.__chart__=k,l(n),c(n),s(n))})},n.translate=function(t){return arguments.length?(k={x:+t[0],y:+t[1],k:k.k},o(),n):[k.x,k.y]},n.scale=function(t){return arguments.length?(k={x:k.x,y:k.y,k:null},u(+t),o(),n):k.k},n.scaleExtent=function(t){return arguments.length?(A=null==t?Ba:[+t[0],+t[1]],n):A},n.center=function(t){return arguments.length?(m=t&&[+t[0],+t[1]],n):m},n.size=function(t){return arguments.length?(E=t&&[+t[0],+t[1]],n):E},n.duration=function(t){return arguments.length?(C=+t,n):C},n.x=function(t){return arguments.length?(b=t,x=t.copy(),k={x:0,y:0,k:1},n):b},n.y=function(t){return arguments.length?(w=t,_=t.copy(),k={x:0,y:0,k:1},n):w},oa.rebind(n,D,"on")};var Xa,$a,Ba=[0,1/0];oa.color=on,on.prototype.toString=function(){return this.rgb()+""},oa.hsl=ln;var Wa=ln.prototype=new on;Wa.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,this.l/n)},Wa.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new ln(this.h,this.s,n*this.l)},Wa.rgb=function(){return cn(this.h,this.s,this.l)},oa.hcl=sn;var Ja=sn.prototype=new on;Ja.brighter=function(n){return new sn(this.h,this.c,Math.min(100,this.l+Ga*(arguments.length?n:1)))},Ja.darker=function(n){return new sn(this.h,this.c,Math.max(0,this.l-Ga*(arguments.length?n:1)))},Ja.rgb=function(){return fn(this.h,this.c,this.l).rgb()},oa.lab=hn;var Ga=18,Ka=.95047,Qa=1,no=1.08883,to=hn.prototype=new on;to.brighter=function(n){return new hn(Math.min(100,this.l+Ga*(arguments.length?n:1)),this.a,this.b)},to.darker=function(n){return new hn(Math.max(0,this.l-Ga*(arguments.length?n:1)),this.a,this.b)},to.rgb=function(){return gn(this.l,this.a,this.b)},oa.rgb=yn;var eo=yn.prototype=new on;eo.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,u=30;return t||e||r?(t&&u>t&&(t=u),e&&u>e&&(e=u),r&&u>r&&(r=u),new yn(Math.min(255,t/n),Math.min(255,e/n),Math.min(255,r/n))):new yn(u,u,u)},eo.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new yn(n*this.r,n*this.g,n*this.b)},eo.hsl=function(){return wn(this.r,this.g,this.b)},eo.toString=function(){return"#"+bn(this.r)+bn(this.g)+bn(this.b)};var ro=oa.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});ro.forEach(function(n,t){ro.set(n,Mn(t))}),oa.functor=En,oa.xhr=An(y),oa.dsv=function(n,t){function e(n,e,i){arguments.length<3&&(i=e,e=null);var a=Cn(n,t,null==e?r:u(e),i);return a.row=function(n){return arguments.length?a.response(null==(e=n)?r:u(n)):e},a}function r(n){return e.parse(n.responseText)}function u(n){return function(t){return e.parse(t.responseText,n)}}function i(t){return t.map(a).join(n)}function a(n){return o.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var o=new RegExp('["'+n+"\n]"),l=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var u=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(u(n),e)}:u})},e.parseRows=function(n,t){function e(){if(s>=c)return a;if(u)return u=!1,i;var t=s;if(34===n.charCodeAt(t)){for(var e=t;e++s;){var r=n.charCodeAt(s++),o=1;if(10===r)u=!0;else if(13===r)u=!0,10===n.charCodeAt(s)&&(++s,++o);else if(r!==l)continue;return n.slice(t,s-o)}return n.slice(t)}for(var r,u,i={},a={},o=[],c=n.length,s=0,f=0;(r=e())!==a;){for(var h=[];r!==i&&r!==a;)h.push(r),r=e();t&&null==(h=t(h,f++))||o.push(h)}return o},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new m,u=[];return t.forEach(function(n){for(var t in n)r.has(t)||u.push(r.add(t))}),[u.map(a).join(n)].concat(t.map(function(t){return u.map(function(n){return a(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(i).join("\n")},e},oa.csv=oa.dsv(",","text/csv"),oa.tsv=oa.dsv(" ","text/tab-separated-values");var uo,io,ao,oo,lo=this[x(this,"requestAnimationFrame")]||function(n){setTimeout(n,17)};oa.timer=function(){qn.apply(this,arguments)},oa.timer.flush=function(){Rn(),Dn()},oa.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var co=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(jn);oa.formatPrefix=function(n,t){var e=0;return(n=+n)&&(0>n&&(n*=-1),t&&(n=oa.round(n,Pn(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((e-1)/3)))),co[8+e/3]};var so=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,fo=oa.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=oa.round(n,Pn(n,t))).toFixed(Math.max(0,Math.min(20,Pn(n*(1+1e-15),t))))}}),ho=oa.time={},go=Date;Hn.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){po.setUTCDate.apply(this._,arguments)},setDay:function(){po.setUTCDay.apply(this._,arguments)},setFullYear:function(){po.setUTCFullYear.apply(this._,arguments)},setHours:function(){po.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){po.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){po.setUTCMinutes.apply(this._,arguments)},setMonth:function(){po.setUTCMonth.apply(this._,arguments)},setSeconds:function(){po.setUTCSeconds.apply(this._,arguments)},setTime:function(){po.setTime.apply(this._,arguments)}};var po=Date.prototype;ho.year=On(function(n){return n=ho.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),ho.years=ho.year.range,ho.years.utc=ho.year.utc.range,ho.day=On(function(n){var t=new go(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),ho.days=ho.day.range,ho.days.utc=ho.day.utc.range,ho.dayOfYear=function(n){var t=ho.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=ho[n]=On(function(n){return(n=ho.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=ho.year(n).getDay();return Math.floor((ho.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});ho[n+"s"]=e.range,ho[n+"s"].utc=e.utc.range,ho[n+"OfYear"]=function(n){var e=ho.year(n).getDay();return Math.floor((ho.dayOfYear(n)+(e+t)%7)/7)}}),ho.week=ho.sunday,ho.weeks=ho.sunday.range,ho.weeks.utc=ho.sunday.utc.range,ho.weekOfYear=ho.sundayOfYear;var vo={"-":"",_:" ",0:"0"},mo=/^\s*\d+/,yo=/^%/;oa.locale=function(n){return{numberFormat:Un(n),timeFormat:Yn(n)}};var Mo=oa.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"], +shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});oa.format=Mo.numberFormat,oa.geo={},st.prototype={s:0,t:0,add:function(n){ft(n,this.t,xo),ft(xo.s,this.s,this),this.s?this.t+=xo.t:this.s=xo.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var xo=new st;oa.geo.stream=function(n,t){n&&bo.hasOwnProperty(n.type)?bo[n.type](n,t):ht(n,t)};var bo={Feature:function(n,t){ht(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,u=e.length;++rn?4*ja+n:n,ko.lineStart=ko.lineEnd=ko.point=b}};oa.geo.bounds=function(){function n(n,t){M.push(x=[s=n,h=n]),f>t&&(f=t),t>g&&(g=t)}function t(t,e){var r=dt([t*Oa,e*Oa]);if(m){var u=yt(m,r),i=[u[1],-u[0],0],a=yt(i,u);bt(a),a=_t(a);var l=t-p,c=l>0?1:-1,v=a[0]*Ia*c,d=Ma(l)>180;if(d^(v>c*p&&c*t>v)){var y=a[1]*Ia;y>g&&(g=y)}else if(v=(v+360)%360-180,d^(v>c*p&&c*t>v)){var y=-a[1]*Ia;f>y&&(f=y)}else f>e&&(f=e),e>g&&(g=e);d?p>t?o(s,t)>o(s,h)&&(h=t):o(t,h)>o(s,h)&&(s=t):h>=s?(s>t&&(s=t),t>h&&(h=t)):t>p?o(s,t)>o(s,h)&&(h=t):o(t,h)>o(s,h)&&(s=t)}else n(t,e);m=r,p=t}function e(){b.point=t}function r(){x[0]=s,x[1]=h,b.point=n,m=null}function u(n,e){if(m){var r=n-p;y+=Ma(r)>180?r+(r>0?360:-360):r}else v=n,d=e;ko.point(n,e),t(n,e)}function i(){ko.lineStart()}function a(){u(v,d),ko.lineEnd(),Ma(y)>Da&&(s=-(h=180)),x[0]=s,x[1]=h,m=null}function o(n,t){return(t-=n)<0?t+360:t}function l(n,t){return n[0]-t[0]}function c(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:nSo?(s=-(h=180),f=-(g=90)):y>Da?g=90:-Da>y&&(f=-90),x[0]=s,x[1]=h}};return function(n){g=h=-(s=f=1/0),M=[],oa.geo.stream(n,b);var t=M.length;if(t){M.sort(l);for(var e,r=1,u=M[0],i=[u];t>r;++r)e=M[r],c(e[0],u)||c(e[1],u)?(o(u[0],e[1])>o(u[0],u[1])&&(u[1]=e[1]),o(e[0],u[1])>o(u[0],u[1])&&(u[0]=e[0])):i.push(u=e);for(var a,e,p=-(1/0),t=i.length-1,r=0,u=i[t];t>=r;u=e,++r)e=i[r],(a=o(u[1],e[0]))>p&&(p=a,s=e[0],h=u[1])}return M=x=null,s===1/0||f===1/0?[[NaN,NaN],[NaN,NaN]]:[[s,f],[h,g]]}}(),oa.geo.centroid=function(n){No=Eo=Ao=Co=zo=Lo=qo=To=Ro=Do=Po=0,oa.geo.stream(n,jo);var t=Ro,e=Do,r=Po,u=t*t+e*e+r*r;return Pa>u&&(t=Lo,e=qo,r=To,Da>Eo&&(t=Ao,e=Co,r=zo),u=t*t+e*e+r*r,Pa>u)?[NaN,NaN]:[Math.atan2(e,t)*Ia,tn(r/Math.sqrt(u))*Ia]};var No,Eo,Ao,Co,zo,Lo,qo,To,Ro,Do,Po,jo={sphere:b,point:St,lineStart:Nt,lineEnd:Et,polygonStart:function(){jo.lineStart=At},polygonEnd:function(){jo.lineStart=Nt}},Uo=Rt(zt,Ut,Ht,[-ja,-ja/2]),Fo=1e9;oa.geo.clipExtent=function(){var n,t,e,r,u,i,a={stream:function(n){return u&&(u.valid=!1),u=i(n),u.valid=!0,u},extent:function(o){return arguments.length?(i=Zt(n=+o[0][0],t=+o[0][1],e=+o[1][0],r=+o[1][1]),u&&(u.valid=!1,u=null),a):[[n,t],[e,r]]}};return a.extent([[0,0],[960,500]])},(oa.geo.conicEqualArea=function(){return Vt(Xt)}).raw=Xt,oa.geo.albers=function(){return oa.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},oa.geo.albersUsa=function(){function n(n){var i=n[0],a=n[1];return t=null,e(i,a),t||(r(i,a),t)||u(i,a),t}var t,e,r,u,i=oa.geo.albers(),a=oa.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),o=oa.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),l={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=i.scale(),e=i.translate(),r=(n[0]-e[0])/t,u=(n[1]-e[1])/t;return(u>=.12&&.234>u&&r>=-.425&&-.214>r?a:u>=.166&&.234>u&&r>=-.214&&-.115>r?o:i).invert(n)},n.stream=function(n){var t=i.stream(n),e=a.stream(n),r=o.stream(n);return{point:function(n,u){t.point(n,u),e.point(n,u),r.point(n,u)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(i.precision(t),a.precision(t),o.precision(t),n):i.precision()},n.scale=function(t){return arguments.length?(i.scale(t),a.scale(.35*t),o.scale(t),n.translate(i.translate())):i.scale()},n.translate=function(t){if(!arguments.length)return i.translate();var c=i.scale(),s=+t[0],f=+t[1];return e=i.translate(t).clipExtent([[s-.455*c,f-.238*c],[s+.455*c,f+.238*c]]).stream(l).point,r=a.translate([s-.307*c,f+.201*c]).clipExtent([[s-.425*c+Da,f+.12*c+Da],[s-.214*c-Da,f+.234*c-Da]]).stream(l).point,u=o.translate([s-.205*c,f+.212*c]).clipExtent([[s-.214*c+Da,f+.166*c+Da],[s-.115*c-Da,f+.234*c-Da]]).stream(l).point,n},n.scale(1070)};var Ho,Oo,Io,Yo,Zo,Vo,Xo={point:b,lineStart:b,lineEnd:b,polygonStart:function(){Oo=0,Xo.lineStart=$t},polygonEnd:function(){Xo.lineStart=Xo.lineEnd=Xo.point=b,Ho+=Ma(Oo/2)}},$o={point:Bt,lineStart:b,lineEnd:b,polygonStart:b,polygonEnd:b},Bo={point:Gt,lineStart:Kt,lineEnd:Qt,polygonStart:function(){Bo.lineStart=ne},polygonEnd:function(){Bo.point=Gt,Bo.lineStart=Kt,Bo.lineEnd=Qt}};oa.geo.path=function(){function n(n){return n&&("function"==typeof o&&i.pointRadius(+o.apply(this,arguments)),a&&a.valid||(a=u(i)),oa.geo.stream(n,a)),i.result()}function t(){return a=null,n}var e,r,u,i,a,o=4.5;return n.area=function(n){return Ho=0,oa.geo.stream(n,u(Xo)),Ho},n.centroid=function(n){return Ao=Co=zo=Lo=qo=To=Ro=Do=Po=0,oa.geo.stream(n,u(Bo)),Po?[Ro/Po,Do/Po]:To?[Lo/To,qo/To]:zo?[Ao/zo,Co/zo]:[NaN,NaN]},n.bounds=function(n){return Zo=Vo=-(Io=Yo=1/0),oa.geo.stream(n,u($o)),[[Io,Yo],[Zo,Vo]]},n.projection=function(n){return arguments.length?(u=(e=n)?n.stream||re(n):y,t()):e},n.context=function(n){return arguments.length?(i=null==(r=n)?new Wt:new te(n),"function"!=typeof o&&i.pointRadius(o),t()):r},n.pointRadius=function(t){return arguments.length?(o="function"==typeof t?t:(i.pointRadius(+t),+t),n):o},n.projection(oa.geo.albersUsa()).context(null)},oa.geo.transform=function(n){return{stream:function(t){var e=new ue(t);for(var r in n)e[r]=n[r];return e}}},ue.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},oa.geo.projection=ae,oa.geo.projectionMutator=oe,(oa.geo.equirectangular=function(){return ae(ce)}).raw=ce.invert=ce,oa.geo.rotation=function(n){function t(t){return t=n(t[0]*Oa,t[1]*Oa),t[0]*=Ia,t[1]*=Ia,t}return n=fe(n[0]%360*Oa,n[1]*Oa,n.length>2?n[2]*Oa:0),t.invert=function(t){return t=n.invert(t[0]*Oa,t[1]*Oa),t[0]*=Ia,t[1]*=Ia,t},t},se.invert=ce,oa.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=fe(-n[0]*Oa,-n[1]*Oa,0).invert,u=[];return e(null,null,1,{point:function(n,e){u.push(n=t(n,e)),n[0]*=Ia,n[1]*=Ia}}),{type:"Polygon",coordinates:[u]}}var t,e,r=[0,0],u=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=ve((t=+r)*Oa,u*Oa),n):t},n.precision=function(r){return arguments.length?(e=ve(t*Oa,(u=+r)*Oa),n):u},n.angle(90)},oa.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Oa,u=n[1]*Oa,i=t[1]*Oa,a=Math.sin(r),o=Math.cos(r),l=Math.sin(u),c=Math.cos(u),s=Math.sin(i),f=Math.cos(i);return Math.atan2(Math.sqrt((e=f*a)*e+(e=c*s-l*f*o)*e),l*s+c*f*o)},oa.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return oa.range(Math.ceil(i/d)*d,u,d).map(h).concat(oa.range(Math.ceil(c/m)*m,l,m).map(g)).concat(oa.range(Math.ceil(r/p)*p,e,p).filter(function(n){return Ma(n%d)>Da}).map(s)).concat(oa.range(Math.ceil(o/v)*v,a,v).filter(function(n){return Ma(n%m)>Da}).map(f))}var e,r,u,i,a,o,l,c,s,f,h,g,p=10,v=p,d=90,m=360,y=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(i).concat(g(l).slice(1),h(u).reverse().slice(1),g(c).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(i=+t[0][0],u=+t[1][0],c=+t[0][1],l=+t[1][1],i>u&&(t=i,i=u,u=t),c>l&&(t=c,c=l,l=t),n.precision(y)):[[i,c],[u,l]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],o=+t[0][1],a=+t[1][1],r>e&&(t=r,r=e,e=t),o>a&&(t=o,o=a,a=t),n.precision(y)):[[r,o],[e,a]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],m=+t[1],n):[d,m]},n.minorStep=function(t){return arguments.length?(p=+t[0],v=+t[1],n):[p,v]},n.precision=function(t){return arguments.length?(y=+t,s=me(o,a,90),f=ye(r,e,y),h=me(c,l,90),g=ye(i,u,y),n):y},n.majorExtent([[-180,-90+Da],[180,90-Da]]).minorExtent([[-180,-80-Da],[180,80+Da]])},oa.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||u.apply(this,arguments)]}}var t,e,r=Me,u=xe;return n.distance=function(){return oa.geo.distance(t||r.apply(this,arguments),e||u.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(u=t,e="function"==typeof t?null:t,n):u},n.precision=function(){return arguments.length?n:0},n},oa.geo.interpolate=function(n,t){return be(n[0]*Oa,n[1]*Oa,t[0]*Oa,t[1]*Oa)},oa.geo.length=function(n){return Wo=0,oa.geo.stream(n,Jo),Wo};var Wo,Jo={sphere:b,point:b,lineStart:_e,lineEnd:b,polygonStart:b,polygonEnd:b},Go=we(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(oa.geo.azimuthalEqualArea=function(){return ae(Go)}).raw=Go;var Ko=we(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},y);(oa.geo.azimuthalEquidistant=function(){return ae(Ko)}).raw=Ko,(oa.geo.conicConformal=function(){return Vt(Se)}).raw=Se,(oa.geo.conicEquidistant=function(){return Vt(ke)}).raw=ke;var Qo=we(function(n){return 1/n},Math.atan);(oa.geo.gnomonic=function(){return ae(Qo)}).raw=Qo,Ne.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Ha]},(oa.geo.mercator=function(){return Ee(Ne)}).raw=Ne;var nl=we(function(){return 1},Math.asin);(oa.geo.orthographic=function(){return ae(nl)}).raw=nl;var tl=we(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(oa.geo.stereographic=function(){return ae(tl)}).raw=tl,Ae.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Ha]},(oa.geo.transverseMercator=function(){var n=Ee(Ae),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[n[1],-n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},e([0,0,90])}).raw=Ae,oa.geom={},oa.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,u=En(e),i=En(r),a=n.length,o=[],l=[];for(t=0;a>t;t++)o.push([+u.call(this,n[t],t),+i.call(this,n[t],t),t]);for(o.sort(qe),t=0;a>t;t++)l.push([o[t][0],-o[t][1]]);var c=Le(o),s=Le(l),f=s[0]===c[0],h=s[s.length-1]===c[c.length-1],g=[];for(t=c.length-1;t>=0;--t)g.push(n[o[c[t]][2]]);for(t=+f;t=r&&c.x<=i&&c.y>=u&&c.y<=a?[[r,a],[i,a],[i,u],[r,u]]:[];s.point=n[o]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(i(n,t)/Da)*Da,y:Math.round(a(n,t)/Da)*Da,i:t}})}var r=Ce,u=ze,i=r,a=u,o=sl;return n?t(n):(t.links=function(n){return or(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return or(e(n)).cells.forEach(function(e,r){for(var u,i,a=e.site,o=e.edges.sort(Ve),l=-1,c=o.length,s=o[c-1].edge,f=s.l===a?s.r:s.l;++l=c,h=r>=s,g=h<<1|f;n.leaf=!1,n=n.nodes[g]||(n.nodes[g]=hr()),f?u=c:o=c,h?a=s:l=s,i(n,t,e,r,u,a,o,l)}var s,f,h,g,p,v,d,m,y,M=En(o),x=En(l);if(null!=t)v=t,d=e,m=r,y=u;else if(m=y=-(v=d=1/0),f=[],h=[],p=n.length,a)for(g=0;p>g;++g)s=n[g],s.xm&&(m=s.x),s.y>y&&(y=s.y),f.push(s.x),h.push(s.y);else for(g=0;p>g;++g){var b=+M(s=n[g],g),_=+x(s,g);v>b&&(v=b),d>_&&(d=_),b>m&&(m=b),_>y&&(y=_),f.push(b),h.push(_)}var w=m-v,S=y-d;w>S?y=d+w:m=v+S;var k=hr();if(k.add=function(n){i(k,n,+M(n,++g),+x(n,g),v,d,m,y)},k.visit=function(n){gr(n,k,v,d,m,y)},k.find=function(n){return pr(k,n[0],n[1],v,d,m,y)},g=-1,null==t){for(;++g=0?n.slice(0,t):n,r=t>=0?n.slice(t+1):"in";return e=pl.get(e)||gl,r=vl.get(r)||y,br(r(e.apply(null,la.call(arguments,1))))},oa.interpolateHcl=Rr,oa.interpolateHsl=Dr,oa.interpolateLab=Pr,oa.interpolateRound=jr,oa.transform=function(n){var t=sa.createElementNS(oa.ns.prefix.svg,"g");return(oa.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new Ur(e?e.matrix:dl)})(n)},Ur.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var dl={a:1,b:0,c:0,d:1,e:0,f:0};oa.interpolateTransform=$r,oa.layout={},oa.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++eo*o/m){if(v>l){var c=t.charge/l;n.px-=i*c,n.py-=a*c}return!0}if(t.point&&l&&v>l){var c=t.pointCharge/l;n.px-=i*c,n.py-=a*c}}return!t.charge}}function t(n){n.px=oa.event.x,n.py=oa.event.y,l.resume()}var e,r,u,i,a,o,l={},c=oa.dispatch("start","tick","end"),s=[1,1],f=.9,h=ml,g=yl,p=-30,v=Ml,d=.1,m=.64,M=[],x=[];return l.tick=function(){if((u*=.99)<.005)return e=null,c.end({type:"end",alpha:u=0}),!0;var t,r,l,h,g,v,m,y,b,_=M.length,w=x.length;for(r=0;w>r;++r)l=x[r],h=l.source,g=l.target,y=g.x-h.x,b=g.y-h.y,(v=y*y+b*b)&&(v=u*a[r]*((v=Math.sqrt(v))-i[r])/v,y*=v,b*=v,g.x-=y*(m=h.weight+g.weight?h.weight/(h.weight+g.weight):.5),g.y-=b*m,h.x+=y*(m=1-m),h.y+=b*m);if((m=u*d)&&(y=s[0]/2,b=s[1]/2,r=-1,m))for(;++r<_;)l=M[r],l.x+=(y-l.x)*m,l.y+=(b-l.y)*m;if(p)for(ru(t=oa.geom.quadtree(M),u,o),r=-1;++r<_;)(l=M[r]).fixed||t.visit(n(l));for(r=-1;++r<_;)l=M[r],l.fixed?(l.x=l.px,l.y=l.py):(l.x-=(l.px-(l.px=l.x))*f,l.y-=(l.py-(l.py=l.y))*f);c.tick({type:"tick",alpha:u})},l.nodes=function(n){return arguments.length?(M=n,l):M},l.links=function(n){return arguments.length?(x=n,l):x},l.size=function(n){return arguments.length?(s=n,l):s},l.linkDistance=function(n){return arguments.length?(h="function"==typeof n?n:+n,l):h},l.distance=l.linkDistance,l.linkStrength=function(n){return arguments.length?(g="function"==typeof n?n:+n,l):g},l.friction=function(n){return arguments.length?(f=+n,l):f},l.charge=function(n){return arguments.length?(p="function"==typeof n?n:+n,l):p},l.chargeDistance=function(n){return arguments.length?(v=n*n,l):Math.sqrt(v)},l.gravity=function(n){return arguments.length?(d=+n,l):d},l.theta=function(n){return arguments.length?(m=n*n,l):Math.sqrt(m)},l.alpha=function(n){return arguments.length?(n=+n,u?n>0?u=n:(e.c=null,e.t=NaN,e=null,c.end({type:"end",alpha:u=0})):n>0&&(c.start({type:"start",alpha:u=n}),e=qn(l.tick)),l):u},l.start=function(){function n(n,r){if(!e){for(e=new Array(u),l=0;u>l;++l)e[l]=[];for(l=0;c>l;++l){var i=x[l];e[i.source.index].push(i.target),e[i.target.index].push(i.source)}}for(var a,o=e[t],l=-1,s=o.length;++lt;++t)(r=M[t]).index=t,r.weight=0;for(t=0;c>t;++t)r=x[t],"number"==typeof r.source&&(r.source=M[r.source]),"number"==typeof r.target&&(r.target=M[r.target]),++r.source.weight,++r.target.weight;for(t=0;u>t;++t)r=M[t],isNaN(r.x)&&(r.x=n("x",f)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(i=[],"function"==typeof h)for(t=0;c>t;++t)i[t]=+h.call(this,x[t],t);else for(t=0;c>t;++t)i[t]=h;if(a=[],"function"==typeof g)for(t=0;c>t;++t)a[t]=+g.call(this,x[t],t);else for(t=0;c>t;++t)a[t]=g;if(o=[],"function"==typeof p)for(t=0;u>t;++t)o[t]=+p.call(this,M[t],t);else for(t=0;u>t;++t)o[t]=p;return l.resume()},l.resume=function(){return l.alpha(.1)},l.stop=function(){return l.alpha(0)},l.drag=function(){return r||(r=oa.behavior.drag().origin(y).on("dragstart.force",Qr).on("drag.force",t).on("dragend.force",nu)),arguments.length?void this.on("mouseover.force",tu).on("mouseout.force",eu).call(r):r},oa.rebind(l,c,"on")};var ml=20,yl=1,Ml=1/0;oa.layout.hierarchy=function(){function n(u){var i,a=[u],o=[];for(u.depth=0;null!=(i=a.pop());)if(o.push(i),(c=e.call(n,i,i.depth))&&(l=c.length)){for(var l,c,s;--l>=0;)a.push(s=c[l]),s.parent=i,s.depth=i.depth+1;r&&(i.value=0),i.children=c}else r&&(i.value=+r.call(n,i,i.depth)||0),delete i.children;return au(u,function(n){var e,u;t&&(e=n.children)&&e.sort(t),r&&(u=n.parent)&&(u.value+=n.value)}),o}var t=cu,e=ou,r=lu;return n.sort=function(e){return arguments.length?(t=e,n):t},n.children=function(t){return arguments.length?(e=t,n):e},n.value=function(t){return arguments.length?(r=t,n):r},n.revalue=function(t){return r&&(iu(t,function(n){n.children&&(n.value=0)}),au(t,function(t){var e;t.children||(t.value=+r.call(n,t,t.depth)||0),(e=t.parent)&&(e.value+=t.value)})),t},n},oa.layout.partition=function(){function n(t,e,r,u){var i=t.children;if(t.x=e,t.y=t.depth*u,t.dx=r,t.dy=u,i&&(a=i.length)){var a,o,l,c=-1;for(r=t.value?r/t.value:0;++cf?-1:1),p=oa.sum(c),v=p?(f-l*g)/p:0,d=oa.range(l),m=[];return null!=e&&d.sort(e===xl?function(n,t){return c[t]-c[n]}:function(n,t){return e(a[n],a[t])}),d.forEach(function(n){m[n]={data:a[n],value:o=c[n],startAngle:s,endAngle:s+=o*v+g,padAngle:h}}),m}var t=Number,e=xl,r=0,u=Ua,i=0;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(u=t,n):u},n.padAngle=function(t){return arguments.length?(i=t,n):i},n};var xl={};oa.layout.stack=function(){function n(o,l){if(!(h=o.length))return o;var c=o.map(function(e,r){return t.call(n,e,r)}),s=c.map(function(t){return t.map(function(t,e){return[i.call(n,t,e),a.call(n,t,e)]})}),f=e.call(n,s,l);c=oa.permute(c,f),s=oa.permute(s,f);var h,g,p,v,d=r.call(n,s,l),m=c[0].length;for(p=0;m>p;++p)for(u.call(n,c[0][p],v=d[p],s[0][p][1]),g=1;h>g;++g)u.call(n,c[g][p],v+=s[g-1][p][1],s[g][p][1]);return o}var t=y,e=pu,r=vu,u=gu,i=fu,a=hu;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:bl.get(t)||pu,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:_l.get(t)||vu,n):r},n.x=function(t){return arguments.length?(i=t,n):i},n.y=function(t){return arguments.length?(a=t,n):a},n.out=function(t){return arguments.length?(u=t,n):u},n};var bl=oa.map({"inside-out":function(n){var t,e,r=n.length,u=n.map(du),i=n.map(mu),a=oa.range(r).sort(function(n,t){return u[n]-u[t]}),o=0,l=0,c=[],s=[];for(t=0;r>t;++t)e=a[t],l>o?(o+=i[e],c.push(e)):(l+=i[e],s.push(e));return s.reverse().concat(c)},reverse:function(n){return oa.range(n.length).reverse()},"default":pu}),_l=oa.map({silhouette:function(n){var t,e,r,u=n.length,i=n[0].length,a=[],o=0,l=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];r>o&&(o=r),a.push(r)}for(e=0;i>e;++e)l[e]=(o-a[e])/2;return l},wiggle:function(n){var t,e,r,u,i,a,o,l,c,s=n.length,f=n[0],h=f.length,g=[];for(g[0]=l=c=0,e=1;h>e;++e){for(t=0,u=0;s>t;++t)u+=n[t][e][1];for(t=0,i=0,o=f[e][0]-f[e-1][0];s>t;++t){for(r=0,a=(n[t][e][1]-n[t][e-1][1])/(2*o);t>r;++r)a+=(n[r][e][1]-n[r][e-1][1])/o;i+=a*n[t][e][1]}g[e]=l-=u?i/u*o:0,c>l&&(c=l)}for(e=0;h>e;++e)g[e]-=c;return g},expand:function(n){var t,e,r,u=n.length,i=n[0].length,a=1/u,o=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];if(r)for(t=0;u>t;t++)n[t][e][1]/=r;else for(t=0;u>t;t++)n[t][e][1]=a}for(e=0;i>e;++e)o[e]=0;return o},zero:vu});oa.layout.histogram=function(){function n(n,i){for(var a,o,l=[],c=n.map(e,this),s=r.call(this,c,i),f=u.call(this,s,c,i),i=-1,h=c.length,g=f.length-1,p=t?1:1/h;++i0)for(i=-1;++i=s[0]&&o<=s[1]&&(a=l[oa.bisect(f,o,1,g)-1],a.y+=p,a.push(n[i]));return l}var t=!0,e=Number,r=bu,u=Mu;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=En(t),n):r},n.bins=function(t){return arguments.length?(u="number"==typeof t?function(n){return xu(n,t)}:En(t),n):u},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},oa.layout.pack=function(){function n(n,i){var a=e.call(this,n,i),o=a[0],l=u[0],c=u[1],s=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(o.x=o.y=0,au(o,function(n){n.r=+s(n.value)}),au(o,Nu),r){var f=r*(t?1:Math.max(2*o.r/l,2*o.r/c))/2;au(o,function(n){n.r+=f}),au(o,Nu),au(o,function(n){n.r-=f})}return Cu(o,l/2,c/2,t?1:1/Math.max(2*o.r/l,2*o.r/c)),a}var t,e=oa.layout.hierarchy().sort(_u),r=0,u=[1,1];return n.size=function(t){return arguments.length?(u=t,n):u},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},uu(n,e)},oa.layout.tree=function(){function n(n,u){var s=a.call(this,n,u),f=s[0],h=t(f);if(au(h,e),h.parent.m=-h.z,iu(h,r),c)iu(f,i);else{var g=f,p=f,v=f;iu(f,function(n){n.xp.x&&(p=n),n.depth>v.depth&&(v=n)});var d=o(g,p)/2-g.x,m=l[0]/(p.x+o(p,g)/2+d),y=l[1]/(v.depth||1);iu(f,function(n){n.x=(n.x+d)*m,n.y=n.depth*y})}return s}function t(n){for(var t,e={A:null,children:[n]},r=[e];null!=(t=r.pop());)for(var u,i=t.children,a=0,o=i.length;o>a;++a)r.push((i[a]=u={_:i[a],parent:t,children:(u=i[a].children)&&u.slice()||[],A:null,a:null,z:0,m:0,c:0,s:0,t:null,i:a}).a=u);return e.children[0]}function e(n){var t=n.children,e=n.parent.children,r=n.i?e[n.i-1]:null;if(t.length){Du(n);var i=(t[0].z+t[t.length-1].z)/2;r?(n.z=r.z+o(n._,r._),n.m=n.z-i):n.z=i}else r&&(n.z=r.z+o(n._,r._));n.parent.A=u(n,r,n.parent.A||e[0])}function r(n){n._.x=n.z+n.parent.m,n.m+=n.parent.m}function u(n,t,e){if(t){for(var r,u=n,i=n,a=t,l=u.parent.children[0],c=u.m,s=i.m,f=a.m,h=l.m;a=Tu(a),u=qu(u),a&&u;)l=qu(l),i=Tu(i),i.a=n,r=a.z+f-u.z-c+o(a._,u._),r>0&&(Ru(Pu(a,n,e),n,r),c+=r,s+=r),f+=a.m,c+=u.m,h+=l.m,s+=i.m;a&&!Tu(i)&&(i.t=a,i.m+=f-s),u&&!qu(l)&&(l.t=u,l.m+=c-h,e=n)}return e}function i(n){n.x*=l[0],n.y=n.depth*l[1]}var a=oa.layout.hierarchy().sort(null).value(null),o=Lu,l=[1,1],c=null;return n.separation=function(t){return arguments.length?(o=t,n):o},n.size=function(t){return arguments.length?(c=null==(l=t)?i:null,n):c?null:l},n.nodeSize=function(t){return arguments.length?(c=null==(l=t)?null:i,n):c?l:null},uu(n,a)},oa.layout.cluster=function(){function n(n,i){var a,o=t.call(this,n,i),l=o[0],c=0;au(l,function(n){var t=n.children;t&&t.length?(n.x=Uu(t),n.y=ju(t)):(n.x=a?c+=e(n,a):0,n.y=0,a=n)});var s=Fu(l),f=Hu(l),h=s.x-e(s,f)/2,g=f.x+e(f,s)/2;return au(l,u?function(n){n.x=(n.x-l.x)*r[0],n.y=(l.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(g-h)*r[0],n.y=(1-(l.y?n.y/l.y:1))*r[1]}),o}var t=oa.layout.hierarchy().sort(null).value(null),e=Lu,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},uu(n,t)},oa.layout.treemap=function(){function n(n,t){for(var e,r,u=-1,i=n.length;++ut?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var i=e.children;if(i&&i.length){var a,o,l,c=f(e),s=[],h=i.slice(),p=1/0,v="slice"===g?c.dx:"dice"===g?c.dy:"slice-dice"===g?1&e.depth?c.dy:c.dx:Math.min(c.dx,c.dy);for(n(h,c.dx*c.dy/e.value),s.area=0;(l=h.length)>0;)s.push(a=h[l-1]),s.area+=a.area,"squarify"!==g||(o=r(s,v))<=p?(h.pop(),p=o):(s.area-=s.pop().area,u(s,v,c,!1),v=Math.min(c.dx,c.dy),s.length=s.area=0,p=1/0);s.length&&(u(s,v,c,!0),s.length=s.area=0),i.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var i,a=f(t),o=r.slice(),l=[];for(n(o,a.dx*a.dy/t.value),l.area=0;i=o.pop();)l.push(i),l.area+=i.area,null!=i.z&&(u(l,i.z?a.dx:a.dy,a,!o.length),l.length=l.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,u=0,i=1/0,a=-1,o=n.length;++ae&&(i=e),e>u&&(u=e));return r*=r,t*=t,r?Math.max(t*u*p/r,r/(t*i*p)):1/0}function u(n,t,e,r){var u,i=-1,a=n.length,o=e.x,c=e.y,s=t?l(n.area/t):0; +if(t==e.dx){for((r||s>e.dy)&&(s=e.dy);++ie.dx)&&(s=e.dx);++ie&&(t=1),1>e&&(n=0),function(){var e,r,u;do e=2*Math.random()-1,r=2*Math.random()-1,u=e*e+r*r;while(!u||u>1);return n+t*e*Math.sqrt(-2*Math.log(u)/u)}},logNormal:function(){var n=oa.random.normal.apply(oa,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=oa.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},oa.scale={};var wl={floor:y,ceil:y};oa.scale.linear=function(){return Wu([0,1],[0,1],Mr,!1)};var Sl={s:1,g:1,p:1,r:1,e:1};oa.scale.log=function(){return ri(oa.scale.linear().domain([0,1]),10,!0,[1,10])};var kl=oa.format(".0e"),Nl={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};oa.scale.pow=function(){return ui(oa.scale.linear(),1,[0,1])},oa.scale.sqrt=function(){return oa.scale.pow().exponent(.5)},oa.scale.ordinal=function(){return ai([],{t:"range",a:[[]]})},oa.scale.category10=function(){return oa.scale.ordinal().range(El)},oa.scale.category20=function(){return oa.scale.ordinal().range(Al)},oa.scale.category20b=function(){return oa.scale.ordinal().range(Cl)},oa.scale.category20c=function(){return oa.scale.ordinal().range(zl)};var El=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(xn),Al=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(xn),Cl=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(xn),zl=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(xn);oa.scale.quantile=function(){return oi([],[])},oa.scale.quantize=function(){return li(0,1,[0,1])},oa.scale.threshold=function(){return ci([.5],[0,1])},oa.scale.identity=function(){return si([0,1])},oa.svg={},oa.svg.arc=function(){function n(){var n=Math.max(0,+e.apply(this,arguments)),c=Math.max(0,+r.apply(this,arguments)),s=a.apply(this,arguments)-Ha,f=o.apply(this,arguments)-Ha,h=Math.abs(f-s),g=s>f?0:1;if(n>c&&(p=c,c=n,n=p),h>=Fa)return t(c,g)+(n?t(n,1-g):"")+"Z";var p,v,d,m,y,M,x,b,_,w,S,k,N=0,E=0,A=[];if((m=(+l.apply(this,arguments)||0)/2)&&(d=i===Ll?Math.sqrt(n*n+c*c):+i.apply(this,arguments),g||(E*=-1),c&&(E=tn(d/c*Math.sin(m))),n&&(N=tn(d/n*Math.sin(m)))),c){y=c*Math.cos(s+E),M=c*Math.sin(s+E),x=c*Math.cos(f-E),b=c*Math.sin(f-E);var C=Math.abs(f-s-2*E)<=ja?0:1;if(E&&mi(y,M,x,b)===g^C){var z=(s+f)/2;y=c*Math.cos(z),M=c*Math.sin(z),x=b=null}}else y=M=0;if(n){_=n*Math.cos(f-N),w=n*Math.sin(f-N),S=n*Math.cos(s+N),k=n*Math.sin(s+N);var L=Math.abs(s-f+2*N)<=ja?0:1;if(N&&mi(_,w,S,k)===1-g^L){var q=(s+f)/2;_=n*Math.cos(q),w=n*Math.sin(q),S=k=null}}else _=w=0;if(h>Da&&(p=Math.min(Math.abs(c-n)/2,+u.apply(this,arguments)))>.001){v=c>n^g?0:1;var T=p,R=p;if(ja>h){var D=null==S?[_,w]:null==x?[y,M]:Re([y,M],[S,k],[x,b],[_,w]),P=y-D[0],j=M-D[1],U=x-D[0],F=b-D[1],H=1/Math.sin(Math.acos((P*U+j*F)/(Math.sqrt(P*P+j*j)*Math.sqrt(U*U+F*F)))/2),O=Math.sqrt(D[0]*D[0]+D[1]*D[1]);R=Math.min(p,(n-O)/(H-1)),T=Math.min(p,(c-O)/(H+1))}if(null!=x){var I=yi(null==S?[_,w]:[S,k],[y,M],c,T,g),Y=yi([x,b],[_,w],c,T,g);p===T?A.push("M",I[0],"A",T,",",T," 0 0,",v," ",I[1],"A",c,",",c," 0 ",1-g^mi(I[1][0],I[1][1],Y[1][0],Y[1][1]),",",g," ",Y[1],"A",T,",",T," 0 0,",v," ",Y[0]):A.push("M",I[0],"A",T,",",T," 0 1,",v," ",Y[0])}else A.push("M",y,",",M);if(null!=S){var Z=yi([y,M],[S,k],n,-R,g),V=yi([_,w],null==x?[y,M]:[x,b],n,-R,g);p===R?A.push("L",V[0],"A",R,",",R," 0 0,",v," ",V[1],"A",n,",",n," 0 ",g^mi(V[1][0],V[1][1],Z[1][0],Z[1][1]),",",1-g," ",Z[1],"A",R,",",R," 0 0,",v," ",Z[0]):A.push("L",V[0],"A",R,",",R," 0 0,",v," ",Z[0])}else A.push("L",_,",",w)}else A.push("M",y,",",M),null!=x&&A.push("A",c,",",c," 0 ",C,",",g," ",x,",",b),A.push("L",_,",",w),null!=S&&A.push("A",n,",",n," 0 ",L,",",1-g," ",S,",",k);return A.push("Z"),A.join("")}function t(n,t){return"M0,"+n+"A"+n+","+n+" 0 1,"+t+" 0,"+-n+"A"+n+","+n+" 0 1,"+t+" 0,"+n}var e=hi,r=gi,u=fi,i=Ll,a=pi,o=vi,l=di;return n.innerRadius=function(t){return arguments.length?(e=En(t),n):e},n.outerRadius=function(t){return arguments.length?(r=En(t),n):r},n.cornerRadius=function(t){return arguments.length?(u=En(t),n):u},n.padRadius=function(t){return arguments.length?(i=t==Ll?Ll:En(t),n):i},n.startAngle=function(t){return arguments.length?(a=En(t),n):a},n.endAngle=function(t){return arguments.length?(o=En(t),n):o},n.padAngle=function(t){return arguments.length?(l=En(t),n):l},n.centroid=function(){var n=(+e.apply(this,arguments)+ +r.apply(this,arguments))/2,t=(+a.apply(this,arguments)+ +o.apply(this,arguments))/2-Ha;return[Math.cos(t)*n,Math.sin(t)*n]},n};var Ll="auto";oa.svg.line=function(){return Mi(y)};var ql=oa.map({linear:xi,"linear-closed":bi,step:_i,"step-before":wi,"step-after":Si,basis:zi,"basis-open":Li,"basis-closed":qi,bundle:Ti,cardinal:Ei,"cardinal-open":ki,"cardinal-closed":Ni,monotone:Fi});ql.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var Tl=[0,2/3,1/3,0],Rl=[0,1/3,2/3,0],Dl=[0,1/6,2/3,1/6];oa.svg.line.radial=function(){var n=Mi(Hi);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},wi.reverse=Si,Si.reverse=wi,oa.svg.area=function(){return Oi(y)},oa.svg.area.radial=function(){var n=Oi(Hi);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},oa.svg.chord=function(){function n(n,o){var l=t(this,i,n,o),c=t(this,a,n,o);return"M"+l.p0+r(l.r,l.p1,l.a1-l.a0)+(e(l,c)?u(l.r,l.p1,l.r,l.p0):u(l.r,l.p1,c.r,c.p0)+r(c.r,c.p1,c.a1-c.a0)+u(c.r,c.p1,l.r,l.p0))+"Z"}function t(n,t,e,r){var u=t.call(n,e,r),i=o.call(n,u,r),a=l.call(n,u,r)-Ha,s=c.call(n,u,r)-Ha;return{r:i,a0:a,a1:s,p0:[i*Math.cos(a),i*Math.sin(a)],p1:[i*Math.cos(s),i*Math.sin(s)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>ja)+",1 "+t}function u(n,t,e,r){return"Q 0,0 "+r}var i=Me,a=xe,o=Ii,l=pi,c=vi;return n.radius=function(t){return arguments.length?(o=En(t),n):o},n.source=function(t){return arguments.length?(i=En(t),n):i},n.target=function(t){return arguments.length?(a=En(t),n):a},n.startAngle=function(t){return arguments.length?(l=En(t),n):l},n.endAngle=function(t){return arguments.length?(c=En(t),n):c},n},oa.svg.diagonal=function(){function n(n,u){var i=t.call(this,n,u),a=e.call(this,n,u),o=(i.y+a.y)/2,l=[i,{x:i.x,y:o},{x:a.x,y:o},a];return l=l.map(r),"M"+l[0]+"C"+l[1]+" "+l[2]+" "+l[3]}var t=Me,e=xe,r=Yi;return n.source=function(e){return arguments.length?(t=En(e),n):t},n.target=function(t){return arguments.length?(e=En(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},oa.svg.diagonal.radial=function(){var n=oa.svg.diagonal(),t=Yi,e=n.projection;return n.projection=function(n){return arguments.length?e(Zi(t=n)):t},n},oa.svg.symbol=function(){function n(n,r){return(Pl.get(t.call(this,n,r))||$i)(e.call(this,n,r))}var t=Xi,e=Vi;return n.type=function(e){return arguments.length?(t=En(e),n):t},n.size=function(t){return arguments.length?(e=En(t),n):e},n};var Pl=oa.map({circle:$i,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*Ul)),e=t*Ul;return"M0,"+-t+"L"+e+",0 0,"+t+" "+-e+",0Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/jl),e=t*jl/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/jl),e=t*jl/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});oa.svg.symbolTypes=Pl.keys();var jl=Math.sqrt(3),Ul=Math.tan(30*Oa);Aa.transition=function(n){for(var t,e,r=Fl||++Yl,u=Ki(n),i=[],a=Hl||{time:Date.now(),ease:Nr,delay:0,duration:250},o=-1,l=this.length;++oi;i++){u.push(t=[]);for(var e=this[i],o=0,l=e.length;l>o;o++)(r=e[o])&&n.call(r,r.__data__,o,i)&&t.push(r)}return Wi(u,this.namespace,this.id)},Il.tween=function(n,t){var e=this.id,r=this.namespace;return arguments.length<2?this.node()[r][e].tween.get(n):Y(this,null==t?function(t){t[r][e].tween.remove(n)}:function(u){u[r][e].tween.set(n,t)})},Il.attr=function(n,t){function e(){this.removeAttribute(o)}function r(){this.removeAttributeNS(o.space,o.local)}function u(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(o);return e!==n&&(t=a(e,n),function(n){this.setAttribute(o,t(n))})})}function i(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(o.space,o.local);return e!==n&&(t=a(e,n),function(n){this.setAttributeNS(o.space,o.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var a="transform"==n?$r:Mr,o=oa.ns.qualify(n);return Ji(this,"attr."+n,t,o.local?i:u)},Il.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(u));return r&&function(n){this.setAttribute(u,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(u.space,u.local));return r&&function(n){this.setAttributeNS(u.space,u.local,r(n))}}var u=oa.ns.qualify(n);return this.tween("attr."+n,u.local?r:e)},Il.style=function(n,e,r){function u(){this.style.removeProperty(n)}function i(e){return null==e?u:(e+="",function(){var u,i=t(this).getComputedStyle(this,null).getPropertyValue(n);return i!==e&&(u=Mr(i,e),function(t){this.style.setProperty(n,u(t),r)})})}var a=arguments.length;if(3>a){if("string"!=typeof n){2>a&&(e="");for(r in n)this.style(r,n[r],e);return this}r=""}return Ji(this,"style."+n,e,i)},Il.styleTween=function(n,e,r){function u(u,i){var a=e.call(this,u,i,t(this).getComputedStyle(this,null).getPropertyValue(n));return a&&function(t){this.style.setProperty(n,a(t),r)}}return arguments.length<3&&(r=""),this.tween("style."+n,u)},Il.text=function(n){return Ji(this,"text",n,Gi)},Il.remove=function(){var n=this.namespace;return this.each("end.transition",function(){var t;this[n].count<2&&(t=this.parentNode)&&t.removeChild(this)})},Il.ease=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].ease:("function"!=typeof n&&(n=oa.ease.apply(oa,arguments)),Y(this,function(r){r[e][t].ease=n}))},Il.delay=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].delay:Y(this,"function"==typeof n?function(r,u,i){r[e][t].delay=+n.call(r,r.__data__,u,i)}:(n=+n,function(r){r[e][t].delay=n}))},Il.duration=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].duration:Y(this,"function"==typeof n?function(r,u,i){r[e][t].duration=Math.max(1,n.call(r,r.__data__,u,i))}:(n=Math.max(1,n),function(r){r[e][t].duration=n}))},Il.each=function(n,t){var e=this.id,r=this.namespace;if(arguments.length<2){var u=Hl,i=Fl;try{Fl=e,Y(this,function(t,u,i){Hl=t[r][e],n.call(t,t.__data__,u,i)})}finally{Hl=u,Fl=i}}else Y(this,function(u){var i=u[r][e];(i.event||(i.event=oa.dispatch("start","end","interrupt"))).on(n,t)});return this},Il.transition=function(){for(var n,t,e,r,u=this.id,i=++Yl,a=this.namespace,o=[],l=0,c=this.length;c>l;l++){o.push(n=[]);for(var t=this[l],s=0,f=t.length;f>s;s++)(e=t[s])&&(r=e[a][u],Qi(e,s,a,i,{time:r.time,ease:r.ease,delay:r.delay+r.duration,duration:r.duration})),n.push(e)}return Wi(o,a,i)},oa.svg.axis=function(){function n(n){n.each(function(){var n,c=oa.select(this),s=this.__chart__||e,f=this.__chart__=e.copy(),h=null==l?f.ticks?f.ticks.apply(f,o):f.domain():l,g=null==t?f.tickFormat?f.tickFormat.apply(f,o):y:t,p=c.selectAll(".tick").data(h,f),v=p.enter().insert("g",".domain").attr("class","tick").style("opacity",Da),d=oa.transition(p.exit()).style("opacity",Da).remove(),m=oa.transition(p.order()).style("opacity",1),M=Math.max(u,0)+a,x=Zu(f),b=c.selectAll(".domain").data([0]),_=(b.enter().append("path").attr("class","domain"),oa.transition(b));v.append("line"),v.append("text");var w,S,k,N,E=v.select("line"),A=m.select("line"),C=p.select("text").text(g),z=v.select("text"),L=m.select("text"),q="top"===r||"left"===r?-1:1;if("bottom"===r||"top"===r?(n=na,w="x",k="y",S="x2",N="y2",C.attr("dy",0>q?"0em":".71em").style("text-anchor","middle"),_.attr("d","M"+x[0]+","+q*i+"V0H"+x[1]+"V"+q*i)):(n=ta,w="y",k="x",S="y2",N="x2",C.attr("dy",".32em").style("text-anchor",0>q?"end":"start"),_.attr("d","M"+q*i+","+x[0]+"H0V"+x[1]+"H"+q*i)),E.attr(N,q*u),z.attr(k,q*M),A.attr(S,0).attr(N,q*u),L.attr(w,0).attr(k,q*M),f.rangeBand){var T=f,R=T.rangeBand()/2;s=f=function(n){return T(n)+R}}else s.rangeBand?s=f:d.call(n,f,s);v.call(n,s,f),m.call(n,f,f)})}var t,e=oa.scale.linear(),r=Zl,u=6,i=6,a=3,o=[10],l=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in Vl?t+"":Zl,n):r},n.ticks=function(){return arguments.length?(o=ca(arguments),n):o},n.tickValues=function(t){return arguments.length?(l=t,n):l},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(u=+t,i=+arguments[e-1],n):u},n.innerTickSize=function(t){return arguments.length?(u=+t,n):u},n.outerTickSize=function(t){return arguments.length?(i=+t,n):i},n.tickPadding=function(t){return arguments.length?(a=+t,n):a},n.tickSubdivide=function(){return arguments.length&&n},n};var Zl="bottom",Vl={top:1,right:1,bottom:1,left:1};oa.svg.brush=function(){function n(t){t.each(function(){var t=oa.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",i).on("touchstart.brush",i),a=t.selectAll(".background").data([0]);a.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),t.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var o=t.selectAll(".resize").data(v,y);o.exit().remove(),o.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return Xl[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),o.style("display",n.empty()?"none":null);var l,f=oa.transition(t),h=oa.transition(a);c&&(l=Zu(c),h.attr("x",l[0]).attr("width",l[1]-l[0]),r(f)),s&&(l=Zu(s),h.attr("y",l[0]).attr("height",l[1]-l[0]),u(f)),e(f)})}function e(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+f[+/e$/.test(n)]+","+h[+/^s/.test(n)]+")"})}function r(n){n.select(".extent").attr("x",f[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",f[1]-f[0])}function u(n){n.select(".extent").attr("y",h[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",h[1]-h[0])}function i(){function i(){32==oa.event.keyCode&&(C||(M=null,L[0]-=f[1],L[1]-=h[1],C=2),S())}function v(){32==oa.event.keyCode&&2==C&&(L[0]+=f[1],L[1]+=h[1],C=0,S())}function d(){var n=oa.mouse(b),t=!1;x&&(n[0]+=x[0],n[1]+=x[1]),C||(oa.event.altKey?(M||(M=[(f[0]+f[1])/2,(h[0]+h[1])/2]),L[0]=f[+(n[0]s?(u=r,r=s):u=s),v[0]!=r||v[1]!=u?(e?o=null:a=null,v[0]=r,v[1]=u,!0):void 0}function y(){d(),k.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),oa.select("body").style("cursor",null),q.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),z(),w({type:"brushend"})}var M,x,b=this,_=oa.select(oa.event.target),w=l.of(b,arguments),k=oa.select(b),N=_.datum(),E=!/^(n|s)$/.test(N)&&c,A=!/^(e|w)$/.test(N)&&s,C=_.classed("extent"),z=W(b),L=oa.mouse(b),q=oa.select(t(b)).on("keydown.brush",i).on("keyup.brush",v);if(oa.event.changedTouches?q.on("touchmove.brush",d).on("touchend.brush",y):q.on("mousemove.brush",d).on("mouseup.brush",y),k.interrupt().selectAll("*").interrupt(),C)L[0]=f[0]-L[0],L[1]=h[0]-L[1];else if(N){var T=+/w$/.test(N),R=+/^n/.test(N);x=[f[1-T]-L[0],h[1-R]-L[1]],L[0]=f[T],L[1]=h[R]}else oa.event.altKey&&(M=L.slice());k.style("pointer-events","none").selectAll(".resize").style("display",null),oa.select("body").style("cursor",_.style("cursor")),w({type:"brushstart"}),d()}var a,o,l=N(n,"brushstart","brush","brushend"),c=null,s=null,f=[0,0],h=[0,0],g=!0,p=!0,v=$l[0];return n.event=function(n){n.each(function(){var n=l.of(this,arguments),t={x:f,y:h,i:a,j:o},e=this.__chart__||t;this.__chart__=t,Fl?oa.select(this).transition().each("start.brush",function(){a=e.i,o=e.j,f=e.x,h=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=xr(f,t.x),r=xr(h,t.y);return a=o=null,function(u){f=t.x=e(u),h=t.y=r(u),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){a=t.i,o=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(c=t,v=$l[!c<<1|!s],n):c},n.y=function(t){return arguments.length?(s=t,v=$l[!c<<1|!s],n):s},n.clamp=function(t){return arguments.length?(c&&s?(g=!!t[0],p=!!t[1]):c?g=!!t:s&&(p=!!t),n):c&&s?[g,p]:c?g:s?p:null},n.extent=function(t){var e,r,u,i,l;return arguments.length?(c&&(e=t[0],r=t[1],s&&(e=e[0],r=r[0]),a=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(l=e,e=r,r=l),(e!=f[0]||r!=f[1])&&(f=[e,r])),s&&(u=t[0],i=t[1],c&&(u=u[1],i=i[1]),o=[u,i],s.invert&&(u=s(u),i=s(i)),u>i&&(l=u,u=i,i=l),(u!=h[0]||i!=h[1])&&(h=[u,i])),n):(c&&(a?(e=a[0],r=a[1]):(e=f[0],r=f[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(l=e,e=r,r=l))),s&&(o?(u=o[0],i=o[1]):(u=h[0],i=h[1],s.invert&&(u=s.invert(u),i=s.invert(i)),u>i&&(l=u,u=i,i=l))),c&&s?[[e,u],[r,i]]:c?[e,r]:s&&[u,i])},n.clear=function(){return n.empty()||(f=[0,0],h=[0,0],a=o=null),n},n.empty=function(){return!!c&&f[0]==f[1]||!!s&&h[0]==h[1]},oa.rebind(n,l,"on")};var Xl={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},$l=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Bl=ho.format=Mo.timeFormat,Wl=Bl.utc,Jl=Wl("%Y-%m-%dT%H:%M:%S.%LZ");Bl.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?ea:Jl,ea.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},ea.toString=Jl.toString,ho.second=On(function(n){return new go(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),ho.seconds=ho.second.range,ho.seconds.utc=ho.second.utc.range,ho.minute=On(function(n){return new go(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),ho.minutes=ho.minute.range,ho.minutes.utc=ho.minute.utc.range,ho.hour=On(function(n){var t=n.getTimezoneOffset()/60;return new go(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),ho.hours=ho.hour.range,ho.hours.utc=ho.hour.utc.range,ho.month=On(function(n){return n=ho.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),ho.months=ho.month.range,ho.months.utc=ho.month.utc.range;var Gl=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Kl=[[ho.second,1],[ho.second,5],[ho.second,15],[ho.second,30],[ho.minute,1],[ho.minute,5],[ho.minute,15],[ho.minute,30],[ho.hour,1],[ho.hour,3],[ho.hour,6],[ho.hour,12],[ho.day,1],[ho.day,2],[ho.week,1],[ho.month,1],[ho.month,3],[ho.year,1]],Ql=Bl.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",zt]]),nc={range:function(n,t,e){return oa.range(Math.ceil(n/e)*e,+t,e).map(ua)},floor:y,ceil:y};Kl.year=ho.year,ho.scale=function(){return ra(oa.scale.linear(),Kl,Ql)};var tc=Kl.map(function(n){return[n[0].utc,n[1]]}),ec=Wl.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",zt]]);tc.year=ho.year.utc,ho.scale.utc=function(){return ra(oa.scale.linear(),tc,ec)},oa.text=An(function(n){return n.responseText}),oa.json=function(n,t){return Cn(n,"application/json",ia,t)},oa.html=function(n,t){return Cn(n,"text/html",aa,t)},oa.xml=An(function(n){return n.responseXML}),"function"==typeof define&&define.amd?(this.d3=oa,define(oa)):"object"==typeof module&&module.exports?module.exports=oa:this.d3=oa}(); \ No newline at end of file diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/download.js b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/download.js new file mode 100644 index 000000000..11ed5778c --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/download.js @@ -0,0 +1,174 @@ +/* + * 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. + */ + +var modalPopup = ".wr-modalpopup"; +var modalPopupContainer = modalPopup + " .modalpopup-container"; +var modalPopupContent = modalPopup + " .modalpopup-content"; +var body = "body"; + +/* + * set popup maximum height function. + */ +function setPopupMaxHeight() { + $(modalPopupContent).css('max-height', ($(body).height() - ($(body).height() / 100 * 30))); + $(modalPopupContainer).css('margin-top', (-($(modalPopupContainer).height() / 2))); +} + +/* + * show popup function. + */ +function showPopup() { + $(modalPopup).show(); + setPopupMaxHeight(); + $('#downloadForm').validate({ + rules: { + deviceName: { + minlength: 4, + required: true + } + }, + highlight: function (element) { + $(element).closest('.control-group').removeClass('success').addClass('error'); + }, + success: function (element) { + $(element).closest('.control-group').removeClass('error').addClass('success'); + $('label[for=deviceName]').remove(); + } + }); + var deviceType = ""; + $('.deviceType').each(function () { + if (this.value != "") { + deviceType = this.value; + } + }); +} + +/* + * hide popup function. + */ +function hidePopup() { + $('label[for=deviceName]').remove(); + $('.control-group').removeClass('success').removeClass('error'); + $(modalPopupContent).html(''); + $(modalPopup).hide(); +} + +/* + * DOM ready functions. + */ +$(document).ready(function () { + attachEvents(); +}); + +function attachEvents() { + /** + * Following click function would execute + * when a user clicks on "Download" link + * on Device Management page in WSO2 DC Console. + */ + $("a.download-link").click(function () { + var sketchType = $(this).data("sketchtype"); + var deviceType = $(this).data("devicetype"); + var downloadDeviceAPI = "/devicemgt/api/devices/sketch/generate_link"; + var payload = {"sketchType": sketchType, "deviceType": deviceType}; + $(modalPopupContent).html($('#download-device-modal-content').html()); + showPopup(); + var deviceName; + $("a#download-device-download-link").click(function () { + $('.new-device-name').each(function () { + if (this.value != "") { + deviceName = this.value; + } + }); + $('label[for=deviceName]').remove(); + if (deviceName && deviceName.length >= 4) { + payload.deviceName = deviceName; + invokerUtil.post( + downloadDeviceAPI, + payload, + function (data, textStatus, jqxhr) { + doAction(data); + }, + function (data) { + doAction(data); + } + ); + }else if(deviceName){ + $('.controls').append(''); + $('.control-group').removeClass('success').addClass('error'); + } else { + $('.controls').append(''); + $('.control-group').removeClass('success').addClass('error'); + } + }); + + $("a#download-device-cancel-link").click(function () { + hidePopup(); + }); + + }); +} + +function downloadAgent() { + $('#downloadForm').submit(); + + var deviceName; + $('.new-device-name').each(function () { + if (this.value != "") { + deviceName = this.value; + } + }); + if (deviceName && deviceName.length >= 4) { + setTimeout(function () { + hidePopup(); + }, 1000); + } +} + +function doAction(data) { + //if it is saml redirection response + if (data.status == null) { + document.write(data); + } + + if (data.status == "200") { + $(modalPopupContent).html($('#download-device-modal-content-links').html()); + $("input#download-device-url").val(data.responseText); + $("input#download-device-url").focus(function () { + $(this).select(); + }); + showPopup(); + } else if (data.status == "401") { + $(modalPopupContent).html($('#device-401-content').html()); + $("#device-401-link").click(function () { + window.location = "/devicemgt/login"; + }); + showPopup(); + } else if (data == "403") { + $(modalPopupContent).html($('#device-403-content').html()); + $("#device-403-link").click(function () { + window.location = "/devicemgt/login"; + }); + showPopup(); + } else { + $(modalPopupContent).html($('#device-unexpected-error-content').html()); + $("a#device-unexpected-error-link").click(function () { + hidePopup(); + }); + } +} \ No newline at end of file diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/initJs b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/initJs new file mode 100644 index 000000000..57f70e56a --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/initJs @@ -0,0 +1,21 @@ +/* + * 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. + */ +var ajax_handler = new ajax_handler(); +var config_api = new config_api(); +var plotting = new plotting(); +var object_maker = new object_maker(); \ No newline at end of file diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/jQueryRotate.js b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/jQueryRotate.js new file mode 100644 index 000000000..e3a8fa9c3 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/jQueryRotate.js @@ -0,0 +1,357 @@ +/* + * 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. + */ + +// VERSION: 2.3 LAST UPDATE: 11.07.2013 +/* + * Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php + * + * Made by Wilq32, wilq32@gmail.com, Wroclaw, Poland, 01.2009 + * Website: http://jqueryrotate.com + */ + +(function($) { + var supportedCSS,supportedCSSOrigin, styles=document.getElementsByTagName("head")[0].style,toCheck="transformProperty WebkitTransform OTransform msTransform MozTransform".split(" "); + for (var a = 0; a < toCheck.length; a++) if (styles[toCheck[a]] !== undefined) { supportedCSS = toCheck[a]; } + if (supportedCSS) { + supportedCSSOrigin = supportedCSS.replace(/[tT]ransform/,"TransformOrigin"); + if (supportedCSSOrigin[0] == "T") supportedCSSOrigin[0] = "t"; + } + + // Bad eval to preven google closure to remove it from code o_O + eval('IE = "v"=="\v"'); + + jQuery.fn.extend({ + rotate:function(parameters) + { + if (this.length===0||typeof parameters=="undefined") return; + if (typeof parameters=="number") parameters={angle:parameters}; + var returned=[]; + for (var i=0,i0=this.length;i this._parameters.duration; + + // TODO: Bug for animatedGif for static rotation ? (to test) + if (checkEnd && !this._parameters.animatedGif) + { + clearTimeout(this._timer); + } + else + { + if (this._canvas||this._vimage||this._img) { + var angle = this._parameters.easing(0, actualTime - this._animateStartTime, this._animateStartAngle, this._parameters.animateTo - this._animateStartAngle, this._parameters.duration); + this._rotate((~~(angle*10))/10); + } + if (this._parameters.step) { + this._parameters.step(this._angle); + } + var self = this; + this._timer = setTimeout(function() + { + self._animate.call(self); + }, 10); + } + + // To fix Bug that prevents using recursive function in callback I moved this function to back + if (this._parameters.callback && checkEnd){ + this._angle = this._parameters.animateTo; + this._rotate(this._angle); + this._parameters.callback.call(this._rootObj); + } + }, + + _rotate : (function() + { + var rad = Math.PI/180; + if (IE) + return function(angle) + { + this._angle = angle; + this._container.style.rotation=(angle%360)+"deg"; + this._vimage.style.top = -(this._rotationCenterY - this._imgHeight/2) + "px"; + this._vimage.style.left = -(this._rotationCenterX - this._imgWidth/2) + "px"; + this._container.style.top = this._rotationCenterY - this._imgHeight/2 + "px"; + this._container.style.left = this._rotationCenterX - this._imgWidth/2 + "px"; + + } + else if (supportedCSS) + return function(angle){ + this._angle = angle; + this._img.style[supportedCSS]="rotate("+(angle%360)+"deg)"; + this._img.style[supportedCSSOrigin]=this._parameters.center.join(" "); + } + else + return function(angle) + { + this._angle = angle; + angle=(angle%360)* rad; + // clear canvas + this._canvas.width = this._width;//+this._widthAdd; + this._canvas.height = this._height;//+this._heightAdd; + + // REMEMBER: all drawings are read from backwards.. so first function is translate, then rotate, then translate, translate.. + this._cnv.translate(this._imgWidth*this._aspectW,this._imgHeight*this._aspectH); // at least center image on screen + this._cnv.translate(this._rotationCenterX,this._rotationCenterY); // we move image back to its orginal + this._cnv.rotate(angle); // rotate image + this._cnv.translate(-this._rotationCenterX,-this._rotationCenterY); // move image to its center, so we can rotate around its center + this._cnv.scale(this._aspectW,this._aspectH); // SCALE - if needed ;) + this._cnv.drawImage(this._img, 0, 0); // First - we draw image + } + + })() + } + + if (IE) + { + Wilq32.PhotoEffect.prototype.createVMLNode=(function(){ + document.createStyleSheet().addRule(".rvml", "behavior:url(#default#VML)"); + try { + !document.namespaces.rvml && document.namespaces.add("rvml", "urn:schemas-microsoft-com:vml"); + return function (tagName) { + return document.createElement(''); + }; + } catch (e) { + return function (tagName) { + return document.createElement('<' + tagName + ' xmlns="urn:schemas-microsoft.com:vml" class="rvml">'); + }; + } + })(); + } + +})(jQuery); diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/jquery.validate.js b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/jquery.validate.js new file mode 100644 index 000000000..fe7ecf07a --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/jquery.validate.js @@ -0,0 +1,1227 @@ +/* + * 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. + */ + +(function($) { + +$.extend($.fn, { + // http://docs.jquery.com/Plugins/Validation/validate + validate: function( options ) { + + // if nothing is selected, return nothing; can't chain anyway + if ( !this.length ) { + if ( options && options.debug && window.console ) { + console.warn( "Nothing selected, can't validate, returning nothing." ); + } + return; + } + + // check if a validator for this form was already created + var validator = $.data( this[0], "validator" ); + if ( validator ) { + return validator; + } + + // Add novalidate tag if HTML5. + this.attr( "novalidate", "novalidate" ); + + validator = new $.validator( options, this[0] ); + $.data( this[0], "validator", validator ); + + if ( validator.settings.onsubmit ) { + + this.validateDelegate( ":submit", "click", function( event ) { + if ( validator.settings.submitHandler ) { + validator.submitButton = event.target; + } + // allow suppressing validation by adding a cancel class to the submit button + if ( $(event.target).hasClass("cancel") ) { + validator.cancelSubmit = true; + } + }); + + // validate the form on submit + this.submit( function( event ) { + if ( validator.settings.debug ) { + // prevent form submit to be able to see console output + event.preventDefault(); + } + function handle() { + var hidden; + if ( validator.settings.submitHandler ) { + if ( validator.submitButton ) { + // insert a hidden input as a replacement for the missing submit button + hidden = $("").attr("name", validator.submitButton.name).val(validator.submitButton.value).appendTo(validator.currentForm); + } + validator.settings.submitHandler.call( validator, validator.currentForm, event ); + if ( validator.submitButton ) { + // and clean up afterwards; thanks to no-block-scope, hidden can be referenced + hidden.remove(); + } + return false; + } + return true; + } + + // prevent submit for invalid forms or custom submit handlers + if ( validator.cancelSubmit ) { + validator.cancelSubmit = false; + return handle(); + } + if ( validator.form() ) { + if ( validator.pendingRequest ) { + validator.formSubmitted = true; + return false; + } + return handle(); + } else { + validator.focusInvalid(); + return false; + } + }); + } + + return validator; + }, + // http://docs.jquery.com/Plugins/Validation/valid + valid: function() { + if ( $(this[0]).is("form")) { + return this.validate().form(); + } else { + var valid = true; + var validator = $(this[0].form).validate(); + this.each(function() { + valid &= validator.element(this); + }); + return valid; + } + }, + // attributes: space seperated list of attributes to retrieve and remove + removeAttrs: function( attributes ) { + var result = {}, + $element = this; + $.each(attributes.split(/\s/), function( index, value ) { + result[value] = $element.attr(value); + $element.removeAttr(value); + }); + return result; + }, + // http://docs.jquery.com/Plugins/Validation/rules + rules: function( command, argument ) { + var element = this[0]; + + if ( command ) { + var settings = $.data(element.form, "validator").settings; + var staticRules = settings.rules; + var existingRules = $.validator.staticRules(element); + switch(command) { + case "add": + $.extend(existingRules, $.validator.normalizeRule(argument)); + staticRules[element.name] = existingRules; + if ( argument.messages ) { + settings.messages[element.name] = $.extend( settings.messages[element.name], argument.messages ); + } + break; + case "remove": + if ( !argument ) { + delete staticRules[element.name]; + return existingRules; + } + var filtered = {}; + $.each(argument.split(/\s/), function( index, method ) { + filtered[method] = existingRules[method]; + delete existingRules[method]; + }); + return filtered; + } + } + + var data = $.validator.normalizeRules( + $.extend( + {}, + $.validator.classRules(element), + $.validator.attributeRules(element), + $.validator.dataRules(element), + $.validator.staticRules(element) + ), element); + + // make sure required is at front + if ( data.required ) { + var param = data.required; + delete data.required; + data = $.extend({required: param}, data); + } + + return data; + } +}); + +// Custom selectors +$.extend($.expr[":"], { + // http://docs.jquery.com/Plugins/Validation/blank + blank: function( a ) { return !$.trim("" + a.value); }, + // http://docs.jquery.com/Plugins/Validation/filled + filled: function( a ) { return !!$.trim("" + a.value); }, + // http://docs.jquery.com/Plugins/Validation/unchecked + unchecked: function( a ) { return !a.checked; } +}); + +// constructor for validator +$.validator = function( options, form ) { + this.settings = $.extend( true, {}, $.validator.defaults, options ); + this.currentForm = form; + this.init(); +}; + +$.validator.format = function( source, params ) { + if ( arguments.length === 1 ) { + return function() { + var args = $.makeArray(arguments); + args.unshift(source); + return $.validator.format.apply( this, args ); + }; + } + if ( arguments.length > 2 && params.constructor !== Array ) { + params = $.makeArray(arguments).slice(1); + } + if ( params.constructor !== Array ) { + params = [ params ]; + } + $.each(params, function( i, n ) { + source = source.replace( new RegExp("\\{" + i + "\\}", "g"), function() { + return n; + }); + }); + return source; +}; + +$.extend($.validator, { + + defaults: { + messages: {}, + groups: {}, + rules: {}, + errorClass: "error", + validClass: "valid", + errorElement: "label", + focusInvalid: true, + errorContainer: $([]), + errorLabelContainer: $([]), + onsubmit: true, + ignore: ":hidden", + ignoreTitle: false, + onfocusin: function( element, event ) { + this.lastActive = element; + + // hide error label and remove error class on focus if enabled + if ( this.settings.focusCleanup && !this.blockFocusCleanup ) { + if ( this.settings.unhighlight ) { + this.settings.unhighlight.call( this, element, this.settings.errorClass, this.settings.validClass ); + } + this.addWrapper(this.errorsFor(element)).hide(); + } + }, + onfocusout: function( element, event ) { + if ( !this.checkable(element) && (element.name in this.submitted || !this.optional(element)) ) { + this.element(element); + } + }, + onkeyup: function( element, event ) { + if ( event.which === 9 && this.elementValue(element) === "" ) { + return; + } else if ( element.name in this.submitted || element === this.lastElement ) { + this.element(element); + } + }, + onclick: function( element, event ) { + // click on selects, radiobuttons and checkboxes + if ( element.name in this.submitted ) { + this.element(element); + } + // or option elements, check parent select in that case + else if ( element.parentNode.name in this.submitted ) { + this.element(element.parentNode); + } + }, + highlight: function( element, errorClass, validClass ) { + if ( element.type === "radio" ) { + this.findByName(element.name).addClass(errorClass).removeClass(validClass); + } else { + $(element).addClass(errorClass).removeClass(validClass); + } + }, + unhighlight: function( element, errorClass, validClass ) { + if ( element.type === "radio" ) { + this.findByName(element.name).removeClass(errorClass).addClass(validClass); + } else { + $(element).removeClass(errorClass).addClass(validClass); + } + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/setDefaults + setDefaults: function( settings ) { + $.extend( $.validator.defaults, settings ); + }, + + messages: { + required: "This field is required.", + remote: "Please fix this field.", + email: "Please enter a valid email address.", + url: "Please enter a valid URL.", + date: "Please enter a valid date.", + dateISO: "Please enter a valid date (ISO).", + number: "Please enter a valid number.", + digits: "Please enter only digits.", + creditcard: "Please enter a valid credit card number.", + equalTo: "Please enter the same value again.", + maxlength: $.validator.format("Please enter no more than {0} characters."), + minlength: $.validator.format("Please enter at least {0} characters."), + rangelength: $.validator.format("Please enter a value between {0} and {1} characters long."), + range: $.validator.format("Please enter a value between {0} and {1}."), + max: $.validator.format("Please enter a value less than or equal to {0}."), + min: $.validator.format("Please enter a value greater than or equal to {0}.") + }, + + autoCreateRanges: false, + + prototype: { + + init: function() { + this.labelContainer = $(this.settings.errorLabelContainer); + this.errorContext = this.labelContainer.length && this.labelContainer || $(this.currentForm); + this.containers = $(this.settings.errorContainer).add( this.settings.errorLabelContainer ); + this.submitted = {}; + this.valueCache = {}; + this.pendingRequest = 0; + this.pending = {}; + this.invalid = {}; + this.reset(); + + var groups = (this.groups = {}); + $.each(this.settings.groups, function( key, value ) { + if ( typeof value === "string" ) { + value = value.split(/\s/); + } + $.each(value, function( index, name ) { + groups[name] = key; + }); + }); + var rules = this.settings.rules; + $.each(rules, function( key, value ) { + rules[key] = $.validator.normalizeRule(value); + }); + + function delegate(event) { + var validator = $.data(this[0].form, "validator"), + eventType = "on" + event.type.replace(/^validate/, ""); + if ( validator.settings[eventType] ) { + validator.settings[eventType].call(validator, this[0], event); + } + } + $(this.currentForm) + .validateDelegate(":text, [type='password'], [type='file'], select, textarea, " + + "[type='number'], [type='search'] ,[type='tel'], [type='url'], " + + "[type='email'], [type='datetime'], [type='date'], [type='month'], " + + "[type='week'], [type='time'], [type='datetime-local'], " + + "[type='range'], [type='color'] ", + "focusin focusout keyup", delegate) + .validateDelegate("[type='radio'], [type='checkbox'], select, option", "click", delegate); + + if ( this.settings.invalidHandler ) { + $(this.currentForm).bind("invalid-form.validate", this.settings.invalidHandler); + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/form + form: function() { + this.checkForm(); + $.extend(this.submitted, this.errorMap); + this.invalid = $.extend({}, this.errorMap); + if ( !this.valid() ) { + $(this.currentForm).triggerHandler("invalid-form", [this]); + } + this.showErrors(); + return this.valid(); + }, + + checkForm: function() { + this.prepareForm(); + for ( var i = 0, elements = (this.currentElements = this.elements()); elements[i]; i++ ) { + this.check( elements[i] ); + } + return this.valid(); + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/element + element: function( element ) { + element = this.validationTargetFor( this.clean( element ) ); + this.lastElement = element; + this.prepareElement( element ); + this.currentElements = $(element); + var result = this.check( element ) !== false; + if ( result ) { + delete this.invalid[element.name]; + } else { + this.invalid[element.name] = true; + } + if ( !this.numberOfInvalids() ) { + // Hide error containers on last error + this.toHide = this.toHide.add( this.containers ); + } + this.showErrors(); + return result; + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/showErrors + showErrors: function( errors ) { + if ( errors ) { + // add items to error list and map + $.extend( this.errorMap, errors ); + this.errorList = []; + for ( var name in errors ) { + this.errorList.push({ + message: errors[name], + element: this.findByName(name)[0] + }); + } + // remove items from success list + this.successList = $.grep( this.successList, function( element ) { + return !(element.name in errors); + }); + } + if ( this.settings.showErrors ) { + this.settings.showErrors.call( this, this.errorMap, this.errorList ); + } else { + this.defaultShowErrors(); + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/resetForm + resetForm: function() { + if ( $.fn.resetForm ) { + $(this.currentForm).resetForm(); + } + this.submitted = {}; + this.lastElement = null; + this.prepareForm(); + this.hideErrors(); + this.elements().removeClass( this.settings.errorClass ).removeData( "previousValue" ); + }, + + numberOfInvalids: function() { + return this.objectLength(this.invalid); + }, + + objectLength: function( obj ) { + var count = 0; + for ( var i in obj ) { + count++; + } + return count; + }, + + hideErrors: function() { + this.addWrapper( this.toHide ).hide(); + }, + + valid: function() { + return this.size() === 0; + }, + + size: function() { + return this.errorList.length; + }, + + focusInvalid: function() { + if ( this.settings.focusInvalid ) { + try { + $(this.findLastActive() || this.errorList.length && this.errorList[0].element || []) + .filter(":visible") + .focus() + // manually trigger focusin event; without it, focusin handler isn't called, findLastActive won't have anything to find + .trigger("focusin"); + } catch(e) { + // ignore IE throwing errors when focusing hidden elements + } + } + }, + + findLastActive: function() { + var lastActive = this.lastActive; + return lastActive && $.grep(this.errorList, function( n ) { + return n.element.name === lastActive.name; + }).length === 1 && lastActive; + }, + + elements: function() { + var validator = this, + rulesCache = {}; + + // select all valid inputs inside the form (no submit or reset buttons) + return $(this.currentForm) + .find("input, select, textarea") + .not(":submit, :reset, :image, [disabled]") + .not( this.settings.ignore ) + .filter(function() { + if ( !this.name ) { + if ( window.console ) { + console.error( "%o has no name assigned", this ); + } + throw new Error( "Failed to validate, found an element with no name assigned. See console for element reference." ); + } + + // select only the first element for each name, and only those with rules specified + if ( this.name in rulesCache || !validator.objectLength($(this).rules()) ) { + return false; + } + + rulesCache[this.name] = true; + return true; + }); + }, + + clean: function( selector ) { + return $(selector)[0]; + }, + + errors: function() { + var errorClass = this.settings.errorClass.replace(" ", "."); + return $(this.settings.errorElement + "." + errorClass, this.errorContext); + }, + + reset: function() { + this.successList = []; + this.errorList = []; + this.errorMap = {}; + this.toShow = $([]); + this.toHide = $([]); + this.currentElements = $([]); + }, + + prepareForm: function() { + this.reset(); + this.toHide = this.errors().add( this.containers ); + }, + + prepareElement: function( element ) { + this.reset(); + this.toHide = this.errorsFor(element); + }, + + elementValue: function( element ) { + var type = $(element).attr("type"), + val = $(element).val(); + + if ( type === "radio" || type === "checkbox" ) { + return $("input[name='" + $(element).attr("name") + "']:checked").val(); + } + + if ( typeof val === "string" ) { + return val.replace(/\r/g, ""); + } + return val; + }, + + check: function( element ) { + element = this.validationTargetFor( this.clean( element ) ); + + var rules = $(element).rules(); + var dependencyMismatch = false; + var val = this.elementValue(element); + var result; + + for (var method in rules ) { + var rule = { method: method, parameters: rules[method] }; + try { + + result = $.validator.methods[method].call( this, val, element, rule.parameters ); + + // if a method indicates that the field is optional and therefore valid, + // don't mark it as valid when there are no other rules + if ( result === "dependency-mismatch" ) { + dependencyMismatch = true; + continue; + } + dependencyMismatch = false; + + if ( result === "pending" ) { + this.toHide = this.toHide.not( this.errorsFor(element) ); + return; + } + + if ( !result ) { + this.formatAndAdd( element, rule ); + return false; + } + } catch(e) { + if ( this.settings.debug && window.console ) { + console.log( "Exception occured when checking element " + element.id + ", check the '" + rule.method + "' method.", e ); + } + throw e; + } + } + if ( dependencyMismatch ) { + return; + } + if ( this.objectLength(rules) ) { + this.successList.push(element); + } + return true; + }, + + // return the custom message for the given element and validation method + // specified in the element's HTML5 data attribute + customDataMessage: function( element, method ) { + return $(element).data("msg-" + method.toLowerCase()) || (element.attributes && $(element).attr("data-msg-" + method.toLowerCase())); + }, + + // return the custom message for the given element name and validation method + customMessage: function( name, method ) { + var m = this.settings.messages[name]; + return m && (m.constructor === String ? m : m[method]); + }, + + // return the first defined argument, allowing empty strings + findDefined: function() { + for(var i = 0; i < arguments.length; i++) { + if ( arguments[i] !== undefined ) { + return arguments[i]; + } + } + return undefined; + }, + + defaultMessage: function( element, method ) { + return this.findDefined( + this.customMessage( element.name, method ), + this.customDataMessage( element, method ), + // title is never undefined, so handle empty string as undefined + !this.settings.ignoreTitle && element.title || undefined, + $.validator.messages[method], + "Warning: No message defined for " + element.name + "" + ); + }, + + formatAndAdd: function( element, rule ) { + var message = this.defaultMessage( element, rule.method ), + theregex = /\$?\{(\d+)\}/g; + if ( typeof message === "function" ) { + message = message.call(this, rule.parameters, element); + } else if (theregex.test(message)) { + message = $.validator.format(message.replace(theregex, "{$1}"), rule.parameters); + } + this.errorList.push({ + message: message, + element: element + }); + + this.errorMap[element.name] = message; + this.submitted[element.name] = message; + }, + + addWrapper: function( toToggle ) { + if ( this.settings.wrapper ) { + toToggle = toToggle.add( toToggle.parent( this.settings.wrapper ) ); + } + return toToggle; + }, + + defaultShowErrors: function() { + var i, elements; + for ( i = 0; this.errorList[i]; i++ ) { + var error = this.errorList[i]; + if ( this.settings.highlight ) { + this.settings.highlight.call( this, error.element, this.settings.errorClass, this.settings.validClass ); + } + this.showLabel( error.element, error.message ); + } + if ( this.errorList.length ) { + this.toShow = this.toShow.add( this.containers ); + } + if ( this.settings.success ) { + for ( i = 0; this.successList[i]; i++ ) { + this.showLabel( this.successList[i] ); + } + } + if ( this.settings.unhighlight ) { + for ( i = 0, elements = this.validElements(); elements[i]; i++ ) { + this.settings.unhighlight.call( this, elements[i], this.settings.errorClass, this.settings.validClass ); + } + } + this.toHide = this.toHide.not( this.toShow ); + this.hideErrors(); + this.addWrapper( this.toShow ).show(); + }, + + validElements: function() { + return this.currentElements.not(this.invalidElements()); + }, + + invalidElements: function() { + return $(this.errorList).map(function() { + return this.element; + }); + }, + + showLabel: function( element, message ) { + var label = this.errorsFor( element ); + if ( label.length ) { + // refresh error/success class + label.removeClass( this.settings.validClass ).addClass( this.settings.errorClass ); + + // check if we have a generated label, replace the message then + if ( label.attr("generated") ) { + label.html(message); + } + } else { + // create label + label = $("<" + this.settings.errorElement + "/>") + .attr({"for": this.idOrName(element), generated: true}) + .addClass(this.settings.errorClass) + .html(message || ""); + if ( this.settings.wrapper ) { + // make sure the element is visible, even in IE + // actually showing the wrapped element is handled elsewhere + label = label.hide().show().wrap("<" + this.settings.wrapper + "/>").parent(); + } + if ( !this.labelContainer.append(label).length ) { + if ( this.settings.errorPlacement ) { + this.settings.errorPlacement(label, $(element) ); + } else { + label.insertAfter(element); + } + } + } + if ( !message && this.settings.success ) { + label.text(""); + if ( typeof this.settings.success === "string" ) { + label.addClass( this.settings.success ); + } else { + this.settings.success( label, element ); + } + } + this.toShow = this.toShow.add(label); + }, + + errorsFor: function( element ) { + var name = this.idOrName(element); + return this.errors().filter(function() { + return $(this).attr("for") === name; + }); + }, + + idOrName: function( element ) { + return this.groups[element.name] || (this.checkable(element) ? element.name : element.id || element.name); + }, + + validationTargetFor: function( element ) { + // if radio/checkbox, validate first element in group instead + if ( this.checkable(element) ) { + element = this.findByName( element.name ).not(this.settings.ignore)[0]; + } + return element; + }, + + checkable: function( element ) { + return (/radio|checkbox/i).test(element.type); + }, + + findByName: function( name ) { + return $(this.currentForm).find("[name='" + name + "']"); + }, + + getLength: function( value, element ) { + switch( element.nodeName.toLowerCase() ) { + case "select": + return $("option:selected", element).length; + case "input": + if ( this.checkable( element) ) { + return this.findByName(element.name).filter(":checked").length; + } + } + return value.length; + }, + + depend: function( param, element ) { + return this.dependTypes[typeof param] ? this.dependTypes[typeof param](param, element) : true; + }, + + dependTypes: { + "boolean": function( param, element ) { + return param; + }, + "string": function( param, element ) { + return !!$(param, element.form).length; + }, + "function": function( param, element ) { + return param(element); + } + }, + + optional: function( element ) { + var val = this.elementValue(element); + return !$.validator.methods.required.call(this, val, element) && "dependency-mismatch"; + }, + + startRequest: function( element ) { + if ( !this.pending[element.name] ) { + this.pendingRequest++; + this.pending[element.name] = true; + } + }, + + stopRequest: function( element, valid ) { + this.pendingRequest--; + // sometimes synchronization fails, make sure pendingRequest is never < 0 + if ( this.pendingRequest < 0 ) { + this.pendingRequest = 0; + } + delete this.pending[element.name]; + if ( valid && this.pendingRequest === 0 && this.formSubmitted && this.form() ) { + $(this.currentForm).submit(); + this.formSubmitted = false; + } else if (!valid && this.pendingRequest === 0 && this.formSubmitted) { + $(this.currentForm).triggerHandler("invalid-form", [this]); + this.formSubmitted = false; + } + }, + + previousValue: function( element ) { + return $.data(element, "previousValue") || $.data(element, "previousValue", { + old: null, + valid: true, + message: this.defaultMessage( element, "remote" ) + }); + } + + }, + + classRuleSettings: { + required: {required: true}, + email: {email: true}, + url: {url: true}, + date: {date: true}, + dateISO: {dateISO: true}, + number: {number: true}, + digits: {digits: true}, + creditcard: {creditcard: true} + }, + + addClassRules: function( className, rules ) { + if ( className.constructor === String ) { + this.classRuleSettings[className] = rules; + } else { + $.extend(this.classRuleSettings, className); + } + }, + + classRules: function( element ) { + var rules = {}; + var classes = $(element).attr("class"); + if ( classes ) { + $.each(classes.split(" "), function() { + if ( this in $.validator.classRuleSettings ) { + $.extend(rules, $.validator.classRuleSettings[this]); + } + }); + } + return rules; + }, + + attributeRules: function( element ) { + var rules = {}; + var $element = $(element); + + for (var method in $.validator.methods) { + var value; + + // support for in both html5 and older browsers + if ( method === "required" ) { + value = $element.get(0).getAttribute(method); + // Some browsers return an empty string for the required attribute + // and non-HTML5 browsers might have required="" markup + if ( value === "" ) { + value = true; + } + // force non-HTML5 browsers to return bool + value = !!value; + } else { + value = $element.attr(method); + } + + if ( value ) { + rules[method] = value; + } else if ( $element[0].getAttribute("type") === method ) { + rules[method] = true; + } + } + + // maxlength may be returned as -1, 2147483647 (IE) and 524288 (safari) for text inputs + if ( rules.maxlength && /-1|2147483647|524288/.test(rules.maxlength) ) { + delete rules.maxlength; + } + + return rules; + }, + + dataRules: function( element ) { + var method, value, + rules = {}, $element = $(element); + for (method in $.validator.methods) { + value = $element.data("rule-" + method.toLowerCase()); + if ( value !== undefined ) { + rules[method] = value; + } + } + return rules; + }, + + staticRules: function( element ) { + var rules = {}; + var validator = $.data(element.form, "validator"); + if ( validator.settings.rules ) { + rules = $.validator.normalizeRule(validator.settings.rules[element.name]) || {}; + } + return rules; + }, + + normalizeRules: function( rules, element ) { + // handle dependency check + $.each(rules, function( prop, val ) { + // ignore rule when param is explicitly false, eg. required:false + if ( val === false ) { + delete rules[prop]; + return; + } + if ( val.param || val.depends ) { + var keepRule = true; + switch (typeof val.depends) { + case "string": + keepRule = !!$(val.depends, element.form).length; + break; + case "function": + keepRule = val.depends.call(element, element); + break; + } + if ( keepRule ) { + rules[prop] = val.param !== undefined ? val.param : true; + } else { + delete rules[prop]; + } + } + }); + + // evaluate parameters + $.each(rules, function( rule, parameter ) { + rules[rule] = $.isFunction(parameter) ? parameter(element) : parameter; + }); + + // clean number parameters + $.each(["minlength", "maxlength", "min", "max"], function() { + if ( rules[this] ) { + rules[this] = Number(rules[this]); + } + }); + $.each(["rangelength", "range"], function() { + var parts; + if ( rules[this] ) { + if ( $.isArray(rules[this]) ) { + rules[this] = [Number(rules[this][0]), Number(rules[this][1])]; + } else if ( typeof rules[this] === "string" ) { + parts = rules[this].split(/[\s,]+/); + rules[this] = [Number(parts[0]), Number(parts[1])]; + } + } + }); + + if ( $.validator.autoCreateRanges ) { + // auto-create ranges + if ( rules.min && rules.max ) { + rules.range = [rules.min, rules.max]; + delete rules.min; + delete rules.max; + } + if ( rules.minlength && rules.maxlength ) { + rules.rangelength = [rules.minlength, rules.maxlength]; + delete rules.minlength; + delete rules.maxlength; + } + } + + return rules; + }, + + // Converts a simple string to a {string: true} rule, e.g., "required" to {required:true} + normalizeRule: function( data ) { + if ( typeof data === "string" ) { + var transformed = {}; + $.each(data.split(/\s/), function() { + transformed[this] = true; + }); + data = transformed; + } + return data; + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/addMethod + addMethod: function( name, method, message ) { + $.validator.methods[name] = method; + $.validator.messages[name] = message !== undefined ? message : $.validator.messages[name]; + if ( method.length < 3 ) { + $.validator.addClassRules(name, $.validator.normalizeRule(name)); + } + }, + + methods: { + + // http://docs.jquery.com/Plugins/Validation/Methods/required + required: function( value, element, param ) { + // check if dependency is met + if ( !this.depend(param, element) ) { + return "dependency-mismatch"; + } + if ( element.nodeName.toLowerCase() === "select" ) { + // could be an array for select-multiple or a string, both are fine this way + var val = $(element).val(); + return val && val.length > 0; + } + if ( this.checkable(element) ) { + return this.getLength(value, element) > 0; + } + return $.trim(value).length > 0; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/remote + remote: function( value, element, param ) { + if ( this.optional(element) ) { + return "dependency-mismatch"; + } + + var previous = this.previousValue(element); + if (!this.settings.messages[element.name] ) { + this.settings.messages[element.name] = {}; + } + previous.originalMessage = this.settings.messages[element.name].remote; + this.settings.messages[element.name].remote = previous.message; + + param = typeof param === "string" && {url:param} || param; + + if ( previous.old === value ) { + return previous.valid; + } + + previous.old = value; + var validator = this; + this.startRequest(element); + var data = {}; + data[element.name] = value; + $.ajax($.extend(true, { + url: param, + mode: "abort", + port: "validate" + element.name, + dataType: "json", + data: data, + success: function( response ) { + validator.settings.messages[element.name].remote = previous.originalMessage; + var valid = response === true || response === "true"; + if ( valid ) { + var submitted = validator.formSubmitted; + validator.prepareElement(element); + validator.formSubmitted = submitted; + validator.successList.push(element); + delete validator.invalid[element.name]; + validator.showErrors(); + } else { + var errors = {}; + var message = response || validator.defaultMessage( element, "remote" ); + errors[element.name] = previous.message = $.isFunction(message) ? message(value) : message; + validator.invalid[element.name] = true; + validator.showErrors(errors); + } + previous.valid = valid; + validator.stopRequest(element, valid); + } + }, param)); + return "pending"; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/minlength + minlength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || length >= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/maxlength + maxlength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || length <= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/rangelength + rangelength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || ( length >= param[0] && length <= param[1] ); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/min + min: function( value, element, param ) { + return this.optional(element) || value >= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/max + max: function( value, element, param ) { + return this.optional(element) || value <= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/range + range: function( value, element, param ) { + return this.optional(element) || ( value >= param[0] && value <= param[1] ); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/email + email: function( value, element ) { + // contributed by Scott Gonzalez: http://projects.scottsplayground.com/email_address_validation/ + return this.optional(element) || /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/url + url: function( value, element ) { + // contributed by Scott Gonzalez: http://projects.scottsplayground.com/iri/ + return this.optional(element) || /^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/date + date: function( value, element ) { + return this.optional(element) || !/Invalid|NaN/.test(new Date(value).toString()); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/dateISO + dateISO: function( value, element ) { + return this.optional(element) || /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/number + number: function( value, element ) { + return this.optional(element) || /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/digits + digits: function( value, element ) { + return this.optional(element) || /^\d+$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/creditcard + // based on http://en.wikipedia.org/wiki/Luhn + creditcard: function( value, element ) { + if ( this.optional(element) ) { + return "dependency-mismatch"; + } + // accept only spaces, digits and dashes + if ( /[^0-9 \-]+/.test(value) ) { + return false; + } + var nCheck = 0, + nDigit = 0, + bEven = false; + + value = value.replace(/\D/g, ""); + + for (var n = value.length - 1; n >= 0; n--) { + var cDigit = value.charAt(n); + nDigit = parseInt(cDigit, 10); + if ( bEven ) { + if ( (nDigit *= 2) > 9 ) { + nDigit -= 9; + } + } + nCheck += nDigit; + bEven = !bEven; + } + + return (nCheck % 10) === 0; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/equalTo + equalTo: function( value, element, param ) { + // bind to the blur event of the target in order to revalidate whenever the target field is updated + // TODO find a way to bind the event just once, avoiding the unbind-rebind overhead + var target = $(param); + if ( this.settings.onfocusout ) { + target.unbind(".validate-equalTo").bind("blur.validate-equalTo", function() { + $(element).valid(); + }); + } + return value === target.val(); + } + + } + +}); + +// deprecated, use $.validator.format instead +$.format = $.validator.format; + +}(jQuery)); + +// ajax mode: abort +// usage: $.ajax({ mode: "abort"[, port: "uniqueport"]}); +// if mode:"abort" is used, the previous request on that port (port can be undefined) is aborted via XMLHttpRequest.abort() +(function($) { + var pendingRequests = {}; + // Use a prefilter if available (1.5+) + if ( $.ajaxPrefilter ) { + $.ajaxPrefilter(function( settings, _, xhr ) { + var port = settings.port; + if ( settings.mode === "abort" ) { + if ( pendingRequests[port] ) { + pendingRequests[port].abort(); + } + pendingRequests[port] = xhr; + } + }); + } else { + // Proxy ajax + var ajax = $.ajax; + $.ajax = function( settings ) { + var mode = ( "mode" in settings ? settings : $.ajaxSettings ).mode, + port = ( "port" in settings ? settings : $.ajaxSettings ).port; + if ( mode === "abort" ) { + if ( pendingRequests[port] ) { + pendingRequests[port].abort(); + } + return (pendingRequests[port] = ajax.apply(this, arguments)); + } + return ajax.apply(this, arguments); + }; + } +}(jQuery)); + +// provides delegate(type: String, delegate: Selector, handler: Callback) plugin for easier event delegation +// handler is only called when $(event.target).is(delegate), in the scope of the jquery-object for event.target +(function($) { + $.extend($.fn, { + validateDelegate: function( delegate, type, handler ) { + return this.bind(type, function( event ) { + var target = $(event.target); + if ( target.is(delegate) ) { + return handler.apply(target, arguments); + } + }); + } + }); +}(jQuery)); diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/mainHandler b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/mainHandler new file mode 100644 index 000000000..28199109a --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/mainHandler @@ -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. + */ + +object_maker.init(config_api.config_3dobject_holder, $("#objectHolder").width(), $("#objectHolder").width()/1.5); +object_maker.animate(); +var flight_dynamics = new flight_dynamics(); +$("#window_size").slider({ + range: "min", + value: 37, + min: 10, + max: 300, + slide: function (event, ui) { + $("#window_size_current_value").html($("#window_size").slider("value")); + + } +}); +$("#window_update").slider({ + range: "min", + value: 234, + min: 100, + max: 1000, + slide: function (event, ui) { + $("#window_update_value").html($("#window_update").slider("value")); + } +}); +$("#replotting").click(function () { + plotting.finishPlotting(function (status) { + if (status) { + plotting.initPlotting(function (status) { + d3.select("#realtimechart").select("svg").remove(); + plotting.realtime_plotting("#realtimechart", "#range_min", "#range_max", "#window_update_value", + 600, $("#realtimechart").height(), "#window_size_current_value", + '#plotting_attribute'); + }); + } else { + $("#realtimechart").html("There is no data to plot"); + } + }); +}); +$('.btn-minimize').click(function (e) { + e.preventDefault(); + var $target = $(this).parent().parent().next('.box-content'); + if ($target.is(':visible')) { + if ($(this).parent().attr('id') === "RealtimePlotting") { + plotting.forceToRedraw(function (status) { + d3.select("#realtimechart").select("svg").remove(); + plotting.realtime_plotting("#realtimechart", "#range_min", "#range_max", "#window_update_value", + 600, $("#realtimechart").height(), "#window_size_current_value", + '#plotting_attribute'); + }); + } + } else { + } +}); +$('#connectionOpen').on('click', function () { + $('#connectionOpen').toggleClass('active'); +}); +$("#xmppConnectionOpen").on('click', function () { + $('#xmppConnectionOpen').toggleClass('active'); + if ($('#xmppConnectionOpen').html() === "Start") { + sendMessage("Start the process", function(state){ + console.log("sending message to server..."+ state); + if(state<2){ + $('#xmppConnectionOpen').html("Stop"); + }else{ + $('#xmppConnectionOpen').html("Start"); + } + }); + + } else if ($('#xmppConnectionOpen').html() === "Stop") { + closeSocket(function(state){ + console.log("closing WebSocket..."+ state); + if(state<2){ + $('#xmppConnectionOpen').html("Stop"); + }else{ + $('#xmppConnectionOpen').html("Start"); + } + }); + $("#connectionOpen").html("Connect to XMPP Server").removeClass("btn btn-info").addClass("btn btn-primary"); + } +}); + +$('.btn-minimize').parent().parent().next('.box-content').hide(); +var webSocket; +config_api.realtime_plotting_data_window["attitude"] = new Queue(); +var current_status = {}; +function openSocket(wssAddress) { + if (webSocket !== undefined && webSocket.readyState == 1) { + writeResponse("WebSocket is already opened."); + } else { + webSocket = new WebSocket(wssAddress+config_api.web_socket_endpoint); + } + webSocket.onopen = function (event) { + if (event === undefined) { + writeResponse("WebSocket cant open " + event); + $("#connectionOpen").html("Connect to server").removeClass("btn btn-info").addClass("btn btn-primary"); + } else { + + if (event["isTrusted"] == true) { + $("#connectionOpen").html("Connected").removeClass("btn btn-primary").addClass("btn btn-info"); + } + writeResponse(JSON.stringify(event)); + + } + }; + webSocket.onmessage = function (event) { + var sender_message = event.data; + sender_message = isJSON(sender_message); + if (sender_message != null) { + flight_dynamics.processingMessage(sender_message); + } else { + writeResponse("Message has been corrupted."); + } + }; +} + +function sendMessage(message, callback) { + if(webSocket.readyState<2){ + webSocket.send(message); + } + callback(webSocket.readyState); +} + +function closeSocket(callback) { + if(webSocket.readyState<2){ + webSocket.close(); + } + callback(webSocket.readyState); +} + +function writeResponse(text) { + console.log(text); +} + +window.onbeforeunload = function () { + webSocket.close(); +}; + diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/modules/controller.js b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/modules/controller.js new file mode 100644 index 000000000..5381923a2 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/modules/controller.js @@ -0,0 +1,29 @@ +/* + * 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. + */ +$("#module_control button").click(function (index) { + console.log("Asking Server to send the " + $(this).attr('id') + " command to Ar Drone"); + var url = config_api.drone_control; + ajax_handler.ajaxRequest(url, config_api.drone_controlType, {action: $(this).attr('id'), speed: 7, duration: 7}, + config_api.drone_controlDataType, function (data, status) { + console.log(JSON.stringify(data)); + } + ); +}); + + + diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/modules/flight_dynamics.js b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/modules/flight_dynamics.js new file mode 100644 index 000000000..64814b6ab --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/modules/flight_dynamics.js @@ -0,0 +1,77 @@ +/* + * 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. + */ + +var flight_dynamics = function () { + var api = this; + api.processingMessage = function (sender_message) { + if(sender_message.battery_level!= undefined){ + $("#battery_level_holder").width( parseInt(sender_message.battery_level)+"%" ); + $("#battery_level").html(sender_message.battery_level+"%"); + } + if (sender_message.quatanium_val != undefined) { + current_status = object_maker.get_heading_attitude_bank(sender_message.quatanium_val); + object_maker.set_heading_attitude_bank(current_status); + $("#imageTop").animate({rotate: '' + (180 / Math.PI) * 2.890456 + 'deg'}, 2); + } + if (config_api.modules_status.angleOfRotation_2 || config_api.modules_status.angleOfRotation_1) { + console.log(JSON.stringify(current_status)); + object_maker.set_bank("#imageTop", current_status.bank); + object_maker.set_heading("#imageBackSecond", current_status.heading); + + } + if (config_api.modules_status.realtimePlotting) { + if (current_status[$('#plotting_attribute').val()] != undefined) { + plotting.pushData(current_status[$('#plotting_attribute').val()]); + } + } + if (sender_message.basicParam != undefined) { + if (sender_message.basicParam.velocity != undefined) { + var velocity = sender_message.basicParam.velocity; + if (velocity.length == 3) { + $("#velocityx").html(velocity[0]); + $("#velocityy").html(velocity[1]); + $("#velocityz").html(velocity[2]); + } + } else { + $("#velocityx").html(NaN); + $("#velocityy").html(NaN); + $("#velocityz").html(NaN); + } + if (sender_message.basicParam.global_location != undefined) { + var location = sender_message.basicParam.global_location; + if (location.length == 3) { + $("#locationLog").html(location[0]); + $("#locationAlt").html(location[1]); + $("#locationLat").html(location[2]); + } + } else { + $("#locationLog").html(NaN); + $("#locationAlt").html(NaN); + $("#locationLat").html(NaN); + } + } + if (sender_message.battery_voltage != undefined) { + $("#battery_voltage").html(sender_message.battery_voltage); + } else { + $("#battery_voltage").html(NaN); + } + } + +}; + + diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/modules/realtime_plotting b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/modules/realtime_plotting new file mode 100644 index 000000000..064ff2e10 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/public/js/modules/realtime_plotting @@ -0,0 +1,148 @@ +/* + * 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. + */ +var plotting = function () { + var api = this; + api.isDone = false; + api.current_value = 0; + api.finishPlotting = function (callBack) { + api.isDone = true; + callBack(true); + }, + api.initPlotting = function (callback) { + api.isDone = false; + callback(true); + }, + api.forceToRedraw = function (callback) { + api.isDone == true; + callback(true); + }, + api.pushData = function (new_value) { + console.log(new_value); + api.current_value = new_value; + }, + api.realtime_plotting = function (holder, y_min_hollder, y_max_holder, update_interval_holder, holder_width, + holder_height, window_size_holder, title) { + $(holder).html(); + var init_window = function () { + return 0; + } + api.data = d3.range(parseInt($(window_size_holder).html())).map(init_window); + var margin = {top: 20, right: 20, bottom: 20, left: 40}, + width = holder_width - margin.left - margin.right, + height = holder_height - margin.top - margin.bottom; + var x = d3.scale.linear() + .domain([1, parseInt($(window_size_holder).html()) - 2]) + .range([0, width]); + + var y = d3.scale.linear() + .domain([parseInt($(y_min_hollder).val()), parseInt($(y_max_holder).val())]) + .range([height, 0]); + var line = d3.svg.line() + .interpolate("basis") + .x(function (d, i) { + return x(i); + }) + .y(function (d, i) { + return y(d); + }); + + var svg = d3.select(holder).append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + svg.append("defs").append("clipPath") + .attr("id", "clip") + .append("rect") + .attr("width", width) + .attr("height", height); + var axis_x = svg.append("g") + .attr("class", "x_axis") + .attr("transform", "translate(0," + y(0) + ")") + .call(d3.svg.axis().scale(x).orient("bottom")); + var axis_y = svg.append("g") + .attr("class", "y_axis") + .call(d3.svg.axis().scale(y).orient("left")); + var path = svg.append("g") + .attr("clip-path", "url(#clip)") + .append("path") + .datum(api.data) + .attr("class", "line") + .attr("d", line); + svg.append("text") + .attr("class", "yaxis_label") + .attr("transform", "rotate(-90)") + .attr("y", 0 - margin.left - 4) + .attr("x", (0 - (height / 2))) + .attr("dy", "1em") + .style("text-anchor", "middle") + .text($(title).val()); + svg.append("text") + .attr("class", "xaixs_label") + .attr("transform", + "translate(" + (width / 2) + " ," + + (height + margin.bottom) + ")") + .style("text-anchor", "middle") + .text("Window Size"); + svg.append("text") + .attr("class", "title_label") + .attr("x", (width / 2)) + .attr("y", 0 - (margin.top / 4)) + .attr("text-anchor", "middle") + .style("font-size", "16px") + .style("text-decoration", "underline") + .text($(title).val() + " variation within last " + $(window_size_holder).html() + " frames"); + + updateAgain(); + + function updateAgain() { + if (api.isDone)return; + api.data.push(api.current_value); + path + .attr("d", line) + .attr("transform", null) + .transition() + .duration($(update_interval_holder).html()) + .ease("linear") + .attr("transform", "translate(" + x(0) + ",0)") + .each("end", updateAgain); + api.data.shift(); + } + + function rescale() { + y.domain([parseInt($(y_min_hollder).val()), parseInt($(y_max_holder).val())]) + + svg.select(".title_label") + .text($(title).val() + " variation within last " + $(window_size_holder).html() + " frames"); + svg.select(".yaxis_label") + .text($(title).val()); + + } + + function rescale_x() { + x.domain([1, parseInt($(window_size_holder).html()) - 2]).range([0, width]) + svg.select(".x_axis").transition().call(axis_x); + } + + $("#plotting_attribute").change(function () { + rescale(); + }); + + } +} + diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/statistics.hbs b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/statistics.hbs new file mode 100644 index 000000000..2f004fd74 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/statistics.hbs @@ -0,0 +1,250 @@ +{{#zone "topCss"}} + {{css "css/main-app.css" }} +{{/zone}} +

      +
      +
      +
      + +
      +
      + +
      +
      +

      Battery Level

      +
      +
      +
      +
      +

      0%

      +
      +
      +
      +
      +
      +
      + +
      +
      +
      +
      +
      +

      Angle of Rotation

      + +
      + +
      +
      +
      +
      + +
      +
      +
      +
      +
      +
      +
      + +
      + +
      +
      +
      +
      + + + +
      +
      +
      +
      +
      +
      +
      + +
      + +
      +
      +
      +
      + + + +
      +
      +
      +
      + +
      +
      +
      +

      Live Video Stream

      + +
      + +
      +
      +
      + video stream +
      +
      +
      +
      +
      + +
      +
      +
      +
      +
      +

      Sensor Readings

      +
      + +
      +
      +
      +
      +

      Location
      latitude:
      + longitude:
      + altitudes:

      +

      Velocity:
      x :
      + y :
      + z :

      +

      Battery Voltage:

      +
      +
      +
      +
      +
      +
      +
      +

      Realtime Plotting

      + +
      + +
      +
      +
      +
      +
      +
      + Window size : +

      + Update period : +
      +

      +
      Y-axis: +
      +

      +
      + +
      +
      +
      +
      +
      +
      +
      +
      +
      +{{#zone "bottomJs"}} + {{js "/js/d3.min.js" }} + {{js "/js/3dobject_controller/three.min.js" }} + {{js "/js/3dobject_controller/Coordinates.js" }} + {{js "/js/3dobject_controller/OrbitAndPanControls.js" }} + {{js "/js/3dobject_controller/3dObjectControler.js" }} + {{js "/js/jQueryRotate.js" }} + {{js "/js/config/config.js" }} + {{js "/js/common/ajax_handler.js" }} + {{js "/js/modules/realtime_plotting" }} + {{js "/js/initJs" }} + {{js "/js/common/general_handler" }} + {{js "/js/common/websocket_api" }} + {{js "/js/modules/controller.js" }} + {{js "/js/modules/flight_dynamics.js" }} + {{js "/js/mainHandler" }} +{{/zone}} + diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/statistics.js b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/statistics.js new file mode 100644 index 000000000..550eddedc --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/statistics.js @@ -0,0 +1,25 @@ +/* + * 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. + */ +function onRequest (context) { + var log = new Log("statistics.js"); + var serverAddress = require("/app/modules/serverAddress.js").serverAddress; + var wssAddress = serverAddress.getWSSAddress(); + var httpsAddress = serverAddress.getHPPSTSAddress(); + var device = context.unit.params.device; + return { "device": device, "wssAddress": wssAddress, "httpsAddress": httpsAddress}; +} \ No newline at end of file diff --git a/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/statistics.json b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/statistics.json new file mode 100644 index 000000000..688e93980 --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/org.wso2.carbon.device.mgt.iot.droneanalyzer.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.droneanalyzer.statistics/statistics.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0" +} \ No newline at end of file diff --git a/components/iot-plugins/drone-analyzer-plugin/pom.xml b/components/iot-plugins/drone-analyzer-plugin/pom.xml new file mode 100644 index 000000000..4260130be --- /dev/null +++ b/components/iot-plugins/drone-analyzer-plugin/pom.xml @@ -0,0 +1,61 @@ + + + + + + + org.wso2.carbon.devicemgt-plugins + iot-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + drone-analyzer-plugin + pom + WSO2 Carbon - Arduino Plugin + http://wso2.org + + + org.wso2.carbon.device.mgt.iot.droneanalyzer.ui + org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api + org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api + org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin + + + + + + + org.apache.felix + maven-scr-plugin + 1.7.2 + + + generate-scr-scrdescriptor + + scr + + + + + + + + diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/pom.xml b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/pom.xml new file mode 100644 index 000000000..b81253fe7 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/pom.xml @@ -0,0 +1,139 @@ + + + + + + iot-base-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.api + war + WSO2 Carbon - IoT Server API - Common APIs + WSO2 Carbon - IoT Server API for common device functionality + http://wso2.org + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.core + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.policy.mgt.core + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.policy.mgt.common + provided + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + provided + + + org.apache.cxf + cxf-rt-frontend-jaxrs + provided + + + org.apache.cxf + cxf-rt-transports-http + provided + + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot + provided + + + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-jaxrs + + + javax + javaee-web-api + provided + + + javax.ws.rs + jsr311-api + provided + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.analytics + provided + + + + org.wso2.carbon.analytics + org.wso2.carbon.analytics.datasource.commons + provided + + + + + + + + maven-compiler-plugin + + UTF-8 + ${wso2.maven.compiler.source} + ${wso2.maven.compiler.target} + + + + maven-war-plugin + + common + + + + + + + + diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/log4j.properties b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/log4j.properties new file mode 100755 index 000000000..c271582fa --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/log4j.properties @@ -0,0 +1,8 @@ +# Root logger option +log4j.rootLogger=INFO, stdout + +# Direct log messages to stdout +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%-5p %d{yyyy-MM-dd HH:mm:ss} %c{1}:%L - %m%n \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/DeviceUsageDTO.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/DeviceUsageDTO.java new file mode 100644 index 000000000..222f00d41 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/DeviceUsageDTO.java @@ -0,0 +1,42 @@ +/* + * 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.device.mgt.iot.api; + +public class DeviceUsageDTO { + + private String time; + private String value; + + public String getTime() { + return time; + } + + public void setTime(String time) { + this.time = time; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/DevicesManagerService.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/DevicesManagerService.java new file mode 100644 index 000000000..58841062b --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/DevicesManagerService.java @@ -0,0 +1,641 @@ +/* + * 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.device.mgt.iot.api; + +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.device.mgt.common.*; +import org.wso2.carbon.device.mgt.common.configuration.mgt.TenantConfiguration; +import org.wso2.carbon.device.mgt.common.license.mgt.License; +import org.wso2.carbon.device.mgt.core.dto.DeviceType; +import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; +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.jws.WebService; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +@WebService public class DevicesManagerService { + + private static Log log = LogFactory.getLog(DevicesManagerService.class); + + @Context //injected response proxy supporting multiple thread + private HttpServletResponse response; + + private PrivilegedCarbonContext ctx; + + private DeviceManagementProviderService getServiceProvider() { + String tenantDomain = CarbonContext.getThreadLocalCarbonContext().getTenantDomain(); + PrivilegedCarbonContext.startTenantFlow(); + ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + ctx.setTenantDomain(tenantDomain, true); + if (log.isDebugEnabled()) { + log.debug("Getting thread local carbon context for tenant domain: " + tenantDomain); + } + return (DeviceManagementProviderService) ctx.getOSGiService(DeviceManagementProviderService.class, null); + } + + private void endTenantFlow() { + PrivilegedCarbonContext.endTenantFlow(); + ctx = null; + if (log.isDebugEnabled()) { + log.debug("Tenant flow ended"); + } + } + + private Device[] getActiveDevices(List devices){ + List activeDevices = new ArrayList<>(); + if (devices != null) { + for (Device device : devices) { + if (device.getEnrolmentInfo().getStatus().equals(EnrolmentInfo.Status.ACTIVE)) { + activeDevices.add(device); + } + } + } + return activeDevices.toArray(new Device[activeDevices.size()]); + } + + @Path("/device/user/{username}/all") + @GET + @Consumes("application/json") + @Produces("application/json") + public Device[] getDevicesOfUser(@PathParam("username") String username) { + try { + List devices = this.getServiceProvider().getDevicesOfUser(username); + return this.getActiveDevices(devices); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/user/{username}/ungrouped") + @GET + @Consumes("application/json") + @Produces("application/json") + public Device[] getUnGroupedDevices(@PathParam("username") String username){ + try{ + List devices = this.getServiceProvider().getUnGroupedDevices(username); + return this.getActiveDevices(devices); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/user/{username}/all/count") + @GET + @Consumes("application/json") + @Produces("application/json") + public int getDeviceCount(@PathParam("username") String username){ + try { + List devices = this.getServiceProvider().getDevicesOfUser(username); + if (devices != null) { + List activeDevices = new ArrayList<>(); + for (Device device : devices) { + if (device.getEnrolmentInfo().getStatus().equals(EnrolmentInfo.Status.ACTIVE)) { + activeDevices.add(device); + } + } + return activeDevices.size(); + } + return 0; + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return 0; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/type/{type}/identifier/{identifier}") + @GET + @Consumes("application/json") + @Produces("application/json") + public Device getDevice(@PathParam("type") String type, @PathParam("identifier") String identifier){ + + try{ + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(identifier); + deviceIdentifier.setType(type); + return this.getServiceProvider().getDevice(deviceIdentifier); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/type/all") + @GET + @Consumes("application/json") + @Produces("application/json") + public DeviceType[] getDeviceTypes(){ + try{ + List deviceTypes = this.getServiceProvider().getAvailableDeviceTypes(); + return deviceTypes.toArray(new DeviceType[deviceTypes.size()]); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/type/{type}/all") + @GET + @Consumes("application/json") + @Produces("application/json") + public Device[] getAllDevices(@PathParam("type") String type){ + try{ + List devices = this.getServiceProvider().getAllDevices(type); + return this.getActiveDevices(devices); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/all") + @GET + @Consumes("application/json") + @Produces("application/json") + public Device[] getAllDevices(){ + try{ + List devices = this.getServiceProvider().getAllDevices(); + return this.getActiveDevices(devices); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/enrollment/invitation") + @POST + @Consumes("application/json") + @Produces("application/json") + public void sendEnrolmentInvitation(@FormParam("messageBody") String messageBody, + @FormParam("mailTo") String[] mailTo, @FormParam("ccList") String[] ccList, + @FormParam("bccList") String[] bccList, @FormParam("subject") String subject, + @FormParam("firstName") String firstName, @FormParam("enrolmentUrl") String enrolmentUrl, + @FormParam("title") String title, @FormParam("password") String password, + @FormParam("userName") String userName){ + EmailMessageProperties config = new EmailMessageProperties(); + config.setMessageBody(messageBody); + config.setMailTo(mailTo); + config.setCcList(ccList); + config.setBccList(bccList); + config.setSubject(subject); + config.setFirstName(firstName); + config.setEnrolmentUrl(enrolmentUrl); + config.setTitle(title); + config.setUserName(userName); + config.setPassword(password); + try { + this.getServiceProvider().sendEnrolmentInvitation(config); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/registration/invitation") + @POST + @Consumes("application/json") + @Produces("application/json") + public void sendRegistrationEmail(@FormParam("messageBody") String messageBody, + @FormParam("mailTo") String[] mailTo, @FormParam("ccList") String[] ccList, + @FormParam("bccList") String[] bccList, @FormParam("subject") String subject, + @FormParam("firstName") String firstName, @FormParam("enrolmentUrl") String enrolmentUrl, + @FormParam("title") String title, @FormParam("password") String password, + @FormParam("userName") String userName){ + EmailMessageProperties config = new EmailMessageProperties(); + config.setMessageBody(messageBody); + config.setMailTo(mailTo); + config.setCcList(ccList); + config.setBccList(bccList); + config.setSubject(subject); + config.setFirstName(firstName); + config.setEnrolmentUrl(enrolmentUrl); + config.setTitle(title); + config.setUserName(userName); + config.setPassword(password); + try { + this.getServiceProvider().sendRegistrationEmail(config); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/type/{type}/config") + @GET + @Consumes("application/json") + @Produces("application/json") + public TenantConfiguration getConfiguration(@PathParam("type") String type){ + try { + return this.getServiceProvider().getConfiguration(type); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/role/{role}/all") + @GET + @Consumes("application/json") + @Produces("application/json") + public Device[] getAllDevicesOfRole(@PathParam("role") String roleName){ + try{ + List devices = this.getServiceProvider().getAllDevicesOfRole(roleName); + return this.getActiveDevices(devices); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/name/{name}/all") + @GET + @Consumes("application/json") + @Produces("application/json") + public Device[] getDevicesByName(@PathParam("name") String name) { + try{ + List devices = this.getServiceProvider().getDevicesByName(name); + return this.getActiveDevices(devices); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/type/{type}/identifier/{identifier}/status") + @PUT + @Consumes("application/json") + @Produces("application/json") + void updateDeviceEnrolmentInfo(@PathParam("type") String type, @PathParam("identifier") String identifier, + @FormParam("status") EnrolmentInfo.Status status) { + DeviceManagementProviderService providerService = this.getServiceProvider(); + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setType(type); + deviceIdentifier.setId(identifier); + try { + Device device = providerService.getDevice(deviceIdentifier); + providerService.updateDeviceEnrolmentInfo(device, status); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/status/{status}/all") + @GET + @Consumes("application/json") + @Produces("application/json") + public Device[] getDevicesByStatus(@PathParam("status") EnrolmentInfo.Status status) { + try{ + List devices = this.getServiceProvider().getDevicesByStatus(status); + return this.getActiveDevices(devices); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/type/{type}/license") + @GET + @Consumes("application/json") + @Produces("application/json") + public License getLicense(@PathParam("type") String type, @QueryParam("languageCode") String languageCode) { + try{ + return this.getServiceProvider().getLicense(type, languageCode); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/type/{type}/license") + @POST + @Consumes("application/json") + @Produces("application/json") + public void addLicense(@PathParam("type") String type, @FormParam("provider") String provider, + @FormParam("name") String name, @FormParam("version") String version, + @FormParam("language") String language, @FormParam("validFrom") Date validFrom, + @FormParam("validTo") Date validTo, @FormParam("text") String text) { + try{ + License license = new License(); + license.setProvider(provider); + license.setName(name); + license.setVersion(version); + license.setLanguage(language); + license.setValidFrom(validFrom); + license.setValidTo(validTo); + license.setText(text); + this.getServiceProvider().addLicense(type, license); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/type/{type}/identifier/{identifier}") + @PUT + @Consumes("application/json") + @Produces("application/json") + boolean modifyEnrollment(@PathParam("type") String type, @PathParam("identifier") String identifier, + @FormParam("name") String name, @FormParam("description") String description, + @FormParam("groupId") int groupId, @FormParam("enrollmentId") int enrollmentId, + @FormParam("dateOfEnrolment") long dateOfEnrolment, @FormParam("dateOfLastUpdate") long dateOfLastUpdate, + @FormParam("ownership") EnrolmentInfo.OwnerShip ownership, @FormParam("status") EnrolmentInfo.Status status, + @FormParam("owner") String owner){ + + EnrolmentInfo enrolmentInfo = new EnrolmentInfo(); + enrolmentInfo.setId(enrollmentId); + enrolmentInfo.setDateOfEnrolment(dateOfEnrolment); + enrolmentInfo.setDateOfLastUpdate(dateOfLastUpdate); + enrolmentInfo.setOwnership(ownership); + enrolmentInfo.setStatus(status); + enrolmentInfo.setOwner(owner); + + Device device = new Device(); + device.setType(type); + device.setDeviceIdentifier(identifier); + device.setName(name); + device.setDescription(description); + device.setGroupId(groupId); + device.setEnrolmentInfo(enrolmentInfo); + try { + return this.getServiceProvider().modifyEnrollment(device); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device") + @POST + @Consumes("application/json") + @Produces("application/json") + boolean enrollDevice(@FormParam("type") String type, @FormParam("identifier") String identifier, + @FormParam("name") String name, @FormParam("description") String description, + @FormParam("groupId") int groupId, @FormParam("enrollmentId") int enrollmentId, + @FormParam("dateOfEnrolment") long dateOfEnrolment, @FormParam("dateOfLastUpdate") long dateOfLastUpdate, + @FormParam("ownership") EnrolmentInfo.OwnerShip ownership, @FormParam("status") EnrolmentInfo.Status status, + @FormParam("owner") String owner){ + + EnrolmentInfo enrolmentInfo = new EnrolmentInfo(); + enrolmentInfo.setId(enrollmentId); + enrolmentInfo.setDateOfEnrolment(dateOfEnrolment); + enrolmentInfo.setDateOfLastUpdate(dateOfLastUpdate); + enrolmentInfo.setOwnership(ownership); + enrolmentInfo.setStatus(status); + enrolmentInfo.setOwner(owner); + + Device device = new Device(); + device.setType(type); + device.setDeviceIdentifier(identifier); + device.setName(name); + device.setDescription(description); + device.setGroupId(groupId); + device.setEnrolmentInfo(enrolmentInfo); + try { + return this.getServiceProvider().enrollDevice(device); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/tenantconfiguration") + @GET + @Consumes("application/json") + @Produces("application/json") + public TenantConfiguration getConfiguration(){ + try { + return this.getServiceProvider().getConfiguration(); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/tenantconfiguration") + @POST + @Consumes("application/json") + @Produces("application/json") + public boolean saveConfiguration(@FormParam("tenantConfiguration") TenantConfiguration tenantConfiguration){ + try { + return this.getServiceProvider().saveConfiguration(tenantConfiguration); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/type/{type}/identifier/{identifier}") + @DELETE + @Consumes("application/json") + @Produces("application/json") + public boolean disenrollDevice(@PathParam("type") String type, @PathParam("identifier") String identifier){ + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setType(type); + deviceIdentifier.setId(identifier); + try { + return this.getServiceProvider().disenrollDevice(deviceIdentifier); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/type/{type}/identifier/{identifier}/enrolled") + @GET + @Consumes("application/json") + @Produces("application/json") + public boolean isEnrolled(@PathParam("type") String type, @PathParam("identifier") String identifier){ + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setType(type); + deviceIdentifier.setId(identifier); + try { + return this.getServiceProvider().isEnrolled(deviceIdentifier); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + @Path("/device/type/{type}/identifier/{identifier}/active") + @GET + @Consumes("application/json") + @Produces("application/json") + public boolean isActive(@PathParam("type") String type, @PathParam("identifier") String identifier){ + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setType(type); + deviceIdentifier.setId(identifier); + try { + return this.getServiceProvider().isActive(deviceIdentifier); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/type/{type}/identifier/{identifier}/active") + @PUT + @Consumes("application/json") + @Produces("application/json") + public boolean setActive(@PathParam("type") String type, @PathParam("identifier") String identifier, + @FormParam("status") boolean status){ + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setType(type); + deviceIdentifier.setId(identifier); + try { + return this.getServiceProvider().setActive(deviceIdentifier, status); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/type/{type}/identifier/{identifier}/ownership") + @PUT + @Consumes("application/json") + @Produces("application/json") + public boolean setOwnership(@PathParam("type") String type, @PathParam("identifier") String identifier, + @FormParam("ownership") String ownership){ + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setType(type); + deviceIdentifier.setId(identifier); + try { + return this.getServiceProvider().setOwnership(deviceIdentifier, ownership); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/type/{type}/identifier/{identifier}/status") + @PUT + @Consumes("application/json") + @Produces("application/json") + public boolean setStatus(@PathParam("type") String type, @PathParam("identifier") String identifier, + @FormParam("owner") String owner, @FormParam("status") EnrolmentInfo.Status status){ + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setType(type); + deviceIdentifier.setId(identifier); + try { + return this.getServiceProvider().setStatus(deviceIdentifier, owner, status); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/type/{type}/identifier/{identifier}/sensor/{sensorName}") + @POST + @Consumes("application/json") + @Produces("application/json") + public boolean setSensorValue(@PathParam("type") String type, @PathParam("identifier") String deviceId, + @PathParam("sensorName") String sensorName, + @HeaderParam("sensorValue") String sensorValue){ + + try { + return SensorDataManager.getInstance().setSensorRecord(deviceId, sensorName, sensorValue, Calendar + .getInstance().getTimeInMillis()); + } finally { + this.endTenantFlow(); + } + } + + @Path("/device/type/{type}/identifier/{identifier}/sensor/{sensorName}") + @GET + @Consumes("application/json") + @Produces("application/json") + public SensorRecord getSensorValue(@PathParam("type") String type, @PathParam("identifier") String deviceId, + @PathParam("sensorName") String sensorName, @HeaderParam("defaultValue") String defaultValue){ + + try { + return SensorDataManager.getInstance().getSensorRecord(deviceId, sensorName); + } catch (DeviceControllerException e) { + log.error("Error on reading sensor value: " + e.getMessage()); + if(defaultValue != null){ + return new SensorRecord(defaultValue, Calendar.getInstance().getTimeInMillis()); + }else{ + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } + } finally { + this.endTenantFlow(); + } + } + +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/GroupManagerService.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/GroupManagerService.java new file mode 100644 index 000000000..496410386 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/GroupManagerService.java @@ -0,0 +1,584 @@ +/* + * 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.device.mgt.iot.api; + +import org.apache.commons.httpclient.HttpStatus; +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.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.PaginationRequest; +import org.wso2.carbon.device.mgt.common.PaginationResult; +import org.wso2.carbon.device.mgt.group.common.DeviceGroup; +import org.wso2.carbon.device.mgt.group.common.GroupManagementException; +import org.wso2.carbon.device.mgt.group.common.GroupUser; +import org.wso2.carbon.device.mgt.group.core.providers.GroupManagementServiceProvider; +import org.wso2.carbon.device.mgt.iot.util.ResponsePayload; + +import javax.jws.WebService; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import java.util.Date; +import java.util.List; + +@WebService +public class GroupManagerService { + + private static final String DEFAULT_ADMIN_ROLE = "admin"; + private static final String DEFAULT_OPERATOR_ROLE = "invoke-device-operations"; + private static final String DEFAULT_STATS_MONITOR_ROLE = "view-statistics"; + private static final String DEFAULT_VIEW_POLICIES = "view-policies"; + private static final String DEFAULT_MANAGE_POLICIES = "mange-policies"; + private static final String DEFAULT_VIEW_EVENTS = "view-events"; + private static final String[] DEFAULT_ADMIN_PERMISSIONS = {"/permission/device-mgt/admin/groups", + "/permission/device-mgt/user/groups"}; + private static final String[] DEFAULT_OPERATOR_PERMISSIONS = {"/permission/device-mgt/user/groups/device_operation"}; + private static final String[] DEFAULT_STATS_MONITOR_PERMISSIONS = {"/permission/device-mgt/user/groups/device_monitor"}; + private static final String[] DEFAULT_MANAGE_POLICIES_PERMISSIONS = {"/permission/device-mgt/user/groups/device_policies/add"}; + private static final String[] DEFAULT_VIEW_POLICIES_PERMISSIONS = {"/permission/device-mgt/user/groups/device_policies/view"}; + private static final String[] DEFAULT_VIEW_EVENTS_PERMISSIONS = {"/permission/device-mgt/user/groups/device_events"}; + + private static Log log = LogFactory.getLog(GroupManagerService.class); + + @Context //injected response proxy supporting multiple threads + private HttpServletResponse response; + private PrivilegedCarbonContext ctx; + + private GroupManagementServiceProvider getServiceProvider() { + String tenantDomain = CarbonContext.getThreadLocalCarbonContext().getTenantDomain(); + String username = CarbonContext.getThreadLocalCarbonContext().getUsername(); + PrivilegedCarbonContext.startTenantFlow(); + ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + ctx.setTenantDomain(tenantDomain, true); + if (log.isDebugEnabled()) { + log.debug("Getting thread local carbon context for tenant domain: " + tenantDomain); + } + return (GroupManagementServiceProvider) ctx.getOSGiService(GroupManagementServiceProvider.class, null); + } + + private void endTenantFlow() { + PrivilegedCarbonContext.endTenantFlow(); + ctx = null; + if (log.isDebugEnabled()) { + log.debug("Tenant flow ended"); + } + } + + @Path("/group") + @POST + @Consumes("application/json") + @Produces("application/json") + public Response createGroup(@FormParam("name") String name, + @FormParam("username") String username, + @FormParam("description") String description) { + DeviceGroup group = new DeviceGroup(); + group.setName(name); + group.setDescription(description); + group.setOwner(username); + group.setDateOfCreation(new Date().getTime()); + group.setDateOfLastUpdate(new Date().getTime()); + boolean isAdded = false; + try { + GroupManagementServiceProvider groupManagementService = this.getServiceProvider(); + int groupId = groupManagementService.createGroup(group, DEFAULT_ADMIN_ROLE, DEFAULT_ADMIN_PERMISSIONS); + if (groupId == -2) { + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_CONFLICT); + responsePayload.setMessageFromServer("Group name is already exists."); + responsePayload.setResponseContent("CONFLICT"); + return Response.status(HttpStatus.SC_CONFLICT).entity(responsePayload).build(); + } else { + isAdded = (groupId > 0) && groupManagementService.addGroupSharingRole(username, groupId, + DEFAULT_OPERATOR_ROLE, + DEFAULT_OPERATOR_PERMISSIONS); + groupManagementService.addGroupSharingRole(username, groupId, DEFAULT_STATS_MONITOR_ROLE, + DEFAULT_STATS_MONITOR_PERMISSIONS); + groupManagementService.addGroupSharingRole(username, groupId, DEFAULT_VIEW_POLICIES, + DEFAULT_VIEW_POLICIES_PERMISSIONS); + groupManagementService.addGroupSharingRole(username, groupId, DEFAULT_MANAGE_POLICIES, + DEFAULT_MANAGE_POLICIES_PERMISSIONS); + groupManagementService.addGroupSharingRole(username, groupId, DEFAULT_VIEW_EVENTS, + DEFAULT_VIEW_EVENTS_PERMISSIONS); + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_OK); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } + } catch (GroupManagementException e) { + return Response.status(HttpStatus.SC_INTERNAL_SERVER_ERROR).entity(e.getMessage()).build(); + } finally { + this.endTenantFlow(); + } + } + + @Path("/group/id/{groupId}") + @PUT + @Consumes("application/json") + @Produces("application/json") + public boolean updateGroup(@PathParam("groupId") int groupId, @FormParam("name") String name, + @FormParam("username") String username, + @FormParam("description") String description) { + if (!isAuthorized(username, groupId, "/permission/device-mgt/admin/groups/modify")) { + response.setStatus(Response.Status.FORBIDDEN.getStatusCode()); + return false; + } + try { + GroupManagementServiceProvider groupManagementService = this.getServiceProvider(); + DeviceGroup group = groupManagementService.getGroup(groupId); + group.setName(name); + group.setDescription(description); + group.setOwner(username); + group.setDateOfLastUpdate(new Date().getTime()); + response.setStatus(Response.Status.OK.getStatusCode()); + groupManagementService.updateGroup(group); + return true; + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + return false; + } finally { + this.endTenantFlow(); + } + } + + @Path("/group/id/{groupId}") + @DELETE + @Consumes("application/json") + @Produces("application/json") + public boolean deleteGroup(@PathParam("groupId") int groupId, + @QueryParam("username") String username) { + if (!isAuthorized(username, groupId, "/permission/device-mgt/admin/groups/delete")) { + response.setStatus(Response.Status.FORBIDDEN.getStatusCode()); + return false; + } + boolean isDeleted = false; + try { + response.setStatus(Response.Status.OK.getStatusCode()); + isDeleted = this.getServiceProvider().deleteGroup(groupId); + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + } finally { + this.endTenantFlow(); + } + return isDeleted; + } + + @Path("/group/id/{groupId}") + @GET + @Consumes("application/json") + @Produces("application/json") + public DeviceGroup getGroup(@PathParam("groupId") int groupId, + @FormParam("username") String username) { + DeviceGroup deviceGroup = null; + try { + response.setStatus(Response.Status.OK.getStatusCode()); + deviceGroup = this.getServiceProvider().getGroup(groupId); + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + } finally { + this.endTenantFlow(); + } + return deviceGroup; + } + + @Path("/group/name/{groupName}") + @GET + @Consumes("application/json") + @Produces("application/json") + public DeviceGroup[] findGroups(@PathParam("groupName") String groupName, + @FormParam("username") String username) { + DeviceGroup[] deviceGroups = null; + try { + List groups = this.getServiceProvider().findGroups(groupName, username); + deviceGroups = new DeviceGroup[groups.size()]; + response.setStatus(Response.Status.OK.getStatusCode()); + groups.toArray(deviceGroups); + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + } finally { + this.endTenantFlow(); + } + return deviceGroups; + } + + @Path("/group/user/{username}/all") + @GET + @Consumes("application/json") + @Produces("application/json") + public DeviceGroup[] getGroups(@PathParam("username") String username, + @QueryParam("permission") String permission) { + DeviceGroup[] deviceGroups = null; + try { + GroupManagementServiceProvider groupManagementService = this.getServiceProvider(); + List groups; + if (permission != null) { + groups = groupManagementService.getGroups(username, permission); + } else { + groups = groupManagementService.getGroups(username); + } + deviceGroups = new DeviceGroup[groups.size()]; + response.setStatus(Response.Status.OK.getStatusCode()); + groups.toArray(deviceGroups); + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + } finally { + this.endTenantFlow(); + } + return deviceGroups; + } + + @Path("/group/user/{username}/all/count") + @GET + @Consumes("application/json") + @Produces("application/json") + public int getGroupCount(@PathParam("username") String username) { + int count = -1; + try { + response.setStatus(Response.Status.OK.getStatusCode()); + count = this.getServiceProvider().getGroupCount(username); + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + } finally { + this.endTenantFlow(); + } + return count; + } + + @Path("/group/id/{groupId}/share") + @POST + @Consumes("application/json") + @Produces("application/json") + public boolean shareGroup(@FormParam("username") String username, + @FormParam("shareUser") String shareUser, + @PathParam("groupId") int groupId, + @FormParam("role") String sharingRole) { + if (!isAuthorized(username, groupId, "/permission/device-mgt/admin/groups/share")) { + response.setStatus(Response.Status.FORBIDDEN.getStatusCode()); + return false; + } + boolean isShared = false; + try { + response.setStatus(Response.Status.OK.getStatusCode()); + isShared = this.getServiceProvider().shareGroup(shareUser, groupId, sharingRole); + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + } finally { + this.endTenantFlow(); + } + return isShared; + } + + @Path("/group/id/{groupId}/unshare") + @POST + @Consumes("application/json") + @Produces("application/json") + public boolean unShareGroup(@FormParam("username") String username, + @FormParam("unShareUser") String unShareUser, + @PathParam("groupId") int groupId, + @FormParam("role") String sharingRole) { + if (!isAuthorized(username, groupId, "/permission/device-mgt/admin/groups/share")) { + response.setStatus(Response.Status.FORBIDDEN.getStatusCode()); + return false; + } + boolean isUnShared = false; + try { + response.setStatus(Response.Status.OK.getStatusCode()); + isUnShared = this.getServiceProvider().unshareGroup(unShareUser, groupId, sharingRole); + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + } finally { + this.endTenantFlow(); + } + return isUnShared; + } + + @Path("/group/id/{groupId}/role") + @POST + @Consumes("application/json") + @Produces("application/json") + public boolean addSharing(@FormParam("username") String username, + @PathParam("groupId") int groupId, + @FormParam("role") String roleName, + @FormParam("permissions") String[] permissions) { + if (!isAuthorized(username, groupId, "/permission/device-mgt/admin/groups/share")) { + response.setStatus(Response.Status.FORBIDDEN.getStatusCode()); + return false; + } + boolean isAdded = false; + try { + response.setStatus(Response.Status.OK.getStatusCode()); + isAdded = this.getServiceProvider().addGroupSharingRole(username, groupId, roleName, permissions); + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + } finally { + this.endTenantFlow(); + } + return isAdded; + } + + @Path("/group/id/{groupId}/role/{role}") + @DELETE + @Consumes("application/json") + @Produces("application/json") + public boolean removeSharing(@QueryParam("username") String username, + @PathParam("groupId") int groupId, + @PathParam("role") String roleName) { + if (!isAuthorized(username, groupId, "/permission/device-mgt/admin/groups/share")) { + response.setStatus(Response.Status.FORBIDDEN.getStatusCode()); + } + boolean isRemoved = false; + try { + response.setStatus(Response.Status.OK.getStatusCode()); + isRemoved = this.getServiceProvider().removeGroupSharingRole(groupId, roleName); + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + } finally { + this.endTenantFlow(); + } + return isRemoved; + } + + @Path("/group/id/{groupId}/role/all") + @GET + @Consumes("application/json") + @Produces("application/json") + public String[] getRoles(@PathParam("groupId") int groupId) { + String[] rolesArray = null; + try { + List roles = this.getServiceProvider().getRoles(groupId); + rolesArray = new String[roles.size()]; + response.setStatus(Response.Status.OK.getStatusCode()); + roles.toArray(rolesArray); + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + } finally { + this.endTenantFlow(); + } + return rolesArray; + } + + @Path("/group/id/{groupId}/{user}/role/all") + @GET + @Consumes("application/json") + @Produces("application/json") + public String[] getRoles(@PathParam("user") String user, @PathParam("groupId") int groupId) { + String[] rolesArray = null; + try { + List roles = this.getServiceProvider().getRoles(user, groupId); + rolesArray = new String[roles.size()]; + response.setStatus(Response.Status.OK.getStatusCode()); + roles.toArray(rolesArray); + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + } finally { + this.endTenantFlow(); + } + return rolesArray; + } + + @Path("/group/id/{groupId}/user/all") + @GET + @Consumes("application/json") + @Produces("application/json") + public GroupUser[] getUsers(@PathParam("groupId") int groupId) { + GroupUser[] usersArray = null; + try { + List users = this.getServiceProvider().getUsers(groupId); + usersArray = new GroupUser[users.size()]; + response.setStatus(Response.Status.OK.getStatusCode()); + users.toArray(usersArray); + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + } finally { + this.endTenantFlow(); + } + return usersArray; + } + + @Path("/group/id/{groupId}/device/all") + @GET + @Consumes("application/json") + @Produces("application/json") + public Device[] getDevices(@PathParam("groupId") int groupId) { + Device[] deviceArray = null; + try { + List devices = this.getServiceProvider().getDevices(groupId); + deviceArray = new Device[devices.size()]; + response.setStatus(Response.Status.OK.getStatusCode()); + devices.toArray(deviceArray); + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + } finally { + this.endTenantFlow(); + } + return deviceArray; + } + + @Path("/group/id/{groupId}/device/count") + @GET + @Consumes("application/json") + @Produces("application/json") + public int getDeviceCount(@PathParam("groupId") int groupId) { + try { + return this.getServiceProvider().getDeviceCount(groupId); + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + return -1; + } finally { + this.endTenantFlow(); + } + } + + @Path("/group/id/{groupId}/device") + @GET + @Consumes("application/json") + @Produces("application/json") + public PaginationResult getDevices(@PathParam("groupId") int groupId, + @QueryParam("index") int index, + @QueryParam("limit") int limit) { + try { + PaginationRequest request = new PaginationRequest(index, limit); + return this.getServiceProvider().getDevices(groupId, request); + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + return null; + } finally { + this.endTenantFlow(); + } + } + + @Path("/group/id/{groupId}/device/assign") + @PUT + @Consumes("application/json") + @Produces("application/json") + public boolean addDevice(@PathParam("groupId") int groupId, + @FormParam("deviceId") String deviceId, + @FormParam("deviceType") String deviceType, + @FormParam("username") String username) { + if (!isAuthorized(username, groupId, "/permission/device-mgt/admin/groups/add_devices")) { + response.setStatus(Response.Status.FORBIDDEN.getStatusCode()); + return false; + } + boolean isAdded = false; + try { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(deviceId, deviceType); + response.setStatus(Response.Status.OK.getStatusCode()); + isAdded = this.getServiceProvider().addDevice(deviceIdentifier, groupId); + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + } finally { + this.endTenantFlow(); + } + return isAdded; + } + + @Path("/group/id/{groupId}/device/assign") + @DELETE + @Consumes("application/json") + @Produces("application/json") + public boolean removeDevice(@PathParam("groupId") int groupId, + @FormParam("deviceId") String deviceId, + @FormParam("deviceType") String deviceType, + @FormParam("username") String username) { + if (!isAuthorized(username, groupId, "/permission/device-mgt/admin/groups/remove_devices")) { + response.setStatus(Response.Status.FORBIDDEN.getStatusCode()); + return false; + } + boolean isRemoved = false; + try { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(deviceId, deviceType); + response.setStatus(Response.Status.OK.getStatusCode()); + isRemoved = this.getServiceProvider().removeDevice(deviceIdentifier, groupId); + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + } finally { + this.endTenantFlow(); + } + return isRemoved; + } + + @Path("/group/id/{groupId}/user/{username}/permissions") + @GET + @Consumes("application/json") + @Produces("application/json") + public String[] getPermissions(@PathParam("username") String username, + @PathParam("groupId") int groupId) { + String[] permissions = null; + try { + response.setStatus(Response.Status.OK.getStatusCode()); + permissions = this.getServiceProvider().getPermissions(username, groupId); + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + } finally { + this.endTenantFlow(); + } + return permissions; + } + + @Path("/group/id/{groupId}/user/{username}/authorized") + @GET + @Consumes("application/json") + @Produces("application/json") + public boolean isAuthorized(@PathParam("username") String username, + @PathParam("groupId") int groupId, + @QueryParam("permission") String permission) { + boolean isAuthorized = false; + try { + response.setStatus(Response.Status.OK.getStatusCode()); + isAuthorized = this.getServiceProvider().isAuthorized(username, groupId, permission); + } catch (GroupManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.error(e.getErrorMessage(), e); + } finally { + this.endTenantFlow(); + } + return isAuthorized; + } + +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/PolicyManagementService.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/PolicyManagementService.java new file mode 100644 index 000000000..702ea89ed --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/PolicyManagementService.java @@ -0,0 +1,507 @@ +/* + * 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.device.mgt.iot.api; + +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.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.policy.mgt.common.Policy; +import org.wso2.carbon.policy.mgt.common.PolicyAdministratorPoint; +import org.wso2.carbon.policy.mgt.common.PolicyManagementException; +import org.wso2.carbon.policy.mgt.common.PolicyMonitoringTaskException; +import org.wso2.carbon.policy.mgt.common.monitor.ComplianceData; +import org.wso2.carbon.policy.mgt.common.monitor.PolicyComplianceException; +import org.wso2.carbon.policy.mgt.core.PolicyManagerService; +import org.wso2.carbon.policy.mgt.core.task.TaskScheduleService; + +import javax.jws.WebService; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import java.util.List; + +@WebService +public class PolicyManagementService { + + private static Log log = LogFactory.getLog(PolicyManagementService.class); + + @Context //injected response proxy supporting multiple thread + private HttpServletResponse response; + + private PrivilegedCarbonContext ctx; + + private PolicyManagerService getPolicyServiceProvider() throws DeviceManagementException { + String tenantDomain = CarbonContext.getThreadLocalCarbonContext().getTenantDomain(); + PrivilegedCarbonContext.startTenantFlow(); + ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + ctx.setTenantDomain(tenantDomain, true); + + if (log.isDebugEnabled()) { + log.debug("Getting thread local carbon context for tenant domain: " + tenantDomain); + } + + PolicyManagerService policyManagerService = (PolicyManagerService) ctx.getOSGiService( + PolicyManagerService.class, null); + + if (policyManagerService == null) { + String msg = "Policy Management service not initialized"; + log.error(msg); + throw new DeviceManagementException(msg); + } + + return policyManagerService; + } + + private void endTenantFlow() { + PrivilegedCarbonContext.endTenantFlow(); + ctx = null; + if (log.isDebugEnabled()) { + log.debug("Tenant flow ended"); + } + } + + @POST + @Path("/inactive-policy") + @Produces("application/json") + public boolean addPolicy(Policy policy) { + + try { + PolicyManagerService policyManagerService = getPolicyServiceProvider(); + PolicyAdministratorPoint pap = policyManagerService.getPAP(); + pap.addPolicy(policy); + response.setStatus(Response.Status.CREATED.getStatusCode()); + if (log.isDebugEnabled()) { + log.debug("Policy has been added successfully."); + } + return true; + } catch (PolicyManagementException e) { + String error = "Policy Management related exception."; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } catch (DeviceManagementException e) { + String error = "Error occurred while invoking Policy Management Service."; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + + @POST + @Path("/active-policy") + @Produces("application/json") + public boolean addActivePolicy(Policy policy) { + + policy.setActive(true); + + try { + PolicyManagerService policyManagerService = getPolicyServiceProvider(); + PolicyAdministratorPoint pap = policyManagerService.getPAP(); + pap.addPolicy(policy); + response.setStatus(Response.Status.CREATED.getStatusCode()); + if (log.isDebugEnabled()) { + log.debug("Policy has been added successfully."); + } + return true; + } catch (PolicyManagementException e) { + String error = "Policy Management related exception."; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } catch (DeviceManagementException e) { + String error = "Error occurred while invoking Policy Management Service."; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + + @GET + @Produces("application/json") + public Policy[] getAllPolicies() { + try { + PolicyManagerService policyManagerService = getPolicyServiceProvider(); + PolicyAdministratorPoint policyAdministratorPoint = policyManagerService.getPAP(); + List policies = policyAdministratorPoint.getPolicies(); + return policyAdministratorPoint.getPolicies().toArray(new Policy[policies.size()]); + } catch (PolicyManagementException e) { + String error = "Policy Management related exception"; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } catch (DeviceManagementException e) { + String error = "Error occurred while invoking Policy Management Service."; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + this.endTenantFlow(); + } + } + + @GET + @Produces("application/json") + @Path("/{id}") + public Policy getPolicy(@PathParam("id") int policyId) { + try { + PolicyManagerService policyManagerService = getPolicyServiceProvider(); + PolicyAdministratorPoint policyAdministratorPoint = policyManagerService.getPAP(); + Policy policy = policyAdministratorPoint.getPolicy(policyId); + if (policy != null) { + if (log.isDebugEnabled()) { + log.debug("Sending policy for ID " + policyId); + } + return policy; + } else { + log.error("Policy for ID " + policyId + " not found."); + response.setStatus(Response.Status.NOT_FOUND.getStatusCode()); + return null; + } + } catch (PolicyManagementException e) { + String error = "Policy Management related exception"; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } catch (DeviceManagementException e) { + String error = "Error occurred while invoking Policy Management Service."; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + this.endTenantFlow(); + } + } + + @GET + @Path("/count") + public int getPolicyCount() { + try { + PolicyManagerService policyManagerService = getPolicyServiceProvider(); + PolicyAdministratorPoint policyAdministratorPoint = policyManagerService.getPAP(); + return policyAdministratorPoint.getPolicyCount(); + } catch (PolicyManagementException e) { + String error = "Policy Management related exception"; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return -1; + } catch (DeviceManagementException e) { + String error = "Error occurred while invoking Policy Management Service."; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return -1; + } finally { + this.endTenantFlow(); + } + } + + @PUT + @Path("/{id}") + @Produces("application/json") + public boolean updatePolicy(Policy policy, @PathParam("id") int policyId) { + try { + PolicyManagerService policyManagerService = getPolicyServiceProvider(); + PolicyAdministratorPoint pap = policyManagerService.getPAP(); + Policy previousPolicy = pap.getPolicy(policyId); + policy.setProfile(pap.getProfile(previousPolicy.getProfileId())); + policy.setPolicyName(previousPolicy.getPolicyName()); + pap.updatePolicy(policy); + if (log.isDebugEnabled()) { + log.debug("Policy with ID " + policyId + " has been updated successfully."); + } + return true; + } catch (PolicyManagementException e) { + String error = "Policy Management related exception"; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } catch (DeviceManagementException e) { + String error = "Error occurred while invoking Policy Management Service."; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + + @PUT + @Path("/priorities") + @Consumes("application/json") + @Produces("application/json") + public boolean updatePolicyPriorities(List priorityUpdatedPolicies) { + try { + PolicyManagerService policyManagerService = getPolicyServiceProvider(); + PolicyAdministratorPoint pap = policyManagerService.getPAP(); + boolean policiesUpdated = pap.updatePolicyPriorities(priorityUpdatedPolicies); + if (policiesUpdated) { + if (log.isDebugEnabled()) { + log.debug("Policy Priorities successfully updated."); + } + return true; + } else { + if (log.isDebugEnabled()) { + log.debug("Policy priorities did not update. Bad Request."); + } + response.setStatus(Response.Status.BAD_REQUEST.getStatusCode()); + return false; + } + } catch (PolicyManagementException e) { + String error = "Exception in updating policy priorities."; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } catch (DeviceManagementException e) { + String error = "Error occurred while invoking Policy Management Service."; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + + @DELETE + @Path("/{id}") + @Produces("application/json") + public boolean deletePolicy(@PathParam("id") int policyId) { + try { + PolicyManagerService policyManagerService = getPolicyServiceProvider(); + PolicyAdministratorPoint pap = policyManagerService.getPAP(); + Policy policy = pap.getPolicy(policyId); + boolean policyDeleted = pap.deletePolicy(policy); + if (policyDeleted) { + if (log.isDebugEnabled()) { + log.debug("Policy by id:" + policyId + " has been successfully deleted."); + } + return true; + } else { + if (log.isDebugEnabled()) { + log.debug("Policy by id:" + policyId + " does not exist."); + } + response.setStatus(Response.Status.NOT_FOUND.getStatusCode()); + return false; + } + } catch (PolicyManagementException e) { + String error = "Exception in deleting policy by id:" + policyId; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } catch (DeviceManagementException e) { + String error = "Error occurred while invoking Policy Management Service."; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + + @PUT + @Produces("application/json") + @Path("/activate/{id}") + public boolean activatePolicy(@PathParam("id") int policyId) { + try { + PolicyManagerService policyManagerService = getPolicyServiceProvider(); + PolicyAdministratorPoint pap = policyManagerService.getPAP(); + pap.activatePolicy(policyId); + if (log.isDebugEnabled()) { + log.debug("Policy by id:" + policyId + " has been successfully activated."); + } + return true; + } catch (PolicyManagementException e) { + String error = "Exception in activating policy by id:" + policyId; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } catch (DeviceManagementException e) { + String error = "Error occurred while invoking Policy Management Service."; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + + @PUT + @Produces("application/json") + @Path("/inactivate/{id}") + public boolean inactivatePolicy(@PathParam("id") int policyId) { + + try { + PolicyManagerService policyManagerService = getPolicyServiceProvider(); + PolicyAdministratorPoint pap = policyManagerService.getPAP(); + pap.inactivatePolicy(policyId); + if (log.isDebugEnabled()) { + log.debug("Policy by id:" + policyId + " has been successfully inactivated."); + } + return true; + } catch (PolicyManagementException e) { + String error = "Exception in inactivating policy by id:" + policyId; + log.error(error, e); + return false; + } catch (DeviceManagementException e) { + String error = "Error occurred while invoking Policy Management Service."; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + + @PUT + @Produces("application/json") + @Path("/apply-changes") + public boolean applyChanges() { + + try { + PolicyManagerService policyManagerService = getPolicyServiceProvider(); + PolicyAdministratorPoint pap = policyManagerService.getPAP(); + pap.publishChanges(); + if (log.isDebugEnabled()) { + log.debug("Changes have been successfully updated."); + } + return true; + } catch (PolicyManagementException e) { + String error = "Exception in applying changes."; + log.error(error, e); + return false; + } catch (DeviceManagementException e) { + String error = "Error occurred while invoking Policy Management Service."; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + + @GET + @Path("/start-task/{milliseconds}") + public boolean startTaskService(@PathParam("milliseconds") int monitoringFrequency) { + + try { + PolicyManagerService policyManagerService = getPolicyServiceProvider(); + TaskScheduleService taskScheduleService = policyManagerService.getTaskScheduleService(); + taskScheduleService.startTask(monitoringFrequency); + if (log.isDebugEnabled()) { + log.debug("Policy monitoring service started successfully."); + } + return true; + } catch (PolicyMonitoringTaskException e) { + String error = "Policy Management related exception."; + log.error(error, e); + return false; + } catch (DeviceManagementException e) { + String error = "Error occurred while invoking Policy Management Service."; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + + @GET + @Path("/update-task/{milliseconds}") + public boolean updateTaskService(@PathParam("milliseconds") int monitoringFrequency) { + + try { + PolicyManagerService policyManagerService = getPolicyServiceProvider(); + TaskScheduleService taskScheduleService = policyManagerService.getTaskScheduleService(); + taskScheduleService.updateTask(monitoringFrequency); + if (log.isDebugEnabled()) { + log.debug("Policy monitoring service updated successfully."); + } + return true; + } catch (PolicyMonitoringTaskException e) { + String error = "Policy Management related exception."; + log.error(error, e); + return false; + } catch (DeviceManagementException e) { + String error = "Error occurred while invoking Policy Management Service."; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + + @GET + @Path("/stop-task") + public boolean stopTaskService() { + + try { + PolicyManagerService policyManagerService = getPolicyServiceProvider(); + TaskScheduleService taskScheduleService = policyManagerService.getTaskScheduleService(); + taskScheduleService.stopTask(); + if (log.isDebugEnabled()) { + log.debug("Policy monitoring service stopped successfully."); + } + return true; + } catch (PolicyMonitoringTaskException e) { + String error = "Policy Management related exception."; + log.error(error, e); + return false; + } catch (DeviceManagementException e) { + String error = "Error occurred while invoking Policy Management Service."; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + this.endTenantFlow(); + } + } + + @GET + @Path("/{type}/{id}") + public ComplianceData getComplianceDataOfDevice(@PathParam("id") String deviceId, + @PathParam("type") String deviceType) { + try { + PolicyManagerService policyManagerService = getPolicyServiceProvider(); + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setType(deviceType); + deviceIdentifier.setId(deviceId); + return policyManagerService.getDeviceCompliance(deviceIdentifier); + } catch (PolicyComplianceException e) { + String error = "Error occurred while getting the compliance data."; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } catch (DeviceManagementException e) { + String error = "Error occurred while invoking Policy Management Service."; + log.error(error, e); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + this.endTenantFlow(); + } + } + +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/StatsManagerService.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/StatsManagerService.java new file mode 100644 index 000000000..4c696654a --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/api/StatsManagerService.java @@ -0,0 +1,103 @@ +/* + * 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.device.mgt.iot.api; + +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.common.AnalyticsDataRecord; +import org.wso2.carbon.device.mgt.analytics.exception.DeviceManagementAnalyticsException; +import org.wso2.carbon.device.mgt.analytics.service.DeviceAnalyticsService; +import javax.jws.WebService; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +@WebService public class StatsManagerService { + + private static Log log = LogFactory.getLog(StatsManagerService.class); + + @Context //injected response proxy supporting multiple thread + private HttpServletResponse response; + //TODO THIS NEEDS TO BE REMOVED. + @Path("/stats/device/type/{type}/identifier/{identifier}") + @GET + @Consumes("application/json") + @Produces("application/json") + public DeviceUsageDTO[] getDeviceStats(@PathParam("type") String type, @PathParam("identifier") String identifier, + @QueryParam("table") String table, @QueryParam("column") String column, @QueryParam("username") String user, + @QueryParam("from") long from, @QueryParam("to") long to) { + + String fromDate = String.valueOf(from); + String toDate = String.valueOf(to); + + List deviceUsageDTOs = new ArrayList<>(); + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + ctx.setTenantDomain("carbon.super", true); + DeviceAnalyticsService deviceAnalyticsService = (DeviceAnalyticsService) ctx + .getOSGiService(DeviceAnalyticsService.class, null); + String query = "owner:" + user + " AND deviceId:" + identifier + " AND deviceType:" + type + + " AND time : [" + fromDate + " TO " + toDate + "]"; + try { + List records = deviceAnalyticsService.getAllEventsForDevice(table, 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) { + DeviceUsageDTO deviceUsageDTO = new DeviceUsageDTO(); + deviceUsageDTO.setTime("" + (long)record.getValue("time")); + deviceUsageDTO.setValue("" + (float) record.getValue(column.toLowerCase())); + deviceUsageDTOs.add(deviceUsageDTO); + } + return deviceUsageDTOs.toArray(new DeviceUsageDTO[deviceUsageDTOs.size()]); + } catch (DeviceManagementAnalyticsException e) { + String errorMsg= "Error on retrieving stats on table " + table + " with query " + query; + log.error(errorMsg); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return deviceUsageDTOs.toArray(new DeviceUsageDTO[deviceUsageDTOs.size()]); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/util/ResponsePayload.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/util/ResponsePayload.java new file mode 100644 index 000000000..c37d1d3a0 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/java/org/wso2/carbon/device/mgt/iot/util/ResponsePayload.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.device.mgt.iot.util; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement +public class ResponsePayload { + + private int statusCode; + private String messageFromServer; + private Object responseContent; + + public static ResponsePayloadBuilder statusCode(int statusCode) { + ResponsePayload message = new ResponsePayload(); + return message.getBuilder().statusCode(statusCode); + } + + public static ResponsePayloadBuilder messageFromServer( + String messageFromServer) { + ResponsePayload message = new ResponsePayload(); + return message.getBuilder().messageFromServer(messageFromServer); + } + + public static ResponsePayloadBuilder responseContent(String responseContent) { + ResponsePayload message = new ResponsePayload(); + return message.getBuilder().responseContent(responseContent); + } + + @XmlElement + public int getStatusCode() { + return statusCode; + } + + public void setStatusCode(int statusCode) { + this.statusCode = statusCode; + } + + @XmlElement + public String getMessageFromServer() { + return messageFromServer; + } + + public void setMessageFromServer(String messageFromServer) { + this.messageFromServer = messageFromServer; + } + + @XmlElement + public Object getResponseContent() { + return responseContent; + } + + public void setResponseContent(Object responseContent) { + this.responseContent = responseContent; + } + + private ResponsePayloadBuilder getBuilder() { + return new ResponsePayloadBuilder(); + } + + public class ResponsePayloadBuilder { + + private int statusCode; + private String messageFromServer; + private Object responseContent; + + public ResponsePayloadBuilder statusCode(int statusCode) { + this.statusCode = statusCode; + return this; + } + + public ResponsePayloadBuilder messageFromServer(String messageFromServer) { + this.messageFromServer = messageFromServer; + return this; + } + + public ResponsePayloadBuilder responseContent(String responseContent) { + this.responseContent = responseContent; + return this; + } + + public ResponsePayload build() { + ResponsePayload payload = new ResponsePayload(); + payload.setStatusCode(statusCode); + payload.setMessageFromServer(messageFromServer); + payload.setResponseContent(responseContent); + return payload; + } + } + +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/webapp/META-INF/permissions.xml b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/webapp/META-INF/permissions.xml new file mode 100644 index 000000000..aa291109e --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/webapp/META-INF/permissions.xml @@ -0,0 +1,113 @@ + + + + + + + + + Get Device of user + /login + /device_manager/device/user/{username}/all + GET + + + + + Get ungrouped devices of user + /login + /device_manager/device/user/{username}/ungrouped + GET + + + + + Get Device of user + /login + /device_manager/device_manager/device/user/{username}/ungrouped + GET + + + + + Get count of all the devices + /login + /device_manager/device/user/{username}/all/count + GET + + + + + Get specific device + /login + /device_manager/device/type/{type}/identifier/{identifier} + GET + + + + + Get the device type + /login + /device_manager/device/type/all + GET + + + + + Get all devices + /login + /device_manager/device/all + GET + + + + + Get the device by name + /login + /device_manager/device/name/{name}/all + GET + + + + + Update device enrollment info + /login + /device_manager/device/type/{type}/identifier/{identifier}/status + GET + + + + + Update device enrollment info + /login + /device_manager/device/type/{type}/identifier/{identifier}/status + GET + + + + \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/webapp/META-INF/webapp-classloading.xml b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/webapp/META-INF/webapp-classloading.xml new file mode 100644 index 000000000..fa4461919 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/webapp/META-INF/webapp-classloading.xml @@ -0,0 +1,33 @@ + + + + + + + + + false + + + CXF,Carbon + diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/webapp/WEB-INF/cxf-servlet.xml b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/webapp/WEB-INF/cxf-servlet.xml new file mode 100644 index 000000000..1112d721e --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/webapp/WEB-INF/cxf-servlet.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/webapp/WEB-INF/web.xml b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/webapp/WEB-INF/web.xml new file mode 100755 index 000000000..433526519 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.api/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,27 @@ + + + WSO2 IoT Server + WSO2 IoT Server + + + CXFServlet + org.apache.cxf.transport.servlet.CXFServlet + 1 + + + CXFServlet + /* + + + isAdminService + false + + + doAuthentication + true + + diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/pom.xml b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/pom.xml new file mode 100644 index 000000000..2d8b71102 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/pom.xml @@ -0,0 +1,62 @@ + + + + + + + + iot-base-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.ui + WSO2 Carbon - IoT Server UI + pom + + + + + maven-assembly-plugin + 2.5.5 + + ${project.artifactId}-${carbon.device.mgt.version} + false + + src/assembly/src.xml + + + + + create-archive + package + + single + + + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/assembly/src.xml b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/assembly/src.xml new file mode 100644 index 000000000..2797034e0 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/assembly/src.xml @@ -0,0 +1,36 @@ + + + + src + + zip + + false + ${basedir}/src + + + ${basedir}/src/main/resources/jaggeryapps/devicemgt + / + true + + + \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/conf/app-conf.json b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/conf/app-conf.json new file mode 100644 index 000000000..6d4d99825 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/conf/app-conf.json @@ -0,0 +1,35 @@ +{ + "appName": "IoT Server", + "cachingEnabled": true, + "debuggingEnabled": false, + "permissionRoot": "/", + "loginPage": "cdmf.page.sign-in", + "adminServicesUrl": "https://${server.ip}:${server.https_port}/admin/services/", + "authModule": { + "enabled": true, + "login": { + "onSuccess": { + "script": "/app/modules/login.js", + "page": "cdmf.page.dashboard" + }, + "onFail": { + "script": "/app/modules/login.js", + "page": "cdmf.page.sign-in" + } + }, + "logout": { + "onSuccess": { + "page": "cdmf.page.sign-in" + }, + "onFail": { + "page": "cdmf.page.dashboard" + } + }, + "sso": { + "enabled": false + } + }, + "errorPages": { + "default": "uuf.page.error" + } +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.register/public/js/validate-register.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.register/public/js/validate-register.js new file mode 100644 index 000000000..ea67fcc83 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.register/public/js/validate-register.js @@ -0,0 +1,163 @@ +/* + * 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. + */ + +/** + * Checks if provided input is valid against RegEx input. + * + * @param regExp Regular expression + * @param inputString Input string to check + * @returns {boolean} Returns true if input matches RegEx + */ +function inputIsValid(regExp, inputString) { + regExp = new RegExp(regExp); + return regExp.test(inputString); +} + +/** + * Checks if an email address has the valid format or not. + * + * @param email Email address + * @returns {boolean} true if email has the valid format, otherwise false. + */ +function emailIsValid(email) { + var regExp = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/; + return regExp.test(email); +} + +$(document).ready(function(){ + + /** + * Following click function would execute + * when a user clicks on "Add User" button + * on Add User page in WSO2 Devicemgt Console. + */ + $("button#add-user-btn").click(function () { + + var usernameInput = $("input#user_name"); + var firstnameInput = $("input#first_name"); + var lastnameInput = $("input#last_name"); + var emailInput = $("input#email"); + var passwordInput = $("input#password"); + var passwordConfirmationInput = $("input#password_confirmation"); + + var username = usernameInput.val().trim(); + var firstname = firstnameInput.val(); + var lastname = lastnameInput.val(); + var emailAddress = emailInput.val(); + var password = passwordInput.val(); + var passwordConfirmation = passwordConfirmationInput.val(); + var errorMsgWrapper = "#user-create-error-msg"; + var errorMsg = "#user-create-error-msg span"; + + if (!firstname) { + $(errorMsg).text("Firstname is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!inputIsValid(firstnameInput.data("regex"), firstname)) { + $(errorMsg).text(firstnameInput.data("errormsg")); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!lastname) { + $(errorMsg).text("Lastname is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!inputIsValid(lastnameInput.data("regex"), lastname)) { + $(errorMsg).text(lastnameInput.data("errormsg")); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!username) { + $(errorMsg).text("Username is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!inputIsValid(usernameInput.data("regex"), username)) { + $(errorMsg).text(usernameInput.data("errormsg")); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!emailAddress) { + $(errorMsg).text("Email is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!emailIsValid(emailAddress)) { + $(errorMsg).text(emailInput.data("errormsg")); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!password) { + $(errorMsg).text("Password is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (password.length < 6) { + $(errorMsg).text("Password is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (password != passwordConfirmation) { + $(errorMsg).text("Please enter the same password for confirmation."); + $(errorMsgWrapper).removeClass("hidden"); + } else { + $(errorMsgWrapper).addClass("hidden"); + $("#add-user-btn").prop('disabled', true); + + var addUserFormData = {}; + addUserFormData.username = username; + addUserFormData.firstname = firstname; + addUserFormData.lastname = lastname; + addUserFormData.emailAddress = emailAddress; + addUserFormData.password = $("input#password").val(); + addUserFormData.userRoles = null; + + var context = $(".form-login-box").data("context"); + var addUserAPI = context + "/api/user/register"; + + $.ajax({ + type: 'POST', + url: addUserAPI, + contentType: 'application/json', + data: JSON.stringify(addUserFormData), + success: function (data) { + $("#add-user-btn").prop('disabled', false); + if (data == 200) { + $('.wr-validation-summary strong').html( + " Successfully Submitted."); + $('.wr-validation-summary').removeClass("alert-danger"); + $('.wr-validation-summary').addClass("alert-success"); + } else if (data == 201) { + $('.wr-validation-summary strong').html( + " User created succssfully. You will be " + + "redirected to login page."); + $('.wr-validation-summary').removeClass("alert-danger"); + $('.wr-validation-summary').addClass("alert-success"); + $("#add-user-btn").prop('disabled', true); + setTimeout(function () { + window.location = context + "/login"; + }, 2000); + } else if (data == 400) { + $('.wr-validation-summary strong').html( + " Exception at backend."); + $('.wr-validation-summary').removeClass("alert-danger"); + $('.wr-validation-summary').addClass("alert-warning"); + } else if (data == 403) { + $('.wr-validation-summary strong').html("Action not permitted."); + } else if (data == 409) { + $('.wr-validation-summary strong').html( + " User name already exists."); + $('.wr-validation-summary').removeClass("alert-default"); + $('.wr-validation-summary').addClass("alert-success"); + } + $('.wr-validation-summary').removeClass("hidden"); + $('#password').val(''); + $('#password_confirmation').val(''); + }, + error: function (err) { + $("#add-user-btn").prop('disabled', false); + $('.wr-validation-summary strong').html( + " An unexpected error occurred."); + $('.wr-validation-summary').removeClass("hidden"); + } + }); + } + }); +}); diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.register/register.hbs b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.register/register.hbs new file mode 100644 index 000000000..e68e3ef1d --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.register/register.hbs @@ -0,0 +1,75 @@ +{{unit "cdmf.unit.ui.title" pageTitle="Register"}} + +{{#zone "content"}} + {{unit "uuf.unit.lib.form-validation"}} + + +{{/zone}} + +{{#zone "bottomJs"}} + {{js "js/validate-register.js"}} +{{/zone}} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.register/register.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.register/register.js new file mode 100644 index 000000000..d0bdca27b --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.register/register.js @@ -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. + */ + +/** + * Returns the dynamic state to be populated by add-user page. + * + * @param context Object that gets updated with the dynamic state of this page to be presented + * @returns {*} A context object that returns the dynamic state of this page to be presented + */ +function onRequest(context) { + var devicemgtProps = require('/app/conf/devicemgt-props.js').config(); + var page = {}; + page["usernameJSRegEx"] = devicemgtProps.userValidationConfig.usernameJSRegEx; + page["usernameHelpText"] = devicemgtProps.userValidationConfig.usernameHelpMsg; + page["usernameRegExViolationErrorMsg"] = devicemgtProps.userValidationConfig.usernameRegExViolationErrorMsg; + page["firstnameJSRegEx"] = devicemgtProps.userValidationConfig.firstnameJSRegEx; + page["firstnameRegExViolationErrorMsg"] = devicemgtProps.userValidationConfig.firstnameRegExViolationErrorMsg; + page["lastnameJSRegEx"] = devicemgtProps.userValidationConfig.lastnameJSRegEx; + page["lastnameRegExViolationErrorMsg"] = devicemgtProps.userValidationConfig.lastnameRegExViolationErrorMsg; + page["emailJSRegEx"] = devicemgtProps.userValidationConfig.emailJSRegEx; + page["emailRegExViolationErrorMsg"] = devicemgtProps.userValidationConfig.emailRegExViolationErrorMsg; + return page; +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.register/register.json b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.register/register.json new file mode 100644 index 000000000..58346ba14 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.register/register.json @@ -0,0 +1,6 @@ +{ + "version": "1.0.0", + "uri": "/register", + "isAnonymous": true, + "layout": "uuf.layout.sign-in" +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.sign-in/sign-in.hbs b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.sign-in/sign-in.hbs new file mode 100644 index 000000000..4ab4ff871 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.sign-in/sign-in.hbs @@ -0,0 +1,5 @@ +{{#zone "signInForm-below"}} + +{{/zone}} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.sign-in/sign-in.json b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.sign-in/sign-in.json new file mode 100644 index 000000000..dea3950ba --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/iot.page.sign-in/sign-in.json @@ -0,0 +1,4 @@ +{ +"version": "1.0.0", +"extends": "cdmf.page.sign-in" +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.operation-bar/operation-bar.hbs b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.operation-bar/operation-bar.hbs new file mode 100644 index 000000000..bdeac1967 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.operation-bar/operation-bar.hbs @@ -0,0 +1,75 @@ +{{#if control_operations}} +
      + + {{#each control_operations}} + + {{#if icon}} + + {{else}} + + {{/if}} + {{name}} + + +
      +
      +
      +
      +

      + + + + + {{name}} +
      +

      +

      + {{description}} +
      +

      + +
      + {{#each params}} + +
      + {{/each}} + + + +
      +
      +
      +
      +
      + {{/each}} +
      +{{else}} +
      +

      + Operations Loading Failed!

      +
      +{{/if}} + +{{#zone "bottomJs"}} + {{js "js/operation-bar.js"}} +{{/zone}} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.operation-bar/operation-bar.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.operation-bar/operation-bar.js new file mode 100644 index 000000000..6a90314ea --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.operation-bar/operation-bar.js @@ -0,0 +1,31 @@ +/* + * 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. + */ + +function onRequest(context) { + var log = new Log("operation.js"); + var operationModule = require("/app/modules/operation.js").operationModule; + var device = context.unit.params.device; + var controlOperations; + try { + controlOperations = operationModule.getControlOperations(device.type); + }catch(e){ + log.error("Control operation loading failed."); + controlOperations = null; + } + return {"control_operations": controlOperations, "device": device}; +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.operation-bar/operation-bar.json b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.operation-bar/operation-bar.json new file mode 100644 index 000000000..688e93980 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.operation-bar/operation-bar.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0" +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.operation-bar/public/js/operation-bar.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.operation-bar/public/js/operation-bar.js new file mode 100644 index 000000000..efaa79aca --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.operation-bar/public/js/operation-bar.js @@ -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. + */ + +/* + * On operation click function. + * @param selection: Selected operation + */ +function operationSelect (selection) { + $(modalPopupContent).addClass("operation-data"); + $(modalPopupContent).html($(" .operation[data-operation-code=" + selection + "]").html()); + $(modalPopupContent).data("operation-code", selection); + showPopup(); +} + +$(document).on('submit', 'form', function (e) { + e.preventDefault(); + var postOperationRequest = $.ajax({ + url: $(this).attr("action") + '&' + $(this).serialize(), + method: "post" + }); + + var btnSubmit = $('#btnSend', this); + btnSubmit.addClass('hidden'); + + var lblSending = $('#lblSending', this); + lblSending.removeClass('hidden'); + + var lblSent = $('#lblSent', this); + postOperationRequest.done(function (data) { + lblSending.addClass('hidden'); + lblSent.removeClass('hidden'); + setTimeout(function () { + hidePopup(); + }, 3000); + }); + + postOperationRequest.fail(function (jqXHR, textStatus) { + lblSending.addClass('hidden'); + lblSent.addClass('hidden'); + }); +}); \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/css/graph.css b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/css/graph.css new file mode 100644 index 000000000..7d0c9bba1 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/css/graph.css @@ -0,0 +1,471 @@ +/* + * 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. + */ + +/* graph */ + +.rickshaw_graph { + position: relative; +} + +.rickshaw_graph svg { + display: block; + overflow: hidden; +} + +/* ticks */ + +.rickshaw_graph .x_tick { + position: absolute; + top: 0; + bottom: 0; + width: 0; + border-left: 1px dotted rgba(0, 0, 0, 0.2); + pointer-events: none; +} + +.rickshaw_graph .x_tick .title { + position: absolute; + font-size: 12px; + font-family: Arial, sans-serif; + opacity: 0.5; + white-space: nowrap; + margin-left: 3px; + bottom: -20px; + height: auto; + border-bottom: none; +} + +/* annotations */ + +.rickshaw_annotation_timeline { + height: 1px; + border-top: 1px solid #e0e0e0; + margin-top: 10px; + position: relative; +} + +.rickshaw_annotation_timeline .annotation { + position: absolute; + height: 6px; + width: 6px; + margin-left: -2px; + top: -3px; + border-radius: 5px; + background-color: rgba(0, 0, 0, 0.25); +} + +.rickshaw_graph .annotation_line { + position: absolute; + top: 0; + bottom: -6px; + width: 0; + border-left: 2px solid rgba(0, 0, 0, 0.3); + display: none; +} + +.rickshaw_graph .annotation_line.active { + display: block; +} + +.rickshaw_graph .annotation_range { + background: rgba(0, 0, 0, 0.1); + display: none; + position: absolute; + top: 0; + bottom: -6px; +} + +.rickshaw_graph .annotation_range.active { + display: block; +} + +.rickshaw_graph .annotation_range.active.offscreen { + display: none; +} + +.rickshaw_annotation_timeline .annotation .content { + background: white; + color: black; + opacity: 0.9; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.8); + border-radius: 3px; + position: relative; + z-index: 20; + font-size: 12px; + padding: 6px 8px 8px; + top: 18px; + left: -11px; + width: 160px; + display: none; + cursor: pointer; +} + +.rickshaw_annotation_timeline .annotation .content:before { + content: "\25b2"; + position: absolute; + top: -11px; + color: white; + text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.8); +} + +.rickshaw_annotation_timeline .annotation.active, +.rickshaw_annotation_timeline .annotation:hover { + background-color: rgba(0, 0, 0, 0.8); + cursor: none; +} + +.rickshaw_annotation_timeline .annotation .content:hover { + z-index: 50; +} + +.rickshaw_annotation_timeline .annotation.active .content { + display: block; +} + +.rickshaw_annotation_timeline .annotation:hover .content { + display: block; + z-index: 50; +} + +.rickshaw_graph .y_axis, +.rickshaw_graph .x_axis_d3 { + fill: none; +} + +.rickshaw_graph .y_ticks .tick line, +.rickshaw_graph .x_ticks_d3 .tick { + stroke: rgba(0, 0, 0, 0.16); + stroke-width: 2px; + shape-rendering: crisp-edges; + pointer-events: none; +} + +.rickshaw_graph .y_grid .tick, +.rickshaw_graph .x_grid_d3 .tick { + z-index: -1; + stroke: rgba(0, 0, 0, 0.20); + stroke-width: 1px; + stroke-dasharray: 1 1; +} + +.rickshaw_graph .y_grid .tick[data-y-value="0"] { + stroke-dasharray: 1 0; +} + +.rickshaw_graph .y_grid path, +.rickshaw_graph .x_grid_d3 path { + fill: none; + stroke: none; +} + +.rickshaw_graph .y_ticks path, +.rickshaw_graph .x_ticks_d3 path { + fill: none; + stroke: #808080; +} + +.rickshaw_graph .y_ticks text, +.rickshaw_graph .x_ticks_d3 text { + opacity: 0.5; + font-size: 12px; + pointer-events: none; +} + +.rickshaw_graph .x_tick.glow .title, +.rickshaw_graph .y_ticks.glow text { + fill: black; + color: black; + text-shadow: -1px 1px 0 rgba(255, 255, 255, 0.1), + 1px -1px 0 rgba(255, 255, 255, 0.1), + 1px 1px 0 rgba(255, 255, 255, 0.1), + 0 1px 0 rgba(255, 255, 255, 0.1), + 0 -1px 0 rgba(255, 255, 255, 0.1), + 1px 0 0 rgba(255, 255, 255, 0.1), + -1px 0 0 rgba(255, 255, 255, 0.1), + -1px -1px 0 rgba(255, 255, 255, 0.1); +} + +.rickshaw_graph .x_tick.inverse .title, +.rickshaw_graph .y_ticks.inverse text { + fill: white; + color: white; + text-shadow: -1px 1px 0 rgba(0, 0, 0, 0.8), + 1px -1px 0 rgba(0, 0, 0, 0.8), + 1px 1px 0 rgba(0, 0, 0, 0.8), + 0 1px 0 rgba(0, 0, 0, 0.8), + 0 -1px 0 rgba(0, 0, 0, 0.8), + 1px 0 0 rgba(0, 0, 0, 0.8), + -1px 0 0 rgba(0, 0, 0, 0.8), + -1px -1px 0 rgba(0, 0, 0, 0.8); +} + +.custom_rickshaw_graph { + position: relative; + left: 40px; +} + +.custom_y_axis { + position: absolute; + width: 40px; + margin-top: -20px; +} + +.custom_slider { + left: 40px; +} + +.custom_x_axis { + position: relative; + left: 40px; + height: 30px; + width: 97%; + top: 20px; + text-align: right; +} + +.chartWrapper { + padding-top: 50px; +} + +/*detail*/ + +.rickshaw_graph .detail { + pointer-events: none; + position: absolute; + top: 0; + z-index: 2; + background: rgba(0, 0, 0, 0.1); + bottom: 0; + width: 1px; + transition: opacity 0.25s linear; + -moz-transition: opacity 0.25s linear; + -o-transition: opacity 0.25s linear; + -webkit-transition: opacity 0.25s linear; +} + +.rickshaw_graph .detail.inactive { + opacity: 0; +} + +.rickshaw_graph .detail .item.active { + opacity: 1; +} + +.rickshaw_graph .detail .x_label { + font-family: Arial, sans-serif; + border-radius: 3px; + padding: 6px; + opacity: 0.5; + border: 1px solid #e0e0e0; + font-size: 12px; + position: absolute; + background: white; + white-space: nowrap; +} + +.rickshaw_graph .detail .x_label.left { + left: 0; +} + +.rickshaw_graph .detail .x_label.right { + right: 0; +} + +.rickshaw_graph .detail .item { + position: absolute; + z-index: 2; + border-radius: 3px; + padding: 0.25em; + font-size: 12px; + font-family: Arial, sans-serif; + opacity: 0; + background: rgba(0, 0, 0, 0.4); + color: white; + border: 1px solid rgba(0, 0, 0, 0.4); + margin-left: 1em; + margin-right: 1em; + margin-top: -1em; + white-space: nowrap; +} + +.rickshaw_graph .detail .item.left { + left: 0; +} + +.rickshaw_graph .detail .item.right { + right: 0; +} + +.rickshaw_graph .detail .item.active { + opacity: 1; + background: rgba(0, 0, 0, 0.8); +} + +.rickshaw_graph .detail .item:after { + position: absolute; + display: block; + width: 0; + height: 0; + + content: ""; + + border: 5px solid transparent; +} + +.rickshaw_graph .detail .item.left:after { + top: 1em; + left: -5px; + margin-top: -5px; + border-right-color: rgba(0, 0, 0, 0.8); + border-left-width: 0; +} + +.rickshaw_graph .detail .item.right:after { + top: 1em; + right: -5px; + margin-top: -5px; + border-left-color: rgba(0, 0, 0, 0.8); + border-right-width: 0; +} + +.rickshaw_graph .detail .dot { + width: 4px; + height: 4px; + margin-left: -3px; + margin-top: -3.5px; + border-radius: 5px; + position: absolute; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.6); + box-sizing: content-box; + -moz-box-sizing: content-box; + background: white; + border-width: 2px; + border-style: solid; + display: none; + background-clip: padding-box; +} + +.rickshaw_graph .detail .dot.active { + display: block; +} + +/*legend*/ +.rickshaw_legend { + font-family: Arial; + font-size: 12px; + color: white; + background: #404040; + display: inline-block; + padding: 12px 5px; + border-radius: 2px; + position: relative; + float: right; +} + +.rickshaw_legend:hover { + z-index: 10; +} + +.rickshaw_legend .swatch { + width: 10px; + height: 10px; + border: 1px solid rgba(0, 0, 0, 0.2); +} + +.rickshaw_legend .line { + clear: both; + line-height: 140%; + padding-right: 15px; +} + +.rickshaw_legend .line .swatch { + display: inline-block; + margin-right: 3px; + border-radius: 2px; +} + +.rickshaw_legend .label { + margin: 0; + white-space: nowrap; + display: inline; + font-size: inherit; + background-color: transparent; + color: inherit; + font-weight: normal; + line-height: normal; + padding: 0; + text-shadow: none; +} + +.rickshaw_legend .action:hover { + opacity: 0.6; +} + +.rickshaw_legend .action { + margin-right: 0.2em; + opacity: 0.2; + cursor: pointer; + font-size: 14px; +} + +.rickshaw_legend .line.disabled { + opacity: 0.4; +} + +.rickshaw_legend ul { + list-style-type: none; + padding: 0; + margin: 2px; + cursor: pointer; +} + +.rickshaw_legend li { + padding: 0 0 0 2px; + min-width: 80px; + white-space: nowrap; +} + +.rickshaw_legend li:hover { + background: rgba(255, 255, 255, 0.08); + border-radius: 3px; +} + +.rickshaw_legend li:active { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; +} + +.legend { + display: inline-block; + position: relative; + left: 8px; +} + +.legend_container { + float: right; + padding-right: 10px; + width: 0; + z-index: 1; + position: relative; + opacity: 0.7; +} + +.spaced { + margin-top: 20px !important; +} + diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/images/map-marker-1.png b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/images/map-marker-1.png new file mode 100644 index 0000000000000000000000000000000000000000..82ae21a32bbd0a1cee52a59766e7b84e20fea2ad GIT binary patch literal 2824 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 b@ND}(Ny`Cj7IZbQ00000NkvXXu0mjfUyWc` literal 0 HcmV?d00001 diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/js/d3.min.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/js/d3.min.js new file mode 100644 index 000000000..71f9715ee --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/js/d3.min.js @@ -0,0 +1,9488 @@ +/* + * 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. + */ + +!function() { + var d3 = { + version: "3.5.2" + }; + if (!Date.now) Date.now = function() { + return +new Date(); + }; + var d3_arraySlice = [].slice, d3_array = function(list) { + return d3_arraySlice.call(list); + }; + var d3_document = document, d3_documentElement = d3_document.documentElement, d3_window = window; + try { + d3_array(d3_documentElement.childNodes)[0].nodeType; + } catch (e) { + d3_array = function(list) { + var i = list.length, array = new Array(i); + while (i--) array[i] = list[i]; + return array; + }; + } + try { + d3_document.createElement("div").style.setProperty("opacity", 0, ""); + } catch (error) { + var d3_element_prototype = d3_window.Element.prototype, d3_element_setAttribute = d3_element_prototype.setAttribute, d3_element_setAttributeNS = d3_element_prototype.setAttributeNS, d3_style_prototype = d3_window.CSSStyleDeclaration.prototype, d3_style_setProperty = d3_style_prototype.setProperty; + d3_element_prototype.setAttribute = function(name, value) { + d3_element_setAttribute.call(this, name, value + ""); + }; + d3_element_prototype.setAttributeNS = function(space, local, value) { + d3_element_setAttributeNS.call(this, space, local, value + ""); + }; + d3_style_prototype.setProperty = function(name, value, priority) { + d3_style_setProperty.call(this, name, value + "", priority); + }; + } + d3.ascending = d3_ascending; + function d3_ascending(a, b) { + return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN; + } + d3.descending = function(a, b) { + return b < a ? -1 : b > a ? 1 : b >= a ? 0 : NaN; + }; + d3.min = function(array, f) { + var i = -1, n = array.length, a, b; + if (arguments.length === 1) { + while (++i < n) if ((b = array[i]) != null && b >= b) { + a = b; + break; + } + while (++i < n) if ((b = array[i]) != null && a > b) a = b; + } else { + while (++i < n) if ((b = f.call(array, array[i], i)) != null && b >= b) { + a = b; + break; + } + while (++i < n) if ((b = f.call(array, array[i], i)) != null && a > b) a = b; + } + return a; + }; + d3.max = function(array, f) { + var i = -1, n = array.length, a, b; + if (arguments.length === 1) { + while (++i < n) if ((b = array[i]) != null && b >= b) { + a = b; + break; + } + while (++i < n) if ((b = array[i]) != null && b > a) a = b; + } else { + while (++i < n) if ((b = f.call(array, array[i], i)) != null && b >= b) { + a = b; + break; + } + while (++i < n) if ((b = f.call(array, array[i], i)) != null && b > a) a = b; + } + return a; + }; + d3.extent = function(array, f) { + var i = -1, n = array.length, a, b, c; + if (arguments.length === 1) { + while (++i < n) if ((b = array[i]) != null && b >= b) { + a = c = b; + break; + } + while (++i < n) if ((b = array[i]) != null) { + if (a > b) a = b; + if (c < b) c = b; + } + } else { + while (++i < n) if ((b = f.call(array, array[i], i)) != null && b >= b) { + a = c = b; + break; + } + while (++i < n) if ((b = f.call(array, array[i], i)) != null) { + if (a > b) a = b; + if (c < b) c = b; + } + } + return [ a, c ]; + }; + function d3_number(x) { + return x === null ? NaN : +x; + } + function d3_numeric(x) { + return !isNaN(x); + } + d3.sum = function(array, f) { + var s = 0, n = array.length, a, i = -1; + if (arguments.length === 1) { + while (++i < n) if (d3_numeric(a = +array[i])) s += a; + } else { + while (++i < n) if (d3_numeric(a = +f.call(array, array[i], i))) s += a; + } + return s; + }; + d3.mean = function(array, f) { + var s = 0, n = array.length, a, i = -1, j = n; + if (arguments.length === 1) { + while (++i < n) if (d3_numeric(a = d3_number(array[i]))) s += a; else --j; + } else { + while (++i < n) if (d3_numeric(a = d3_number(f.call(array, array[i], i)))) s += a; else --j; + } + if (j) return s / j; + }; + d3.quantile = function(values, p) { + var H = (values.length - 1) * p + 1, h = Math.floor(H), v = +values[h - 1], e = H - h; + return e ? v + e * (values[h] - v) : v; + }; + d3.median = function(array, f) { + var numbers = [], n = array.length, a, i = -1; + if (arguments.length === 1) { + while (++i < n) if (d3_numeric(a = d3_number(array[i]))) numbers.push(a); + } else { + while (++i < n) if (d3_numeric(a = d3_number(f.call(array, array[i], i)))) numbers.push(a); + } + if (numbers.length) return d3.quantile(numbers.sort(d3_ascending), .5); + }; + d3.variance = function(array, f) { + var n = array.length, m = 0, a, d, s = 0, i = -1, j = 0; + if (arguments.length === 1) { + while (++i < n) { + if (d3_numeric(a = d3_number(array[i]))) { + d = a - m; + m += d / ++j; + s += d * (a - m); + } + } + } else { + while (++i < n) { + if (d3_numeric(a = d3_number(f.call(array, array[i], i)))) { + d = a - m; + m += d / ++j; + s += d * (a - m); + } + } + } + if (j > 1) return s / (j - 1); + }; + d3.deviation = function() { + var v = d3.variance.apply(this, arguments); + return v ? Math.sqrt(v) : v; + }; + function d3_bisector(compare) { + return { + left: function(a, x, lo, hi) { + if (arguments.length < 3) lo = 0; + if (arguments.length < 4) hi = a.length; + while (lo < hi) { + var mid = lo + hi >>> 1; + if (compare(a[mid], x) < 0) lo = mid + 1; else hi = mid; + } + return lo; + }, + right: function(a, x, lo, hi) { + if (arguments.length < 3) lo = 0; + if (arguments.length < 4) hi = a.length; + while (lo < hi) { + var mid = lo + hi >>> 1; + if (compare(a[mid], x) > 0) hi = mid; else lo = mid + 1; + } + return lo; + } + }; + } + var d3_bisect = d3_bisector(d3_ascending); + d3.bisectLeft = d3_bisect.left; + d3.bisect = d3.bisectRight = d3_bisect.right; + d3.bisector = function(f) { + return d3_bisector(f.length === 1 ? function(d, x) { + return d3_ascending(f(d), x); + } : f); + }; + d3.shuffle = function(array, i0, i1) { + if ((m = arguments.length) < 3) { + i1 = array.length; + if (m < 2) i0 = 0; + } + var m = i1 - i0, t, i; + while (m) { + i = Math.random() * m-- | 0; + t = array[m + i0], array[m + i0] = array[i + i0], array[i + i0] = t; + } + return array; + }; + d3.permute = function(array, indexes) { + var i = indexes.length, permutes = new Array(i); + while (i--) permutes[i] = array[indexes[i]]; + return permutes; + }; + d3.pairs = function(array) { + var i = 0, n = array.length - 1, p0, p1 = array[0], pairs = new Array(n < 0 ? 0 : n); + while (i < n) pairs[i] = [ p0 = p1, p1 = array[++i] ]; + return pairs; + }; + d3.zip = function() { + if (!(n = arguments.length)) return []; + for (var i = -1, m = d3.min(arguments, d3_zipLength), zips = new Array(m); ++i < m; ) { + for (var j = -1, n, zip = zips[i] = new Array(n); ++j < n; ) { + zip[j] = arguments[j][i]; + } + } + return zips; + }; + function d3_zipLength(d) { + return d.length; + } + d3.transpose = function(matrix) { + return d3.zip.apply(d3, matrix); + }; + d3.keys = function(map) { + var keys = []; + for (var key in map) keys.push(key); + return keys; + }; + d3.values = function(map) { + var values = []; + for (var key in map) values.push(map[key]); + return values; + }; + d3.entries = function(map) { + var entries = []; + for (var key in map) entries.push({ + key: key, + value: map[key] + }); + return entries; + }; + d3.merge = function(arrays) { + var n = arrays.length, m, i = -1, j = 0, merged, array; + while (++i < n) j += arrays[i].length; + merged = new Array(j); + while (--n >= 0) { + array = arrays[n]; + m = array.length; + while (--m >= 0) { + merged[--j] = array[m]; + } + } + return merged; + }; + var abs = Math.abs; + d3.range = function(start, stop, step) { + if (arguments.length < 3) { + step = 1; + if (arguments.length < 2) { + stop = start; + start = 0; + } + } + if ((stop - start) / step === Infinity) throw new Error("infinite range"); + var range = [], k = d3_range_integerScale(abs(step)), i = -1, j; + start *= k, stop *= k, step *= k; + if (step < 0) while ((j = start + step * ++i) > stop) range.push(j / k); else while ((j = start + step * ++i) < stop) range.push(j / k); + return range; + }; + function d3_range_integerScale(x) { + var k = 1; + while (x * k % 1) k *= 10; + return k; + } + function d3_class(ctor, properties) { + for (var key in properties) { + Object.defineProperty(ctor.prototype, key, { + value: properties[key], + enumerable: false + }); + } + } + d3.map = function(object, f) { + var map = new d3_Map(); + if (object instanceof d3_Map) { + object.forEach(function(key, value) { + map.set(key, value); + }); + } else if (Array.isArray(object)) { + var i = -1, n = object.length, o; + if (arguments.length === 1) while (++i < n) map.set(i, object[i]); else while (++i < n) map.set(f.call(object, o = object[i], i), o); + } else { + for (var key in object) map.set(key, object[key]); + } + return map; + }; + function d3_Map() { + this._ = Object.create(null); + } + var d3_map_proto = "__proto__", d3_map_zero = "\x00"; + d3_class(d3_Map, { + has: d3_map_has, + get: function(key) { + return this._[d3_map_escape(key)]; + }, + set: function(key, value) { + return this._[d3_map_escape(key)] = value; + }, + remove: d3_map_remove, + keys: d3_map_keys, + values: function() { + var values = []; + for (var key in this._) values.push(this._[key]); + return values; + }, + entries: function() { + var entries = []; + for (var key in this._) entries.push({ + key: d3_map_unescape(key), + value: this._[key] + }); + return entries; + }, + size: d3_map_size, + empty: d3_map_empty, + forEach: function(f) { + for (var key in this._) f.call(this, d3_map_unescape(key), this._[key]); + } + }); + function d3_map_escape(key) { + return (key += "") === d3_map_proto || key[0] === d3_map_zero ? d3_map_zero + key : key; + } + function d3_map_unescape(key) { + return (key += "")[0] === d3_map_zero ? key.slice(1) : key; + } + function d3_map_has(key) { + return d3_map_escape(key) in this._; + } + function d3_map_remove(key) { + return (key = d3_map_escape(key)) in this._ && delete this._[key]; + } + function d3_map_keys() { + var keys = []; + for (var key in this._) keys.push(d3_map_unescape(key)); + return keys; + } + function d3_map_size() { + var size = 0; + for (var key in this._) ++size; + return size; + } + function d3_map_empty() { + for (var key in this._) return false; + return true; + } + d3.nest = function() { + var nest = {}, keys = [], sortKeys = [], sortValues, rollup; + function map(mapType, array, depth) { + if (depth >= keys.length) return rollup ? rollup.call(nest, array) : sortValues ? array.sort(sortValues) : array; + var i = -1, n = array.length, key = keys[depth++], keyValue, object, setter, valuesByKey = new d3_Map(), values; + while (++i < n) { + if (values = valuesByKey.get(keyValue = key(object = array[i]))) { + values.push(object); + } else { + valuesByKey.set(keyValue, [ object ]); + } + } + if (mapType) { + object = mapType(); + setter = function(keyValue, values) { + object.set(keyValue, map(mapType, values, depth)); + }; + } else { + object = {}; + setter = function(keyValue, values) { + object[keyValue] = map(mapType, values, depth); + }; + } + valuesByKey.forEach(setter); + return object; + } + function entries(map, depth) { + if (depth >= keys.length) return map; + var array = [], sortKey = sortKeys[depth++]; + map.forEach(function(key, keyMap) { + array.push({ + key: key, + values: entries(keyMap, depth) + }); + }); + return sortKey ? array.sort(function(a, b) { + return sortKey(a.key, b.key); + }) : array; + } + nest.map = function(array, mapType) { + return map(mapType, array, 0); + }; + nest.entries = function(array) { + return entries(map(d3.map, array, 0), 0); + }; + nest.key = function(d) { + keys.push(d); + return nest; + }; + nest.sortKeys = function(order) { + sortKeys[keys.length - 1] = order; + return nest; + }; + nest.sortValues = function(order) { + sortValues = order; + return nest; + }; + nest.rollup = function(f) { + rollup = f; + return nest; + }; + return nest; + }; + d3.set = function(array) { + var set = new d3_Set(); + if (array) for (var i = 0, n = array.length; i < n; ++i) set.add(array[i]); + return set; + }; + function d3_Set() { + this._ = Object.create(null); + } + d3_class(d3_Set, { + has: d3_map_has, + add: function(key) { + this._[d3_map_escape(key += "")] = true; + return key; + }, + remove: d3_map_remove, + values: d3_map_keys, + size: d3_map_size, + empty: d3_map_empty, + forEach: function(f) { + for (var key in this._) f.call(this, d3_map_unescape(key)); + } + }); + d3.behavior = {}; + d3.rebind = function(target, source) { + var i = 1, n = arguments.length, method; + while (++i < n) target[method = arguments[i]] = d3_rebind(target, source, source[method]); + return target; + }; + function d3_rebind(target, source, method) { + return function() { + var value = method.apply(source, arguments); + return value === source ? target : value; + }; + } + function d3_vendorSymbol(object, name) { + if (name in object) return name; + name = name.charAt(0).toUpperCase() + name.slice(1); + for (var i = 0, n = d3_vendorPrefixes.length; i < n; ++i) { + var prefixName = d3_vendorPrefixes[i] + name; + if (prefixName in object) return prefixName; + } + } + var d3_vendorPrefixes = [ "webkit", "ms", "moz", "Moz", "o", "O" ]; + function d3_noop() {} + d3.dispatch = function() { + var dispatch = new d3_dispatch(), i = -1, n = arguments.length; + while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch); + return dispatch; + }; + function d3_dispatch() {} + d3_dispatch.prototype.on = function(type, listener) { + var i = type.indexOf("."), name = ""; + if (i >= 0) { + name = type.slice(i + 1); + type = type.slice(0, i); + } + if (type) return arguments.length < 2 ? this[type].on(name) : this[type].on(name, listener); + if (arguments.length === 2) { + if (listener == null) for (type in this) { + if (this.hasOwnProperty(type)) this[type].on(name, null); + } + return this; + } + }; + function d3_dispatch_event(dispatch) { + var listeners = [], listenerByName = new d3_Map(); + function event() { + var z = listeners, i = -1, n = z.length, l; + while (++i < n) if (l = z[i].on) l.apply(this, arguments); + return dispatch; + } + event.on = function(name, listener) { + var l = listenerByName.get(name), i; + if (arguments.length < 2) return l && l.on; + if (l) { + l.on = null; + listeners = listeners.slice(0, i = listeners.indexOf(l)).concat(listeners.slice(i + 1)); + listenerByName.remove(name); + } + if (listener) listeners.push(listenerByName.set(name, { + on: listener + })); + return dispatch; + }; + return event; + } + d3.event = null; + function d3_eventPreventDefault() { + d3.event.preventDefault(); + } + function d3_eventSource() { + var e = d3.event, s; + while (s = e.sourceEvent) e = s; + return e; + } + function d3_eventDispatch(target) { + var dispatch = new d3_dispatch(), i = 0, n = arguments.length; + while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch); + dispatch.of = function(thiz, argumentz) { + return function(e1) { + try { + var e0 = e1.sourceEvent = d3.event; + e1.target = target; + d3.event = e1; + dispatch[e1.type].apply(thiz, argumentz); + } finally { + d3.event = e0; + } + }; + }; + return dispatch; + } + d3.requote = function(s) { + return s.replace(d3_requote_re, "\\$&"); + }; + var d3_requote_re = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g; + var d3_subclass = {}.__proto__ ? function(object, prototype) { + object.__proto__ = prototype; + } : function(object, prototype) { + for (var property in prototype) object[property] = prototype[property]; + }; + function d3_selection(groups) { + d3_subclass(groups, d3_selectionPrototype); + return groups; + } + var d3_select = function(s, n) { + return n.querySelector(s); + }, d3_selectAll = function(s, n) { + return n.querySelectorAll(s); + }, d3_selectMatcher = d3_documentElement.matches || d3_documentElement[d3_vendorSymbol(d3_documentElement, "matchesSelector")], d3_selectMatches = function(n, s) { + return d3_selectMatcher.call(n, s); + }; + if (typeof Sizzle === "function") { + d3_select = function(s, n) { + return Sizzle(s, n)[0] || null; + }; + d3_selectAll = Sizzle; + d3_selectMatches = Sizzle.matchesSelector; + } + d3.selection = function() { + return d3_selectionRoot; + }; + var d3_selectionPrototype = d3.selection.prototype = []; + d3_selectionPrototype.select = function(selector) { + var subgroups = [], subgroup, subnode, group, node; + selector = d3_selection_selector(selector); + for (var j = -1, m = this.length; ++j < m; ) { + subgroups.push(subgroup = []); + subgroup.parentNode = (group = this[j]).parentNode; + for (var i = -1, n = group.length; ++i < n; ) { + if (node = group[i]) { + subgroup.push(subnode = selector.call(node, node.__data__, i, j)); + if (subnode && "__data__" in node) subnode.__data__ = node.__data__; + } else { + subgroup.push(null); + } + } + } + return d3_selection(subgroups); + }; + function d3_selection_selector(selector) { + return typeof selector === "function" ? selector : function() { + return d3_select(selector, this); + }; + } + d3_selectionPrototype.selectAll = function(selector) { + var subgroups = [], subgroup, node; + selector = d3_selection_selectorAll(selector); + for (var j = -1, m = this.length; ++j < m; ) { + for (var group = this[j], i = -1, n = group.length; ++i < n; ) { + if (node = group[i]) { + subgroups.push(subgroup = d3_array(selector.call(node, node.__data__, i, j))); + subgroup.parentNode = node; + } + } + } + return d3_selection(subgroups); + }; + function d3_selection_selectorAll(selector) { + return typeof selector === "function" ? selector : function() { + return d3_selectAll(selector, this); + }; + } + var d3_nsPrefix = { + svg: "http://www.w3.org/2000/svg", + xhtml: "http://www.w3.org/1999/xhtml", + xlink: "http://www.w3.org/1999/xlink", + xml: "http://www.w3.org/XML/1998/namespace", + xmlns: "http://www.w3.org/2000/xmlns/" + }; + d3.ns = { + prefix: d3_nsPrefix, + qualify: function(name) { + var i = name.indexOf(":"), prefix = name; + if (i >= 0) { + prefix = name.slice(0, i); + name = name.slice(i + 1); + } + return d3_nsPrefix.hasOwnProperty(prefix) ? { + space: d3_nsPrefix[prefix], + local: name + } : name; + } + }; + d3_selectionPrototype.attr = function(name, value) { + if (arguments.length < 2) { + if (typeof name === "string") { + var node = this.node(); + name = d3.ns.qualify(name); + return name.local ? node.getAttributeNS(name.space, name.local) : node.getAttribute(name); + } + for (value in name) this.each(d3_selection_attr(value, name[value])); + return this; + } + return this.each(d3_selection_attr(name, value)); + }; + function d3_selection_attr(name, value) { + name = d3.ns.qualify(name); + function attrNull() { + this.removeAttribute(name); + } + function attrNullNS() { + this.removeAttributeNS(name.space, name.local); + } + function attrConstant() { + this.setAttribute(name, value); + } + function attrConstantNS() { + this.setAttributeNS(name.space, name.local, value); + } + function attrFunction() { + var x = value.apply(this, arguments); + if (x == null) this.removeAttribute(name); else this.setAttribute(name, x); + } + function attrFunctionNS() { + var x = value.apply(this, arguments); + if (x == null) this.removeAttributeNS(name.space, name.local); else this.setAttributeNS(name.space, name.local, x); + } + return value == null ? name.local ? attrNullNS : attrNull : typeof value === "function" ? name.local ? attrFunctionNS : attrFunction : name.local ? attrConstantNS : attrConstant; + } + function d3_collapse(s) { + return s.trim().replace(/\s+/g, " "); + } + d3_selectionPrototype.classed = function(name, value) { + if (arguments.length < 2) { + if (typeof name === "string") { + var node = this.node(), n = (name = d3_selection_classes(name)).length, i = -1; + if (value = node.classList) { + while (++i < n) if (!value.contains(name[i])) return false; + } else { + value = node.getAttribute("class"); + while (++i < n) if (!d3_selection_classedRe(name[i]).test(value)) return false; + } + return true; + } + for (value in name) this.each(d3_selection_classed(value, name[value])); + return this; + } + return this.each(d3_selection_classed(name, value)); + }; + function d3_selection_classedRe(name) { + return new RegExp("(?:^|\\s+)" + d3.requote(name) + "(?:\\s+|$)", "g"); + } + function d3_selection_classes(name) { + return (name + "").trim().split(/^|\s+/); + } + function d3_selection_classed(name, value) { + name = d3_selection_classes(name).map(d3_selection_classedName); + var n = name.length; + function classedConstant() { + var i = -1; + while (++i < n) name[i](this, value); + } + function classedFunction() { + var i = -1, x = value.apply(this, arguments); + while (++i < n) name[i](this, x); + } + return typeof value === "function" ? classedFunction : classedConstant; + } + function d3_selection_classedName(name) { + var re = d3_selection_classedRe(name); + return function(node, value) { + if (c = node.classList) return value ? c.add(name) : c.remove(name); + var c = node.getAttribute("class") || ""; + if (value) { + re.lastIndex = 0; + if (!re.test(c)) node.setAttribute("class", d3_collapse(c + " " + name)); + } else { + node.setAttribute("class", d3_collapse(c.replace(re, " "))); + } + }; + } + d3_selectionPrototype.style = function(name, value, priority) { + var n = arguments.length; + if (n < 3) { + if (typeof name !== "string") { + if (n < 2) value = ""; + for (priority in name) this.each(d3_selection_style(priority, name[priority], value)); + return this; + } + if (n < 2) return d3_window.getComputedStyle(this.node(), null).getPropertyValue(name); + priority = ""; + } + return this.each(d3_selection_style(name, value, priority)); + }; + function d3_selection_style(name, value, priority) { + function styleNull() { + this.style.removeProperty(name); + } + function styleConstant() { + this.style.setProperty(name, value, priority); + } + function styleFunction() { + var x = value.apply(this, arguments); + if (x == null) this.style.removeProperty(name); else this.style.setProperty(name, x, priority); + } + return value == null ? styleNull : typeof value === "function" ? styleFunction : styleConstant; + } + d3_selectionPrototype.property = function(name, value) { + if (arguments.length < 2) { + if (typeof name === "string") return this.node()[name]; + for (value in name) this.each(d3_selection_property(value, name[value])); + return this; + } + return this.each(d3_selection_property(name, value)); + }; + function d3_selection_property(name, value) { + function propertyNull() { + delete this[name]; + } + function propertyConstant() { + this[name] = value; + } + function propertyFunction() { + var x = value.apply(this, arguments); + if (x == null) delete this[name]; else this[name] = x; + } + return value == null ? propertyNull : typeof value === "function" ? propertyFunction : propertyConstant; + } + d3_selectionPrototype.text = function(value) { + return arguments.length ? this.each(typeof value === "function" ? function() { + var v = value.apply(this, arguments); + this.textContent = v == null ? "" : v; + } : value == null ? function() { + this.textContent = ""; + } : function() { + this.textContent = value; + }) : this.node().textContent; + }; + d3_selectionPrototype.html = function(value) { + return arguments.length ? this.each(typeof value === "function" ? function() { + var v = value.apply(this, arguments); + this.innerHTML = v == null ? "" : v; + } : value == null ? function() { + this.innerHTML = ""; + } : function() { + this.innerHTML = value; + }) : this.node().innerHTML; + }; + d3_selectionPrototype.append = function(name) { + name = d3_selection_creator(name); + return this.select(function() { + return this.appendChild(name.apply(this, arguments)); + }); + }; + function d3_selection_creator(name) { + return typeof name === "function" ? name : (name = d3.ns.qualify(name)).local ? function() { + return this.ownerDocument.createElementNS(name.space, name.local); + } : function() { + return this.ownerDocument.createElementNS(this.namespaceURI, name); + }; + } + d3_selectionPrototype.insert = function(name, before) { + name = d3_selection_creator(name); + before = d3_selection_selector(before); + return this.select(function() { + return this.insertBefore(name.apply(this, arguments), before.apply(this, arguments) || null); + }); + }; + d3_selectionPrototype.remove = function() { + return this.each(d3_selectionRemove); + }; + function d3_selectionRemove() { + var parent = this.parentNode; + if (parent) parent.removeChild(this); + } + d3_selectionPrototype.data = function(value, key) { + var i = -1, n = this.length, group, node; + if (!arguments.length) { + value = new Array(n = (group = this[0]).length); + while (++i < n) { + if (node = group[i]) { + value[i] = node.__data__; + } + } + return value; + } + function bind(group, groupData) { + var i, n = group.length, m = groupData.length, n0 = Math.min(n, m), updateNodes = new Array(m), enterNodes = new Array(m), exitNodes = new Array(n), node, nodeData; + if (key) { + var nodeByKeyValue = new d3_Map(), keyValues = new Array(n), keyValue; + for (i = -1; ++i < n; ) { + if (nodeByKeyValue.has(keyValue = key.call(node = group[i], node.__data__, i))) { + exitNodes[i] = node; + } else { + nodeByKeyValue.set(keyValue, node); + } + keyValues[i] = keyValue; + } + for (i = -1; ++i < m; ) { + if (!(node = nodeByKeyValue.get(keyValue = key.call(groupData, nodeData = groupData[i], i)))) { + enterNodes[i] = d3_selection_dataNode(nodeData); + } else if (node !== true) { + updateNodes[i] = node; + node.__data__ = nodeData; + } + nodeByKeyValue.set(keyValue, true); + } + for (i = -1; ++i < n; ) { + if (nodeByKeyValue.get(keyValues[i]) !== true) { + exitNodes[i] = group[i]; + } + } + } else { + for (i = -1; ++i < n0; ) { + node = group[i]; + nodeData = groupData[i]; + if (node) { + node.__data__ = nodeData; + updateNodes[i] = node; + } else { + enterNodes[i] = d3_selection_dataNode(nodeData); + } + } + for (;i < m; ++i) { + enterNodes[i] = d3_selection_dataNode(groupData[i]); + } + for (;i < n; ++i) { + exitNodes[i] = group[i]; + } + } + enterNodes.update = updateNodes; + enterNodes.parentNode = updateNodes.parentNode = exitNodes.parentNode = group.parentNode; + enter.push(enterNodes); + update.push(updateNodes); + exit.push(exitNodes); + } + var enter = d3_selection_enter([]), update = d3_selection([]), exit = d3_selection([]); + if (typeof value === "function") { + while (++i < n) { + bind(group = this[i], value.call(group, group.parentNode.__data__, i)); + } + } else { + while (++i < n) { + bind(group = this[i], value); + } + } + update.enter = function() { + return enter; + }; + update.exit = function() { + return exit; + }; + return update; + }; + function d3_selection_dataNode(data) { + return { + __data__: data + }; + } + d3_selectionPrototype.datum = function(value) { + return arguments.length ? this.property("__data__", value) : this.property("__data__"); + }; + d3_selectionPrototype.filter = function(filter) { + var subgroups = [], subgroup, group, node; + if (typeof filter !== "function") filter = d3_selection_filter(filter); + for (var j = 0, m = this.length; j < m; j++) { + subgroups.push(subgroup = []); + subgroup.parentNode = (group = this[j]).parentNode; + for (var i = 0, n = group.length; i < n; i++) { + if ((node = group[i]) && filter.call(node, node.__data__, i, j)) { + subgroup.push(node); + } + } + } + return d3_selection(subgroups); + }; + function d3_selection_filter(selector) { + return function() { + return d3_selectMatches(this, selector); + }; + } + d3_selectionPrototype.order = function() { + for (var j = -1, m = this.length; ++j < m; ) { + for (var group = this[j], i = group.length - 1, next = group[i], node; --i >= 0; ) { + if (node = group[i]) { + if (next && next !== node.nextSibling) next.parentNode.insertBefore(node, next); + next = node; + } + } + } + return this; + }; + d3_selectionPrototype.sort = function(comparator) { + comparator = d3_selection_sortComparator.apply(this, arguments); + for (var j = -1, m = this.length; ++j < m; ) this[j].sort(comparator); + return this.order(); + }; + function d3_selection_sortComparator(comparator) { + if (!arguments.length) comparator = d3_ascending; + return function(a, b) { + return a && b ? comparator(a.__data__, b.__data__) : !a - !b; + }; + } + d3_selectionPrototype.each = function(callback) { + return d3_selection_each(this, function(node, i, j) { + callback.call(node, node.__data__, i, j); + }); + }; + function d3_selection_each(groups, callback) { + for (var j = 0, m = groups.length; j < m; j++) { + for (var group = groups[j], i = 0, n = group.length, node; i < n; i++) { + if (node = group[i]) callback(node, i, j); + } + } + return groups; + } + d3_selectionPrototype.call = function(callback) { + var args = d3_array(arguments); + callback.apply(args[0] = this, args); + return this; + }; + d3_selectionPrototype.empty = function() { + return !this.node(); + }; + d3_selectionPrototype.node = function() { + for (var j = 0, m = this.length; j < m; j++) { + for (var group = this[j], i = 0, n = group.length; i < n; i++) { + var node = group[i]; + if (node) return node; + } + } + return null; + }; + d3_selectionPrototype.size = function() { + var n = 0; + d3_selection_each(this, function() { + ++n; + }); + return n; + }; + function d3_selection_enter(selection) { + d3_subclass(selection, d3_selection_enterPrototype); + return selection; + } + var d3_selection_enterPrototype = []; + d3.selection.enter = d3_selection_enter; + d3.selection.enter.prototype = d3_selection_enterPrototype; + d3_selection_enterPrototype.append = d3_selectionPrototype.append; + d3_selection_enterPrototype.empty = d3_selectionPrototype.empty; + d3_selection_enterPrototype.node = d3_selectionPrototype.node; + d3_selection_enterPrototype.call = d3_selectionPrototype.call; + d3_selection_enterPrototype.size = d3_selectionPrototype.size; + d3_selection_enterPrototype.select = function(selector) { + var subgroups = [], subgroup, subnode, upgroup, group, node; + for (var j = -1, m = this.length; ++j < m; ) { + upgroup = (group = this[j]).update; + subgroups.push(subgroup = []); + subgroup.parentNode = group.parentNode; + for (var i = -1, n = group.length; ++i < n; ) { + if (node = group[i]) { + subgroup.push(upgroup[i] = subnode = selector.call(group.parentNode, node.__data__, i, j)); + subnode.__data__ = node.__data__; + } else { + subgroup.push(null); + } + } + } + return d3_selection(subgroups); + }; + d3_selection_enterPrototype.insert = function(name, before) { + if (arguments.length < 2) before = d3_selection_enterInsertBefore(this); + return d3_selectionPrototype.insert.call(this, name, before); + }; + function d3_selection_enterInsertBefore(enter) { + var i0, j0; + return function(d, i, j) { + var group = enter[j].update, n = group.length, node; + if (j != j0) j0 = j, i0 = 0; + if (i >= i0) i0 = i + 1; + while (!(node = group[i0]) && ++i0 < n) ; + return node; + }; + } + d3.select = function(node) { + var group = [ typeof node === "string" ? d3_select(node, d3_document) : node ]; + group.parentNode = d3_documentElement; + return d3_selection([ group ]); + }; + d3.selectAll = function(nodes) { + var group = d3_array(typeof nodes === "string" ? d3_selectAll(nodes, d3_document) : nodes); + group.parentNode = d3_documentElement; + return d3_selection([ group ]); + }; + var d3_selectionRoot = d3.select(d3_documentElement); + d3_selectionPrototype.on = function(type, listener, capture) { + var n = arguments.length; + if (n < 3) { + if (typeof type !== "string") { + if (n < 2) listener = false; + for (capture in type) this.each(d3_selection_on(capture, type[capture], listener)); + return this; + } + if (n < 2) return (n = this.node()["__on" + type]) && n._; + capture = false; + } + return this.each(d3_selection_on(type, listener, capture)); + }; + function d3_selection_on(type, listener, capture) { + var name = "__on" + type, i = type.indexOf("."), wrap = d3_selection_onListener; + if (i > 0) type = type.slice(0, i); + var filter = d3_selection_onFilters.get(type); + if (filter) type = filter, wrap = d3_selection_onFilter; + function onRemove() { + var l = this[name]; + if (l) { + this.removeEventListener(type, l, l.$); + delete this[name]; + } + } + function onAdd() { + var l = wrap(listener, d3_array(arguments)); + onRemove.call(this); + this.addEventListener(type, this[name] = l, l.$ = capture); + l._ = listener; + } + function removeAll() { + var re = new RegExp("^__on([^.]+)" + d3.requote(type) + "$"), match; + for (var name in this) { + if (match = name.match(re)) { + var l = this[name]; + this.removeEventListener(match[1], l, l.$); + delete this[name]; + } + } + } + return i ? listener ? onAdd : onRemove : listener ? d3_noop : removeAll; + } + var d3_selection_onFilters = d3.map({ + mouseenter: "mouseover", + mouseleave: "mouseout" + }); + d3_selection_onFilters.forEach(function(k) { + if ("on" + k in d3_document) d3_selection_onFilters.remove(k); + }); + function d3_selection_onListener(listener, argumentz) { + return function(e) { + var o = d3.event; + d3.event = e; + argumentz[0] = this.__data__; + try { + listener.apply(this, argumentz); + } finally { + d3.event = o; + } + }; + } + function d3_selection_onFilter(listener, argumentz) { + var l = d3_selection_onListener(listener, argumentz); + return function(e) { + var target = this, related = e.relatedTarget; + if (!related || related !== target && !(related.compareDocumentPosition(target) & 8)) { + l.call(target, e); + } + }; + } + var d3_event_dragSelect = "onselectstart" in d3_document ? null : d3_vendorSymbol(d3_documentElement.style, "userSelect"), d3_event_dragId = 0; + function d3_event_dragSuppress() { + var name = ".dragsuppress-" + ++d3_event_dragId, click = "click" + name, w = d3.select(d3_window).on("touchmove" + name, d3_eventPreventDefault).on("dragstart" + name, d3_eventPreventDefault).on("selectstart" + name, d3_eventPreventDefault); + if (d3_event_dragSelect) { + var style = d3_documentElement.style, select = style[d3_event_dragSelect]; + style[d3_event_dragSelect] = "none"; + } + return function(suppressClick) { + w.on(name, null); + if (d3_event_dragSelect) style[d3_event_dragSelect] = select; + if (suppressClick) { + var off = function() { + w.on(click, null); + }; + w.on(click, function() { + d3_eventPreventDefault(); + off(); + }, true); + setTimeout(off, 0); + } + }; + } + d3.mouse = function(container) { + return d3_mousePoint(container, d3_eventSource()); + }; + var d3_mouse_bug44083 = /WebKit/.test(d3_window.navigator.userAgent) ? -1 : 0; + function d3_mousePoint(container, e) { + if (e.changedTouches) e = e.changedTouches[0]; + var svg = container.ownerSVGElement || container; + if (svg.createSVGPoint) { + var point = svg.createSVGPoint(); + if (d3_mouse_bug44083 < 0 && (d3_window.scrollX || d3_window.scrollY)) { + svg = d3.select("body").append("svg").style({ + position: "absolute", + top: 0, + left: 0, + margin: 0, + padding: 0, + border: "none" + }, "important"); + var ctm = svg[0][0].getScreenCTM(); + d3_mouse_bug44083 = !(ctm.f || ctm.e); + svg.remove(); + } + if (d3_mouse_bug44083) point.x = e.pageX, point.y = e.pageY; else point.x = e.clientX, + point.y = e.clientY; + point = point.matrixTransform(container.getScreenCTM().inverse()); + return [ point.x, point.y ]; + } + var rect = container.getBoundingClientRect(); + return [ e.clientX - rect.left - container.clientLeft, e.clientY - rect.top - container.clientTop ]; + } + d3.touch = function(container, touches, identifier) { + if (arguments.length < 3) identifier = touches, touches = d3_eventSource().changedTouches; + if (touches) for (var i = 0, n = touches.length, touch; i < n; ++i) { + if ((touch = touches[i]).identifier === identifier) { + return d3_mousePoint(container, touch); + } + } + }; + d3.behavior.drag = function() { + var event = d3_eventDispatch(drag, "drag", "dragstart", "dragend"), origin = null, mousedown = dragstart(d3_noop, d3.mouse, d3_behavior_dragMouseSubject, "mousemove", "mouseup"), touchstart = dragstart(d3_behavior_dragTouchId, d3.touch, d3_behavior_dragTouchSubject, "touchmove", "touchend"); + function drag() { + this.on("mousedown.drag", mousedown).on("touchstart.drag", touchstart); + } + function dragstart(id, position, subject, move, end) { + return function() { + var that = this, target = d3.event.target, parent = that.parentNode, dispatch = event.of(that, arguments), dragged = 0, dragId = id(), dragName = ".drag" + (dragId == null ? "" : "-" + dragId), dragOffset, dragSubject = d3.select(subject()).on(move + dragName, moved).on(end + dragName, ended), dragRestore = d3_event_dragSuppress(), position0 = position(parent, dragId); + if (origin) { + dragOffset = origin.apply(that, arguments); + dragOffset = [ dragOffset.x - position0[0], dragOffset.y - position0[1] ]; + } else { + dragOffset = [ 0, 0 ]; + } + dispatch({ + type: "dragstart" + }); + function moved() { + var position1 = position(parent, dragId), dx, dy; + if (!position1) return; + dx = position1[0] - position0[0]; + dy = position1[1] - position0[1]; + dragged |= dx | dy; + position0 = position1; + dispatch({ + type: "drag", + x: position1[0] + dragOffset[0], + y: position1[1] + dragOffset[1], + dx: dx, + dy: dy + }); + } + function ended() { + if (!position(parent, dragId)) return; + dragSubject.on(move + dragName, null).on(end + dragName, null); + dragRestore(dragged && d3.event.target === target); + dispatch({ + type: "dragend" + }); + } + }; + } + drag.origin = function(x) { + if (!arguments.length) return origin; + origin = x; + return drag; + }; + return d3.rebind(drag, event, "on"); + }; + function d3_behavior_dragTouchId() { + return d3.event.changedTouches[0].identifier; + } + function d3_behavior_dragTouchSubject() { + return d3.event.target; + } + function d3_behavior_dragMouseSubject() { + return d3_window; + } + d3.touches = function(container, touches) { + if (arguments.length < 2) touches = d3_eventSource().touches; + return touches ? d3_array(touches).map(function(touch) { + var point = d3_mousePoint(container, touch); + point.identifier = touch.identifier; + return point; + }) : []; + }; + var ε = 1e-6, ε2 = ε * ε, π = Math.PI, τ = 2 * π, τε = τ - ε, halfπ = π / 2, d3_radians = π / 180, d3_degrees = 180 / π; + function d3_sgn(x) { + return x > 0 ? 1 : x < 0 ? -1 : 0; + } + function d3_cross2d(a, b, c) { + return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]); + } + function d3_acos(x) { + return x > 1 ? 0 : x < -1 ? π : Math.acos(x); + } + function d3_asin(x) { + return x > 1 ? halfπ : x < -1 ? -halfπ : Math.asin(x); + } + function d3_sinh(x) { + return ((x = Math.exp(x)) - 1 / x) / 2; + } + function d3_cosh(x) { + return ((x = Math.exp(x)) + 1 / x) / 2; + } + function d3_tanh(x) { + return ((x = Math.exp(2 * x)) - 1) / (x + 1); + } + function d3_haversin(x) { + return (x = Math.sin(x / 2)) * x; + } + var ρ = Math.SQRT2, ρ2 = 2, ρ4 = 4; + d3.interpolateZoom = function(p0, p1) { + var ux0 = p0[0], uy0 = p0[1], w0 = p0[2], ux1 = p1[0], uy1 = p1[1], w1 = p1[2]; + var dx = ux1 - ux0, dy = uy1 - uy0, d2 = dx * dx + dy * dy, d1 = Math.sqrt(d2), b0 = (w1 * w1 - w0 * w0 + ρ4 * d2) / (2 * w0 * ρ2 * d1), b1 = (w1 * w1 - w0 * w0 - ρ4 * d2) / (2 * w1 * ρ2 * d1), r0 = Math.log(Math.sqrt(b0 * b0 + 1) - b0), r1 = Math.log(Math.sqrt(b1 * b1 + 1) - b1), dr = r1 - r0, S = (dr || Math.log(w1 / w0)) / ρ; + function interpolate(t) { + var s = t * S; + if (dr) { + var coshr0 = d3_cosh(r0), u = w0 / (ρ2 * d1) * (coshr0 * d3_tanh(ρ * s + r0) - d3_sinh(r0)); + return [ ux0 + u * dx, uy0 + u * dy, w0 * coshr0 / d3_cosh(ρ * s + r0) ]; + } + return [ ux0 + t * dx, uy0 + t * dy, w0 * Math.exp(ρ * s) ]; + } + interpolate.duration = S * 1e3; + return interpolate; + }; + d3.behavior.zoom = function() { + var view = { + x: 0, + y: 0, + k: 1 + }, translate0, center0, center, size = [ 960, 500 ], scaleExtent = d3_behavior_zoomInfinity, duration = 250, zooming = 0, mousedown = "mousedown.zoom", mousemove = "mousemove.zoom", mouseup = "mouseup.zoom", mousewheelTimer, touchstart = "touchstart.zoom", touchtime, event = d3_eventDispatch(zoom, "zoomstart", "zoom", "zoomend"), x0, x1, y0, y1; + function zoom(g) { + g.on(mousedown, mousedowned).on(d3_behavior_zoomWheel + ".zoom", mousewheeled).on("dblclick.zoom", dblclicked).on(touchstart, touchstarted); + } + zoom.event = function(g) { + g.each(function() { + var dispatch = event.of(this, arguments), view1 = view; + if (d3_transitionInheritId) { + d3.select(this).transition().each("start.zoom", function() { + view = this.__chart__ || { + x: 0, + y: 0, + k: 1 + }; + zoomstarted(dispatch); + }).tween("zoom:zoom", function() { + var dx = size[0], dy = size[1], cx = center0 ? center0[0] : dx / 2, cy = center0 ? center0[1] : dy / 2, i = d3.interpolateZoom([ (cx - view.x) / view.k, (cy - view.y) / view.k, dx / view.k ], [ (cx - view1.x) / view1.k, (cy - view1.y) / view1.k, dx / view1.k ]); + return function(t) { + var l = i(t), k = dx / l[2]; + this.__chart__ = view = { + x: cx - l[0] * k, + y: cy - l[1] * k, + k: k + }; + zoomed(dispatch); + }; + }).each("interrupt.zoom", function() { + zoomended(dispatch); + }).each("end.zoom", function() { + zoomended(dispatch); + }); + } else { + this.__chart__ = view; + zoomstarted(dispatch); + zoomed(dispatch); + zoomended(dispatch); + } + }); + }; + zoom.translate = function(_) { + if (!arguments.length) return [ view.x, view.y ]; + view = { + x: +_[0], + y: +_[1], + k: view.k + }; + rescale(); + return zoom; + }; + zoom.scale = function(_) { + if (!arguments.length) return view.k; + view = { + x: view.x, + y: view.y, + k: +_ + }; + rescale(); + return zoom; + }; + zoom.scaleExtent = function(_) { + if (!arguments.length) return scaleExtent; + scaleExtent = _ == null ? d3_behavior_zoomInfinity : [ +_[0], +_[1] ]; + return zoom; + }; + zoom.center = function(_) { + if (!arguments.length) return center; + center = _ && [ +_[0], +_[1] ]; + return zoom; + }; + zoom.size = function(_) { + if (!arguments.length) return size; + size = _ && [ +_[0], +_[1] ]; + return zoom; + }; + zoom.duration = function(_) { + if (!arguments.length) return duration; + duration = +_; + return zoom; + }; + zoom.x = function(z) { + if (!arguments.length) return x1; + x1 = z; + x0 = z.copy(); + view = { + x: 0, + y: 0, + k: 1 + }; + return zoom; + }; + zoom.y = function(z) { + if (!arguments.length) return y1; + y1 = z; + y0 = z.copy(); + view = { + x: 0, + y: 0, + k: 1 + }; + return zoom; + }; + function location(p) { + return [ (p[0] - view.x) / view.k, (p[1] - view.y) / view.k ]; + } + function point(l) { + return [ l[0] * view.k + view.x, l[1] * view.k + view.y ]; + } + function scaleTo(s) { + view.k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], s)); + } + function translateTo(p, l) { + l = point(l); + view.x += p[0] - l[0]; + view.y += p[1] - l[1]; + } + function zoomTo(that, p, l, k) { + that.__chart__ = { + x: view.x, + y: view.y, + k: view.k + }; + scaleTo(Math.pow(2, k)); + translateTo(center0 = p, l); + that = d3.select(that); + if (duration > 0) that = that.transition().duration(duration); + that.call(zoom.event); + } + function rescale() { + if (x1) x1.domain(x0.range().map(function(x) { + return (x - view.x) / view.k; + }).map(x0.invert)); + if (y1) y1.domain(y0.range().map(function(y) { + return (y - view.y) / view.k; + }).map(y0.invert)); + } + function zoomstarted(dispatch) { + if (!zooming++) dispatch({ + type: "zoomstart" + }); + } + function zoomed(dispatch) { + rescale(); + dispatch({ + type: "zoom", + scale: view.k, + translate: [ view.x, view.y ] + }); + } + function zoomended(dispatch) { + if (!--zooming) dispatch({ + type: "zoomend" + }); + center0 = null; + } + function mousedowned() { + var that = this, target = d3.event.target, dispatch = event.of(that, arguments), dragged = 0, subject = d3.select(d3_window).on(mousemove, moved).on(mouseup, ended), location0 = location(d3.mouse(that)), dragRestore = d3_event_dragSuppress(); + d3_selection_interrupt.call(that); + zoomstarted(dispatch); + function moved() { + dragged = 1; + translateTo(d3.mouse(that), location0); + zoomed(dispatch); + } + function ended() { + subject.on(mousemove, null).on(mouseup, null); + dragRestore(dragged && d3.event.target === target); + zoomended(dispatch); + } + } + function touchstarted() { + var that = this, dispatch = event.of(that, arguments), locations0 = {}, distance0 = 0, scale0, zoomName = ".zoom-" + d3.event.changedTouches[0].identifier, touchmove = "touchmove" + zoomName, touchend = "touchend" + zoomName, targets = [], subject = d3.select(that), dragRestore = d3_event_dragSuppress(); + started(); + zoomstarted(dispatch); + subject.on(mousedown, null).on(touchstart, started); + function relocate() { + var touches = d3.touches(that); + scale0 = view.k; + touches.forEach(function(t) { + if (t.identifier in locations0) locations0[t.identifier] = location(t); + }); + return touches; + } + function started() { + var target = d3.event.target; + d3.select(target).on(touchmove, moved).on(touchend, ended); + targets.push(target); + var changed = d3.event.changedTouches; + for (var i = 0, n = changed.length; i < n; ++i) { + locations0[changed[i].identifier] = null; + } + var touches = relocate(), now = Date.now(); + if (touches.length === 1) { + if (now - touchtime < 500) { + var p = touches[0]; + zoomTo(that, p, locations0[p.identifier], Math.floor(Math.log(view.k) / Math.LN2) + 1); + d3_eventPreventDefault(); + } + touchtime = now; + } else if (touches.length > 1) { + var p = touches[0], q = touches[1], dx = p[0] - q[0], dy = p[1] - q[1]; + distance0 = dx * dx + dy * dy; + } + } + function moved() { + var touches = d3.touches(that), p0, l0, p1, l1; + d3_selection_interrupt.call(that); + for (var i = 0, n = touches.length; i < n; ++i, l1 = null) { + p1 = touches[i]; + if (l1 = locations0[p1.identifier]) { + if (l0) break; + p0 = p1, l0 = l1; + } + } + if (l1) { + var distance1 = (distance1 = p1[0] - p0[0]) * distance1 + (distance1 = p1[1] - p0[1]) * distance1, scale1 = distance0 && Math.sqrt(distance1 / distance0); + p0 = [ (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2 ]; + l0 = [ (l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2 ]; + scaleTo(scale1 * scale0); + } + touchtime = null; + translateTo(p0, l0); + zoomed(dispatch); + } + function ended() { + if (d3.event.touches.length) { + var changed = d3.event.changedTouches; + for (var i = 0, n = changed.length; i < n; ++i) { + delete locations0[changed[i].identifier]; + } + for (var identifier in locations0) { + return void relocate(); + } + } + d3.selectAll(targets).on(zoomName, null); + subject.on(mousedown, mousedowned).on(touchstart, touchstarted); + dragRestore(); + zoomended(dispatch); + } + } + function mousewheeled() { + var dispatch = event.of(this, arguments); + if (mousewheelTimer) clearTimeout(mousewheelTimer); else translate0 = location(center0 = center || d3.mouse(this)), + d3_selection_interrupt.call(this), zoomstarted(dispatch); + mousewheelTimer = setTimeout(function() { + mousewheelTimer = null; + zoomended(dispatch); + }, 50); + d3_eventPreventDefault(); + scaleTo(Math.pow(2, d3_behavior_zoomDelta() * .002) * view.k); + translateTo(center0, translate0); + zoomed(dispatch); + } + function dblclicked() { + var p = d3.mouse(this), k = Math.log(view.k) / Math.LN2; + zoomTo(this, p, location(p), d3.event.shiftKey ? Math.ceil(k) - 1 : Math.floor(k) + 1); + } + return d3.rebind(zoom, event, "on"); + }; + var d3_behavior_zoomInfinity = [ 0, Infinity ]; + var d3_behavior_zoomDelta, d3_behavior_zoomWheel = "onwheel" in d3_document ? (d3_behavior_zoomDelta = function() { + return -d3.event.deltaY * (d3.event.deltaMode ? 120 : 1); + }, "wheel") : "onmousewheel" in d3_document ? (d3_behavior_zoomDelta = function() { + return d3.event.wheelDelta; + }, "mousewheel") : (d3_behavior_zoomDelta = function() { + return -d3.event.detail; + }, "MozMousePixelScroll"); + d3.color = d3_color; + function d3_color() {} + d3_color.prototype.toString = function() { + return this.rgb() + ""; + }; + d3.hsl = d3_hsl; + function d3_hsl(h, s, l) { + return this instanceof d3_hsl ? void (this.h = +h, this.s = +s, this.l = +l) : arguments.length < 2 ? h instanceof d3_hsl ? new d3_hsl(h.h, h.s, h.l) : d3_rgb_parse("" + h, d3_rgb_hsl, d3_hsl) : new d3_hsl(h, s, l); + } + var d3_hslPrototype = d3_hsl.prototype = new d3_color(); + d3_hslPrototype.brighter = function(k) { + k = Math.pow(.7, arguments.length ? k : 1); + return new d3_hsl(this.h, this.s, this.l / k); + }; + d3_hslPrototype.darker = function(k) { + k = Math.pow(.7, arguments.length ? k : 1); + return new d3_hsl(this.h, this.s, k * this.l); + }; + d3_hslPrototype.rgb = function() { + return d3_hsl_rgb(this.h, this.s, this.l); + }; + function d3_hsl_rgb(h, s, l) { + var m1, m2; + h = isNaN(h) ? 0 : (h %= 360) < 0 ? h + 360 : h; + s = isNaN(s) ? 0 : s < 0 ? 0 : s > 1 ? 1 : s; + l = l < 0 ? 0 : l > 1 ? 1 : l; + m2 = l <= .5 ? l * (1 + s) : l + s - l * s; + m1 = 2 * l - m2; + function v(h) { + if (h > 360) h -= 360; else if (h < 0) h += 360; + if (h < 60) return m1 + (m2 - m1) * h / 60; + if (h < 180) return m2; + if (h < 240) return m1 + (m2 - m1) * (240 - h) / 60; + return m1; + } + function vv(h) { + return Math.round(v(h) * 255); + } + return new d3_rgb(vv(h + 120), vv(h), vv(h - 120)); + } + d3.hcl = d3_hcl; + function d3_hcl(h, c, l) { + return this instanceof d3_hcl ? void (this.h = +h, this.c = +c, this.l = +l) : arguments.length < 2 ? h instanceof d3_hcl ? new d3_hcl(h.h, h.c, h.l) : h instanceof d3_lab ? d3_lab_hcl(h.l, h.a, h.b) : d3_lab_hcl((h = d3_rgb_lab((h = d3.rgb(h)).r, h.g, h.b)).l, h.a, h.b) : new d3_hcl(h, c, l); + } + var d3_hclPrototype = d3_hcl.prototype = new d3_color(); + d3_hclPrototype.brighter = function(k) { + return new d3_hcl(this.h, this.c, Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1))); + }; + d3_hclPrototype.darker = function(k) { + return new d3_hcl(this.h, this.c, Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1))); + }; + d3_hclPrototype.rgb = function() { + return d3_hcl_lab(this.h, this.c, this.l).rgb(); + }; + function d3_hcl_lab(h, c, l) { + if (isNaN(h)) h = 0; + if (isNaN(c)) c = 0; + return new d3_lab(l, Math.cos(h *= d3_radians) * c, Math.sin(h) * c); + } + d3.lab = d3_lab; + function d3_lab(l, a, b) { + return this instanceof d3_lab ? void (this.l = +l, this.a = +a, this.b = +b) : arguments.length < 2 ? l instanceof d3_lab ? new d3_lab(l.l, l.a, l.b) : l instanceof d3_hcl ? d3_hcl_lab(l.h, l.c, l.l) : d3_rgb_lab((l = d3_rgb(l)).r, l.g, l.b) : new d3_lab(l, a, b); + } + var d3_lab_K = 18; + var d3_lab_X = .95047, d3_lab_Y = 1, d3_lab_Z = 1.08883; + var d3_labPrototype = d3_lab.prototype = new d3_color(); + d3_labPrototype.brighter = function(k) { + return new d3_lab(Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1)), this.a, this.b); + }; + d3_labPrototype.darker = function(k) { + return new d3_lab(Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1)), this.a, this.b); + }; + d3_labPrototype.rgb = function() { + return d3_lab_rgb(this.l, this.a, this.b); + }; + function d3_lab_rgb(l, a, b) { + var y = (l + 16) / 116, x = y + a / 500, z = y - b / 200; + x = d3_lab_xyz(x) * d3_lab_X; + y = d3_lab_xyz(y) * d3_lab_Y; + z = d3_lab_xyz(z) * d3_lab_Z; + return new d3_rgb(d3_xyz_rgb(3.2404542 * x - 1.5371385 * y - .4985314 * z), d3_xyz_rgb(-.969266 * x + 1.8760108 * y + .041556 * z), d3_xyz_rgb(.0556434 * x - .2040259 * y + 1.0572252 * z)); + } + function d3_lab_hcl(l, a, b) { + return l > 0 ? new d3_hcl(Math.atan2(b, a) * d3_degrees, Math.sqrt(a * a + b * b), l) : new d3_hcl(NaN, NaN, l); + } + function d3_lab_xyz(x) { + return x > .206893034 ? x * x * x : (x - 4 / 29) / 7.787037; + } + function d3_xyz_lab(x) { + return x > .008856 ? Math.pow(x, 1 / 3) : 7.787037 * x + 4 / 29; + } + function d3_xyz_rgb(r) { + return Math.round(255 * (r <= .00304 ? 12.92 * r : 1.055 * Math.pow(r, 1 / 2.4) - .055)); + } + d3.rgb = d3_rgb; + function d3_rgb(r, g, b) { + return this instanceof d3_rgb ? void (this.r = ~~r, this.g = ~~g, this.b = ~~b) : arguments.length < 2 ? r instanceof d3_rgb ? new d3_rgb(r.r, r.g, r.b) : d3_rgb_parse("" + r, d3_rgb, d3_hsl_rgb) : new d3_rgb(r, g, b); + } + function d3_rgbNumber(value) { + return new d3_rgb(value >> 16, value >> 8 & 255, value & 255); + } + function d3_rgbString(value) { + return d3_rgbNumber(value) + ""; + } + var d3_rgbPrototype = d3_rgb.prototype = new d3_color(); + d3_rgbPrototype.brighter = function(k) { + k = Math.pow(.7, arguments.length ? k : 1); + var r = this.r, g = this.g, b = this.b, i = 30; + if (!r && !g && !b) return new d3_rgb(i, i, i); + if (r && r < i) r = i; + if (g && g < i) g = i; + if (b && b < i) b = i; + return new d3_rgb(Math.min(255, r / k), Math.min(255, g / k), Math.min(255, b / k)); + }; + d3_rgbPrototype.darker = function(k) { + k = Math.pow(.7, arguments.length ? k : 1); + return new d3_rgb(k * this.r, k * this.g, k * this.b); + }; + d3_rgbPrototype.hsl = function() { + return d3_rgb_hsl(this.r, this.g, this.b); + }; + d3_rgbPrototype.toString = function() { + return "#" + d3_rgb_hex(this.r) + d3_rgb_hex(this.g) + d3_rgb_hex(this.b); + }; + function d3_rgb_hex(v) { + return v < 16 ? "0" + Math.max(0, v).toString(16) : Math.min(255, v).toString(16); + } + function d3_rgb_parse(format, rgb, hsl) { + var r = 0, g = 0, b = 0, m1, m2, color; + m1 = /([a-z]+)\((.*)\)/i.exec(format); + if (m1) { + m2 = m1[2].split(","); + switch (m1[1]) { + case "hsl": + { + return hsl(parseFloat(m2[0]), parseFloat(m2[1]) / 100, parseFloat(m2[2]) / 100); + } + + case "rgb": + { + return rgb(d3_rgb_parseNumber(m2[0]), d3_rgb_parseNumber(m2[1]), d3_rgb_parseNumber(m2[2])); + } + } + } + if (color = d3_rgb_names.get(format)) return rgb(color.r, color.g, color.b); + if (format != null && format.charAt(0) === "#" && !isNaN(color = parseInt(format.slice(1), 16))) { + if (format.length === 4) { + r = (color & 3840) >> 4; + r = r >> 4 | r; + g = color & 240; + g = g >> 4 | g; + b = color & 15; + b = b << 4 | b; + } else if (format.length === 7) { + r = (color & 16711680) >> 16; + g = (color & 65280) >> 8; + b = color & 255; + } + } + return rgb(r, g, b); + } + function d3_rgb_hsl(r, g, b) { + var min = Math.min(r /= 255, g /= 255, b /= 255), max = Math.max(r, g, b), d = max - min, h, s, l = (max + min) / 2; + if (d) { + s = l < .5 ? d / (max + min) : d / (2 - max - min); + if (r == max) h = (g - b) / d + (g < b ? 6 : 0); else if (g == max) h = (b - r) / d + 2; else h = (r - g) / d + 4; + h *= 60; + } else { + h = NaN; + s = l > 0 && l < 1 ? 0 : h; + } + return new d3_hsl(h, s, l); + } + function d3_rgb_lab(r, g, b) { + r = d3_rgb_xyz(r); + g = d3_rgb_xyz(g); + b = d3_rgb_xyz(b); + var x = d3_xyz_lab((.4124564 * r + .3575761 * g + .1804375 * b) / d3_lab_X), y = d3_xyz_lab((.2126729 * r + .7151522 * g + .072175 * b) / d3_lab_Y), z = d3_xyz_lab((.0193339 * r + .119192 * g + .9503041 * b) / d3_lab_Z); + return d3_lab(116 * y - 16, 500 * (x - y), 200 * (y - z)); + } + function d3_rgb_xyz(r) { + return (r /= 255) <= .04045 ? r / 12.92 : Math.pow((r + .055) / 1.055, 2.4); + } + function d3_rgb_parseNumber(c) { + var f = parseFloat(c); + return c.charAt(c.length - 1) === "%" ? Math.round(f * 2.55) : f; + } + var d3_rgb_names = d3.map({ + aliceblue: 15792383, + antiquewhite: 16444375, + aqua: 65535, + aquamarine: 8388564, + azure: 15794175, + beige: 16119260, + bisque: 16770244, + black: 0, + blanchedalmond: 16772045, + blue: 255, + blueviolet: 9055202, + brown: 10824234, + burlywood: 14596231, + cadetblue: 6266528, + chartreuse: 8388352, + chocolate: 13789470, + coral: 16744272, + cornflowerblue: 6591981, + cornsilk: 16775388, + crimson: 14423100, + cyan: 65535, + darkblue: 139, + darkcyan: 35723, + darkgoldenrod: 12092939, + darkgray: 11119017, + darkgreen: 25600, + darkgrey: 11119017, + darkkhaki: 12433259, + darkmagenta: 9109643, + darkolivegreen: 5597999, + darkorange: 16747520, + darkorchid: 10040012, + darkred: 9109504, + darksalmon: 15308410, + darkseagreen: 9419919, + darkslateblue: 4734347, + darkslategray: 3100495, + darkslategrey: 3100495, + darkturquoise: 52945, + darkviolet: 9699539, + deeppink: 16716947, + deepskyblue: 49151, + dimgray: 6908265, + dimgrey: 6908265, + dodgerblue: 2003199, + firebrick: 11674146, + floralwhite: 16775920, + forestgreen: 2263842, + fuchsia: 16711935, + gainsboro: 14474460, + ghostwhite: 16316671, + gold: 16766720, + goldenrod: 14329120, + gray: 8421504, + green: 32768, + greenyellow: 11403055, + grey: 8421504, + honeydew: 15794160, + hotpink: 16738740, + indianred: 13458524, + indigo: 4915330, + ivory: 16777200, + khaki: 15787660, + lavender: 15132410, + lavenderblush: 16773365, + lawngreen: 8190976, + lemonchiffon: 16775885, + lightblue: 11393254, + lightcoral: 15761536, + lightcyan: 14745599, + lightgoldenrodyellow: 16448210, + lightgray: 13882323, + lightgreen: 9498256, + lightgrey: 13882323, + lightpink: 16758465, + lightsalmon: 16752762, + lightseagreen: 2142890, + lightskyblue: 8900346, + lightslategray: 7833753, + lightslategrey: 7833753, + lightsteelblue: 11584734, + lightyellow: 16777184, + lime: 65280, + limegreen: 3329330, + linen: 16445670, + magenta: 16711935, + maroon: 8388608, + mediumaquamarine: 6737322, + mediumblue: 205, + mediumorchid: 12211667, + mediumpurple: 9662683, + mediumseagreen: 3978097, + mediumslateblue: 8087790, + mediumspringgreen: 64154, + mediumturquoise: 4772300, + mediumvioletred: 13047173, + midnightblue: 1644912, + mintcream: 16121850, + mistyrose: 16770273, + moccasin: 16770229, + navajowhite: 16768685, + navy: 128, + oldlace: 16643558, + olive: 8421376, + olivedrab: 7048739, + orange: 16753920, + orangered: 16729344, + orchid: 14315734, + palegoldenrod: 15657130, + palegreen: 10025880, + paleturquoise: 11529966, + palevioletred: 14381203, + papayawhip: 16773077, + peachpuff: 16767673, + peru: 13468991, + pink: 16761035, + plum: 14524637, + powderblue: 11591910, + purple: 8388736, + red: 16711680, + rosybrown: 12357519, + royalblue: 4286945, + saddlebrown: 9127187, + salmon: 16416882, + sandybrown: 16032864, + seagreen: 3050327, + seashell: 16774638, + sienna: 10506797, + silver: 12632256, + skyblue: 8900331, + slateblue: 6970061, + slategray: 7372944, + slategrey: 7372944, + snow: 16775930, + springgreen: 65407, + steelblue: 4620980, + tan: 13808780, + teal: 32896, + thistle: 14204888, + tomato: 16737095, + turquoise: 4251856, + violet: 15631086, + wheat: 16113331, + white: 16777215, + whitesmoke: 16119285, + yellow: 16776960, + yellowgreen: 10145074 + }); + d3_rgb_names.forEach(function(key, value) { + d3_rgb_names.set(key, d3_rgbNumber(value)); + }); + function d3_functor(v) { + return typeof v === "function" ? v : function() { + return v; + }; + } + d3.functor = d3_functor; + function d3_identity(d) { + return d; + } + d3.xhr = d3_xhrType(d3_identity); + function d3_xhrType(response) { + return function(url, mimeType, callback) { + if (arguments.length === 2 && typeof mimeType === "function") callback = mimeType, + mimeType = null; + return d3_xhr(url, mimeType, response, callback); + }; + } + function d3_xhr(url, mimeType, response, callback) { + var xhr = {}, dispatch = d3.dispatch("beforesend", "progress", "load", "error"), headers = {}, request = new XMLHttpRequest(), responseType = null; + if (d3_window.XDomainRequest && !("withCredentials" in request) && /^(http(s)?:)?\/\//.test(url)) request = new XDomainRequest(); + "onload" in request ? request.onload = request.onerror = respond : request.onreadystatechange = function() { + request.readyState > 3 && respond(); + }; + function respond() { + var status = request.status, result; + if (!status && d3_xhrHasResponse(request) || status >= 200 && status < 300 || status === 304) { + try { + result = response.call(xhr, request); + } catch (e) { + dispatch.error.call(xhr, e); + return; + } + dispatch.load.call(xhr, result); + } else { + dispatch.error.call(xhr, request); + } + } + request.onprogress = function(event) { + var o = d3.event; + d3.event = event; + try { + dispatch.progress.call(xhr, request); + } finally { + d3.event = o; + } + }; + xhr.header = function(name, value) { + name = (name + "").toLowerCase(); + if (arguments.length < 2) return headers[name]; + if (value == null) delete headers[name]; else headers[name] = value + ""; + return xhr; + }; + xhr.mimeType = function(value) { + if (!arguments.length) return mimeType; + mimeType = value == null ? null : value + ""; + return xhr; + }; + xhr.responseType = function(value) { + if (!arguments.length) return responseType; + responseType = value; + return xhr; + }; + xhr.response = function(value) { + response = value; + return xhr; + }; + [ "get", "post" ].forEach(function(method) { + xhr[method] = function() { + return xhr.send.apply(xhr, [ method ].concat(d3_array(arguments))); + }; + }); + xhr.send = function(method, data, callback) { + if (arguments.length === 2 && typeof data === "function") callback = data, data = null; + request.open(method, url, true); + if (mimeType != null && !("accept" in headers)) headers["accept"] = mimeType + ",*/*"; + if (request.setRequestHeader) for (var name in headers) request.setRequestHeader(name, headers[name]); + if (mimeType != null && request.overrideMimeType) request.overrideMimeType(mimeType); + if (responseType != null) request.responseType = responseType; + if (callback != null) xhr.on("error", callback).on("load", function(request) { + callback(null, request); + }); + dispatch.beforesend.call(xhr, request); + request.send(data == null ? null : data); + return xhr; + }; + xhr.abort = function() { + request.abort(); + return xhr; + }; + d3.rebind(xhr, dispatch, "on"); + return callback == null ? xhr : xhr.get(d3_xhr_fixCallback(callback)); + } + function d3_xhr_fixCallback(callback) { + return callback.length === 1 ? function(error, request) { + callback(error == null ? request : null); + } : callback; + } + function d3_xhrHasResponse(request) { + var type = request.responseType; + return type && type !== "text" ? request.response : request.responseText; + } + d3.dsv = function(delimiter, mimeType) { + var reFormat = new RegExp('["' + delimiter + "\n]"), delimiterCode = delimiter.charCodeAt(0); + function dsv(url, row, callback) { + if (arguments.length < 3) callback = row, row = null; + var xhr = d3_xhr(url, mimeType, row == null ? response : typedResponse(row), callback); + xhr.row = function(_) { + return arguments.length ? xhr.response((row = _) == null ? response : typedResponse(_)) : row; + }; + return xhr; + } + function response(request) { + return dsv.parse(request.responseText); + } + function typedResponse(f) { + return function(request) { + return dsv.parse(request.responseText, f); + }; + } + dsv.parse = function(text, f) { + var o; + return dsv.parseRows(text, function(row, i) { + if (o) return o(row, i - 1); + var a = new Function("d", "return {" + row.map(function(name, i) { + return JSON.stringify(name) + ": d[" + i + "]"; + }).join(",") + "}"); + o = f ? function(row, i) { + return f(a(row), i); + } : a; + }); + }; + dsv.parseRows = function(text, f) { + var EOL = {}, EOF = {}, rows = [], N = text.length, I = 0, n = 0, t, eol; + function token() { + if (I >= N) return EOF; + if (eol) return eol = false, EOL; + var j = I; + if (text.charCodeAt(j) === 34) { + var i = j; + while (i++ < N) { + if (text.charCodeAt(i) === 34) { + if (text.charCodeAt(i + 1) !== 34) break; + ++i; + } + } + I = i + 2; + var c = text.charCodeAt(i + 1); + if (c === 13) { + eol = true; + if (text.charCodeAt(i + 2) === 10) ++I; + } else if (c === 10) { + eol = true; + } + return text.slice(j + 1, i).replace(/""/g, '"'); + } + while (I < N) { + var c = text.charCodeAt(I++), k = 1; + if (c === 10) eol = true; else if (c === 13) { + eol = true; + if (text.charCodeAt(I) === 10) ++I, ++k; + } else if (c !== delimiterCode) continue; + return text.slice(j, I - k); + } + return text.slice(j); + } + while ((t = token()) !== EOF) { + var a = []; + while (t !== EOL && t !== EOF) { + a.push(t); + t = token(); + } + if (f && (a = f(a, n++)) == null) continue; + rows.push(a); + } + return rows; + }; + dsv.format = function(rows) { + if (Array.isArray(rows[0])) return dsv.formatRows(rows); + var fieldSet = new d3_Set(), fields = []; + rows.forEach(function(row) { + for (var field in row) { + if (!fieldSet.has(field)) { + fields.push(fieldSet.add(field)); + } + } + }); + return [ fields.map(formatValue).join(delimiter) ].concat(rows.map(function(row) { + return fields.map(function(field) { + return formatValue(row[field]); + }).join(delimiter); + })).join("\n"); + }; + dsv.formatRows = function(rows) { + return rows.map(formatRow).join("\n"); + }; + function formatRow(row) { + return row.map(formatValue).join(delimiter); + } + function formatValue(text) { + return reFormat.test(text) ? '"' + text.replace(/\"/g, '""') + '"' : text; + } + return dsv; + }; + d3.csv = d3.dsv(",", "text/csv"); + d3.tsv = d3.dsv(" ", "text/tab-separated-values"); + var d3_timer_queueHead, d3_timer_queueTail, d3_timer_interval, d3_timer_timeout, d3_timer_active, d3_timer_frame = d3_window[d3_vendorSymbol(d3_window, "requestAnimationFrame")] || function(callback) { + setTimeout(callback, 17); + }; + d3.timer = function(callback, delay, then) { + var n = arguments.length; + if (n < 2) delay = 0; + if (n < 3) then = Date.now(); + var time = then + delay, timer = { + c: callback, + t: time, + f: false, + n: null + }; + if (d3_timer_queueTail) d3_timer_queueTail.n = timer; else d3_timer_queueHead = timer; + d3_timer_queueTail = timer; + if (!d3_timer_interval) { + d3_timer_timeout = clearTimeout(d3_timer_timeout); + d3_timer_interval = 1; + d3_timer_frame(d3_timer_step); + } + }; + function d3_timer_step() { + var now = d3_timer_mark(), delay = d3_timer_sweep() - now; + if (delay > 24) { + if (isFinite(delay)) { + clearTimeout(d3_timer_timeout); + d3_timer_timeout = setTimeout(d3_timer_step, delay); + } + d3_timer_interval = 0; + } else { + d3_timer_interval = 1; + d3_timer_frame(d3_timer_step); + } + } + d3.timer.flush = function() { + d3_timer_mark(); + d3_timer_sweep(); + }; + function d3_timer_mark() { + var now = Date.now(); + d3_timer_active = d3_timer_queueHead; + while (d3_timer_active) { + if (now >= d3_timer_active.t) d3_timer_active.f = d3_timer_active.c(now - d3_timer_active.t); + d3_timer_active = d3_timer_active.n; + } + return now; + } + function d3_timer_sweep() { + var t0, t1 = d3_timer_queueHead, time = Infinity; + while (t1) { + if (t1.f) { + t1 = t0 ? t0.n = t1.n : d3_timer_queueHead = t1.n; + } else { + if (t1.t < time) time = t1.t; + t1 = (t0 = t1).n; + } + } + d3_timer_queueTail = t0; + return time; + } + function d3_format_precision(x, p) { + return p - (x ? Math.ceil(Math.log(x) / Math.LN10) : 1); + } + d3.round = function(x, n) { + return n ? Math.round(x * (n = Math.pow(10, n))) / n : Math.round(x); + }; + var d3_formatPrefixes = [ "y", "z", "a", "f", "p", "n", "µ", "m", "", "k", "M", "G", "T", "P", "E", "Z", "Y" ].map(d3_formatPrefix); + d3.formatPrefix = function(value, precision) { + var i = 0; + if (value) { + if (value < 0) value *= -1; + if (precision) value = d3.round(value, d3_format_precision(value, precision)); + i = 1 + Math.floor(1e-12 + Math.log(value) / Math.LN10); + i = Math.max(-24, Math.min(24, Math.floor((i - 1) / 3) * 3)); + } + return d3_formatPrefixes[8 + i / 3]; + }; + function d3_formatPrefix(d, i) { + var k = Math.pow(10, abs(8 - i) * 3); + return { + scale: i > 8 ? function(d) { + return d / k; + } : function(d) { + return d * k; + }, + symbol: d + }; + } + function d3_locale_numberFormat(locale) { + var locale_decimal = locale.decimal, locale_thousands = locale.thousands, locale_grouping = locale.grouping, locale_currency = locale.currency, formatGroup = locale_grouping && locale_thousands ? function(value, width) { + var i = value.length, t = [], j = 0, g = locale_grouping[0], length = 0; + while (i > 0 && g > 0) { + if (length + g + 1 > width) g = Math.max(1, width - length); + t.push(value.substring(i -= g, i + g)); + if ((length += g + 1) > width) break; + g = locale_grouping[j = (j + 1) % locale_grouping.length]; + } + return t.reverse().join(locale_thousands); + } : d3_identity; + return function(specifier) { + var match = d3_format_re.exec(specifier), fill = match[1] || " ", align = match[2] || ">", sign = match[3] || "-", symbol = match[4] || "", zfill = match[5], width = +match[6], comma = match[7], precision = match[8], type = match[9], scale = 1, prefix = "", suffix = "", integer = false, exponent = true; + if (precision) precision = +precision.substring(1); + if (zfill || fill === "0" && align === "=") { + zfill = fill = "0"; + align = "="; + } + switch (type) { + case "n": + comma = true; + type = "g"; + break; + + case "%": + scale = 100; + suffix = "%"; + type = "f"; + break; + + case "p": + scale = 100; + suffix = "%"; + type = "r"; + break; + + case "b": + case "o": + case "x": + case "X": + if (symbol === "#") prefix = "0" + type.toLowerCase(); + + case "c": + exponent = false; + + case "d": + integer = true; + precision = 0; + break; + + case "s": + scale = -1; + type = "r"; + break; + } + if (symbol === "$") prefix = locale_currency[0], suffix = locale_currency[1]; + if (type == "r" && !precision) type = "g"; + if (precision != null) { + if (type == "g") precision = Math.max(1, Math.min(21, precision)); else if (type == "e" || type == "f") precision = Math.max(0, Math.min(20, precision)); + } + type = d3_format_types.get(type) || d3_format_typeDefault; + var zcomma = zfill && comma; + return function(value) { + var fullSuffix = suffix; + if (integer && value % 1) return ""; + var negative = value < 0 || value === 0 && 1 / value < 0 ? (value = -value, "-") : sign === "-" ? "" : sign; + if (scale < 0) { + var unit = d3.formatPrefix(value, precision); + value = unit.scale(value); + fullSuffix = unit.symbol + suffix; + } else { + value *= scale; + } + value = type(value, precision); + var i = value.lastIndexOf("."), before, after; + if (i < 0) { + var j = exponent ? value.lastIndexOf("e") : -1; + if (j < 0) before = value, after = ""; else before = value.substring(0, j), after = value.substring(j); + } else { + before = value.substring(0, i); + after = locale_decimal + value.substring(i + 1); + } + if (!zfill && comma) before = formatGroup(before, Infinity); + var length = prefix.length + before.length + after.length + (zcomma ? 0 : negative.length), padding = length < width ? new Array(length = width - length + 1).join(fill) : ""; + if (zcomma) before = formatGroup(padding + before, padding.length ? width - after.length : Infinity); + negative += prefix; + value = before + after; + return (align === "<" ? negative + value + padding : align === ">" ? padding + negative + value : align === "^" ? padding.substring(0, length >>= 1) + negative + value + padding.substring(length) : negative + (zcomma ? value : padding + value)) + fullSuffix; + }; + }; + } + var d3_format_re = /(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i; + var d3_format_types = d3.map({ + b: function(x) { + return x.toString(2); + }, + c: function(x) { + return String.fromCharCode(x); + }, + o: function(x) { + return x.toString(8); + }, + x: function(x) { + return x.toString(16); + }, + X: function(x) { + return x.toString(16).toUpperCase(); + }, + g: function(x, p) { + return x.toPrecision(p); + }, + e: function(x, p) { + return x.toExponential(p); + }, + f: function(x, p) { + return x.toFixed(p); + }, + r: function(x, p) { + return (x = d3.round(x, d3_format_precision(x, p))).toFixed(Math.max(0, Math.min(20, d3_format_precision(x * (1 + 1e-15), p)))); + } + }); + function d3_format_typeDefault(x) { + return x + ""; + } + var d3_time = d3.time = {}, d3_date = Date; + function d3_date_utc() { + this._ = new Date(arguments.length > 1 ? Date.UTC.apply(this, arguments) : arguments[0]); + } + d3_date_utc.prototype = { + getDate: function() { + return this._.getUTCDate(); + }, + getDay: function() { + return this._.getUTCDay(); + }, + getFullYear: function() { + return this._.getUTCFullYear(); + }, + getHours: function() { + return this._.getUTCHours(); + }, + getMilliseconds: function() { + return this._.getUTCMilliseconds(); + }, + getMinutes: function() { + return this._.getUTCMinutes(); + }, + getMonth: function() { + return this._.getUTCMonth(); + }, + getSeconds: function() { + return this._.getUTCSeconds(); + }, + getTime: function() { + return this._.getTime(); + }, + getTimezoneOffset: function() { + return 0; + }, + valueOf: function() { + return this._.valueOf(); + }, + setDate: function() { + d3_time_prototype.setUTCDate.apply(this._, arguments); + }, + setDay: function() { + d3_time_prototype.setUTCDay.apply(this._, arguments); + }, + setFullYear: function() { + d3_time_prototype.setUTCFullYear.apply(this._, arguments); + }, + setHours: function() { + d3_time_prototype.setUTCHours.apply(this._, arguments); + }, + setMilliseconds: function() { + d3_time_prototype.setUTCMilliseconds.apply(this._, arguments); + }, + setMinutes: function() { + d3_time_prototype.setUTCMinutes.apply(this._, arguments); + }, + setMonth: function() { + d3_time_prototype.setUTCMonth.apply(this._, arguments); + }, + setSeconds: function() { + d3_time_prototype.setUTCSeconds.apply(this._, arguments); + }, + setTime: function() { + d3_time_prototype.setTime.apply(this._, arguments); + } + }; + var d3_time_prototype = Date.prototype; + function d3_time_interval(local, step, number) { + function round(date) { + var d0 = local(date), d1 = offset(d0, 1); + return date - d0 < d1 - date ? d0 : d1; + } + function ceil(date) { + step(date = local(new d3_date(date - 1)), 1); + return date; + } + function offset(date, k) { + step(date = new d3_date(+date), k); + return date; + } + function range(t0, t1, dt) { + var time = ceil(t0), times = []; + if (dt > 1) { + while (time < t1) { + if (!(number(time) % dt)) times.push(new Date(+time)); + step(time, 1); + } + } else { + while (time < t1) times.push(new Date(+time)), step(time, 1); + } + return times; + } + function range_utc(t0, t1, dt) { + try { + d3_date = d3_date_utc; + var utc = new d3_date_utc(); + utc._ = t0; + return range(utc, t1, dt); + } finally { + d3_date = Date; + } + } + local.floor = local; + local.round = round; + local.ceil = ceil; + local.offset = offset; + local.range = range; + var utc = local.utc = d3_time_interval_utc(local); + utc.floor = utc; + utc.round = d3_time_interval_utc(round); + utc.ceil = d3_time_interval_utc(ceil); + utc.offset = d3_time_interval_utc(offset); + utc.range = range_utc; + return local; + } + function d3_time_interval_utc(method) { + return function(date, k) { + try { + d3_date = d3_date_utc; + var utc = new d3_date_utc(); + utc._ = date; + return method(utc, k)._; + } finally { + d3_date = Date; + } + }; + } + d3_time.year = d3_time_interval(function(date) { + date = d3_time.day(date); + date.setMonth(0, 1); + return date; + }, function(date, offset) { + date.setFullYear(date.getFullYear() + offset); + }, function(date) { + return date.getFullYear(); + }); + d3_time.years = d3_time.year.range; + d3_time.years.utc = d3_time.year.utc.range; + d3_time.day = d3_time_interval(function(date) { + var day = new d3_date(2e3, 0); + day.setFullYear(date.getFullYear(), date.getMonth(), date.getDate()); + return day; + }, function(date, offset) { + date.setDate(date.getDate() + offset); + }, function(date) { + return date.getDate() - 1; + }); + d3_time.days = d3_time.day.range; + d3_time.days.utc = d3_time.day.utc.range; + d3_time.dayOfYear = function(date) { + var year = d3_time.year(date); + return Math.floor((date - year - (date.getTimezoneOffset() - year.getTimezoneOffset()) * 6e4) / 864e5); + }; + [ "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday" ].forEach(function(day, i) { + i = 7 - i; + var interval = d3_time[day] = d3_time_interval(function(date) { + (date = d3_time.day(date)).setDate(date.getDate() - (date.getDay() + i) % 7); + return date; + }, function(date, offset) { + date.setDate(date.getDate() + Math.floor(offset) * 7); + }, function(date) { + var day = d3_time.year(date).getDay(); + return Math.floor((d3_time.dayOfYear(date) + (day + i) % 7) / 7) - (day !== i); + }); + d3_time[day + "s"] = interval.range; + d3_time[day + "s"].utc = interval.utc.range; + d3_time[day + "OfYear"] = function(date) { + var day = d3_time.year(date).getDay(); + return Math.floor((d3_time.dayOfYear(date) + (day + i) % 7) / 7); + }; + }); + d3_time.week = d3_time.sunday; + d3_time.weeks = d3_time.sunday.range; + d3_time.weeks.utc = d3_time.sunday.utc.range; + d3_time.weekOfYear = d3_time.sundayOfYear; + function d3_locale_timeFormat(locale) { + var locale_dateTime = locale.dateTime, locale_date = locale.date, locale_time = locale.time, locale_periods = locale.periods, locale_days = locale.days, locale_shortDays = locale.shortDays, locale_months = locale.months, locale_shortMonths = locale.shortMonths; + function d3_time_format(template) { + var n = template.length; + function format(date) { + var string = [], i = -1, j = 0, c, p, f; + while (++i < n) { + if (template.charCodeAt(i) === 37) { + string.push(template.slice(j, i)); + if ((p = d3_time_formatPads[c = template.charAt(++i)]) != null) c = template.charAt(++i); + if (f = d3_time_formats[c]) c = f(date, p == null ? c === "e" ? " " : "0" : p); + string.push(c); + j = i + 1; + } + } + string.push(template.slice(j, i)); + return string.join(""); + } + format.parse = function(string) { + var d = { + y: 1900, + m: 0, + d: 1, + H: 0, + M: 0, + S: 0, + L: 0, + Z: null + }, i = d3_time_parse(d, template, string, 0); + if (i != string.length) return null; + if ("p" in d) d.H = d.H % 12 + d.p * 12; + var localZ = d.Z != null && d3_date !== d3_date_utc, date = new (localZ ? d3_date_utc : d3_date)(); + if ("j" in d) date.setFullYear(d.y, 0, d.j); else if ("w" in d && ("W" in d || "U" in d)) { + date.setFullYear(d.y, 0, 1); + date.setFullYear(d.y, 0, "W" in d ? (d.w + 6) % 7 + d.W * 7 - (date.getDay() + 5) % 7 : d.w + d.U * 7 - (date.getDay() + 6) % 7); + } else date.setFullYear(d.y, d.m, d.d); + date.setHours(d.H + (d.Z / 100 | 0), d.M + d.Z % 100, d.S, d.L); + return localZ ? date._ : date; + }; + format.toString = function() { + return template; + }; + return format; + } + function d3_time_parse(date, template, string, j) { + var c, p, t, i = 0, n = template.length, m = string.length; + while (i < n) { + if (j >= m) return -1; + c = template.charCodeAt(i++); + if (c === 37) { + t = template.charAt(i++); + p = d3_time_parsers[t in d3_time_formatPads ? template.charAt(i++) : t]; + if (!p || (j = p(date, string, j)) < 0) return -1; + } else if (c != string.charCodeAt(j++)) { + return -1; + } + } + return j; + } + d3_time_format.utc = function(template) { + var local = d3_time_format(template); + function format(date) { + try { + d3_date = d3_date_utc; + var utc = new d3_date(); + utc._ = date; + return local(utc); + } finally { + d3_date = Date; + } + } + format.parse = function(string) { + try { + d3_date = d3_date_utc; + var date = local.parse(string); + return date && date._; + } finally { + d3_date = Date; + } + }; + format.toString = local.toString; + return format; + }; + d3_time_format.multi = d3_time_format.utc.multi = d3_time_formatMulti; + var d3_time_periodLookup = d3.map(), d3_time_dayRe = d3_time_formatRe(locale_days), d3_time_dayLookup = d3_time_formatLookup(locale_days), d3_time_dayAbbrevRe = d3_time_formatRe(locale_shortDays), d3_time_dayAbbrevLookup = d3_time_formatLookup(locale_shortDays), d3_time_monthRe = d3_time_formatRe(locale_months), d3_time_monthLookup = d3_time_formatLookup(locale_months), d3_time_monthAbbrevRe = d3_time_formatRe(locale_shortMonths), d3_time_monthAbbrevLookup = d3_time_formatLookup(locale_shortMonths); + locale_periods.forEach(function(p, i) { + d3_time_periodLookup.set(p.toLowerCase(), i); + }); + var d3_time_formats = { + a: function(d) { + return locale_shortDays[d.getDay()]; + }, + A: function(d) { + return locale_days[d.getDay()]; + }, + b: function(d) { + return locale_shortMonths[d.getMonth()]; + }, + B: function(d) { + return locale_months[d.getMonth()]; + }, + c: d3_time_format(locale_dateTime), + d: function(d, p) { + return d3_time_formatPad(d.getDate(), p, 2); + }, + e: function(d, p) { + return d3_time_formatPad(d.getDate(), p, 2); + }, + H: function(d, p) { + return d3_time_formatPad(d.getHours(), p, 2); + }, + I: function(d, p) { + return d3_time_formatPad(d.getHours() % 12 || 12, p, 2); + }, + j: function(d, p) { + return d3_time_formatPad(1 + d3_time.dayOfYear(d), p, 3); + }, + L: function(d, p) { + return d3_time_formatPad(d.getMilliseconds(), p, 3); + }, + m: function(d, p) { + return d3_time_formatPad(d.getMonth() + 1, p, 2); + }, + M: function(d, p) { + return d3_time_formatPad(d.getMinutes(), p, 2); + }, + p: function(d) { + return locale_periods[+(d.getHours() >= 12)]; + }, + S: function(d, p) { + return d3_time_formatPad(d.getSeconds(), p, 2); + }, + U: function(d, p) { + return d3_time_formatPad(d3_time.sundayOfYear(d), p, 2); + }, + w: function(d) { + return d.getDay(); + }, + W: function(d, p) { + return d3_time_formatPad(d3_time.mondayOfYear(d), p, 2); + }, + x: d3_time_format(locale_date), + X: d3_time_format(locale_time), + y: function(d, p) { + return d3_time_formatPad(d.getFullYear() % 100, p, 2); + }, + Y: function(d, p) { + return d3_time_formatPad(d.getFullYear() % 1e4, p, 4); + }, + Z: d3_time_zone, + "%": function() { + return "%"; + } + }; + var d3_time_parsers = { + a: d3_time_parseWeekdayAbbrev, + A: d3_time_parseWeekday, + b: d3_time_parseMonthAbbrev, + B: d3_time_parseMonth, + c: d3_time_parseLocaleFull, + d: d3_time_parseDay, + e: d3_time_parseDay, + H: d3_time_parseHour24, + I: d3_time_parseHour24, + j: d3_time_parseDayOfYear, + L: d3_time_parseMilliseconds, + m: d3_time_parseMonthNumber, + M: d3_time_parseMinutes, + p: d3_time_parseAmPm, + S: d3_time_parseSeconds, + U: d3_time_parseWeekNumberSunday, + w: d3_time_parseWeekdayNumber, + W: d3_time_parseWeekNumberMonday, + x: d3_time_parseLocaleDate, + X: d3_time_parseLocaleTime, + y: d3_time_parseYear, + Y: d3_time_parseFullYear, + Z: d3_time_parseZone, + "%": d3_time_parseLiteralPercent + }; + function d3_time_parseWeekdayAbbrev(date, string, i) { + d3_time_dayAbbrevRe.lastIndex = 0; + var n = d3_time_dayAbbrevRe.exec(string.slice(i)); + return n ? (date.w = d3_time_dayAbbrevLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; + } + function d3_time_parseWeekday(date, string, i) { + d3_time_dayRe.lastIndex = 0; + var n = d3_time_dayRe.exec(string.slice(i)); + return n ? (date.w = d3_time_dayLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; + } + function d3_time_parseMonthAbbrev(date, string, i) { + d3_time_monthAbbrevRe.lastIndex = 0; + var n = d3_time_monthAbbrevRe.exec(string.slice(i)); + return n ? (date.m = d3_time_monthAbbrevLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; + } + function d3_time_parseMonth(date, string, i) { + d3_time_monthRe.lastIndex = 0; + var n = d3_time_monthRe.exec(string.slice(i)); + return n ? (date.m = d3_time_monthLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; + } + function d3_time_parseLocaleFull(date, string, i) { + return d3_time_parse(date, d3_time_formats.c.toString(), string, i); + } + function d3_time_parseLocaleDate(date, string, i) { + return d3_time_parse(date, d3_time_formats.x.toString(), string, i); + } + function d3_time_parseLocaleTime(date, string, i) { + return d3_time_parse(date, d3_time_formats.X.toString(), string, i); + } + function d3_time_parseAmPm(date, string, i) { + var n = d3_time_periodLookup.get(string.slice(i, i += 2).toLowerCase()); + return n == null ? -1 : (date.p = n, i); + } + return d3_time_format; + } + var d3_time_formatPads = { + "-": "", + _: " ", + "0": "0" + }, d3_time_numberRe = /^\s*\d+/, d3_time_percentRe = /^%/; + function d3_time_formatPad(value, fill, width) { + var sign = value < 0 ? "-" : "", string = (sign ? -value : value) + "", length = string.length; + return sign + (length < width ? new Array(width - length + 1).join(fill) + string : string); + } + function d3_time_formatRe(names) { + return new RegExp("^(?:" + names.map(d3.requote).join("|") + ")", "i"); + } + function d3_time_formatLookup(names) { + var map = new d3_Map(), i = -1, n = names.length; + while (++i < n) map.set(names[i].toLowerCase(), i); + return map; + } + function d3_time_parseWeekdayNumber(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 1)); + return n ? (date.w = +n[0], i + n[0].length) : -1; + } + function d3_time_parseWeekNumberSunday(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i)); + return n ? (date.U = +n[0], i + n[0].length) : -1; + } + function d3_time_parseWeekNumberMonday(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i)); + return n ? (date.W = +n[0], i + n[0].length) : -1; + } + function d3_time_parseFullYear(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 4)); + return n ? (date.y = +n[0], i + n[0].length) : -1; + } + function d3_time_parseYear(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); + return n ? (date.y = d3_time_expandYear(+n[0]), i + n[0].length) : -1; + } + function d3_time_parseZone(date, string, i) { + return /^[+-]\d{4}$/.test(string = string.slice(i, i + 5)) ? (date.Z = -string, + i + 5) : -1; + } + function d3_time_expandYear(d) { + return d + (d > 68 ? 1900 : 2e3); + } + function d3_time_parseMonthNumber(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); + return n ? (date.m = n[0] - 1, i + n[0].length) : -1; + } + function d3_time_parseDay(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); + return n ? (date.d = +n[0], i + n[0].length) : -1; + } + function d3_time_parseDayOfYear(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 3)); + return n ? (date.j = +n[0], i + n[0].length) : -1; + } + function d3_time_parseHour24(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); + return n ? (date.H = +n[0], i + n[0].length) : -1; + } + function d3_time_parseMinutes(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); + return n ? (date.M = +n[0], i + n[0].length) : -1; + } + function d3_time_parseSeconds(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); + return n ? (date.S = +n[0], i + n[0].length) : -1; + } + function d3_time_parseMilliseconds(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 3)); + return n ? (date.L = +n[0], i + n[0].length) : -1; + } + function d3_time_zone(d) { + var z = d.getTimezoneOffset(), zs = z > 0 ? "-" : "+", zh = abs(z) / 60 | 0, zm = abs(z) % 60; + return zs + d3_time_formatPad(zh, "0", 2) + d3_time_formatPad(zm, "0", 2); + } + function d3_time_parseLiteralPercent(date, string, i) { + d3_time_percentRe.lastIndex = 0; + var n = d3_time_percentRe.exec(string.slice(i, i + 1)); + return n ? i + n[0].length : -1; + } + function d3_time_formatMulti(formats) { + var n = formats.length, i = -1; + while (++i < n) formats[i][0] = this(formats[i][0]); + return function(date) { + var i = 0, f = formats[i]; + while (!f[1](date)) f = formats[++i]; + return f[0](date); + }; + } + d3.locale = function(locale) { + return { + numberFormat: d3_locale_numberFormat(locale), + timeFormat: d3_locale_timeFormat(locale) + }; + }; + var d3_locale_enUS = d3.locale({ + decimal: ".", + thousands: ",", + grouping: [ 3 ], + currency: [ "$", "" ], + dateTime: "%a %b %e %X %Y", + date: "%m/%d/%Y", + time: "%H:%M:%S", + periods: [ "AM", "PM" ], + days: [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ], + shortDays: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ], + months: [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], + shortMonths: [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ] + }); + d3.format = d3_locale_enUS.numberFormat; + d3.geo = {}; + function d3_adder() {} + d3_adder.prototype = { + s: 0, + t: 0, + add: function(y) { + d3_adderSum(y, this.t, d3_adderTemp); + d3_adderSum(d3_adderTemp.s, this.s, this); + if (this.s) this.t += d3_adderTemp.t; else this.s = d3_adderTemp.t; + }, + reset: function() { + this.s = this.t = 0; + }, + valueOf: function() { + return this.s; + } + }; + var d3_adderTemp = new d3_adder(); + function d3_adderSum(a, b, o) { + var x = o.s = a + b, bv = x - a, av = x - bv; + o.t = a - av + (b - bv); + } + d3.geo.stream = function(object, listener) { + if (object && d3_geo_streamObjectType.hasOwnProperty(object.type)) { + d3_geo_streamObjectType[object.type](object, listener); + } else { + d3_geo_streamGeometry(object, listener); + } + }; + function d3_geo_streamGeometry(geometry, listener) { + if (geometry && d3_geo_streamGeometryType.hasOwnProperty(geometry.type)) { + d3_geo_streamGeometryType[geometry.type](geometry, listener); + } + } + var d3_geo_streamObjectType = { + Feature: function(feature, listener) { + d3_geo_streamGeometry(feature.geometry, listener); + }, + FeatureCollection: function(object, listener) { + var features = object.features, i = -1, n = features.length; + while (++i < n) d3_geo_streamGeometry(features[i].geometry, listener); + } + }; + var d3_geo_streamGeometryType = { + Sphere: function(object, listener) { + listener.sphere(); + }, + Point: function(object, listener) { + object = object.coordinates; + listener.point(object[0], object[1], object[2]); + }, + MultiPoint: function(object, listener) { + var coordinates = object.coordinates, i = -1, n = coordinates.length; + while (++i < n) object = coordinates[i], listener.point(object[0], object[1], object[2]); + }, + LineString: function(object, listener) { + d3_geo_streamLine(object.coordinates, listener, 0); + }, + MultiLineString: function(object, listener) { + var coordinates = object.coordinates, i = -1, n = coordinates.length; + while (++i < n) d3_geo_streamLine(coordinates[i], listener, 0); + }, + Polygon: function(object, listener) { + d3_geo_streamPolygon(object.coordinates, listener); + }, + MultiPolygon: function(object, listener) { + var coordinates = object.coordinates, i = -1, n = coordinates.length; + while (++i < n) d3_geo_streamPolygon(coordinates[i], listener); + }, + GeometryCollection: function(object, listener) { + var geometries = object.geometries, i = -1, n = geometries.length; + while (++i < n) d3_geo_streamGeometry(geometries[i], listener); + } + }; + function d3_geo_streamLine(coordinates, listener, closed) { + var i = -1, n = coordinates.length - closed, coordinate; + listener.lineStart(); + while (++i < n) coordinate = coordinates[i], listener.point(coordinate[0], coordinate[1], coordinate[2]); + listener.lineEnd(); + } + function d3_geo_streamPolygon(coordinates, listener) { + var i = -1, n = coordinates.length; + listener.polygonStart(); + while (++i < n) d3_geo_streamLine(coordinates[i], listener, 1); + listener.polygonEnd(); + } + d3.geo.area = function(object) { + d3_geo_areaSum = 0; + d3.geo.stream(object, d3_geo_area); + return d3_geo_areaSum; + }; + var d3_geo_areaSum, d3_geo_areaRingSum = new d3_adder(); + var d3_geo_area = { + sphere: function() { + d3_geo_areaSum += 4 * π; + }, + point: d3_noop, + lineStart: d3_noop, + lineEnd: d3_noop, + polygonStart: function() { + d3_geo_areaRingSum.reset(); + d3_geo_area.lineStart = d3_geo_areaRingStart; + }, + polygonEnd: function() { + var area = 2 * d3_geo_areaRingSum; + d3_geo_areaSum += area < 0 ? 4 * π + area : area; + d3_geo_area.lineStart = d3_geo_area.lineEnd = d3_geo_area.point = d3_noop; + } + }; + function d3_geo_areaRingStart() { + var λ00, φ00, λ0, cosφ0, sinφ0; + d3_geo_area.point = function(λ, φ) { + d3_geo_area.point = nextPoint; + λ0 = (λ00 = λ) * d3_radians, cosφ0 = Math.cos(φ = (φ00 = φ) * d3_radians / 2 + π / 4), + sinφ0 = Math.sin(φ); + }; + function nextPoint(λ, φ) { + λ *= d3_radians; + φ = φ * d3_radians / 2 + π / 4; + var dλ = λ - λ0, sdλ = dλ >= 0 ? 1 : -1, adλ = sdλ * dλ, cosφ = Math.cos(φ), sinφ = Math.sin(φ), k = sinφ0 * sinφ, u = cosφ0 * cosφ + k * Math.cos(adλ), v = k * sdλ * Math.sin(adλ); + d3_geo_areaRingSum.add(Math.atan2(v, u)); + λ0 = λ, cosφ0 = cosφ, sinφ0 = sinφ; + } + d3_geo_area.lineEnd = function() { + nextPoint(λ00, φ00); + }; + } + function d3_geo_cartesian(spherical) { + var λ = spherical[0], φ = spherical[1], cosφ = Math.cos(φ); + return [ cosφ * Math.cos(λ), cosφ * Math.sin(λ), Math.sin(φ) ]; + } + function d3_geo_cartesianDot(a, b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + } + function d3_geo_cartesianCross(a, b) { + return [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]; + } + function d3_geo_cartesianAdd(a, b) { + a[0] += b[0]; + a[1] += b[1]; + a[2] += b[2]; + } + function d3_geo_cartesianScale(vector, k) { + return [ vector[0] * k, vector[1] * k, vector[2] * k ]; + } + function d3_geo_cartesianNormalize(d) { + var l = Math.sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]); + d[0] /= l; + d[1] /= l; + d[2] /= l; + } + function d3_geo_spherical(cartesian) { + return [ Math.atan2(cartesian[1], cartesian[0]), d3_asin(cartesian[2]) ]; + } + function d3_geo_sphericalEqual(a, b) { + return abs(a[0] - b[0]) < ε && abs(a[1] - b[1]) < ε; + } + d3.geo.bounds = function() { + var λ0, φ0, λ1, φ1, λ_, λ__, φ__, p0, dλSum, ranges, range; + var bound = { + point: point, + lineStart: lineStart, + lineEnd: lineEnd, + polygonStart: function() { + bound.point = ringPoint; + bound.lineStart = ringStart; + bound.lineEnd = ringEnd; + dλSum = 0; + d3_geo_area.polygonStart(); + }, + polygonEnd: function() { + d3_geo_area.polygonEnd(); + bound.point = point; + bound.lineStart = lineStart; + bound.lineEnd = lineEnd; + if (d3_geo_areaRingSum < 0) λ0 = -(λ1 = 180), φ0 = -(φ1 = 90); else if (dλSum > ε) φ1 = 90; else if (dλSum < -ε) φ0 = -90; + range[0] = λ0, range[1] = λ1; + } + }; + function point(λ, φ) { + ranges.push(range = [ λ0 = λ, λ1 = λ ]); + if (φ < φ0) φ0 = φ; + if (φ > φ1) φ1 = φ; + } + function linePoint(λ, φ) { + var p = d3_geo_cartesian([ λ * d3_radians, φ * d3_radians ]); + if (p0) { + var normal = d3_geo_cartesianCross(p0, p), equatorial = [ normal[1], -normal[0], 0 ], inflection = d3_geo_cartesianCross(equatorial, normal); + d3_geo_cartesianNormalize(inflection); + inflection = d3_geo_spherical(inflection); + var dλ = λ - λ_, s = dλ > 0 ? 1 : -1, λi = inflection[0] * d3_degrees * s, antimeridian = abs(dλ) > 180; + if (antimeridian ^ (s * λ_ < λi && λi < s * λ)) { + var φi = inflection[1] * d3_degrees; + if (φi > φ1) φ1 = φi; + } else if (λi = (λi + 360) % 360 - 180, antimeridian ^ (s * λ_ < λi && λi < s * λ)) { + var φi = -inflection[1] * d3_degrees; + if (φi < φ0) φ0 = φi; + } else { + if (φ < φ0) φ0 = φ; + if (φ > φ1) φ1 = φ; + } + if (antimeridian) { + if (λ < λ_) { + if (angle(λ0, λ) > angle(λ0, λ1)) λ1 = λ; + } else { + if (angle(λ, λ1) > angle(λ0, λ1)) λ0 = λ; + } + } else { + if (λ1 >= λ0) { + if (λ < λ0) λ0 = λ; + if (λ > λ1) λ1 = λ; + } else { + if (λ > λ_) { + if (angle(λ0, λ) > angle(λ0, λ1)) λ1 = λ; + } else { + if (angle(λ, λ1) > angle(λ0, λ1)) λ0 = λ; + } + } + } + } else { + point(λ, φ); + } + p0 = p, λ_ = λ; + } + function lineStart() { + bound.point = linePoint; + } + function lineEnd() { + range[0] = λ0, range[1] = λ1; + bound.point = point; + p0 = null; + } + function ringPoint(λ, φ) { + if (p0) { + var dλ = λ - λ_; + dλSum += abs(dλ) > 180 ? dλ + (dλ > 0 ? 360 : -360) : dλ; + } else λ__ = λ, φ__ = φ; + d3_geo_area.point(λ, φ); + linePoint(λ, φ); + } + function ringStart() { + d3_geo_area.lineStart(); + } + function ringEnd() { + ringPoint(λ__, φ__); + d3_geo_area.lineEnd(); + if (abs(dλSum) > ε) λ0 = -(λ1 = 180); + range[0] = λ0, range[1] = λ1; + p0 = null; + } + function angle(λ0, λ1) { + return (λ1 -= λ0) < 0 ? λ1 + 360 : λ1; + } + function compareRanges(a, b) { + return a[0] - b[0]; + } + function withinRange(x, range) { + return range[0] <= range[1] ? range[0] <= x && x <= range[1] : x < range[0] || range[1] < x; + } + return function(feature) { + φ1 = λ1 = -(λ0 = φ0 = Infinity); + ranges = []; + d3.geo.stream(feature, bound); + var n = ranges.length; + if (n) { + ranges.sort(compareRanges); + for (var i = 1, a = ranges[0], b, merged = [ a ]; i < n; ++i) { + b = ranges[i]; + if (withinRange(b[0], a) || withinRange(b[1], a)) { + if (angle(a[0], b[1]) > angle(a[0], a[1])) a[1] = b[1]; + if (angle(b[0], a[1]) > angle(a[0], a[1])) a[0] = b[0]; + } else { + merged.push(a = b); + } + } + var best = -Infinity, dλ; + for (var n = merged.length - 1, i = 0, a = merged[n], b; i <= n; a = b, ++i) { + b = merged[i]; + if ((dλ = angle(a[1], b[0])) > best) best = dλ, λ0 = b[0], λ1 = a[1]; + } + } + ranges = range = null; + return λ0 === Infinity || φ0 === Infinity ? [ [ NaN, NaN ], [ NaN, NaN ] ] : [ [ λ0, φ0 ], [ λ1, φ1 ] ]; + }; + }(); + d3.geo.centroid = function(object) { + d3_geo_centroidW0 = d3_geo_centroidW1 = d3_geo_centroidX0 = d3_geo_centroidY0 = d3_geo_centroidZ0 = d3_geo_centroidX1 = d3_geo_centroidY1 = d3_geo_centroidZ1 = d3_geo_centroidX2 = d3_geo_centroidY2 = d3_geo_centroidZ2 = 0; + d3.geo.stream(object, d3_geo_centroid); + var x = d3_geo_centroidX2, y = d3_geo_centroidY2, z = d3_geo_centroidZ2, m = x * x + y * y + z * z; + if (m < ε2) { + x = d3_geo_centroidX1, y = d3_geo_centroidY1, z = d3_geo_centroidZ1; + if (d3_geo_centroidW1 < ε) x = d3_geo_centroidX0, y = d3_geo_centroidY0, z = d3_geo_centroidZ0; + m = x * x + y * y + z * z; + if (m < ε2) return [ NaN, NaN ]; + } + return [ Math.atan2(y, x) * d3_degrees, d3_asin(z / Math.sqrt(m)) * d3_degrees ]; + }; + var d3_geo_centroidW0, d3_geo_centroidW1, d3_geo_centroidX0, d3_geo_centroidY0, d3_geo_centroidZ0, d3_geo_centroidX1, d3_geo_centroidY1, d3_geo_centroidZ1, d3_geo_centroidX2, d3_geo_centroidY2, d3_geo_centroidZ2; + var d3_geo_centroid = { + sphere: d3_noop, + point: d3_geo_centroidPoint, + lineStart: d3_geo_centroidLineStart, + lineEnd: d3_geo_centroidLineEnd, + polygonStart: function() { + d3_geo_centroid.lineStart = d3_geo_centroidRingStart; + }, + polygonEnd: function() { + d3_geo_centroid.lineStart = d3_geo_centroidLineStart; + } + }; + function d3_geo_centroidPoint(λ, φ) { + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians); + d3_geo_centroidPointXYZ(cosφ * Math.cos(λ), cosφ * Math.sin(λ), Math.sin(φ)); + } + function d3_geo_centroidPointXYZ(x, y, z) { + ++d3_geo_centroidW0; + d3_geo_centroidX0 += (x - d3_geo_centroidX0) / d3_geo_centroidW0; + d3_geo_centroidY0 += (y - d3_geo_centroidY0) / d3_geo_centroidW0; + d3_geo_centroidZ0 += (z - d3_geo_centroidZ0) / d3_geo_centroidW0; + } + function d3_geo_centroidLineStart() { + var x0, y0, z0; + d3_geo_centroid.point = function(λ, φ) { + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians); + x0 = cosφ * Math.cos(λ); + y0 = cosφ * Math.sin(λ); + z0 = Math.sin(φ); + d3_geo_centroid.point = nextPoint; + d3_geo_centroidPointXYZ(x0, y0, z0); + }; + function nextPoint(λ, φ) { + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians), x = cosφ * Math.cos(λ), y = cosφ * Math.sin(λ), z = Math.sin(φ), w = Math.atan2(Math.sqrt((w = y0 * z - z0 * y) * w + (w = z0 * x - x0 * z) * w + (w = x0 * y - y0 * x) * w), x0 * x + y0 * y + z0 * z); + d3_geo_centroidW1 += w; + d3_geo_centroidX1 += w * (x0 + (x0 = x)); + d3_geo_centroidY1 += w * (y0 + (y0 = y)); + d3_geo_centroidZ1 += w * (z0 + (z0 = z)); + d3_geo_centroidPointXYZ(x0, y0, z0); + } + } + function d3_geo_centroidLineEnd() { + d3_geo_centroid.point = d3_geo_centroidPoint; + } + function d3_geo_centroidRingStart() { + var λ00, φ00, x0, y0, z0; + d3_geo_centroid.point = function(λ, φ) { + λ00 = λ, φ00 = φ; + d3_geo_centroid.point = nextPoint; + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians); + x0 = cosφ * Math.cos(λ); + y0 = cosφ * Math.sin(λ); + z0 = Math.sin(φ); + d3_geo_centroidPointXYZ(x0, y0, z0); + }; + d3_geo_centroid.lineEnd = function() { + nextPoint(λ00, φ00); + d3_geo_centroid.lineEnd = d3_geo_centroidLineEnd; + d3_geo_centroid.point = d3_geo_centroidPoint; + }; + function nextPoint(λ, φ) { + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians), x = cosφ * Math.cos(λ), y = cosφ * Math.sin(λ), z = Math.sin(φ), cx = y0 * z - z0 * y, cy = z0 * x - x0 * z, cz = x0 * y - y0 * x, m = Math.sqrt(cx * cx + cy * cy + cz * cz), u = x0 * x + y0 * y + z0 * z, v = m && -d3_acos(u) / m, w = Math.atan2(m, u); + d3_geo_centroidX2 += v * cx; + d3_geo_centroidY2 += v * cy; + d3_geo_centroidZ2 += v * cz; + d3_geo_centroidW1 += w; + d3_geo_centroidX1 += w * (x0 + (x0 = x)); + d3_geo_centroidY1 += w * (y0 + (y0 = y)); + d3_geo_centroidZ1 += w * (z0 + (z0 = z)); + d3_geo_centroidPointXYZ(x0, y0, z0); + } + } + function d3_geo_compose(a, b) { + function compose(x, y) { + return x = a(x, y), b(x[0], x[1]); + } + if (a.invert && b.invert) compose.invert = function(x, y) { + return x = b.invert(x, y), x && a.invert(x[0], x[1]); + }; + return compose; + } + function d3_true() { + return true; + } + function d3_geo_clipPolygon(segments, compare, clipStartInside, interpolate, listener) { + var subject = [], clip = []; + segments.forEach(function(segment) { + if ((n = segment.length - 1) <= 0) return; + var n, p0 = segment[0], p1 = segment[n]; + if (d3_geo_sphericalEqual(p0, p1)) { + listener.lineStart(); + for (var i = 0; i < n; ++i) listener.point((p0 = segment[i])[0], p0[1]); + listener.lineEnd(); + return; + } + var a = new d3_geo_clipPolygonIntersection(p0, segment, null, true), b = new d3_geo_clipPolygonIntersection(p0, null, a, false); + a.o = b; + subject.push(a); + clip.push(b); + a = new d3_geo_clipPolygonIntersection(p1, segment, null, false); + b = new d3_geo_clipPolygonIntersection(p1, null, a, true); + a.o = b; + subject.push(a); + clip.push(b); + }); + clip.sort(compare); + d3_geo_clipPolygonLinkCircular(subject); + d3_geo_clipPolygonLinkCircular(clip); + if (!subject.length) return; + for (var i = 0, entry = clipStartInside, n = clip.length; i < n; ++i) { + clip[i].e = entry = !entry; + } + var start = subject[0], points, point; + while (1) { + var current = start, isSubject = true; + while (current.v) if ((current = current.n) === start) return; + points = current.z; + listener.lineStart(); + do { + current.v = current.o.v = true; + if (current.e) { + if (isSubject) { + for (var i = 0, n = points.length; i < n; ++i) listener.point((point = points[i])[0], point[1]); + } else { + interpolate(current.x, current.n.x, 1, listener); + } + current = current.n; + } else { + if (isSubject) { + points = current.p.z; + for (var i = points.length - 1; i >= 0; --i) listener.point((point = points[i])[0], point[1]); + } else { + interpolate(current.x, current.p.x, -1, listener); + } + current = current.p; + } + current = current.o; + points = current.z; + isSubject = !isSubject; + } while (!current.v); + listener.lineEnd(); + } + } + function d3_geo_clipPolygonLinkCircular(array) { + if (!(n = array.length)) return; + var n, i = 0, a = array[0], b; + while (++i < n) { + a.n = b = array[i]; + b.p = a; + a = b; + } + a.n = b = array[0]; + b.p = a; + } + function d3_geo_clipPolygonIntersection(point, points, other, entry) { + this.x = point; + this.z = points; + this.o = other; + this.e = entry; + this.v = false; + this.n = this.p = null; + } + function d3_geo_clip(pointVisible, clipLine, interpolate, clipStart) { + return function(rotate, listener) { + var line = clipLine(listener), rotatedClipStart = rotate.invert(clipStart[0], clipStart[1]); + var clip = { + point: point, + lineStart: lineStart, + lineEnd: lineEnd, + polygonStart: function() { + clip.point = pointRing; + clip.lineStart = ringStart; + clip.lineEnd = ringEnd; + segments = []; + polygon = []; + }, + polygonEnd: function() { + clip.point = point; + clip.lineStart = lineStart; + clip.lineEnd = lineEnd; + segments = d3.merge(segments); + var clipStartInside = d3_geo_pointInPolygon(rotatedClipStart, polygon); + if (segments.length) { + if (!polygonStarted) listener.polygonStart(), polygonStarted = true; + d3_geo_clipPolygon(segments, d3_geo_clipSort, clipStartInside, interpolate, listener); + } else if (clipStartInside) { + if (!polygonStarted) listener.polygonStart(), polygonStarted = true; + listener.lineStart(); + interpolate(null, null, 1, listener); + listener.lineEnd(); + } + if (polygonStarted) listener.polygonEnd(), polygonStarted = false; + segments = polygon = null; + }, + sphere: function() { + listener.polygonStart(); + listener.lineStart(); + interpolate(null, null, 1, listener); + listener.lineEnd(); + listener.polygonEnd(); + } + }; + function point(λ, φ) { + var point = rotate(λ, φ); + if (pointVisible(λ = point[0], φ = point[1])) listener.point(λ, φ); + } + function pointLine(λ, φ) { + var point = rotate(λ, φ); + line.point(point[0], point[1]); + } + function lineStart() { + clip.point = pointLine; + line.lineStart(); + } + function lineEnd() { + clip.point = point; + line.lineEnd(); + } + var segments; + var buffer = d3_geo_clipBufferListener(), ringListener = clipLine(buffer), polygonStarted = false, polygon, ring; + function pointRing(λ, φ) { + ring.push([ λ, φ ]); + var point = rotate(λ, φ); + ringListener.point(point[0], point[1]); + } + function ringStart() { + ringListener.lineStart(); + ring = []; + } + function ringEnd() { + pointRing(ring[0][0], ring[0][1]); + ringListener.lineEnd(); + var clean = ringListener.clean(), ringSegments = buffer.buffer(), segment, n = ringSegments.length; + ring.pop(); + polygon.push(ring); + ring = null; + if (!n) return; + if (clean & 1) { + segment = ringSegments[0]; + var n = segment.length - 1, i = -1, point; + if (n > 0) { + if (!polygonStarted) listener.polygonStart(), polygonStarted = true; + listener.lineStart(); + while (++i < n) listener.point((point = segment[i])[0], point[1]); + listener.lineEnd(); + } + return; + } + if (n > 1 && clean & 2) ringSegments.push(ringSegments.pop().concat(ringSegments.shift())); + segments.push(ringSegments.filter(d3_geo_clipSegmentLength1)); + } + return clip; + }; + } + function d3_geo_clipSegmentLength1(segment) { + return segment.length > 1; + } + function d3_geo_clipBufferListener() { + var lines = [], line; + return { + lineStart: function() { + lines.push(line = []); + }, + point: function(λ, φ) { + line.push([ λ, φ ]); + }, + lineEnd: d3_noop, + buffer: function() { + var buffer = lines; + lines = []; + line = null; + return buffer; + }, + rejoin: function() { + if (lines.length > 1) lines.push(lines.pop().concat(lines.shift())); + } + }; + } + function d3_geo_clipSort(a, b) { + return ((a = a.x)[0] < 0 ? a[1] - halfπ - ε : halfπ - a[1]) - ((b = b.x)[0] < 0 ? b[1] - halfπ - ε : halfπ - b[1]); + } + var d3_geo_clipAntimeridian = d3_geo_clip(d3_true, d3_geo_clipAntimeridianLine, d3_geo_clipAntimeridianInterpolate, [ -π, -π / 2 ]); + function d3_geo_clipAntimeridianLine(listener) { + var λ0 = NaN, φ0 = NaN, sλ0 = NaN, clean; + return { + lineStart: function() { + listener.lineStart(); + clean = 1; + }, + point: function(λ1, φ1) { + var sλ1 = λ1 > 0 ? π : -π, dλ = abs(λ1 - λ0); + if (abs(dλ - π) < ε) { + listener.point(λ0, φ0 = (φ0 + φ1) / 2 > 0 ? halfπ : -halfπ); + listener.point(sλ0, φ0); + listener.lineEnd(); + listener.lineStart(); + listener.point(sλ1, φ0); + listener.point(λ1, φ0); + clean = 0; + } else if (sλ0 !== sλ1 && dλ >= π) { + if (abs(λ0 - sλ0) < ε) λ0 -= sλ0 * ε; + if (abs(λ1 - sλ1) < ε) λ1 -= sλ1 * ε; + φ0 = d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1); + listener.point(sλ0, φ0); + listener.lineEnd(); + listener.lineStart(); + listener.point(sλ1, φ0); + clean = 0; + } + listener.point(λ0 = λ1, φ0 = φ1); + sλ0 = sλ1; + }, + lineEnd: function() { + listener.lineEnd(); + λ0 = φ0 = NaN; + }, + clean: function() { + return 2 - clean; + } + }; + } + function d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1) { + var cosφ0, cosφ1, sinλ0_λ1 = Math.sin(λ0 - λ1); + return abs(sinλ0_λ1) > ε ? Math.atan((Math.sin(φ0) * (cosφ1 = Math.cos(φ1)) * Math.sin(λ1) - Math.sin(φ1) * (cosφ0 = Math.cos(φ0)) * Math.sin(λ0)) / (cosφ0 * cosφ1 * sinλ0_λ1)) : (φ0 + φ1) / 2; + } + function d3_geo_clipAntimeridianInterpolate(from, to, direction, listener) { + var φ; + if (from == null) { + φ = direction * halfπ; + listener.point(-π, φ); + listener.point(0, φ); + listener.point(π, φ); + listener.point(π, 0); + listener.point(π, -φ); + listener.point(0, -φ); + listener.point(-π, -φ); + listener.point(-π, 0); + listener.point(-π, φ); + } else if (abs(from[0] - to[0]) > ε) { + var s = from[0] < to[0] ? π : -π; + φ = direction * s / 2; + listener.point(-s, φ); + listener.point(0, φ); + listener.point(s, φ); + } else { + listener.point(to[0], to[1]); + } + } + function d3_geo_pointInPolygon(point, polygon) { + var meridian = point[0], parallel = point[1], meridianNormal = [ Math.sin(meridian), -Math.cos(meridian), 0 ], polarAngle = 0, winding = 0; + d3_geo_areaRingSum.reset(); + for (var i = 0, n = polygon.length; i < n; ++i) { + var ring = polygon[i], m = ring.length; + if (!m) continue; + var point0 = ring[0], λ0 = point0[0], φ0 = point0[1] / 2 + π / 4, sinφ0 = Math.sin(φ0), cosφ0 = Math.cos(φ0), j = 1; + while (true) { + if (j === m) j = 0; + point = ring[j]; + var λ = point[0], φ = point[1] / 2 + π / 4, sinφ = Math.sin(φ), cosφ = Math.cos(φ), dλ = λ - λ0, sdλ = dλ >= 0 ? 1 : -1, adλ = sdλ * dλ, antimeridian = adλ > π, k = sinφ0 * sinφ; + d3_geo_areaRingSum.add(Math.atan2(k * sdλ * Math.sin(adλ), cosφ0 * cosφ + k * Math.cos(adλ))); + polarAngle += antimeridian ? dλ + sdλ * τ : dλ; + if (antimeridian ^ λ0 >= meridian ^ λ >= meridian) { + var arc = d3_geo_cartesianCross(d3_geo_cartesian(point0), d3_geo_cartesian(point)); + d3_geo_cartesianNormalize(arc); + var intersection = d3_geo_cartesianCross(meridianNormal, arc); + d3_geo_cartesianNormalize(intersection); + var φarc = (antimeridian ^ dλ >= 0 ? -1 : 1) * d3_asin(intersection[2]); + if (parallel > φarc || parallel === φarc && (arc[0] || arc[1])) { + winding += antimeridian ^ dλ >= 0 ? 1 : -1; + } + } + if (!j++) break; + λ0 = λ, sinφ0 = sinφ, cosφ0 = cosφ, point0 = point; + } + } + return (polarAngle < -ε || polarAngle < ε && d3_geo_areaRingSum < 0) ^ winding & 1; + } + function d3_geo_clipCircle(radius) { + var cr = Math.cos(radius), smallRadius = cr > 0, notHemisphere = abs(cr) > ε, interpolate = d3_geo_circleInterpolate(radius, 6 * d3_radians); + return d3_geo_clip(visible, clipLine, interpolate, smallRadius ? [ 0, -radius ] : [ -π, radius - π ]); + function visible(λ, φ) { + return Math.cos(λ) * Math.cos(φ) > cr; + } + function clipLine(listener) { + var point0, c0, v0, v00, clean; + return { + lineStart: function() { + v00 = v0 = false; + clean = 1; + }, + point: function(λ, φ) { + var point1 = [ λ, φ ], point2, v = visible(λ, φ), c = smallRadius ? v ? 0 : code(λ, φ) : v ? code(λ + (λ < 0 ? π : -π), φ) : 0; + if (!point0 && (v00 = v0 = v)) listener.lineStart(); + if (v !== v0) { + point2 = intersect(point0, point1); + if (d3_geo_sphericalEqual(point0, point2) || d3_geo_sphericalEqual(point1, point2)) { + point1[0] += ε; + point1[1] += ε; + v = visible(point1[0], point1[1]); + } + } + if (v !== v0) { + clean = 0; + if (v) { + listener.lineStart(); + point2 = intersect(point1, point0); + listener.point(point2[0], point2[1]); + } else { + point2 = intersect(point0, point1); + listener.point(point2[0], point2[1]); + listener.lineEnd(); + } + point0 = point2; + } else if (notHemisphere && point0 && smallRadius ^ v) { + var t; + if (!(c & c0) && (t = intersect(point1, point0, true))) { + clean = 0; + if (smallRadius) { + listener.lineStart(); + listener.point(t[0][0], t[0][1]); + listener.point(t[1][0], t[1][1]); + listener.lineEnd(); + } else { + listener.point(t[1][0], t[1][1]); + listener.lineEnd(); + listener.lineStart(); + listener.point(t[0][0], t[0][1]); + } + } + } + if (v && (!point0 || !d3_geo_sphericalEqual(point0, point1))) { + listener.point(point1[0], point1[1]); + } + point0 = point1, v0 = v, c0 = c; + }, + lineEnd: function() { + if (v0) listener.lineEnd(); + point0 = null; + }, + clean: function() { + return clean | (v00 && v0) << 1; + } + }; + } + function intersect(a, b, two) { + var pa = d3_geo_cartesian(a), pb = d3_geo_cartesian(b); + var n1 = [ 1, 0, 0 ], n2 = d3_geo_cartesianCross(pa, pb), n2n2 = d3_geo_cartesianDot(n2, n2), n1n2 = n2[0], determinant = n2n2 - n1n2 * n1n2; + if (!determinant) return !two && a; + var c1 = cr * n2n2 / determinant, c2 = -cr * n1n2 / determinant, n1xn2 = d3_geo_cartesianCross(n1, n2), A = d3_geo_cartesianScale(n1, c1), B = d3_geo_cartesianScale(n2, c2); + d3_geo_cartesianAdd(A, B); + var u = n1xn2, w = d3_geo_cartesianDot(A, u), uu = d3_geo_cartesianDot(u, u), t2 = w * w - uu * (d3_geo_cartesianDot(A, A) - 1); + if (t2 < 0) return; + var t = Math.sqrt(t2), q = d3_geo_cartesianScale(u, (-w - t) / uu); + d3_geo_cartesianAdd(q, A); + q = d3_geo_spherical(q); + if (!two) return q; + var λ0 = a[0], λ1 = b[0], φ0 = a[1], φ1 = b[1], z; + if (λ1 < λ0) z = λ0, λ0 = λ1, λ1 = z; + var δλ = λ1 - λ0, polar = abs(δλ - π) < ε, meridian = polar || δλ < ε; + if (!polar && φ1 < φ0) z = φ0, φ0 = φ1, φ1 = z; + if (meridian ? polar ? φ0 + φ1 > 0 ^ q[1] < (abs(q[0] - λ0) < ε ? φ0 : φ1) : φ0 <= q[1] && q[1] <= φ1 : δλ > π ^ (λ0 <= q[0] && q[0] <= λ1)) { + var q1 = d3_geo_cartesianScale(u, (-w + t) / uu); + d3_geo_cartesianAdd(q1, A); + return [ q, d3_geo_spherical(q1) ]; + } + } + function code(λ, φ) { + var r = smallRadius ? radius : π - radius, code = 0; + if (λ < -r) code |= 1; else if (λ > r) code |= 2; + if (φ < -r) code |= 4; else if (φ > r) code |= 8; + return code; + } + } + function d3_geom_clipLine(x0, y0, x1, y1) { + return function(line) { + var a = line.a, b = line.b, ax = a.x, ay = a.y, bx = b.x, by = b.y, t0 = 0, t1 = 1, dx = bx - ax, dy = by - ay, r; + r = x0 - ax; + if (!dx && r > 0) return; + r /= dx; + if (dx < 0) { + if (r < t0) return; + if (r < t1) t1 = r; + } else if (dx > 0) { + if (r > t1) return; + if (r > t0) t0 = r; + } + r = x1 - ax; + if (!dx && r < 0) return; + r /= dx; + if (dx < 0) { + if (r > t1) return; + if (r > t0) t0 = r; + } else if (dx > 0) { + if (r < t0) return; + if (r < t1) t1 = r; + } + r = y0 - ay; + if (!dy && r > 0) return; + r /= dy; + if (dy < 0) { + if (r < t0) return; + if (r < t1) t1 = r; + } else if (dy > 0) { + if (r > t1) return; + if (r > t0) t0 = r; + } + r = y1 - ay; + if (!dy && r < 0) return; + r /= dy; + if (dy < 0) { + if (r > t1) return; + if (r > t0) t0 = r; + } else if (dy > 0) { + if (r < t0) return; + if (r < t1) t1 = r; + } + if (t0 > 0) line.a = { + x: ax + t0 * dx, + y: ay + t0 * dy + }; + if (t1 < 1) line.b = { + x: ax + t1 * dx, + y: ay + t1 * dy + }; + return line; + }; + } + var d3_geo_clipExtentMAX = 1e9; + d3.geo.clipExtent = function() { + var x0, y0, x1, y1, stream, clip, clipExtent = { + stream: function(output) { + if (stream) stream.valid = false; + stream = clip(output); + stream.valid = true; + return stream; + }, + extent: function(_) { + if (!arguments.length) return [ [ x0, y0 ], [ x1, y1 ] ]; + clip = d3_geo_clipExtent(x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1]); + if (stream) stream.valid = false, stream = null; + return clipExtent; + } + }; + return clipExtent.extent([ [ 0, 0 ], [ 960, 500 ] ]); + }; + function d3_geo_clipExtent(x0, y0, x1, y1) { + return function(listener) { + var listener_ = listener, bufferListener = d3_geo_clipBufferListener(), clipLine = d3_geom_clipLine(x0, y0, x1, y1), segments, polygon, ring; + var clip = { + point: point, + lineStart: lineStart, + lineEnd: lineEnd, + polygonStart: function() { + listener = bufferListener; + segments = []; + polygon = []; + clean = true; + }, + polygonEnd: function() { + listener = listener_; + segments = d3.merge(segments); + var clipStartInside = insidePolygon([ x0, y1 ]), inside = clean && clipStartInside, visible = segments.length; + if (inside || visible) { + listener.polygonStart(); + if (inside) { + listener.lineStart(); + interpolate(null, null, 1, listener); + listener.lineEnd(); + } + if (visible) { + d3_geo_clipPolygon(segments, compare, clipStartInside, interpolate, listener); + } + listener.polygonEnd(); + } + segments = polygon = ring = null; + } + }; + function insidePolygon(p) { + var wn = 0, n = polygon.length, y = p[1]; + for (var i = 0; i < n; ++i) { + for (var j = 1, v = polygon[i], m = v.length, a = v[0], b; j < m; ++j) { + b = v[j]; + if (a[1] <= y) { + if (b[1] > y && d3_cross2d(a, b, p) > 0) ++wn; + } else { + if (b[1] <= y && d3_cross2d(a, b, p) < 0) --wn; + } + a = b; + } + } + return wn !== 0; + } + function interpolate(from, to, direction, listener) { + var a = 0, a1 = 0; + if (from == null || (a = corner(from, direction)) !== (a1 = corner(to, direction)) || comparePoints(from, to) < 0 ^ direction > 0) { + do { + listener.point(a === 0 || a === 3 ? x0 : x1, a > 1 ? y1 : y0); + } while ((a = (a + direction + 4) % 4) !== a1); + } else { + listener.point(to[0], to[1]); + } + } + function pointVisible(x, y) { + return x0 <= x && x <= x1 && y0 <= y && y <= y1; + } + function point(x, y) { + if (pointVisible(x, y)) listener.point(x, y); + } + var x__, y__, v__, x_, y_, v_, first, clean; + function lineStart() { + clip.point = linePoint; + if (polygon) polygon.push(ring = []); + first = true; + v_ = false; + x_ = y_ = NaN; + } + function lineEnd() { + if (segments) { + linePoint(x__, y__); + if (v__ && v_) bufferListener.rejoin(); + segments.push(bufferListener.buffer()); + } + clip.point = point; + if (v_) listener.lineEnd(); + } + function linePoint(x, y) { + x = Math.max(-d3_geo_clipExtentMAX, Math.min(d3_geo_clipExtentMAX, x)); + y = Math.max(-d3_geo_clipExtentMAX, Math.min(d3_geo_clipExtentMAX, y)); + var v = pointVisible(x, y); + if (polygon) ring.push([ x, y ]); + if (first) { + x__ = x, y__ = y, v__ = v; + first = false; + if (v) { + listener.lineStart(); + listener.point(x, y); + } + } else { + if (v && v_) listener.point(x, y); else { + var l = { + a: { + x: x_, + y: y_ + }, + b: { + x: x, + y: y + } + }; + if (clipLine(l)) { + if (!v_) { + listener.lineStart(); + listener.point(l.a.x, l.a.y); + } + listener.point(l.b.x, l.b.y); + if (!v) listener.lineEnd(); + clean = false; + } else if (v) { + listener.lineStart(); + listener.point(x, y); + clean = false; + } + } + } + x_ = x, y_ = y, v_ = v; + } + return clip; + }; + function corner(p, direction) { + return abs(p[0] - x0) < ε ? direction > 0 ? 0 : 3 : abs(p[0] - x1) < ε ? direction > 0 ? 2 : 1 : abs(p[1] - y0) < ε ? direction > 0 ? 1 : 0 : direction > 0 ? 3 : 2; + } + function compare(a, b) { + return comparePoints(a.x, b.x); + } + function comparePoints(a, b) { + var ca = corner(a, 1), cb = corner(b, 1); + return ca !== cb ? ca - cb : ca === 0 ? b[1] - a[1] : ca === 1 ? a[0] - b[0] : ca === 2 ? a[1] - b[1] : b[0] - a[0]; + } + } + function d3_geo_conic(projectAt) { + var φ0 = 0, φ1 = π / 3, m = d3_geo_projectionMutator(projectAt), p = m(φ0, φ1); + p.parallels = function(_) { + if (!arguments.length) return [ φ0 / π * 180, φ1 / π * 180 ]; + return m(φ0 = _[0] * π / 180, φ1 = _[1] * π / 180); + }; + return p; + } + function d3_geo_conicEqualArea(φ0, φ1) { + var sinφ0 = Math.sin(φ0), n = (sinφ0 + Math.sin(φ1)) / 2, C = 1 + sinφ0 * (2 * n - sinφ0), ρ0 = Math.sqrt(C) / n; + function forward(λ, φ) { + var ρ = Math.sqrt(C - 2 * n * Math.sin(φ)) / n; + return [ ρ * Math.sin(λ *= n), ρ0 - ρ * Math.cos(λ) ]; + } + forward.invert = function(x, y) { + var ρ0_y = ρ0 - y; + return [ Math.atan2(x, ρ0_y) / n, d3_asin((C - (x * x + ρ0_y * ρ0_y) * n * n) / (2 * n)) ]; + }; + return forward; + } + (d3.geo.conicEqualArea = function() { + return d3_geo_conic(d3_geo_conicEqualArea); + }).raw = d3_geo_conicEqualArea; + d3.geo.albers = function() { + return d3.geo.conicEqualArea().rotate([ 96, 0 ]).center([ -.6, 38.7 ]).parallels([ 29.5, 45.5 ]).scale(1070); + }; + d3.geo.albersUsa = function() { + var lower48 = d3.geo.albers(); + var alaska = d3.geo.conicEqualArea().rotate([ 154, 0 ]).center([ -2, 58.5 ]).parallels([ 55, 65 ]); + var hawaii = d3.geo.conicEqualArea().rotate([ 157, 0 ]).center([ -3, 19.9 ]).parallels([ 8, 18 ]); + var point, pointStream = { + point: function(x, y) { + point = [ x, y ]; + } + }, lower48Point, alaskaPoint, hawaiiPoint; + function albersUsa(coordinates) { + var x = coordinates[0], y = coordinates[1]; + point = null; + (lower48Point(x, y), point) || (alaskaPoint(x, y), point) || hawaiiPoint(x, y); + return point; + } + albersUsa.invert = function(coordinates) { + var k = lower48.scale(), t = lower48.translate(), x = (coordinates[0] - t[0]) / k, y = (coordinates[1] - t[1]) / k; + return (y >= .12 && y < .234 && x >= -.425 && x < -.214 ? alaska : y >= .166 && y < .234 && x >= -.214 && x < -.115 ? hawaii : lower48).invert(coordinates); + }; + albersUsa.stream = function(stream) { + var lower48Stream = lower48.stream(stream), alaskaStream = alaska.stream(stream), hawaiiStream = hawaii.stream(stream); + return { + point: function(x, y) { + lower48Stream.point(x, y); + alaskaStream.point(x, y); + hawaiiStream.point(x, y); + }, + sphere: function() { + lower48Stream.sphere(); + alaskaStream.sphere(); + hawaiiStream.sphere(); + }, + lineStart: function() { + lower48Stream.lineStart(); + alaskaStream.lineStart(); + hawaiiStream.lineStart(); + }, + lineEnd: function() { + lower48Stream.lineEnd(); + alaskaStream.lineEnd(); + hawaiiStream.lineEnd(); + }, + polygonStart: function() { + lower48Stream.polygonStart(); + alaskaStream.polygonStart(); + hawaiiStream.polygonStart(); + }, + polygonEnd: function() { + lower48Stream.polygonEnd(); + alaskaStream.polygonEnd(); + hawaiiStream.polygonEnd(); + } + }; + }; + albersUsa.precision = function(_) { + if (!arguments.length) return lower48.precision(); + lower48.precision(_); + alaska.precision(_); + hawaii.precision(_); + return albersUsa; + }; + albersUsa.scale = function(_) { + if (!arguments.length) return lower48.scale(); + lower48.scale(_); + alaska.scale(_ * .35); + hawaii.scale(_); + return albersUsa.translate(lower48.translate()); + }; + albersUsa.translate = function(_) { + if (!arguments.length) return lower48.translate(); + var k = lower48.scale(), x = +_[0], y = +_[1]; + lower48Point = lower48.translate(_).clipExtent([ [ x - .455 * k, y - .238 * k ], [ x + .455 * k, y + .238 * k ] ]).stream(pointStream).point; + alaskaPoint = alaska.translate([ x - .307 * k, y + .201 * k ]).clipExtent([ [ x - .425 * k + ε, y + .12 * k + ε ], [ x - .214 * k - ε, y + .234 * k - ε ] ]).stream(pointStream).point; + hawaiiPoint = hawaii.translate([ x - .205 * k, y + .212 * k ]).clipExtent([ [ x - .214 * k + ε, y + .166 * k + ε ], [ x - .115 * k - ε, y + .234 * k - ε ] ]).stream(pointStream).point; + return albersUsa; + }; + return albersUsa.scale(1070); + }; + var d3_geo_pathAreaSum, d3_geo_pathAreaPolygon, d3_geo_pathArea = { + point: d3_noop, + lineStart: d3_noop, + lineEnd: d3_noop, + polygonStart: function() { + d3_geo_pathAreaPolygon = 0; + d3_geo_pathArea.lineStart = d3_geo_pathAreaRingStart; + }, + polygonEnd: function() { + d3_geo_pathArea.lineStart = d3_geo_pathArea.lineEnd = d3_geo_pathArea.point = d3_noop; + d3_geo_pathAreaSum += abs(d3_geo_pathAreaPolygon / 2); + } + }; + function d3_geo_pathAreaRingStart() { + var x00, y00, x0, y0; + d3_geo_pathArea.point = function(x, y) { + d3_geo_pathArea.point = nextPoint; + x00 = x0 = x, y00 = y0 = y; + }; + function nextPoint(x, y) { + d3_geo_pathAreaPolygon += y0 * x - x0 * y; + x0 = x, y0 = y; + } + d3_geo_pathArea.lineEnd = function() { + nextPoint(x00, y00); + }; + } + var d3_geo_pathBoundsX0, d3_geo_pathBoundsY0, d3_geo_pathBoundsX1, d3_geo_pathBoundsY1; + var d3_geo_pathBounds = { + point: d3_geo_pathBoundsPoint, + lineStart: d3_noop, + lineEnd: d3_noop, + polygonStart: d3_noop, + polygonEnd: d3_noop + }; + function d3_geo_pathBoundsPoint(x, y) { + if (x < d3_geo_pathBoundsX0) d3_geo_pathBoundsX0 = x; + if (x > d3_geo_pathBoundsX1) d3_geo_pathBoundsX1 = x; + if (y < d3_geo_pathBoundsY0) d3_geo_pathBoundsY0 = y; + if (y > d3_geo_pathBoundsY1) d3_geo_pathBoundsY1 = y; + } + function d3_geo_pathBuffer() { + var pointCircle = d3_geo_pathBufferCircle(4.5), buffer = []; + var stream = { + point: point, + lineStart: function() { + stream.point = pointLineStart; + }, + lineEnd: lineEnd, + polygonStart: function() { + stream.lineEnd = lineEndPolygon; + }, + polygonEnd: function() { + stream.lineEnd = lineEnd; + stream.point = point; + }, + pointRadius: function(_) { + pointCircle = d3_geo_pathBufferCircle(_); + return stream; + }, + result: function() { + if (buffer.length) { + var result = buffer.join(""); + buffer = []; + return result; + } + } + }; + function point(x, y) { + buffer.push("M", x, ",", y, pointCircle); + } + function pointLineStart(x, y) { + buffer.push("M", x, ",", y); + stream.point = pointLine; + } + function pointLine(x, y) { + buffer.push("L", x, ",", y); + } + function lineEnd() { + stream.point = point; + } + function lineEndPolygon() { + buffer.push("Z"); + } + return stream; + } + function d3_geo_pathBufferCircle(radius) { + return "m0," + radius + "a" + radius + "," + radius + " 0 1,1 0," + -2 * radius + "a" + radius + "," + radius + " 0 1,1 0," + 2 * radius + "z"; + } + var d3_geo_pathCentroid = { + point: d3_geo_pathCentroidPoint, + lineStart: d3_geo_pathCentroidLineStart, + lineEnd: d3_geo_pathCentroidLineEnd, + polygonStart: function() { + d3_geo_pathCentroid.lineStart = d3_geo_pathCentroidRingStart; + }, + polygonEnd: function() { + d3_geo_pathCentroid.point = d3_geo_pathCentroidPoint; + d3_geo_pathCentroid.lineStart = d3_geo_pathCentroidLineStart; + d3_geo_pathCentroid.lineEnd = d3_geo_pathCentroidLineEnd; + } + }; + function d3_geo_pathCentroidPoint(x, y) { + d3_geo_centroidX0 += x; + d3_geo_centroidY0 += y; + ++d3_geo_centroidZ0; + } + function d3_geo_pathCentroidLineStart() { + var x0, y0; + d3_geo_pathCentroid.point = function(x, y) { + d3_geo_pathCentroid.point = nextPoint; + d3_geo_pathCentroidPoint(x0 = x, y0 = y); + }; + function nextPoint(x, y) { + var dx = x - x0, dy = y - y0, z = Math.sqrt(dx * dx + dy * dy); + d3_geo_centroidX1 += z * (x0 + x) / 2; + d3_geo_centroidY1 += z * (y0 + y) / 2; + d3_geo_centroidZ1 += z; + d3_geo_pathCentroidPoint(x0 = x, y0 = y); + } + } + function d3_geo_pathCentroidLineEnd() { + d3_geo_pathCentroid.point = d3_geo_pathCentroidPoint; + } + function d3_geo_pathCentroidRingStart() { + var x00, y00, x0, y0; + d3_geo_pathCentroid.point = function(x, y) { + d3_geo_pathCentroid.point = nextPoint; + d3_geo_pathCentroidPoint(x00 = x0 = x, y00 = y0 = y); + }; + function nextPoint(x, y) { + var dx = x - x0, dy = y - y0, z = Math.sqrt(dx * dx + dy * dy); + d3_geo_centroidX1 += z * (x0 + x) / 2; + d3_geo_centroidY1 += z * (y0 + y) / 2; + d3_geo_centroidZ1 += z; + z = y0 * x - x0 * y; + d3_geo_centroidX2 += z * (x0 + x); + d3_geo_centroidY2 += z * (y0 + y); + d3_geo_centroidZ2 += z * 3; + d3_geo_pathCentroidPoint(x0 = x, y0 = y); + } + d3_geo_pathCentroid.lineEnd = function() { + nextPoint(x00, y00); + }; + } + function d3_geo_pathContext(context) { + var pointRadius = 4.5; + var stream = { + point: point, + lineStart: function() { + stream.point = pointLineStart; + }, + lineEnd: lineEnd, + polygonStart: function() { + stream.lineEnd = lineEndPolygon; + }, + polygonEnd: function() { + stream.lineEnd = lineEnd; + stream.point = point; + }, + pointRadius: function(_) { + pointRadius = _; + return stream; + }, + result: d3_noop + }; + function point(x, y) { + context.moveTo(x + pointRadius, y); + context.arc(x, y, pointRadius, 0, τ); + } + function pointLineStart(x, y) { + context.moveTo(x, y); + stream.point = pointLine; + } + function pointLine(x, y) { + context.lineTo(x, y); + } + function lineEnd() { + stream.point = point; + } + function lineEndPolygon() { + context.closePath(); + } + return stream; + } + function d3_geo_resample(project) { + var δ2 = .5, cosMinDistance = Math.cos(30 * d3_radians), maxDepth = 16; + function resample(stream) { + return (maxDepth ? resampleRecursive : resampleNone)(stream); + } + function resampleNone(stream) { + return d3_geo_transformPoint(stream, function(x, y) { + x = project(x, y); + stream.point(x[0], x[1]); + }); + } + function resampleRecursive(stream) { + var λ00, φ00, x00, y00, a00, b00, c00, λ0, x0, y0, a0, b0, c0; + var resample = { + point: point, + lineStart: lineStart, + lineEnd: lineEnd, + polygonStart: function() { + stream.polygonStart(); + resample.lineStart = ringStart; + }, + polygonEnd: function() { + stream.polygonEnd(); + resample.lineStart = lineStart; + } + }; + function point(x, y) { + x = project(x, y); + stream.point(x[0], x[1]); + } + function lineStart() { + x0 = NaN; + resample.point = linePoint; + stream.lineStart(); + } + function linePoint(λ, φ) { + var c = d3_geo_cartesian([ λ, φ ]), p = project(λ, φ); + resampleLineTo(x0, y0, λ0, a0, b0, c0, x0 = p[0], y0 = p[1], λ0 = λ, a0 = c[0], b0 = c[1], c0 = c[2], maxDepth, stream); + stream.point(x0, y0); + } + function lineEnd() { + resample.point = point; + stream.lineEnd(); + } + function ringStart() { + lineStart(); + resample.point = ringPoint; + resample.lineEnd = ringEnd; + } + function ringPoint(λ, φ) { + linePoint(λ00 = λ, φ00 = φ), x00 = x0, y00 = y0, a00 = a0, b00 = b0, c00 = c0; + resample.point = linePoint; + } + function ringEnd() { + resampleLineTo(x0, y0, λ0, a0, b0, c0, x00, y00, λ00, a00, b00, c00, maxDepth, stream); + resample.lineEnd = lineEnd; + lineEnd(); + } + return resample; + } + function resampleLineTo(x0, y0, λ0, a0, b0, c0, x1, y1, λ1, a1, b1, c1, depth, stream) { + var dx = x1 - x0, dy = y1 - y0, d2 = dx * dx + dy * dy; + if (d2 > 4 * δ2 && depth--) { + var a = a0 + a1, b = b0 + b1, c = c0 + c1, m = Math.sqrt(a * a + b * b + c * c), φ2 = Math.asin(c /= m), λ2 = abs(abs(c) - 1) < ε || abs(λ0 - λ1) < ε ? (λ0 + λ1) / 2 : Math.atan2(b, a), p = project(λ2, φ2), x2 = p[0], y2 = p[1], dx2 = x2 - x0, dy2 = y2 - y0, dz = dy * dx2 - dx * dy2; + if (dz * dz / d2 > δ2 || abs((dx * dx2 + dy * dy2) / d2 - .5) > .3 || a0 * a1 + b0 * b1 + c0 * c1 < cosMinDistance) { + resampleLineTo(x0, y0, λ0, a0, b0, c0, x2, y2, λ2, a /= m, b /= m, c, depth, stream); + stream.point(x2, y2); + resampleLineTo(x2, y2, λ2, a, b, c, x1, y1, λ1, a1, b1, c1, depth, stream); + } + } + } + resample.precision = function(_) { + if (!arguments.length) return Math.sqrt(δ2); + maxDepth = (δ2 = _ * _) > 0 && 16; + return resample; + }; + return resample; + } + d3.geo.path = function() { + var pointRadius = 4.5, projection, context, projectStream, contextStream, cacheStream; + function path(object) { + if (object) { + if (typeof pointRadius === "function") contextStream.pointRadius(+pointRadius.apply(this, arguments)); + if (!cacheStream || !cacheStream.valid) cacheStream = projectStream(contextStream); + d3.geo.stream(object, cacheStream); + } + return contextStream.result(); + } + path.area = function(object) { + d3_geo_pathAreaSum = 0; + d3.geo.stream(object, projectStream(d3_geo_pathArea)); + return d3_geo_pathAreaSum; + }; + path.centroid = function(object) { + d3_geo_centroidX0 = d3_geo_centroidY0 = d3_geo_centroidZ0 = d3_geo_centroidX1 = d3_geo_centroidY1 = d3_geo_centroidZ1 = d3_geo_centroidX2 = d3_geo_centroidY2 = d3_geo_centroidZ2 = 0; + d3.geo.stream(object, projectStream(d3_geo_pathCentroid)); + return d3_geo_centroidZ2 ? [ d3_geo_centroidX2 / d3_geo_centroidZ2, d3_geo_centroidY2 / d3_geo_centroidZ2 ] : d3_geo_centroidZ1 ? [ d3_geo_centroidX1 / d3_geo_centroidZ1, d3_geo_centroidY1 / d3_geo_centroidZ1 ] : d3_geo_centroidZ0 ? [ d3_geo_centroidX0 / d3_geo_centroidZ0, d3_geo_centroidY0 / d3_geo_centroidZ0 ] : [ NaN, NaN ]; + }; + path.bounds = function(object) { + d3_geo_pathBoundsX1 = d3_geo_pathBoundsY1 = -(d3_geo_pathBoundsX0 = d3_geo_pathBoundsY0 = Infinity); + d3.geo.stream(object, projectStream(d3_geo_pathBounds)); + return [ [ d3_geo_pathBoundsX0, d3_geo_pathBoundsY0 ], [ d3_geo_pathBoundsX1, d3_geo_pathBoundsY1 ] ]; + }; + path.projection = function(_) { + if (!arguments.length) return projection; + projectStream = (projection = _) ? _.stream || d3_geo_pathProjectStream(_) : d3_identity; + return reset(); + }; + path.context = function(_) { + if (!arguments.length) return context; + contextStream = (context = _) == null ? new d3_geo_pathBuffer() : new d3_geo_pathContext(_); + if (typeof pointRadius !== "function") contextStream.pointRadius(pointRadius); + return reset(); + }; + path.pointRadius = function(_) { + if (!arguments.length) return pointRadius; + pointRadius = typeof _ === "function" ? _ : (contextStream.pointRadius(+_), +_); + return path; + }; + function reset() { + cacheStream = null; + return path; + } + return path.projection(d3.geo.albersUsa()).context(null); + }; + function d3_geo_pathProjectStream(project) { + var resample = d3_geo_resample(function(x, y) { + return project([ x * d3_degrees, y * d3_degrees ]); + }); + return function(stream) { + return d3_geo_projectionRadians(resample(stream)); + }; + } + d3.geo.transform = function(methods) { + return { + stream: function(stream) { + var transform = new d3_geo_transform(stream); + for (var k in methods) transform[k] = methods[k]; + return transform; + } + }; + }; + function d3_geo_transform(stream) { + this.stream = stream; + } + d3_geo_transform.prototype = { + point: function(x, y) { + this.stream.point(x, y); + }, + sphere: function() { + this.stream.sphere(); + }, + lineStart: function() { + this.stream.lineStart(); + }, + lineEnd: function() { + this.stream.lineEnd(); + }, + polygonStart: function() { + this.stream.polygonStart(); + }, + polygonEnd: function() { + this.stream.polygonEnd(); + } + }; + function d3_geo_transformPoint(stream, point) { + return { + point: point, + sphere: function() { + stream.sphere(); + }, + lineStart: function() { + stream.lineStart(); + }, + lineEnd: function() { + stream.lineEnd(); + }, + polygonStart: function() { + stream.polygonStart(); + }, + polygonEnd: function() { + stream.polygonEnd(); + } + }; + } + d3.geo.projection = d3_geo_projection; + d3.geo.projectionMutator = d3_geo_projectionMutator; + function d3_geo_projection(project) { + return d3_geo_projectionMutator(function() { + return project; + })(); + } + function d3_geo_projectionMutator(projectAt) { + var project, rotate, projectRotate, projectResample = d3_geo_resample(function(x, y) { + x = project(x, y); + return [ x[0] * k + δx, δy - x[1] * k ]; + }), k = 150, x = 480, y = 250, λ = 0, φ = 0, δλ = 0, δφ = 0, δγ = 0, δx, δy, preclip = d3_geo_clipAntimeridian, postclip = d3_identity, clipAngle = null, clipExtent = null, stream; + function projection(point) { + point = projectRotate(point[0] * d3_radians, point[1] * d3_radians); + return [ point[0] * k + δx, δy - point[1] * k ]; + } + function invert(point) { + point = projectRotate.invert((point[0] - δx) / k, (δy - point[1]) / k); + return point && [ point[0] * d3_degrees, point[1] * d3_degrees ]; + } + projection.stream = function(output) { + if (stream) stream.valid = false; + stream = d3_geo_projectionRadians(preclip(rotate, projectResample(postclip(output)))); + stream.valid = true; + return stream; + }; + projection.clipAngle = function(_) { + if (!arguments.length) return clipAngle; + preclip = _ == null ? (clipAngle = _, d3_geo_clipAntimeridian) : d3_geo_clipCircle((clipAngle = +_) * d3_radians); + return invalidate(); + }; + projection.clipExtent = function(_) { + if (!arguments.length) return clipExtent; + clipExtent = _; + postclip = _ ? d3_geo_clipExtent(_[0][0], _[0][1], _[1][0], _[1][1]) : d3_identity; + return invalidate(); + }; + projection.scale = function(_) { + if (!arguments.length) return k; + k = +_; + return reset(); + }; + projection.translate = function(_) { + if (!arguments.length) return [ x, y ]; + x = +_[0]; + y = +_[1]; + return reset(); + }; + projection.center = function(_) { + if (!arguments.length) return [ λ * d3_degrees, φ * d3_degrees ]; + λ = _[0] % 360 * d3_radians; + φ = _[1] % 360 * d3_radians; + return reset(); + }; + projection.rotate = function(_) { + if (!arguments.length) return [ δλ * d3_degrees, δφ * d3_degrees, δγ * d3_degrees ]; + δλ = _[0] % 360 * d3_radians; + δφ = _[1] % 360 * d3_radians; + δγ = _.length > 2 ? _[2] % 360 * d3_radians : 0; + return reset(); + }; + d3.rebind(projection, projectResample, "precision"); + function reset() { + projectRotate = d3_geo_compose(rotate = d3_geo_rotation(δλ, δφ, δγ), project); + var center = project(λ, φ); + δx = x - center[0] * k; + δy = y + center[1] * k; + return invalidate(); + } + function invalidate() { + if (stream) stream.valid = false, stream = null; + return projection; + } + return function() { + project = projectAt.apply(this, arguments); + projection.invert = project.invert && invert; + return reset(); + }; + } + function d3_geo_projectionRadians(stream) { + return d3_geo_transformPoint(stream, function(x, y) { + stream.point(x * d3_radians, y * d3_radians); + }); + } + function d3_geo_equirectangular(λ, φ) { + return [ λ, φ ]; + } + (d3.geo.equirectangular = function() { + return d3_geo_projection(d3_geo_equirectangular); + }).raw = d3_geo_equirectangular.invert = d3_geo_equirectangular; + d3.geo.rotation = function(rotate) { + rotate = d3_geo_rotation(rotate[0] % 360 * d3_radians, rotate[1] * d3_radians, rotate.length > 2 ? rotate[2] * d3_radians : 0); + function forward(coordinates) { + coordinates = rotate(coordinates[0] * d3_radians, coordinates[1] * d3_radians); + return coordinates[0] *= d3_degrees, coordinates[1] *= d3_degrees, coordinates; + } + forward.invert = function(coordinates) { + coordinates = rotate.invert(coordinates[0] * d3_radians, coordinates[1] * d3_radians); + return coordinates[0] *= d3_degrees, coordinates[1] *= d3_degrees, coordinates; + }; + return forward; + }; + function d3_geo_identityRotation(λ, φ) { + return [ λ > π ? λ - τ : λ < -π ? λ + τ : λ, φ ]; + } + d3_geo_identityRotation.invert = d3_geo_equirectangular; + function d3_geo_rotation(δλ, δφ, δγ) { + return δλ ? δφ || δγ ? d3_geo_compose(d3_geo_rotationλ(δλ), d3_geo_rotationφγ(δφ, δγ)) : d3_geo_rotationλ(δλ) : δφ || δγ ? d3_geo_rotationφγ(δφ, δγ) : d3_geo_identityRotation; + } + function d3_geo_forwardRotationλ(δλ) { + return function(λ, φ) { + return λ += δλ, [ λ > π ? λ - τ : λ < -π ? λ + τ : λ, φ ]; + }; + } + function d3_geo_rotationλ(δλ) { + var rotation = d3_geo_forwardRotationλ(δλ); + rotation.invert = d3_geo_forwardRotationλ(-δλ); + return rotation; + } + function d3_geo_rotationφγ(δφ, δγ) { + var cosδφ = Math.cos(δφ), sinδφ = Math.sin(δφ), cosδγ = Math.cos(δγ), sinδγ = Math.sin(δγ); + function rotation(λ, φ) { + var cosφ = Math.cos(φ), x = Math.cos(λ) * cosφ, y = Math.sin(λ) * cosφ, z = Math.sin(φ), k = z * cosδφ + x * sinδφ; + return [ Math.atan2(y * cosδγ - k * sinδγ, x * cosδφ - z * sinδφ), d3_asin(k * cosδγ + y * sinδγ) ]; + } + rotation.invert = function(λ, φ) { + var cosφ = Math.cos(φ), x = Math.cos(λ) * cosφ, y = Math.sin(λ) * cosφ, z = Math.sin(φ), k = z * cosδγ - y * sinδγ; + return [ Math.atan2(y * cosδγ + z * sinδγ, x * cosδφ + k * sinδφ), d3_asin(k * cosδφ - x * sinδφ) ]; + }; + return rotation; + } + d3.geo.circle = function() { + var origin = [ 0, 0 ], angle, precision = 6, interpolate; + function circle() { + var center = typeof origin === "function" ? origin.apply(this, arguments) : origin, rotate = d3_geo_rotation(-center[0] * d3_radians, -center[1] * d3_radians, 0).invert, ring = []; + interpolate(null, null, 1, { + point: function(x, y) { + ring.push(x = rotate(x, y)); + x[0] *= d3_degrees, x[1] *= d3_degrees; + } + }); + return { + type: "Polygon", + coordinates: [ ring ] + }; + } + circle.origin = function(x) { + if (!arguments.length) return origin; + origin = x; + return circle; + }; + circle.angle = function(x) { + if (!arguments.length) return angle; + interpolate = d3_geo_circleInterpolate((angle = +x) * d3_radians, precision * d3_radians); + return circle; + }; + circle.precision = function(_) { + if (!arguments.length) return precision; + interpolate = d3_geo_circleInterpolate(angle * d3_radians, (precision = +_) * d3_radians); + return circle; + }; + return circle.angle(90); + }; + function d3_geo_circleInterpolate(radius, precision) { + var cr = Math.cos(radius), sr = Math.sin(radius); + return function(from, to, direction, listener) { + var step = direction * precision; + if (from != null) { + from = d3_geo_circleAngle(cr, from); + to = d3_geo_circleAngle(cr, to); + if (direction > 0 ? from < to : from > to) from += direction * τ; + } else { + from = radius + direction * τ; + to = radius - .5 * step; + } + for (var point, t = from; direction > 0 ? t > to : t < to; t -= step) { + listener.point((point = d3_geo_spherical([ cr, -sr * Math.cos(t), -sr * Math.sin(t) ]))[0], point[1]); + } + }; + } + function d3_geo_circleAngle(cr, point) { + var a = d3_geo_cartesian(point); + a[0] -= cr; + d3_geo_cartesianNormalize(a); + var angle = d3_acos(-a[1]); + return ((-a[2] < 0 ? -angle : angle) + 2 * Math.PI - ε) % (2 * Math.PI); + } + d3.geo.distance = function(a, b) { + var Δλ = (b[0] - a[0]) * d3_radians, φ0 = a[1] * d3_radians, φ1 = b[1] * d3_radians, sinΔλ = Math.sin(Δλ), cosΔλ = Math.cos(Δλ), sinφ0 = Math.sin(φ0), cosφ0 = Math.cos(φ0), sinφ1 = Math.sin(φ1), cosφ1 = Math.cos(φ1), t; + return Math.atan2(Math.sqrt((t = cosφ1 * sinΔλ) * t + (t = cosφ0 * sinφ1 - sinφ0 * cosφ1 * cosΔλ) * t), sinφ0 * sinφ1 + cosφ0 * cosφ1 * cosΔλ); + }; + d3.geo.graticule = function() { + var x1, x0, X1, X0, y1, y0, Y1, Y0, dx = 10, dy = dx, DX = 90, DY = 360, x, y, X, Y, precision = 2.5; + function graticule() { + return { + type: "MultiLineString", + coordinates: lines() + }; + } + function lines() { + return d3.range(Math.ceil(X0 / DX) * DX, X1, DX).map(X).concat(d3.range(Math.ceil(Y0 / DY) * DY, Y1, DY).map(Y)).concat(d3.range(Math.ceil(x0 / dx) * dx, x1, dx).filter(function(x) { + return abs(x % DX) > ε; + }).map(x)).concat(d3.range(Math.ceil(y0 / dy) * dy, y1, dy).filter(function(y) { + return abs(y % DY) > ε; + }).map(y)); + } + graticule.lines = function() { + return lines().map(function(coordinates) { + return { + type: "LineString", + coordinates: coordinates + }; + }); + }; + graticule.outline = function() { + return { + type: "Polygon", + coordinates: [ X(X0).concat(Y(Y1).slice(1), X(X1).reverse().slice(1), Y(Y0).reverse().slice(1)) ] + }; + }; + graticule.extent = function(_) { + if (!arguments.length) return graticule.minorExtent(); + return graticule.majorExtent(_).minorExtent(_); + }; + graticule.majorExtent = function(_) { + if (!arguments.length) return [ [ X0, Y0 ], [ X1, Y1 ] ]; + X0 = +_[0][0], X1 = +_[1][0]; + Y0 = +_[0][1], Y1 = +_[1][1]; + if (X0 > X1) _ = X0, X0 = X1, X1 = _; + if (Y0 > Y1) _ = Y0, Y0 = Y1, Y1 = _; + return graticule.precision(precision); + }; + graticule.minorExtent = function(_) { + if (!arguments.length) return [ [ x0, y0 ], [ x1, y1 ] ]; + x0 = +_[0][0], x1 = +_[1][0]; + y0 = +_[0][1], y1 = +_[1][1]; + if (x0 > x1) _ = x0, x0 = x1, x1 = _; + if (y0 > y1) _ = y0, y0 = y1, y1 = _; + return graticule.precision(precision); + }; + graticule.step = function(_) { + if (!arguments.length) return graticule.minorStep(); + return graticule.majorStep(_).minorStep(_); + }; + graticule.majorStep = function(_) { + if (!arguments.length) return [ DX, DY ]; + DX = +_[0], DY = +_[1]; + return graticule; + }; + graticule.minorStep = function(_) { + if (!arguments.length) return [ dx, dy ]; + dx = +_[0], dy = +_[1]; + return graticule; + }; + graticule.precision = function(_) { + if (!arguments.length) return precision; + precision = +_; + x = d3_geo_graticuleX(y0, y1, 90); + y = d3_geo_graticuleY(x0, x1, precision); + X = d3_geo_graticuleX(Y0, Y1, 90); + Y = d3_geo_graticuleY(X0, X1, precision); + return graticule; + }; + return graticule.majorExtent([ [ -180, -90 + ε ], [ 180, 90 - ε ] ]).minorExtent([ [ -180, -80 - ε ], [ 180, 80 + ε ] ]); + }; + function d3_geo_graticuleX(y0, y1, dy) { + var y = d3.range(y0, y1 - ε, dy).concat(y1); + return function(x) { + return y.map(function(y) { + return [ x, y ]; + }); + }; + } + function d3_geo_graticuleY(x0, x1, dx) { + var x = d3.range(x0, x1 - ε, dx).concat(x1); + return function(y) { + return x.map(function(x) { + return [ x, y ]; + }); + }; + } + function d3_source(d) { + return d.source; + } + function d3_target(d) { + return d.target; + } + d3.geo.greatArc = function() { + var source = d3_source, source_, target = d3_target, target_; + function greatArc() { + return { + type: "LineString", + coordinates: [ source_ || source.apply(this, arguments), target_ || target.apply(this, arguments) ] + }; + } + greatArc.distance = function() { + return d3.geo.distance(source_ || source.apply(this, arguments), target_ || target.apply(this, arguments)); + }; + greatArc.source = function(_) { + if (!arguments.length) return source; + source = _, source_ = typeof _ === "function" ? null : _; + return greatArc; + }; + greatArc.target = function(_) { + if (!arguments.length) return target; + target = _, target_ = typeof _ === "function" ? null : _; + return greatArc; + }; + greatArc.precision = function() { + return arguments.length ? greatArc : 0; + }; + return greatArc; + }; + d3.geo.interpolate = function(source, target) { + return d3_geo_interpolate(source[0] * d3_radians, source[1] * d3_radians, target[0] * d3_radians, target[1] * d3_radians); + }; + function d3_geo_interpolate(x0, y0, x1, y1) { + var cy0 = Math.cos(y0), sy0 = Math.sin(y0), cy1 = Math.cos(y1), sy1 = Math.sin(y1), kx0 = cy0 * Math.cos(x0), ky0 = cy0 * Math.sin(x0), kx1 = cy1 * Math.cos(x1), ky1 = cy1 * Math.sin(x1), d = 2 * Math.asin(Math.sqrt(d3_haversin(y1 - y0) + cy0 * cy1 * d3_haversin(x1 - x0))), k = 1 / Math.sin(d); + var interpolate = d ? function(t) { + var B = Math.sin(t *= d) * k, A = Math.sin(d - t) * k, x = A * kx0 + B * kx1, y = A * ky0 + B * ky1, z = A * sy0 + B * sy1; + return [ Math.atan2(y, x) * d3_degrees, Math.atan2(z, Math.sqrt(x * x + y * y)) * d3_degrees ]; + } : function() { + return [ x0 * d3_degrees, y0 * d3_degrees ]; + }; + interpolate.distance = d; + return interpolate; + } + d3.geo.length = function(object) { + d3_geo_lengthSum = 0; + d3.geo.stream(object, d3_geo_length); + return d3_geo_lengthSum; + }; + var d3_geo_lengthSum; + var d3_geo_length = { + sphere: d3_noop, + point: d3_noop, + lineStart: d3_geo_lengthLineStart, + lineEnd: d3_noop, + polygonStart: d3_noop, + polygonEnd: d3_noop + }; + function d3_geo_lengthLineStart() { + var λ0, sinφ0, cosφ0; + d3_geo_length.point = function(λ, φ) { + λ0 = λ * d3_radians, sinφ0 = Math.sin(φ *= d3_radians), cosφ0 = Math.cos(φ); + d3_geo_length.point = nextPoint; + }; + d3_geo_length.lineEnd = function() { + d3_geo_length.point = d3_geo_length.lineEnd = d3_noop; + }; + function nextPoint(λ, φ) { + var sinφ = Math.sin(φ *= d3_radians), cosφ = Math.cos(φ), t = abs((λ *= d3_radians) - λ0), cosΔλ = Math.cos(t); + d3_geo_lengthSum += Math.atan2(Math.sqrt((t = cosφ * Math.sin(t)) * t + (t = cosφ0 * sinφ - sinφ0 * cosφ * cosΔλ) * t), sinφ0 * sinφ + cosφ0 * cosφ * cosΔλ); + λ0 = λ, sinφ0 = sinφ, cosφ0 = cosφ; + } + } + function d3_geo_azimuthal(scale, angle) { + function azimuthal(λ, φ) { + var cosλ = Math.cos(λ), cosφ = Math.cos(φ), k = scale(cosλ * cosφ); + return [ k * cosφ * Math.sin(λ), k * Math.sin(φ) ]; + } + azimuthal.invert = function(x, y) { + var ρ = Math.sqrt(x * x + y * y), c = angle(ρ), sinc = Math.sin(c), cosc = Math.cos(c); + return [ Math.atan2(x * sinc, ρ * cosc), Math.asin(ρ && y * sinc / ρ) ]; + }; + return azimuthal; + } + var d3_geo_azimuthalEqualArea = d3_geo_azimuthal(function(cosλcosφ) { + return Math.sqrt(2 / (1 + cosλcosφ)); + }, function(ρ) { + return 2 * Math.asin(ρ / 2); + }); + (d3.geo.azimuthalEqualArea = function() { + return d3_geo_projection(d3_geo_azimuthalEqualArea); + }).raw = d3_geo_azimuthalEqualArea; + var d3_geo_azimuthalEquidistant = d3_geo_azimuthal(function(cosλcosφ) { + var c = Math.acos(cosλcosφ); + return c && c / Math.sin(c); + }, d3_identity); + (d3.geo.azimuthalEquidistant = function() { + return d3_geo_projection(d3_geo_azimuthalEquidistant); + }).raw = d3_geo_azimuthalEquidistant; + function d3_geo_conicConformal(φ0, φ1) { + var cosφ0 = Math.cos(φ0), t = function(φ) { + return Math.tan(π / 4 + φ / 2); + }, n = φ0 === φ1 ? Math.sin(φ0) : Math.log(cosφ0 / Math.cos(φ1)) / Math.log(t(φ1) / t(φ0)), F = cosφ0 * Math.pow(t(φ0), n) / n; + if (!n) return d3_geo_mercator; + function forward(λ, φ) { + if (F > 0) { + if (φ < -halfπ + ε) φ = -halfπ + ε; + } else { + if (φ > halfπ - ε) φ = halfπ - ε; + } + var ρ = F / Math.pow(t(φ), n); + return [ ρ * Math.sin(n * λ), F - ρ * Math.cos(n * λ) ]; + } + forward.invert = function(x, y) { + var ρ0_y = F - y, ρ = d3_sgn(n) * Math.sqrt(x * x + ρ0_y * ρ0_y); + return [ Math.atan2(x, ρ0_y) / n, 2 * Math.atan(Math.pow(F / ρ, 1 / n)) - halfπ ]; + }; + return forward; + } + (d3.geo.conicConformal = function() { + return d3_geo_conic(d3_geo_conicConformal); + }).raw = d3_geo_conicConformal; + function d3_geo_conicEquidistant(φ0, φ1) { + var cosφ0 = Math.cos(φ0), n = φ0 === φ1 ? Math.sin(φ0) : (cosφ0 - Math.cos(φ1)) / (φ1 - φ0), G = cosφ0 / n + φ0; + if (abs(n) < ε) return d3_geo_equirectangular; + function forward(λ, φ) { + var ρ = G - φ; + return [ ρ * Math.sin(n * λ), G - ρ * Math.cos(n * λ) ]; + } + forward.invert = function(x, y) { + var ρ0_y = G - y; + return [ Math.atan2(x, ρ0_y) / n, G - d3_sgn(n) * Math.sqrt(x * x + ρ0_y * ρ0_y) ]; + }; + return forward; + } + (d3.geo.conicEquidistant = function() { + return d3_geo_conic(d3_geo_conicEquidistant); + }).raw = d3_geo_conicEquidistant; + var d3_geo_gnomonic = d3_geo_azimuthal(function(cosλcosφ) { + return 1 / cosλcosφ; + }, Math.atan); + (d3.geo.gnomonic = function() { + return d3_geo_projection(d3_geo_gnomonic); + }).raw = d3_geo_gnomonic; + function d3_geo_mercator(λ, φ) { + return [ λ, Math.log(Math.tan(π / 4 + φ / 2)) ]; + } + d3_geo_mercator.invert = function(x, y) { + return [ x, 2 * Math.atan(Math.exp(y)) - halfπ ]; + }; + function d3_geo_mercatorProjection(project) { + var m = d3_geo_projection(project), scale = m.scale, translate = m.translate, clipExtent = m.clipExtent, clipAuto; + m.scale = function() { + var v = scale.apply(m, arguments); + return v === m ? clipAuto ? m.clipExtent(null) : m : v; + }; + m.translate = function() { + var v = translate.apply(m, arguments); + return v === m ? clipAuto ? m.clipExtent(null) : m : v; + }; + m.clipExtent = function(_) { + var v = clipExtent.apply(m, arguments); + if (v === m) { + if (clipAuto = _ == null) { + var k = π * scale(), t = translate(); + clipExtent([ [ t[0] - k, t[1] - k ], [ t[0] + k, t[1] + k ] ]); + } + } else if (clipAuto) { + v = null; + } + return v; + }; + return m.clipExtent(null); + } + (d3.geo.mercator = function() { + return d3_geo_mercatorProjection(d3_geo_mercator); + }).raw = d3_geo_mercator; + var d3_geo_orthographic = d3_geo_azimuthal(function() { + return 1; + }, Math.asin); + (d3.geo.orthographic = function() { + return d3_geo_projection(d3_geo_orthographic); + }).raw = d3_geo_orthographic; + var d3_geo_stereographic = d3_geo_azimuthal(function(cosλcosφ) { + return 1 / (1 + cosλcosφ); + }, function(ρ) { + return 2 * Math.atan(ρ); + }); + (d3.geo.stereographic = function() { + return d3_geo_projection(d3_geo_stereographic); + }).raw = d3_geo_stereographic; + function d3_geo_transverseMercator(λ, φ) { + return [ Math.log(Math.tan(π / 4 + φ / 2)), -λ ]; + } + d3_geo_transverseMercator.invert = function(x, y) { + return [ -y, 2 * Math.atan(Math.exp(x)) - halfπ ]; + }; + (d3.geo.transverseMercator = function() { + var projection = d3_geo_mercatorProjection(d3_geo_transverseMercator), center = projection.center, rotate = projection.rotate; + projection.center = function(_) { + return _ ? center([ -_[1], _[0] ]) : (_ = center(), [ _[1], -_[0] ]); + }; + projection.rotate = function(_) { + return _ ? rotate([ _[0], _[1], _.length > 2 ? _[2] + 90 : 90 ]) : (_ = rotate(), + [ _[0], _[1], _[2] - 90 ]); + }; + return rotate([ 0, 0, 90 ]); + }).raw = d3_geo_transverseMercator; + d3.geom = {}; + function d3_geom_pointX(d) { + return d[0]; + } + function d3_geom_pointY(d) { + return d[1]; + } + d3.geom.hull = function(vertices) { + var x = d3_geom_pointX, y = d3_geom_pointY; + if (arguments.length) return hull(vertices); + function hull(data) { + if (data.length < 3) return []; + var fx = d3_functor(x), fy = d3_functor(y), i, n = data.length, points = [], flippedPoints = []; + for (i = 0; i < n; i++) { + points.push([ +fx.call(this, data[i], i), +fy.call(this, data[i], i), i ]); + } + points.sort(d3_geom_hullOrder); + for (i = 0; i < n; i++) flippedPoints.push([ points[i][0], -points[i][1] ]); + var upper = d3_geom_hullUpper(points), lower = d3_geom_hullUpper(flippedPoints); + var skipLeft = lower[0] === upper[0], skipRight = lower[lower.length - 1] === upper[upper.length - 1], polygon = []; + for (i = upper.length - 1; i >= 0; --i) polygon.push(data[points[upper[i]][2]]); + for (i = +skipLeft; i < lower.length - skipRight; ++i) polygon.push(data[points[lower[i]][2]]); + return polygon; + } + hull.x = function(_) { + return arguments.length ? (x = _, hull) : x; + }; + hull.y = function(_) { + return arguments.length ? (y = _, hull) : y; + }; + return hull; + }; + function d3_geom_hullUpper(points) { + var n = points.length, hull = [ 0, 1 ], hs = 2; + for (var i = 2; i < n; i++) { + while (hs > 1 && d3_cross2d(points[hull[hs - 2]], points[hull[hs - 1]], points[i]) <= 0) --hs; + hull[hs++] = i; + } + return hull.slice(0, hs); + } + function d3_geom_hullOrder(a, b) { + return a[0] - b[0] || a[1] - b[1]; + } + d3.geom.polygon = function(coordinates) { + d3_subclass(coordinates, d3_geom_polygonPrototype); + return coordinates; + }; + var d3_geom_polygonPrototype = d3.geom.polygon.prototype = []; + d3_geom_polygonPrototype.area = function() { + var i = -1, n = this.length, a, b = this[n - 1], area = 0; + while (++i < n) { + a = b; + b = this[i]; + area += a[1] * b[0] - a[0] * b[1]; + } + return area * .5; + }; + d3_geom_polygonPrototype.centroid = function(k) { + var i = -1, n = this.length, x = 0, y = 0, a, b = this[n - 1], c; + if (!arguments.length) k = -1 / (6 * this.area()); + while (++i < n) { + a = b; + b = this[i]; + c = a[0] * b[1] - b[0] * a[1]; + x += (a[0] + b[0]) * c; + y += (a[1] + b[1]) * c; + } + return [ x * k, y * k ]; + }; + d3_geom_polygonPrototype.clip = function(subject) { + var input, closed = d3_geom_polygonClosed(subject), i = -1, n = this.length - d3_geom_polygonClosed(this), j, m, a = this[n - 1], b, c, d; + while (++i < n) { + input = subject.slice(); + subject.length = 0; + b = this[i]; + c = input[(m = input.length - closed) - 1]; + j = -1; + while (++j < m) { + d = input[j]; + if (d3_geom_polygonInside(d, a, b)) { + if (!d3_geom_polygonInside(c, a, b)) { + subject.push(d3_geom_polygonIntersect(c, d, a, b)); + } + subject.push(d); + } else if (d3_geom_polygonInside(c, a, b)) { + subject.push(d3_geom_polygonIntersect(c, d, a, b)); + } + c = d; + } + if (closed) subject.push(subject[0]); + a = b; + } + return subject; + }; + function d3_geom_polygonInside(p, a, b) { + return (b[0] - a[0]) * (p[1] - a[1]) < (b[1] - a[1]) * (p[0] - a[0]); + } + function d3_geom_polygonIntersect(c, d, a, b) { + var x1 = c[0], x3 = a[0], x21 = d[0] - x1, x43 = b[0] - x3, y1 = c[1], y3 = a[1], y21 = d[1] - y1, y43 = b[1] - y3, ua = (x43 * (y1 - y3) - y43 * (x1 - x3)) / (y43 * x21 - x43 * y21); + return [ x1 + ua * x21, y1 + ua * y21 ]; + } + function d3_geom_polygonClosed(coordinates) { + var a = coordinates[0], b = coordinates[coordinates.length - 1]; + return !(a[0] - b[0] || a[1] - b[1]); + } + var d3_geom_voronoiEdges, d3_geom_voronoiCells, d3_geom_voronoiBeaches, d3_geom_voronoiBeachPool = [], d3_geom_voronoiFirstCircle, d3_geom_voronoiCircles, d3_geom_voronoiCirclePool = []; + function d3_geom_voronoiBeach() { + d3_geom_voronoiRedBlackNode(this); + this.edge = this.site = this.circle = null; + } + function d3_geom_voronoiCreateBeach(site) { + var beach = d3_geom_voronoiBeachPool.pop() || new d3_geom_voronoiBeach(); + beach.site = site; + return beach; + } + function d3_geom_voronoiDetachBeach(beach) { + d3_geom_voronoiDetachCircle(beach); + d3_geom_voronoiBeaches.remove(beach); + d3_geom_voronoiBeachPool.push(beach); + d3_geom_voronoiRedBlackNode(beach); + } + function d3_geom_voronoiRemoveBeach(beach) { + var circle = beach.circle, x = circle.x, y = circle.cy, vertex = { + x: x, + y: y + }, previous = beach.P, next = beach.N, disappearing = [ beach ]; + d3_geom_voronoiDetachBeach(beach); + var lArc = previous; + while (lArc.circle && abs(x - lArc.circle.x) < ε && abs(y - lArc.circle.cy) < ε) { + previous = lArc.P; + disappearing.unshift(lArc); + d3_geom_voronoiDetachBeach(lArc); + lArc = previous; + } + disappearing.unshift(lArc); + d3_geom_voronoiDetachCircle(lArc); + var rArc = next; + while (rArc.circle && abs(x - rArc.circle.x) < ε && abs(y - rArc.circle.cy) < ε) { + next = rArc.N; + disappearing.push(rArc); + d3_geom_voronoiDetachBeach(rArc); + rArc = next; + } + disappearing.push(rArc); + d3_geom_voronoiDetachCircle(rArc); + var nArcs = disappearing.length, iArc; + for (iArc = 1; iArc < nArcs; ++iArc) { + rArc = disappearing[iArc]; + lArc = disappearing[iArc - 1]; + d3_geom_voronoiSetEdgeEnd(rArc.edge, lArc.site, rArc.site, vertex); + } + lArc = disappearing[0]; + rArc = disappearing[nArcs - 1]; + rArc.edge = d3_geom_voronoiCreateEdge(lArc.site, rArc.site, null, vertex); + d3_geom_voronoiAttachCircle(lArc); + d3_geom_voronoiAttachCircle(rArc); + } + function d3_geom_voronoiAddBeach(site) { + var x = site.x, directrix = site.y, lArc, rArc, dxl, dxr, node = d3_geom_voronoiBeaches._; + while (node) { + dxl = d3_geom_voronoiLeftBreakPoint(node, directrix) - x; + if (dxl > ε) node = node.L; else { + dxr = x - d3_geom_voronoiRightBreakPoint(node, directrix); + if (dxr > ε) { + if (!node.R) { + lArc = node; + break; + } + node = node.R; + } else { + if (dxl > -ε) { + lArc = node.P; + rArc = node; + } else if (dxr > -ε) { + lArc = node; + rArc = node.N; + } else { + lArc = rArc = node; + } + break; + } + } + } + var newArc = d3_geom_voronoiCreateBeach(site); + d3_geom_voronoiBeaches.insert(lArc, newArc); + if (!lArc && !rArc) return; + if (lArc === rArc) { + d3_geom_voronoiDetachCircle(lArc); + rArc = d3_geom_voronoiCreateBeach(lArc.site); + d3_geom_voronoiBeaches.insert(newArc, rArc); + newArc.edge = rArc.edge = d3_geom_voronoiCreateEdge(lArc.site, newArc.site); + d3_geom_voronoiAttachCircle(lArc); + d3_geom_voronoiAttachCircle(rArc); + return; + } + if (!rArc) { + newArc.edge = d3_geom_voronoiCreateEdge(lArc.site, newArc.site); + return; + } + d3_geom_voronoiDetachCircle(lArc); + d3_geom_voronoiDetachCircle(rArc); + var lSite = lArc.site, ax = lSite.x, ay = lSite.y, bx = site.x - ax, by = site.y - ay, rSite = rArc.site, cx = rSite.x - ax, cy = rSite.y - ay, d = 2 * (bx * cy - by * cx), hb = bx * bx + by * by, hc = cx * cx + cy * cy, vertex = { + x: (cy * hb - by * hc) / d + ax, + y: (bx * hc - cx * hb) / d + ay + }; + d3_geom_voronoiSetEdgeEnd(rArc.edge, lSite, rSite, vertex); + newArc.edge = d3_geom_voronoiCreateEdge(lSite, site, null, vertex); + rArc.edge = d3_geom_voronoiCreateEdge(site, rSite, null, vertex); + d3_geom_voronoiAttachCircle(lArc); + d3_geom_voronoiAttachCircle(rArc); + } + function d3_geom_voronoiLeftBreakPoint(arc, directrix) { + var site = arc.site, rfocx = site.x, rfocy = site.y, pby2 = rfocy - directrix; + if (!pby2) return rfocx; + var lArc = arc.P; + if (!lArc) return -Infinity; + site = lArc.site; + var lfocx = site.x, lfocy = site.y, plby2 = lfocy - directrix; + if (!plby2) return lfocx; + var hl = lfocx - rfocx, aby2 = 1 / pby2 - 1 / plby2, b = hl / plby2; + if (aby2) return (-b + Math.sqrt(b * b - 2 * aby2 * (hl * hl / (-2 * plby2) - lfocy + plby2 / 2 + rfocy - pby2 / 2))) / aby2 + rfocx; + return (rfocx + lfocx) / 2; + } + function d3_geom_voronoiRightBreakPoint(arc, directrix) { + var rArc = arc.N; + if (rArc) return d3_geom_voronoiLeftBreakPoint(rArc, directrix); + var site = arc.site; + return site.y === directrix ? site.x : Infinity; + } + function d3_geom_voronoiCell(site) { + this.site = site; + this.edges = []; + } + d3_geom_voronoiCell.prototype.prepare = function() { + var halfEdges = this.edges, iHalfEdge = halfEdges.length, edge; + while (iHalfEdge--) { + edge = halfEdges[iHalfEdge].edge; + if (!edge.b || !edge.a) halfEdges.splice(iHalfEdge, 1); + } + halfEdges.sort(d3_geom_voronoiHalfEdgeOrder); + return halfEdges.length; + }; + function d3_geom_voronoiCloseCells(extent) { + var x0 = extent[0][0], x1 = extent[1][0], y0 = extent[0][1], y1 = extent[1][1], x2, y2, x3, y3, cells = d3_geom_voronoiCells, iCell = cells.length, cell, iHalfEdge, halfEdges, nHalfEdges, start, end; + while (iCell--) { + cell = cells[iCell]; + if (!cell || !cell.prepare()) continue; + halfEdges = cell.edges; + nHalfEdges = halfEdges.length; + iHalfEdge = 0; + while (iHalfEdge < nHalfEdges) { + end = halfEdges[iHalfEdge].end(), x3 = end.x, y3 = end.y; + start = halfEdges[++iHalfEdge % nHalfEdges].start(), x2 = start.x, y2 = start.y; + if (abs(x3 - x2) > ε || abs(y3 - y2) > ε) { + halfEdges.splice(iHalfEdge, 0, new d3_geom_voronoiHalfEdge(d3_geom_voronoiCreateBorderEdge(cell.site, end, abs(x3 - x0) < ε && y1 - y3 > ε ? { + x: x0, + y: abs(x2 - x0) < ε ? y2 : y1 + } : abs(y3 - y1) < ε && x1 - x3 > ε ? { + x: abs(y2 - y1) < ε ? x2 : x1, + y: y1 + } : abs(x3 - x1) < ε && y3 - y0 > ε ? { + x: x1, + y: abs(x2 - x1) < ε ? y2 : y0 + } : abs(y3 - y0) < ε && x3 - x0 > ε ? { + x: abs(y2 - y0) < ε ? x2 : x0, + y: y0 + } : null), cell.site, null)); + ++nHalfEdges; + } + } + } + } + function d3_geom_voronoiHalfEdgeOrder(a, b) { + return b.angle - a.angle; + } + function d3_geom_voronoiCircle() { + d3_geom_voronoiRedBlackNode(this); + this.x = this.y = this.arc = this.site = this.cy = null; + } + function d3_geom_voronoiAttachCircle(arc) { + var lArc = arc.P, rArc = arc.N; + if (!lArc || !rArc) return; + var lSite = lArc.site, cSite = arc.site, rSite = rArc.site; + if (lSite === rSite) return; + var bx = cSite.x, by = cSite.y, ax = lSite.x - bx, ay = lSite.y - by, cx = rSite.x - bx, cy = rSite.y - by; + var d = 2 * (ax * cy - ay * cx); + if (d >= -ε2) return; + var ha = ax * ax + ay * ay, hc = cx * cx + cy * cy, x = (cy * ha - ay * hc) / d, y = (ax * hc - cx * ha) / d, cy = y + by; + var circle = d3_geom_voronoiCirclePool.pop() || new d3_geom_voronoiCircle(); + circle.arc = arc; + circle.site = cSite; + circle.x = x + bx; + circle.y = cy + Math.sqrt(x * x + y * y); + circle.cy = cy; + arc.circle = circle; + var before = null, node = d3_geom_voronoiCircles._; + while (node) { + if (circle.y < node.y || circle.y === node.y && circle.x <= node.x) { + if (node.L) node = node.L; else { + before = node.P; + break; + } + } else { + if (node.R) node = node.R; else { + before = node; + break; + } + } + } + d3_geom_voronoiCircles.insert(before, circle); + if (!before) d3_geom_voronoiFirstCircle = circle; + } + function d3_geom_voronoiDetachCircle(arc) { + var circle = arc.circle; + if (circle) { + if (!circle.P) d3_geom_voronoiFirstCircle = circle.N; + d3_geom_voronoiCircles.remove(circle); + d3_geom_voronoiCirclePool.push(circle); + d3_geom_voronoiRedBlackNode(circle); + arc.circle = null; + } + } + function d3_geom_voronoiClipEdges(extent) { + var edges = d3_geom_voronoiEdges, clip = d3_geom_clipLine(extent[0][0], extent[0][1], extent[1][0], extent[1][1]), i = edges.length, e; + while (i--) { + e = edges[i]; + if (!d3_geom_voronoiConnectEdge(e, extent) || !clip(e) || abs(e.a.x - e.b.x) < ε && abs(e.a.y - e.b.y) < ε) { + e.a = e.b = null; + edges.splice(i, 1); + } + } + } + function d3_geom_voronoiConnectEdge(edge, extent) { + var vb = edge.b; + if (vb) return true; + var va = edge.a, x0 = extent[0][0], x1 = extent[1][0], y0 = extent[0][1], y1 = extent[1][1], lSite = edge.l, rSite = edge.r, lx = lSite.x, ly = lSite.y, rx = rSite.x, ry = rSite.y, fx = (lx + rx) / 2, fy = (ly + ry) / 2, fm, fb; + if (ry === ly) { + if (fx < x0 || fx >= x1) return; + if (lx > rx) { + if (!va) va = { + x: fx, + y: y0 + }; else if (va.y >= y1) return; + vb = { + x: fx, + y: y1 + }; + } else { + if (!va) va = { + x: fx, + y: y1 + }; else if (va.y < y0) return; + vb = { + x: fx, + y: y0 + }; + } + } else { + fm = (lx - rx) / (ry - ly); + fb = fy - fm * fx; + if (fm < -1 || fm > 1) { + if (lx > rx) { + if (!va) va = { + x: (y0 - fb) / fm, + y: y0 + }; else if (va.y >= y1) return; + vb = { + x: (y1 - fb) / fm, + y: y1 + }; + } else { + if (!va) va = { + x: (y1 - fb) / fm, + y: y1 + }; else if (va.y < y0) return; + vb = { + x: (y0 - fb) / fm, + y: y0 + }; + } + } else { + if (ly < ry) { + if (!va) va = { + x: x0, + y: fm * x0 + fb + }; else if (va.x >= x1) return; + vb = { + x: x1, + y: fm * x1 + fb + }; + } else { + if (!va) va = { + x: x1, + y: fm * x1 + fb + }; else if (va.x < x0) return; + vb = { + x: x0, + y: fm * x0 + fb + }; + } + } + } + edge.a = va; + edge.b = vb; + return true; + } + function d3_geom_voronoiEdge(lSite, rSite) { + this.l = lSite; + this.r = rSite; + this.a = this.b = null; + } + function d3_geom_voronoiCreateEdge(lSite, rSite, va, vb) { + var edge = new d3_geom_voronoiEdge(lSite, rSite); + d3_geom_voronoiEdges.push(edge); + if (va) d3_geom_voronoiSetEdgeEnd(edge, lSite, rSite, va); + if (vb) d3_geom_voronoiSetEdgeEnd(edge, rSite, lSite, vb); + d3_geom_voronoiCells[lSite.i].edges.push(new d3_geom_voronoiHalfEdge(edge, lSite, rSite)); + d3_geom_voronoiCells[rSite.i].edges.push(new d3_geom_voronoiHalfEdge(edge, rSite, lSite)); + return edge; + } + function d3_geom_voronoiCreateBorderEdge(lSite, va, vb) { + var edge = new d3_geom_voronoiEdge(lSite, null); + edge.a = va; + edge.b = vb; + d3_geom_voronoiEdges.push(edge); + return edge; + } + function d3_geom_voronoiSetEdgeEnd(edge, lSite, rSite, vertex) { + if (!edge.a && !edge.b) { + edge.a = vertex; + edge.l = lSite; + edge.r = rSite; + } else if (edge.l === rSite) { + edge.b = vertex; + } else { + edge.a = vertex; + } + } + function d3_geom_voronoiHalfEdge(edge, lSite, rSite) { + var va = edge.a, vb = edge.b; + this.edge = edge; + this.site = lSite; + this.angle = rSite ? Math.atan2(rSite.y - lSite.y, rSite.x - lSite.x) : edge.l === lSite ? Math.atan2(vb.x - va.x, va.y - vb.y) : Math.atan2(va.x - vb.x, vb.y - va.y); + } + d3_geom_voronoiHalfEdge.prototype = { + start: function() { + return this.edge.l === this.site ? this.edge.a : this.edge.b; + }, + end: function() { + return this.edge.l === this.site ? this.edge.b : this.edge.a; + } + }; + function d3_geom_voronoiRedBlackTree() { + this._ = null; + } + function d3_geom_voronoiRedBlackNode(node) { + node.U = node.C = node.L = node.R = node.P = node.N = null; + } + d3_geom_voronoiRedBlackTree.prototype = { + insert: function(after, node) { + var parent, grandpa, uncle; + if (after) { + node.P = after; + node.N = after.N; + if (after.N) after.N.P = node; + after.N = node; + if (after.R) { + after = after.R; + while (after.L) after = after.L; + after.L = node; + } else { + after.R = node; + } + parent = after; + } else if (this._) { + after = d3_geom_voronoiRedBlackFirst(this._); + node.P = null; + node.N = after; + after.P = after.L = node; + parent = after; + } else { + node.P = node.N = null; + this._ = node; + parent = null; + } + node.L = node.R = null; + node.U = parent; + node.C = true; + after = node; + while (parent && parent.C) { + grandpa = parent.U; + if (parent === grandpa.L) { + uncle = grandpa.R; + if (uncle && uncle.C) { + parent.C = uncle.C = false; + grandpa.C = true; + after = grandpa; + } else { + if (after === parent.R) { + d3_geom_voronoiRedBlackRotateLeft(this, parent); + after = parent; + parent = after.U; + } + parent.C = false; + grandpa.C = true; + d3_geom_voronoiRedBlackRotateRight(this, grandpa); + } + } else { + uncle = grandpa.L; + if (uncle && uncle.C) { + parent.C = uncle.C = false; + grandpa.C = true; + after = grandpa; + } else { + if (after === parent.L) { + d3_geom_voronoiRedBlackRotateRight(this, parent); + after = parent; + parent = after.U; + } + parent.C = false; + grandpa.C = true; + d3_geom_voronoiRedBlackRotateLeft(this, grandpa); + } + } + parent = after.U; + } + this._.C = false; + }, + remove: function(node) { + if (node.N) node.N.P = node.P; + if (node.P) node.P.N = node.N; + node.N = node.P = null; + var parent = node.U, sibling, left = node.L, right = node.R, next, red; + if (!left) next = right; else if (!right) next = left; else next = d3_geom_voronoiRedBlackFirst(right); + if (parent) { + if (parent.L === node) parent.L = next; else parent.R = next; + } else { + this._ = next; + } + if (left && right) { + red = next.C; + next.C = node.C; + next.L = left; + left.U = next; + if (next !== right) { + parent = next.U; + next.U = node.U; + node = next.R; + parent.L = node; + next.R = right; + right.U = next; + } else { + next.U = parent; + parent = next; + node = next.R; + } + } else { + red = node.C; + node = next; + } + if (node) node.U = parent; + if (red) return; + if (node && node.C) { + node.C = false; + return; + } + do { + if (node === this._) break; + if (node === parent.L) { + sibling = parent.R; + if (sibling.C) { + sibling.C = false; + parent.C = true; + d3_geom_voronoiRedBlackRotateLeft(this, parent); + sibling = parent.R; + } + if (sibling.L && sibling.L.C || sibling.R && sibling.R.C) { + if (!sibling.R || !sibling.R.C) { + sibling.L.C = false; + sibling.C = true; + d3_geom_voronoiRedBlackRotateRight(this, sibling); + sibling = parent.R; + } + sibling.C = parent.C; + parent.C = sibling.R.C = false; + d3_geom_voronoiRedBlackRotateLeft(this, parent); + node = this._; + break; + } + } else { + sibling = parent.L; + if (sibling.C) { + sibling.C = false; + parent.C = true; + d3_geom_voronoiRedBlackRotateRight(this, parent); + sibling = parent.L; + } + if (sibling.L && sibling.L.C || sibling.R && sibling.R.C) { + if (!sibling.L || !sibling.L.C) { + sibling.R.C = false; + sibling.C = true; + d3_geom_voronoiRedBlackRotateLeft(this, sibling); + sibling = parent.L; + } + sibling.C = parent.C; + parent.C = sibling.L.C = false; + d3_geom_voronoiRedBlackRotateRight(this, parent); + node = this._; + break; + } + } + sibling.C = true; + node = parent; + parent = parent.U; + } while (!node.C); + if (node) node.C = false; + } + }; + function d3_geom_voronoiRedBlackRotateLeft(tree, node) { + var p = node, q = node.R, parent = p.U; + if (parent) { + if (parent.L === p) parent.L = q; else parent.R = q; + } else { + tree._ = q; + } + q.U = parent; + p.U = q; + p.R = q.L; + if (p.R) p.R.U = p; + q.L = p; + } + function d3_geom_voronoiRedBlackRotateRight(tree, node) { + var p = node, q = node.L, parent = p.U; + if (parent) { + if (parent.L === p) parent.L = q; else parent.R = q; + } else { + tree._ = q; + } + q.U = parent; + p.U = q; + p.L = q.R; + if (p.L) p.L.U = p; + q.R = p; + } + function d3_geom_voronoiRedBlackFirst(node) { + while (node.L) node = node.L; + return node; + } + function d3_geom_voronoi(sites, bbox) { + var site = sites.sort(d3_geom_voronoiVertexOrder).pop(), x0, y0, circle; + d3_geom_voronoiEdges = []; + d3_geom_voronoiCells = new Array(sites.length); + d3_geom_voronoiBeaches = new d3_geom_voronoiRedBlackTree(); + d3_geom_voronoiCircles = new d3_geom_voronoiRedBlackTree(); + while (true) { + circle = d3_geom_voronoiFirstCircle; + if (site && (!circle || site.y < circle.y || site.y === circle.y && site.x < circle.x)) { + if (site.x !== x0 || site.y !== y0) { + d3_geom_voronoiCells[site.i] = new d3_geom_voronoiCell(site); + d3_geom_voronoiAddBeach(site); + x0 = site.x, y0 = site.y; + } + site = sites.pop(); + } else if (circle) { + d3_geom_voronoiRemoveBeach(circle.arc); + } else { + break; + } + } + if (bbox) d3_geom_voronoiClipEdges(bbox), d3_geom_voronoiCloseCells(bbox); + var diagram = { + cells: d3_geom_voronoiCells, + edges: d3_geom_voronoiEdges + }; + d3_geom_voronoiBeaches = d3_geom_voronoiCircles = d3_geom_voronoiEdges = d3_geom_voronoiCells = null; + return diagram; + } + function d3_geom_voronoiVertexOrder(a, b) { + return b.y - a.y || b.x - a.x; + } + d3.geom.voronoi = function(points) { + var x = d3_geom_pointX, y = d3_geom_pointY, fx = x, fy = y, clipExtent = d3_geom_voronoiClipExtent; + if (points) return voronoi(points); + function voronoi(data) { + var polygons = new Array(data.length), x0 = clipExtent[0][0], y0 = clipExtent[0][1], x1 = clipExtent[1][0], y1 = clipExtent[1][1]; + d3_geom_voronoi(sites(data), clipExtent).cells.forEach(function(cell, i) { + var edges = cell.edges, site = cell.site, polygon = polygons[i] = edges.length ? edges.map(function(e) { + var s = e.start(); + return [ s.x, s.y ]; + }) : site.x >= x0 && site.x <= x1 && site.y >= y0 && site.y <= y1 ? [ [ x0, y1 ], [ x1, y1 ], [ x1, y0 ], [ x0, y0 ] ] : []; + polygon.point = data[i]; + }); + return polygons; + } + function sites(data) { + return data.map(function(d, i) { + return { + x: Math.round(fx(d, i) / ε) * ε, + y: Math.round(fy(d, i) / ε) * ε, + i: i + }; + }); + } + voronoi.links = function(data) { + return d3_geom_voronoi(sites(data)).edges.filter(function(edge) { + return edge.l && edge.r; + }).map(function(edge) { + return { + source: data[edge.l.i], + target: data[edge.r.i] + }; + }); + }; + voronoi.triangles = function(data) { + var triangles = []; + d3_geom_voronoi(sites(data)).cells.forEach(function(cell, i) { + var site = cell.site, edges = cell.edges.sort(d3_geom_voronoiHalfEdgeOrder), j = -1, m = edges.length, e0, s0, e1 = edges[m - 1].edge, s1 = e1.l === site ? e1.r : e1.l; + while (++j < m) { + e0 = e1; + s0 = s1; + e1 = edges[j].edge; + s1 = e1.l === site ? e1.r : e1.l; + if (i < s0.i && i < s1.i && d3_geom_voronoiTriangleArea(site, s0, s1) < 0) { + triangles.push([ data[i], data[s0.i], data[s1.i] ]); + } + } + }); + return triangles; + }; + voronoi.x = function(_) { + return arguments.length ? (fx = d3_functor(x = _), voronoi) : x; + }; + voronoi.y = function(_) { + return arguments.length ? (fy = d3_functor(y = _), voronoi) : y; + }; + voronoi.clipExtent = function(_) { + if (!arguments.length) return clipExtent === d3_geom_voronoiClipExtent ? null : clipExtent; + clipExtent = _ == null ? d3_geom_voronoiClipExtent : _; + return voronoi; + }; + voronoi.size = function(_) { + if (!arguments.length) return clipExtent === d3_geom_voronoiClipExtent ? null : clipExtent && clipExtent[1]; + return voronoi.clipExtent(_ && [ [ 0, 0 ], _ ]); + }; + return voronoi; + }; + var d3_geom_voronoiClipExtent = [ [ -1e6, -1e6 ], [ 1e6, 1e6 ] ]; + function d3_geom_voronoiTriangleArea(a, b, c) { + return (a.x - c.x) * (b.y - a.y) - (a.x - b.x) * (c.y - a.y); + } + d3.geom.delaunay = function(vertices) { + return d3.geom.voronoi().triangles(vertices); + }; + d3.geom.quadtree = function(points, x1, y1, x2, y2) { + var x = d3_geom_pointX, y = d3_geom_pointY, compat; + if (compat = arguments.length) { + x = d3_geom_quadtreeCompatX; + y = d3_geom_quadtreeCompatY; + if (compat === 3) { + y2 = y1; + x2 = x1; + y1 = x1 = 0; + } + return quadtree(points); + } + function quadtree(data) { + var d, fx = d3_functor(x), fy = d3_functor(y), xs, ys, i, n, x1_, y1_, x2_, y2_; + if (x1 != null) { + x1_ = x1, y1_ = y1, x2_ = x2, y2_ = y2; + } else { + x2_ = y2_ = -(x1_ = y1_ = Infinity); + xs = [], ys = []; + n = data.length; + if (compat) for (i = 0; i < n; ++i) { + d = data[i]; + if (d.x < x1_) x1_ = d.x; + if (d.y < y1_) y1_ = d.y; + if (d.x > x2_) x2_ = d.x; + if (d.y > y2_) y2_ = d.y; + xs.push(d.x); + ys.push(d.y); + } else for (i = 0; i < n; ++i) { + var x_ = +fx(d = data[i], i), y_ = +fy(d, i); + if (x_ < x1_) x1_ = x_; + if (y_ < y1_) y1_ = y_; + if (x_ > x2_) x2_ = x_; + if (y_ > y2_) y2_ = y_; + xs.push(x_); + ys.push(y_); + } + } + var dx = x2_ - x1_, dy = y2_ - y1_; + if (dx > dy) y2_ = y1_ + dx; else x2_ = x1_ + dy; + function insert(n, d, x, y, x1, y1, x2, y2) { + if (isNaN(x) || isNaN(y)) return; + if (n.leaf) { + var nx = n.x, ny = n.y; + if (nx != null) { + if (abs(nx - x) + abs(ny - y) < .01) { + insertChild(n, d, x, y, x1, y1, x2, y2); + } else { + var nPoint = n.point; + n.x = n.y = n.point = null; + insertChild(n, nPoint, nx, ny, x1, y1, x2, y2); + insertChild(n, d, x, y, x1, y1, x2, y2); + } + } else { + n.x = x, n.y = y, n.point = d; + } + } else { + insertChild(n, d, x, y, x1, y1, x2, y2); + } + } + function insertChild(n, d, x, y, x1, y1, x2, y2) { + var xm = (x1 + x2) * .5, ym = (y1 + y2) * .5, right = x >= xm, below = y >= ym, i = below << 1 | right; + n.leaf = false; + n = n.nodes[i] || (n.nodes[i] = d3_geom_quadtreeNode()); + if (right) x1 = xm; else x2 = xm; + if (below) y1 = ym; else y2 = ym; + insert(n, d, x, y, x1, y1, x2, y2); + } + var root = d3_geom_quadtreeNode(); + root.add = function(d) { + insert(root, d, +fx(d, ++i), +fy(d, i), x1_, y1_, x2_, y2_); + }; + root.visit = function(f) { + d3_geom_quadtreeVisit(f, root, x1_, y1_, x2_, y2_); + }; + root.find = function(point) { + return d3_geom_quadtreeFind(root, point[0], point[1], x1_, y1_, x2_, y2_); + }; + i = -1; + if (x1 == null) { + while (++i < n) { + insert(root, data[i], xs[i], ys[i], x1_, y1_, x2_, y2_); + } + --i; + } else data.forEach(root.add); + xs = ys = data = d = null; + return root; + } + quadtree.x = function(_) { + return arguments.length ? (x = _, quadtree) : x; + }; + quadtree.y = function(_) { + return arguments.length ? (y = _, quadtree) : y; + }; + quadtree.extent = function(_) { + if (!arguments.length) return x1 == null ? null : [ [ x1, y1 ], [ x2, y2 ] ]; + if (_ == null) x1 = y1 = x2 = y2 = null; else x1 = +_[0][0], y1 = +_[0][1], x2 = +_[1][0], + y2 = +_[1][1]; + return quadtree; + }; + quadtree.size = function(_) { + if (!arguments.length) return x1 == null ? null : [ x2 - x1, y2 - y1 ]; + if (_ == null) x1 = y1 = x2 = y2 = null; else x1 = y1 = 0, x2 = +_[0], y2 = +_[1]; + return quadtree; + }; + return quadtree; + }; + function d3_geom_quadtreeCompatX(d) { + return d.x; + } + function d3_geom_quadtreeCompatY(d) { + return d.y; + } + function d3_geom_quadtreeNode() { + return { + leaf: true, + nodes: [], + point: null, + x: null, + y: null + }; + } + function d3_geom_quadtreeVisit(f, node, x1, y1, x2, y2) { + if (!f(node, x1, y1, x2, y2)) { + var sx = (x1 + x2) * .5, sy = (y1 + y2) * .5, children = node.nodes; + if (children[0]) d3_geom_quadtreeVisit(f, children[0], x1, y1, sx, sy); + if (children[1]) d3_geom_quadtreeVisit(f, children[1], sx, y1, x2, sy); + if (children[2]) d3_geom_quadtreeVisit(f, children[2], x1, sy, sx, y2); + if (children[3]) d3_geom_quadtreeVisit(f, children[3], sx, sy, x2, y2); + } + } + function d3_geom_quadtreeFind(root, x, y, x0, y0, x3, y3) { + var minDistance2 = Infinity, closestPoint; + (function find(node, x1, y1, x2, y2) { + if (x1 > x3 || y1 > y3 || x2 < x0 || y2 < y0) return; + if (point = node.point) { + var point, dx = x - point[0], dy = y - point[1], distance2 = dx * dx + dy * dy; + if (distance2 < minDistance2) { + var distance = Math.sqrt(minDistance2 = distance2); + x0 = x - distance, y0 = y - distance; + x3 = x + distance, y3 = y + distance; + closestPoint = point; + } + } + var children = node.nodes, xm = (x1 + x2) * .5, ym = (y1 + y2) * .5, right = x >= xm, below = y >= ym; + for (var i = below << 1 | right, j = i + 4; i < j; ++i) { + if (node = children[i & 3]) switch (i & 3) { + case 0: + find(node, x1, y1, xm, ym); + break; + + case 1: + find(node, xm, y1, x2, ym); + break; + + case 2: + find(node, x1, ym, xm, y2); + break; + + case 3: + find(node, xm, ym, x2, y2); + break; + } + } + })(root, x0, y0, x3, y3); + return closestPoint; + } + d3.interpolateRgb = d3_interpolateRgb; + function d3_interpolateRgb(a, b) { + a = d3.rgb(a); + b = d3.rgb(b); + var ar = a.r, ag = a.g, ab = a.b, br = b.r - ar, bg = b.g - ag, bb = b.b - ab; + return function(t) { + return "#" + d3_rgb_hex(Math.round(ar + br * t)) + d3_rgb_hex(Math.round(ag + bg * t)) + d3_rgb_hex(Math.round(ab + bb * t)); + }; + } + d3.interpolateObject = d3_interpolateObject; + function d3_interpolateObject(a, b) { + var i = {}, c = {}, k; + for (k in a) { + if (k in b) { + i[k] = d3_interpolate(a[k], b[k]); + } else { + c[k] = a[k]; + } + } + for (k in b) { + if (!(k in a)) { + c[k] = b[k]; + } + } + return function(t) { + for (k in i) c[k] = i[k](t); + return c; + }; + } + d3.interpolateNumber = d3_interpolateNumber; + function d3_interpolateNumber(a, b) { + a = +a, b = +b; + return function(t) { + return a * (1 - t) + b * t; + }; + } + d3.interpolateString = d3_interpolateString; + function d3_interpolateString(a, b) { + var bi = d3_interpolate_numberA.lastIndex = d3_interpolate_numberB.lastIndex = 0, am, bm, bs, i = -1, s = [], q = []; + a = a + "", b = b + ""; + while ((am = d3_interpolate_numberA.exec(a)) && (bm = d3_interpolate_numberB.exec(b))) { + if ((bs = bm.index) > bi) { + bs = b.slice(bi, bs); + if (s[i]) s[i] += bs; else s[++i] = bs; + } + if ((am = am[0]) === (bm = bm[0])) { + if (s[i]) s[i] += bm; else s[++i] = bm; + } else { + s[++i] = null; + q.push({ + i: i, + x: d3_interpolateNumber(am, bm) + }); + } + bi = d3_interpolate_numberB.lastIndex; + } + if (bi < b.length) { + bs = b.slice(bi); + if (s[i]) s[i] += bs; else s[++i] = bs; + } + return s.length < 2 ? q[0] ? (b = q[0].x, function(t) { + return b(t) + ""; + }) : function() { + return b; + } : (b = q.length, function(t) { + for (var i = 0, o; i < b; ++i) s[(o = q[i]).i] = o.x(t); + return s.join(""); + }); + } + var d3_interpolate_numberA = /[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g, d3_interpolate_numberB = new RegExp(d3_interpolate_numberA.source, "g"); + d3.interpolate = d3_interpolate; + function d3_interpolate(a, b) { + var i = d3.interpolators.length, f; + while (--i >= 0 && !(f = d3.interpolators[i](a, b))) ; + return f; + } + d3.interpolators = [ function(a, b) { + var t = typeof b; + return (t === "string" ? d3_rgb_names.has(b) || /^(#|rgb\(|hsl\()/.test(b) ? d3_interpolateRgb : d3_interpolateString : b instanceof d3_color ? d3_interpolateRgb : Array.isArray(b) ? d3_interpolateArray : t === "object" && isNaN(b) ? d3_interpolateObject : d3_interpolateNumber)(a, b); + } ]; + d3.interpolateArray = d3_interpolateArray; + function d3_interpolateArray(a, b) { + var x = [], c = [], na = a.length, nb = b.length, n0 = Math.min(a.length, b.length), i; + for (i = 0; i < n0; ++i) x.push(d3_interpolate(a[i], b[i])); + for (;i < na; ++i) c[i] = a[i]; + for (;i < nb; ++i) c[i] = b[i]; + return function(t) { + for (i = 0; i < n0; ++i) c[i] = x[i](t); + return c; + }; + } + var d3_ease_default = function() { + return d3_identity; + }; + var d3_ease = d3.map({ + linear: d3_ease_default, + poly: d3_ease_poly, + quad: function() { + return d3_ease_quad; + }, + cubic: function() { + return d3_ease_cubic; + }, + sin: function() { + return d3_ease_sin; + }, + exp: function() { + return d3_ease_exp; + }, + circle: function() { + return d3_ease_circle; + }, + elastic: d3_ease_elastic, + back: d3_ease_back, + bounce: function() { + return d3_ease_bounce; + } + }); + var d3_ease_mode = d3.map({ + "in": d3_identity, + out: d3_ease_reverse, + "in-out": d3_ease_reflect, + "out-in": function(f) { + return d3_ease_reflect(d3_ease_reverse(f)); + } + }); + d3.ease = function(name) { + var i = name.indexOf("-"), t = i >= 0 ? name.slice(0, i) : name, m = i >= 0 ? name.slice(i + 1) : "in"; + t = d3_ease.get(t) || d3_ease_default; + m = d3_ease_mode.get(m) || d3_identity; + return d3_ease_clamp(m(t.apply(null, d3_arraySlice.call(arguments, 1)))); + }; + function d3_ease_clamp(f) { + return function(t) { + return t <= 0 ? 0 : t >= 1 ? 1 : f(t); + }; + } + function d3_ease_reverse(f) { + return function(t) { + return 1 - f(1 - t); + }; + } + function d3_ease_reflect(f) { + return function(t) { + return .5 * (t < .5 ? f(2 * t) : 2 - f(2 - 2 * t)); + }; + } + function d3_ease_quad(t) { + return t * t; + } + function d3_ease_cubic(t) { + return t * t * t; + } + function d3_ease_cubicInOut(t) { + if (t <= 0) return 0; + if (t >= 1) return 1; + var t2 = t * t, t3 = t2 * t; + return 4 * (t < .5 ? t3 : 3 * (t - t2) + t3 - .75); + } + function d3_ease_poly(e) { + return function(t) { + return Math.pow(t, e); + }; + } + function d3_ease_sin(t) { + return 1 - Math.cos(t * halfπ); + } + function d3_ease_exp(t) { + return Math.pow(2, 10 * (t - 1)); + } + function d3_ease_circle(t) { + return 1 - Math.sqrt(1 - t * t); + } + function d3_ease_elastic(a, p) { + var s; + if (arguments.length < 2) p = .45; + if (arguments.length) s = p / τ * Math.asin(1 / a); else a = 1, s = p / 4; + return function(t) { + return 1 + a * Math.pow(2, -10 * t) * Math.sin((t - s) * τ / p); + }; + } + function d3_ease_back(s) { + if (!s) s = 1.70158; + return function(t) { + return t * t * ((s + 1) * t - s); + }; + } + function d3_ease_bounce(t) { + return t < 1 / 2.75 ? 7.5625 * t * t : t < 2 / 2.75 ? 7.5625 * (t -= 1.5 / 2.75) * t + .75 : t < 2.5 / 2.75 ? 7.5625 * (t -= 2.25 / 2.75) * t + .9375 : 7.5625 * (t -= 2.625 / 2.75) * t + .984375; + } + d3.interpolateHcl = d3_interpolateHcl; + function d3_interpolateHcl(a, b) { + a = d3.hcl(a); + b = d3.hcl(b); + var ah = a.h, ac = a.c, al = a.l, bh = b.h - ah, bc = b.c - ac, bl = b.l - al; + if (isNaN(bc)) bc = 0, ac = isNaN(ac) ? b.c : ac; + if (isNaN(bh)) bh = 0, ah = isNaN(ah) ? b.h : ah; else if (bh > 180) bh -= 360; else if (bh < -180) bh += 360; + return function(t) { + return d3_hcl_lab(ah + bh * t, ac + bc * t, al + bl * t) + ""; + }; + } + d3.interpolateHsl = d3_interpolateHsl; + function d3_interpolateHsl(a, b) { + a = d3.hsl(a); + b = d3.hsl(b); + var ah = a.h, as = a.s, al = a.l, bh = b.h - ah, bs = b.s - as, bl = b.l - al; + if (isNaN(bs)) bs = 0, as = isNaN(as) ? b.s : as; + if (isNaN(bh)) bh = 0, ah = isNaN(ah) ? b.h : ah; else if (bh > 180) bh -= 360; else if (bh < -180) bh += 360; + return function(t) { + return d3_hsl_rgb(ah + bh * t, as + bs * t, al + bl * t) + ""; + }; + } + d3.interpolateLab = d3_interpolateLab; + function d3_interpolateLab(a, b) { + a = d3.lab(a); + b = d3.lab(b); + var al = a.l, aa = a.a, ab = a.b, bl = b.l - al, ba = b.a - aa, bb = b.b - ab; + return function(t) { + return d3_lab_rgb(al + bl * t, aa + ba * t, ab + bb * t) + ""; + }; + } + d3.interpolateRound = d3_interpolateRound; + function d3_interpolateRound(a, b) { + b -= a; + return function(t) { + return Math.round(a + b * t); + }; + } + d3.transform = function(string) { + var g = d3_document.createElementNS(d3.ns.prefix.svg, "g"); + return (d3.transform = function(string) { + if (string != null) { + g.setAttribute("transform", string); + var t = g.transform.baseVal.consolidate(); + } + return new d3_transform(t ? t.matrix : d3_transformIdentity); + })(string); + }; + function d3_transform(m) { + var r0 = [ m.a, m.b ], r1 = [ m.c, m.d ], kx = d3_transformNormalize(r0), kz = d3_transformDot(r0, r1), ky = d3_transformNormalize(d3_transformCombine(r1, r0, -kz)) || 0; + if (r0[0] * r1[1] < r1[0] * r0[1]) { + r0[0] *= -1; + r0[1] *= -1; + kx *= -1; + kz *= -1; + } + this.rotate = (kx ? Math.atan2(r0[1], r0[0]) : Math.atan2(-r1[0], r1[1])) * d3_degrees; + this.translate = [ m.e, m.f ]; + this.scale = [ kx, ky ]; + this.skew = ky ? Math.atan2(kz, ky) * d3_degrees : 0; + } + d3_transform.prototype.toString = function() { + return "translate(" + this.translate + ")rotate(" + this.rotate + ")skewX(" + this.skew + ")scale(" + this.scale + ")"; + }; + function d3_transformDot(a, b) { + return a[0] * b[0] + a[1] * b[1]; + } + function d3_transformNormalize(a) { + var k = Math.sqrt(d3_transformDot(a, a)); + if (k) { + a[0] /= k; + a[1] /= k; + } + return k; + } + function d3_transformCombine(a, b, k) { + a[0] += k * b[0]; + a[1] += k * b[1]; + return a; + } + var d3_transformIdentity = { + a: 1, + b: 0, + c: 0, + d: 1, + e: 0, + f: 0 + }; + d3.interpolateTransform = d3_interpolateTransform; + function d3_interpolateTransform(a, b) { + var s = [], q = [], n, A = d3.transform(a), B = d3.transform(b), ta = A.translate, tb = B.translate, ra = A.rotate, rb = B.rotate, wa = A.skew, wb = B.skew, ka = A.scale, kb = B.scale; + if (ta[0] != tb[0] || ta[1] != tb[1]) { + s.push("translate(", null, ",", null, ")"); + q.push({ + i: 1, + x: d3_interpolateNumber(ta[0], tb[0]) + }, { + i: 3, + x: d3_interpolateNumber(ta[1], tb[1]) + }); + } else if (tb[0] || tb[1]) { + s.push("translate(" + tb + ")"); + } else { + s.push(""); + } + if (ra != rb) { + if (ra - rb > 180) rb += 360; else if (rb - ra > 180) ra += 360; + q.push({ + i: s.push(s.pop() + "rotate(", null, ")") - 2, + x: d3_interpolateNumber(ra, rb) + }); + } else if (rb) { + s.push(s.pop() + "rotate(" + rb + ")"); + } + if (wa != wb) { + q.push({ + i: s.push(s.pop() + "skewX(", null, ")") - 2, + x: d3_interpolateNumber(wa, wb) + }); + } else if (wb) { + s.push(s.pop() + "skewX(" + wb + ")"); + } + if (ka[0] != kb[0] || ka[1] != kb[1]) { + n = s.push(s.pop() + "scale(", null, ",", null, ")"); + q.push({ + i: n - 4, + x: d3_interpolateNumber(ka[0], kb[0]) + }, { + i: n - 2, + x: d3_interpolateNumber(ka[1], kb[1]) + }); + } else if (kb[0] != 1 || kb[1] != 1) { + s.push(s.pop() + "scale(" + kb + ")"); + } + n = q.length; + return function(t) { + var i = -1, o; + while (++i < n) s[(o = q[i]).i] = o.x(t); + return s.join(""); + }; + } + function d3_uninterpolateNumber(a, b) { + b = (b -= a = +a) || 1 / b; + return function(x) { + return (x - a) / b; + }; + } + function d3_uninterpolateClamp(a, b) { + b = (b -= a = +a) || 1 / b; + return function(x) { + return Math.max(0, Math.min(1, (x - a) / b)); + }; + } + d3.layout = {}; + d3.layout.bundle = function() { + return function(links) { + var paths = [], i = -1, n = links.length; + while (++i < n) paths.push(d3_layout_bundlePath(links[i])); + return paths; + }; + }; + function d3_layout_bundlePath(link) { + var start = link.source, end = link.target, lca = d3_layout_bundleLeastCommonAncestor(start, end), points = [ start ]; + while (start !== lca) { + start = start.parent; + points.push(start); + } + var k = points.length; + while (end !== lca) { + points.splice(k, 0, end); + end = end.parent; + } + return points; + } + function d3_layout_bundleAncestors(node) { + var ancestors = [], parent = node.parent; + while (parent != null) { + ancestors.push(node); + node = parent; + parent = parent.parent; + } + ancestors.push(node); + return ancestors; + } + function d3_layout_bundleLeastCommonAncestor(a, b) { + if (a === b) return a; + var aNodes = d3_layout_bundleAncestors(a), bNodes = d3_layout_bundleAncestors(b), aNode = aNodes.pop(), bNode = bNodes.pop(), sharedNode = null; + while (aNode === bNode) { + sharedNode = aNode; + aNode = aNodes.pop(); + bNode = bNodes.pop(); + } + return sharedNode; + } + d3.layout.chord = function() { + var chord = {}, chords, groups, matrix, n, padding = 0, sortGroups, sortSubgroups, sortChords; + function relayout() { + var subgroups = {}, groupSums = [], groupIndex = d3.range(n), subgroupIndex = [], k, x, x0, i, j; + chords = []; + groups = []; + k = 0, i = -1; + while (++i < n) { + x = 0, j = -1; + while (++j < n) { + x += matrix[i][j]; + } + groupSums.push(x); + subgroupIndex.push(d3.range(n)); + k += x; + } + if (sortGroups) { + groupIndex.sort(function(a, b) { + return sortGroups(groupSums[a], groupSums[b]); + }); + } + if (sortSubgroups) { + subgroupIndex.forEach(function(d, i) { + d.sort(function(a, b) { + return sortSubgroups(matrix[i][a], matrix[i][b]); + }); + }); + } + k = (τ - padding * n) / k; + x = 0, i = -1; + while (++i < n) { + x0 = x, j = -1; + while (++j < n) { + var di = groupIndex[i], dj = subgroupIndex[di][j], v = matrix[di][dj], a0 = x, a1 = x += v * k; + subgroups[di + "-" + dj] = { + index: di, + subindex: dj, + startAngle: a0, + endAngle: a1, + value: v + }; + } + groups[di] = { + index: di, + startAngle: x0, + endAngle: x, + value: (x - x0) / k + }; + x += padding; + } + i = -1; + while (++i < n) { + j = i - 1; + while (++j < n) { + var source = subgroups[i + "-" + j], target = subgroups[j + "-" + i]; + if (source.value || target.value) { + chords.push(source.value < target.value ? { + source: target, + target: source + } : { + source: source, + target: target + }); + } + } + } + if (sortChords) resort(); + } + function resort() { + chords.sort(function(a, b) { + return sortChords((a.source.value + a.target.value) / 2, (b.source.value + b.target.value) / 2); + }); + } + chord.matrix = function(x) { + if (!arguments.length) return matrix; + n = (matrix = x) && matrix.length; + chords = groups = null; + return chord; + }; + chord.padding = function(x) { + if (!arguments.length) return padding; + padding = x; + chords = groups = null; + return chord; + }; + chord.sortGroups = function(x) { + if (!arguments.length) return sortGroups; + sortGroups = x; + chords = groups = null; + return chord; + }; + chord.sortSubgroups = function(x) { + if (!arguments.length) return sortSubgroups; + sortSubgroups = x; + chords = null; + return chord; + }; + chord.sortChords = function(x) { + if (!arguments.length) return sortChords; + sortChords = x; + if (chords) resort(); + return chord; + }; + chord.chords = function() { + if (!chords) relayout(); + return chords; + }; + chord.groups = function() { + if (!groups) relayout(); + return groups; + }; + return chord; + }; + d3.layout.force = function() { + var force = {}, event = d3.dispatch("start", "tick", "end"), size = [ 1, 1 ], drag, alpha, friction = .9, linkDistance = d3_layout_forceLinkDistance, linkStrength = d3_layout_forceLinkStrength, charge = -30, chargeDistance2 = d3_layout_forceChargeDistance2, gravity = .1, theta2 = .64, nodes = [], links = [], distances, strengths, charges; + function repulse(node) { + return function(quad, x1, _, x2) { + if (quad.point !== node) { + var dx = quad.cx - node.x, dy = quad.cy - node.y, dw = x2 - x1, dn = dx * dx + dy * dy; + if (dw * dw / theta2 < dn) { + if (dn < chargeDistance2) { + var k = quad.charge / dn; + node.px -= dx * k; + node.py -= dy * k; + } + return true; + } + if (quad.point && dn && dn < chargeDistance2) { + var k = quad.pointCharge / dn; + node.px -= dx * k; + node.py -= dy * k; + } + } + return !quad.charge; + }; + } + force.tick = function() { + if ((alpha *= .99) < .005) { + event.end({ + type: "end", + alpha: alpha = 0 + }); + return true; + } + var n = nodes.length, m = links.length, q, i, o, s, t, l, k, x, y; + for (i = 0; i < m; ++i) { + o = links[i]; + s = o.source; + t = o.target; + x = t.x - s.x; + y = t.y - s.y; + if (l = x * x + y * y) { + l = alpha * strengths[i] * ((l = Math.sqrt(l)) - distances[i]) / l; + x *= l; + y *= l; + t.x -= x * (k = s.weight / (t.weight + s.weight)); + t.y -= y * k; + s.x += x * (k = 1 - k); + s.y += y * k; + } + } + if (k = alpha * gravity) { + x = size[0] / 2; + y = size[1] / 2; + i = -1; + if (k) while (++i < n) { + o = nodes[i]; + o.x += (x - o.x) * k; + o.y += (y - o.y) * k; + } + } + if (charge) { + d3_layout_forceAccumulate(q = d3.geom.quadtree(nodes), alpha, charges); + i = -1; + while (++i < n) { + if (!(o = nodes[i]).fixed) { + q.visit(repulse(o)); + } + } + } + i = -1; + while (++i < n) { + o = nodes[i]; + if (o.fixed) { + o.x = o.px; + o.y = o.py; + } else { + o.x -= (o.px - (o.px = o.x)) * friction; + o.y -= (o.py - (o.py = o.y)) * friction; + } + } + event.tick({ + type: "tick", + alpha: alpha + }); + }; + force.nodes = function(x) { + if (!arguments.length) return nodes; + nodes = x; + return force; + }; + force.links = function(x) { + if (!arguments.length) return links; + links = x; + return force; + }; + force.size = function(x) { + if (!arguments.length) return size; + size = x; + return force; + }; + force.linkDistance = function(x) { + if (!arguments.length) return linkDistance; + linkDistance = typeof x === "function" ? x : +x; + return force; + }; + force.distance = force.linkDistance; + force.linkStrength = function(x) { + if (!arguments.length) return linkStrength; + linkStrength = typeof x === "function" ? x : +x; + return force; + }; + force.friction = function(x) { + if (!arguments.length) return friction; + friction = +x; + return force; + }; + force.charge = function(x) { + if (!arguments.length) return charge; + charge = typeof x === "function" ? x : +x; + return force; + }; + force.chargeDistance = function(x) { + if (!arguments.length) return Math.sqrt(chargeDistance2); + chargeDistance2 = x * x; + return force; + }; + force.gravity = function(x) { + if (!arguments.length) return gravity; + gravity = +x; + return force; + }; + force.theta = function(x) { + if (!arguments.length) return Math.sqrt(theta2); + theta2 = x * x; + return force; + }; + force.alpha = function(x) { + if (!arguments.length) return alpha; + x = +x; + if (alpha) { + if (x > 0) alpha = x; else alpha = 0; + } else if (x > 0) { + event.start({ + type: "start", + alpha: alpha = x + }); + d3.timer(force.tick); + } + return force; + }; + force.start = function() { + var i, n = nodes.length, m = links.length, w = size[0], h = size[1], neighbors, o; + for (i = 0; i < n; ++i) { + (o = nodes[i]).index = i; + o.weight = 0; + } + for (i = 0; i < m; ++i) { + o = links[i]; + if (typeof o.source == "number") o.source = nodes[o.source]; + if (typeof o.target == "number") o.target = nodes[o.target]; + ++o.source.weight; + ++o.target.weight; + } + for (i = 0; i < n; ++i) { + o = nodes[i]; + if (isNaN(o.x)) o.x = position("x", w); + if (isNaN(o.y)) o.y = position("y", h); + if (isNaN(o.px)) o.px = o.x; + if (isNaN(o.py)) o.py = o.y; + } + distances = []; + if (typeof linkDistance === "function") for (i = 0; i < m; ++i) distances[i] = +linkDistance.call(this, links[i], i); else for (i = 0; i < m; ++i) distances[i] = linkDistance; + strengths = []; + if (typeof linkStrength === "function") for (i = 0; i < m; ++i) strengths[i] = +linkStrength.call(this, links[i], i); else for (i = 0; i < m; ++i) strengths[i] = linkStrength; + charges = []; + if (typeof charge === "function") for (i = 0; i < n; ++i) charges[i] = +charge.call(this, nodes[i], i); else for (i = 0; i < n; ++i) charges[i] = charge; + function position(dimension, size) { + if (!neighbors) { + neighbors = new Array(n); + for (j = 0; j < n; ++j) { + neighbors[j] = []; + } + for (j = 0; j < m; ++j) { + var o = links[j]; + neighbors[o.source.index].push(o.target); + neighbors[o.target.index].push(o.source); + } + } + var candidates = neighbors[i], j = -1, m = candidates.length, x; + while (++j < m) if (!isNaN(x = candidates[j][dimension])) return x; + return Math.random() * size; + } + return force.resume(); + }; + force.resume = function() { + return force.alpha(.1); + }; + force.stop = function() { + return force.alpha(0); + }; + force.drag = function() { + if (!drag) drag = d3.behavior.drag().origin(d3_identity).on("dragstart.force", d3_layout_forceDragstart).on("drag.force", dragmove).on("dragend.force", d3_layout_forceDragend); + if (!arguments.length) return drag; + this.on("mouseover.force", d3_layout_forceMouseover).on("mouseout.force", d3_layout_forceMouseout).call(drag); + }; + function dragmove(d) { + d.px = d3.event.x, d.py = d3.event.y; + force.resume(); + } + return d3.rebind(force, event, "on"); + }; + function d3_layout_forceDragstart(d) { + d.fixed |= 2; + } + function d3_layout_forceDragend(d) { + d.fixed &= ~6; + } + function d3_layout_forceMouseover(d) { + d.fixed |= 4; + d.px = d.x, d.py = d.y; + } + function d3_layout_forceMouseout(d) { + d.fixed &= ~4; + } + function d3_layout_forceAccumulate(quad, alpha, charges) { + var cx = 0, cy = 0; + quad.charge = 0; + if (!quad.leaf) { + var nodes = quad.nodes, n = nodes.length, i = -1, c; + while (++i < n) { + c = nodes[i]; + if (c == null) continue; + d3_layout_forceAccumulate(c, alpha, charges); + quad.charge += c.charge; + cx += c.charge * c.cx; + cy += c.charge * c.cy; + } + } + if (quad.point) { + if (!quad.leaf) { + quad.point.x += Math.random() - .5; + quad.point.y += Math.random() - .5; + } + var k = alpha * charges[quad.point.index]; + quad.charge += quad.pointCharge = k; + cx += k * quad.point.x; + cy += k * quad.point.y; + } + quad.cx = cx / quad.charge; + quad.cy = cy / quad.charge; + } + var d3_layout_forceLinkDistance = 20, d3_layout_forceLinkStrength = 1, d3_layout_forceChargeDistance2 = Infinity; + d3.layout.hierarchy = function() { + var sort = d3_layout_hierarchySort, children = d3_layout_hierarchyChildren, value = d3_layout_hierarchyValue; + function hierarchy(root) { + var stack = [ root ], nodes = [], node; + root.depth = 0; + while ((node = stack.pop()) != null) { + nodes.push(node); + if ((childs = children.call(hierarchy, node, node.depth)) && (n = childs.length)) { + var n, childs, child; + while (--n >= 0) { + stack.push(child = childs[n]); + child.parent = node; + child.depth = node.depth + 1; + } + if (value) node.value = 0; + node.children = childs; + } else { + if (value) node.value = +value.call(hierarchy, node, node.depth) || 0; + delete node.children; + } + } + d3_layout_hierarchyVisitAfter(root, function(node) { + var childs, parent; + if (sort && (childs = node.children)) childs.sort(sort); + if (value && (parent = node.parent)) parent.value += node.value; + }); + return nodes; + } + hierarchy.sort = function(x) { + if (!arguments.length) return sort; + sort = x; + return hierarchy; + }; + hierarchy.children = function(x) { + if (!arguments.length) return children; + children = x; + return hierarchy; + }; + hierarchy.value = function(x) { + if (!arguments.length) return value; + value = x; + return hierarchy; + }; + hierarchy.revalue = function(root) { + if (value) { + d3_layout_hierarchyVisitBefore(root, function(node) { + if (node.children) node.value = 0; + }); + d3_layout_hierarchyVisitAfter(root, function(node) { + var parent; + if (!node.children) node.value = +value.call(hierarchy, node, node.depth) || 0; + if (parent = node.parent) parent.value += node.value; + }); + } + return root; + }; + return hierarchy; + }; + function d3_layout_hierarchyRebind(object, hierarchy) { + d3.rebind(object, hierarchy, "sort", "children", "value"); + object.nodes = object; + object.links = d3_layout_hierarchyLinks; + return object; + } + function d3_layout_hierarchyVisitBefore(node, callback) { + var nodes = [ node ]; + while ((node = nodes.pop()) != null) { + callback(node); + if ((children = node.children) && (n = children.length)) { + var n, children; + while (--n >= 0) nodes.push(children[n]); + } + } + } + function d3_layout_hierarchyVisitAfter(node, callback) { + var nodes = [ node ], nodes2 = []; + while ((node = nodes.pop()) != null) { + nodes2.push(node); + if ((children = node.children) && (n = children.length)) { + var i = -1, n, children; + while (++i < n) nodes.push(children[i]); + } + } + while ((node = nodes2.pop()) != null) { + callback(node); + } + } + function d3_layout_hierarchyChildren(d) { + return d.children; + } + function d3_layout_hierarchyValue(d) { + return d.value; + } + function d3_layout_hierarchySort(a, b) { + return b.value - a.value; + } + function d3_layout_hierarchyLinks(nodes) { + return d3.merge(nodes.map(function(parent) { + return (parent.children || []).map(function(child) { + return { + source: parent, + target: child + }; + }); + })); + } + d3.layout.partition = function() { + var hierarchy = d3.layout.hierarchy(), size = [ 1, 1 ]; + function position(node, x, dx, dy) { + var children = node.children; + node.x = x; + node.y = node.depth * dy; + node.dx = dx; + node.dy = dy; + if (children && (n = children.length)) { + var i = -1, n, c, d; + dx = node.value ? dx / node.value : 0; + while (++i < n) { + position(c = children[i], x, d = c.value * dx, dy); + x += d; + } + } + } + function depth(node) { + var children = node.children, d = 0; + if (children && (n = children.length)) { + var i = -1, n; + while (++i < n) d = Math.max(d, depth(children[i])); + } + return 1 + d; + } + function partition(d, i) { + var nodes = hierarchy.call(this, d, i); + position(nodes[0], 0, size[0], size[1] / depth(nodes[0])); + return nodes; + } + partition.size = function(x) { + if (!arguments.length) return size; + size = x; + return partition; + }; + return d3_layout_hierarchyRebind(partition, hierarchy); + }; + d3.layout.pie = function() { + var value = Number, sort = d3_layout_pieSortByValue, startAngle = 0, endAngle = τ, padAngle = 0; + function pie(data) { + var n = data.length, values = data.map(function(d, i) { + return +value.call(pie, d, i); + }), a = +(typeof startAngle === "function" ? startAngle.apply(this, arguments) : startAngle), da = (typeof endAngle === "function" ? endAngle.apply(this, arguments) : endAngle) - a, p = Math.min(Math.abs(da) / n, +(typeof padAngle === "function" ? padAngle.apply(this, arguments) : padAngle)), pa = p * (da < 0 ? -1 : 1), k = (da - n * pa) / d3.sum(values), index = d3.range(n), arcs = [], v; + if (sort != null) index.sort(sort === d3_layout_pieSortByValue ? function(i, j) { + return values[j] - values[i]; + } : function(i, j) { + return sort(data[i], data[j]); + }); + index.forEach(function(i) { + arcs[i] = { + data: data[i], + value: v = values[i], + startAngle: a, + endAngle: a += v * k + pa, + padAngle: p + }; + }); + return arcs; + } + pie.value = function(_) { + if (!arguments.length) return value; + value = _; + return pie; + }; + pie.sort = function(_) { + if (!arguments.length) return sort; + sort = _; + return pie; + }; + pie.startAngle = function(_) { + if (!arguments.length) return startAngle; + startAngle = _; + return pie; + }; + pie.endAngle = function(_) { + if (!arguments.length) return endAngle; + endAngle = _; + return pie; + }; + pie.padAngle = function(_) { + if (!arguments.length) return padAngle; + padAngle = _; + return pie; + }; + return pie; + }; + var d3_layout_pieSortByValue = {}; + d3.layout.stack = function() { + var values = d3_identity, order = d3_layout_stackOrderDefault, offset = d3_layout_stackOffsetZero, out = d3_layout_stackOut, x = d3_layout_stackX, y = d3_layout_stackY; + function stack(data, index) { + if (!(n = data.length)) return data; + var series = data.map(function(d, i) { + return values.call(stack, d, i); + }); + var points = series.map(function(d) { + return d.map(function(v, i) { + return [ x.call(stack, v, i), y.call(stack, v, i) ]; + }); + }); + var orders = order.call(stack, points, index); + series = d3.permute(series, orders); + points = d3.permute(points, orders); + var offsets = offset.call(stack, points, index); + var m = series[0].length, n, i, j, o; + for (j = 0; j < m; ++j) { + out.call(stack, series[0][j], o = offsets[j], points[0][j][1]); + for (i = 1; i < n; ++i) { + out.call(stack, series[i][j], o += points[i - 1][j][1], points[i][j][1]); + } + } + return data; + } + stack.values = function(x) { + if (!arguments.length) return values; + values = x; + return stack; + }; + stack.order = function(x) { + if (!arguments.length) return order; + order = typeof x === "function" ? x : d3_layout_stackOrders.get(x) || d3_layout_stackOrderDefault; + return stack; + }; + stack.offset = function(x) { + if (!arguments.length) return offset; + offset = typeof x === "function" ? x : d3_layout_stackOffsets.get(x) || d3_layout_stackOffsetZero; + return stack; + }; + stack.x = function(z) { + if (!arguments.length) return x; + x = z; + return stack; + }; + stack.y = function(z) { + if (!arguments.length) return y; + y = z; + return stack; + }; + stack.out = function(z) { + if (!arguments.length) return out; + out = z; + return stack; + }; + return stack; + }; + function d3_layout_stackX(d) { + return d.x; + } + function d3_layout_stackY(d) { + return d.y; + } + function d3_layout_stackOut(d, y0, y) { + d.y0 = y0; + d.y = y; + } + var d3_layout_stackOrders = d3.map({ + "inside-out": function(data) { + var n = data.length, i, j, max = data.map(d3_layout_stackMaxIndex), sums = data.map(d3_layout_stackReduceSum), index = d3.range(n).sort(function(a, b) { + return max[a] - max[b]; + }), top = 0, bottom = 0, tops = [], bottoms = []; + for (i = 0; i < n; ++i) { + j = index[i]; + if (top < bottom) { + top += sums[j]; + tops.push(j); + } else { + bottom += sums[j]; + bottoms.push(j); + } + } + return bottoms.reverse().concat(tops); + }, + reverse: function(data) { + return d3.range(data.length).reverse(); + }, + "default": d3_layout_stackOrderDefault + }); + var d3_layout_stackOffsets = d3.map({ + silhouette: function(data) { + var n = data.length, m = data[0].length, sums = [], max = 0, i, j, o, y0 = []; + for (j = 0; j < m; ++j) { + for (i = 0, o = 0; i < n; i++) o += data[i][j][1]; + if (o > max) max = o; + sums.push(o); + } + for (j = 0; j < m; ++j) { + y0[j] = (max - sums[j]) / 2; + } + return y0; + }, + wiggle: function(data) { + var n = data.length, x = data[0], m = x.length, i, j, k, s1, s2, s3, dx, o, o0, y0 = []; + y0[0] = o = o0 = 0; + for (j = 1; j < m; ++j) { + for (i = 0, s1 = 0; i < n; ++i) s1 += data[i][j][1]; + for (i = 0, s2 = 0, dx = x[j][0] - x[j - 1][0]; i < n; ++i) { + for (k = 0, s3 = (data[i][j][1] - data[i][j - 1][1]) / (2 * dx); k < i; ++k) { + s3 += (data[k][j][1] - data[k][j - 1][1]) / dx; + } + s2 += s3 * data[i][j][1]; + } + y0[j] = o -= s1 ? s2 / s1 * dx : 0; + if (o < o0) o0 = o; + } + for (j = 0; j < m; ++j) y0[j] -= o0; + return y0; + }, + expand: function(data) { + var n = data.length, m = data[0].length, k = 1 / n, i, j, o, y0 = []; + for (j = 0; j < m; ++j) { + for (i = 0, o = 0; i < n; i++) o += data[i][j][1]; + if (o) for (i = 0; i < n; i++) data[i][j][1] /= o; else for (i = 0; i < n; i++) data[i][j][1] = k; + } + for (j = 0; j < m; ++j) y0[j] = 0; + return y0; + }, + zero: d3_layout_stackOffsetZero + }); + function d3_layout_stackOrderDefault(data) { + return d3.range(data.length); + } + function d3_layout_stackOffsetZero(data) { + var j = -1, m = data[0].length, y0 = []; + while (++j < m) y0[j] = 0; + return y0; + } + function d3_layout_stackMaxIndex(array) { + var i = 1, j = 0, v = array[0][1], k, n = array.length; + for (;i < n; ++i) { + if ((k = array[i][1]) > v) { + j = i; + v = k; + } + } + return j; + } + function d3_layout_stackReduceSum(d) { + return d.reduce(d3_layout_stackSum, 0); + } + function d3_layout_stackSum(p, d) { + return p + d[1]; + } + d3.layout.histogram = function() { + var frequency = true, valuer = Number, ranger = d3_layout_histogramRange, binner = d3_layout_histogramBinSturges; + function histogram(data, i) { + var bins = [], values = data.map(valuer, this), range = ranger.call(this, values, i), thresholds = binner.call(this, range, values, i), bin, i = -1, n = values.length, m = thresholds.length - 1, k = frequency ? 1 : 1 / n, x; + while (++i < m) { + bin = bins[i] = []; + bin.dx = thresholds[i + 1] - (bin.x = thresholds[i]); + bin.y = 0; + } + if (m > 0) { + i = -1; + while (++i < n) { + x = values[i]; + if (x >= range[0] && x <= range[1]) { + bin = bins[d3.bisect(thresholds, x, 1, m) - 1]; + bin.y += k; + bin.push(data[i]); + } + } + } + return bins; + } + histogram.value = function(x) { + if (!arguments.length) return valuer; + valuer = x; + return histogram; + }; + histogram.range = function(x) { + if (!arguments.length) return ranger; + ranger = d3_functor(x); + return histogram; + }; + histogram.bins = function(x) { + if (!arguments.length) return binner; + binner = typeof x === "number" ? function(range) { + return d3_layout_histogramBinFixed(range, x); + } : d3_functor(x); + return histogram; + }; + histogram.frequency = function(x) { + if (!arguments.length) return frequency; + frequency = !!x; + return histogram; + }; + return histogram; + }; + function d3_layout_histogramBinSturges(range, values) { + return d3_layout_histogramBinFixed(range, Math.ceil(Math.log(values.length) / Math.LN2 + 1)); + } + function d3_layout_histogramBinFixed(range, n) { + var x = -1, b = +range[0], m = (range[1] - b) / n, f = []; + while (++x <= n) f[x] = m * x + b; + return f; + } + function d3_layout_histogramRange(values) { + return [ d3.min(values), d3.max(values) ]; + } + d3.layout.pack = function() { + var hierarchy = d3.layout.hierarchy().sort(d3_layout_packSort), padding = 0, size = [ 1, 1 ], radius; + function pack(d, i) { + var nodes = hierarchy.call(this, d, i), root = nodes[0], w = size[0], h = size[1], r = radius == null ? Math.sqrt : typeof radius === "function" ? radius : function() { + return radius; + }; + root.x = root.y = 0; + d3_layout_hierarchyVisitAfter(root, function(d) { + d.r = +r(d.value); + }); + d3_layout_hierarchyVisitAfter(root, d3_layout_packSiblings); + if (padding) { + var dr = padding * (radius ? 1 : Math.max(2 * root.r / w, 2 * root.r / h)) / 2; + d3_layout_hierarchyVisitAfter(root, function(d) { + d.r += dr; + }); + d3_layout_hierarchyVisitAfter(root, d3_layout_packSiblings); + d3_layout_hierarchyVisitAfter(root, function(d) { + d.r -= dr; + }); + } + d3_layout_packTransform(root, w / 2, h / 2, radius ? 1 : 1 / Math.max(2 * root.r / w, 2 * root.r / h)); + return nodes; + } + pack.size = function(_) { + if (!arguments.length) return size; + size = _; + return pack; + }; + pack.radius = function(_) { + if (!arguments.length) return radius; + radius = _ == null || typeof _ === "function" ? _ : +_; + return pack; + }; + pack.padding = function(_) { + if (!arguments.length) return padding; + padding = +_; + return pack; + }; + return d3_layout_hierarchyRebind(pack, hierarchy); + }; + function d3_layout_packSort(a, b) { + return a.value - b.value; + } + function d3_layout_packInsert(a, b) { + var c = a._pack_next; + a._pack_next = b; + b._pack_prev = a; + b._pack_next = c; + c._pack_prev = b; + } + function d3_layout_packSplice(a, b) { + a._pack_next = b; + b._pack_prev = a; + } + function d3_layout_packIntersects(a, b) { + var dx = b.x - a.x, dy = b.y - a.y, dr = a.r + b.r; + return .999 * dr * dr > dx * dx + dy * dy; + } + function d3_layout_packSiblings(node) { + if (!(nodes = node.children) || !(n = nodes.length)) return; + var nodes, xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity, a, b, c, i, j, k, n; + function bound(node) { + xMin = Math.min(node.x - node.r, xMin); + xMax = Math.max(node.x + node.r, xMax); + yMin = Math.min(node.y - node.r, yMin); + yMax = Math.max(node.y + node.r, yMax); + } + nodes.forEach(d3_layout_packLink); + a = nodes[0]; + a.x = -a.r; + a.y = 0; + bound(a); + if (n > 1) { + b = nodes[1]; + b.x = b.r; + b.y = 0; + bound(b); + if (n > 2) { + c = nodes[2]; + d3_layout_packPlace(a, b, c); + bound(c); + d3_layout_packInsert(a, c); + a._pack_prev = c; + d3_layout_packInsert(c, b); + b = a._pack_next; + for (i = 3; i < n; i++) { + d3_layout_packPlace(a, b, c = nodes[i]); + var isect = 0, s1 = 1, s2 = 1; + for (j = b._pack_next; j !== b; j = j._pack_next, s1++) { + if (d3_layout_packIntersects(j, c)) { + isect = 1; + break; + } + } + if (isect == 1) { + for (k = a._pack_prev; k !== j._pack_prev; k = k._pack_prev, s2++) { + if (d3_layout_packIntersects(k, c)) { + break; + } + } + } + if (isect) { + if (s1 < s2 || s1 == s2 && b.r < a.r) d3_layout_packSplice(a, b = j); else d3_layout_packSplice(a = k, b); + i--; + } else { + d3_layout_packInsert(a, c); + b = c; + bound(c); + } + } + } + } + var cx = (xMin + xMax) / 2, cy = (yMin + yMax) / 2, cr = 0; + for (i = 0; i < n; i++) { + c = nodes[i]; + c.x -= cx; + c.y -= cy; + cr = Math.max(cr, c.r + Math.sqrt(c.x * c.x + c.y * c.y)); + } + node.r = cr; + nodes.forEach(d3_layout_packUnlink); + } + function d3_layout_packLink(node) { + node._pack_next = node._pack_prev = node; + } + function d3_layout_packUnlink(node) { + delete node._pack_next; + delete node._pack_prev; + } + function d3_layout_packTransform(node, x, y, k) { + var children = node.children; + node.x = x += k * node.x; + node.y = y += k * node.y; + node.r *= k; + if (children) { + var i = -1, n = children.length; + while (++i < n) d3_layout_packTransform(children[i], x, y, k); + } + } + function d3_layout_packPlace(a, b, c) { + var db = a.r + c.r, dx = b.x - a.x, dy = b.y - a.y; + if (db && (dx || dy)) { + var da = b.r + c.r, dc = dx * dx + dy * dy; + da *= da; + db *= db; + var x = .5 + (db - da) / (2 * dc), y = Math.sqrt(Math.max(0, 2 * da * (db + dc) - (db -= dc) * db - da * da)) / (2 * dc); + c.x = a.x + x * dx + y * dy; + c.y = a.y + x * dy - y * dx; + } else { + c.x = a.x + db; + c.y = a.y; + } + } + d3.layout.tree = function() { + var hierarchy = d3.layout.hierarchy().sort(null).value(null), separation = d3_layout_treeSeparation, size = [ 1, 1 ], nodeSize = null; + function tree(d, i) { + var nodes = hierarchy.call(this, d, i), root0 = nodes[0], root1 = wrapTree(root0); + d3_layout_hierarchyVisitAfter(root1, firstWalk), root1.parent.m = -root1.z; + d3_layout_hierarchyVisitBefore(root1, secondWalk); + if (nodeSize) d3_layout_hierarchyVisitBefore(root0, sizeNode); else { + var left = root0, right = root0, bottom = root0; + d3_layout_hierarchyVisitBefore(root0, function(node) { + if (node.x < left.x) left = node; + if (node.x > right.x) right = node; + if (node.depth > bottom.depth) bottom = node; + }); + var tx = separation(left, right) / 2 - left.x, kx = size[0] / (right.x + separation(right, left) / 2 + tx), ky = size[1] / (bottom.depth || 1); + d3_layout_hierarchyVisitBefore(root0, function(node) { + node.x = (node.x + tx) * kx; + node.y = node.depth * ky; + }); + } + return nodes; + } + function wrapTree(root0) { + var root1 = { + A: null, + children: [ root0 ] + }, queue = [ root1 ], node1; + while ((node1 = queue.pop()) != null) { + for (var children = node1.children, child, i = 0, n = children.length; i < n; ++i) { + queue.push((children[i] = child = { + _: children[i], + parent: node1, + children: (child = children[i].children) && child.slice() || [], + A: null, + a: null, + z: 0, + m: 0, + c: 0, + s: 0, + t: null, + i: i + }).a = child); + } + } + return root1.children[0]; + } + function firstWalk(v) { + var children = v.children, siblings = v.parent.children, w = v.i ? siblings[v.i - 1] : null; + if (children.length) { + d3_layout_treeShift(v); + var midpoint = (children[0].z + children[children.length - 1].z) / 2; + if (w) { + v.z = w.z + separation(v._, w._); + v.m = v.z - midpoint; + } else { + v.z = midpoint; + } + } else if (w) { + v.z = w.z + separation(v._, w._); + } + v.parent.A = apportion(v, w, v.parent.A || siblings[0]); + } + function secondWalk(v) { + v._.x = v.z + v.parent.m; + v.m += v.parent.m; + } + function apportion(v, w, ancestor) { + if (w) { + var vip = v, vop = v, vim = w, vom = vip.parent.children[0], sip = vip.m, sop = vop.m, sim = vim.m, som = vom.m, shift; + while (vim = d3_layout_treeRight(vim), vip = d3_layout_treeLeft(vip), vim && vip) { + vom = d3_layout_treeLeft(vom); + vop = d3_layout_treeRight(vop); + vop.a = v; + shift = vim.z + sim - vip.z - sip + separation(vim._, vip._); + if (shift > 0) { + d3_layout_treeMove(d3_layout_treeAncestor(vim, v, ancestor), v, shift); + sip += shift; + sop += shift; + } + sim += vim.m; + sip += vip.m; + som += vom.m; + sop += vop.m; + } + if (vim && !d3_layout_treeRight(vop)) { + vop.t = vim; + vop.m += sim - sop; + } + if (vip && !d3_layout_treeLeft(vom)) { + vom.t = vip; + vom.m += sip - som; + ancestor = v; + } + } + return ancestor; + } + function sizeNode(node) { + node.x *= size[0]; + node.y = node.depth * size[1]; + } + tree.separation = function(x) { + if (!arguments.length) return separation; + separation = x; + return tree; + }; + tree.size = function(x) { + if (!arguments.length) return nodeSize ? null : size; + nodeSize = (size = x) == null ? sizeNode : null; + return tree; + }; + tree.nodeSize = function(x) { + if (!arguments.length) return nodeSize ? size : null; + nodeSize = (size = x) == null ? null : sizeNode; + return tree; + }; + return d3_layout_hierarchyRebind(tree, hierarchy); + }; + function d3_layout_treeSeparation(a, b) { + return a.parent == b.parent ? 1 : 2; + } + function d3_layout_treeLeft(v) { + var children = v.children; + return children.length ? children[0] : v.t; + } + function d3_layout_treeRight(v) { + var children = v.children, n; + return (n = children.length) ? children[n - 1] : v.t; + } + function d3_layout_treeMove(wm, wp, shift) { + var change = shift / (wp.i - wm.i); + wp.c -= change; + wp.s += shift; + wm.c += change; + wp.z += shift; + wp.m += shift; + } + function d3_layout_treeShift(v) { + var shift = 0, change = 0, children = v.children, i = children.length, w; + while (--i >= 0) { + w = children[i]; + w.z += shift; + w.m += shift; + shift += w.s + (change += w.c); + } + } + function d3_layout_treeAncestor(vim, v, ancestor) { + return vim.a.parent === v.parent ? vim.a : ancestor; + } + d3.layout.cluster = function() { + var hierarchy = d3.layout.hierarchy().sort(null).value(null), separation = d3_layout_treeSeparation, size = [ 1, 1 ], nodeSize = false; + function cluster(d, i) { + var nodes = hierarchy.call(this, d, i), root = nodes[0], previousNode, x = 0; + d3_layout_hierarchyVisitAfter(root, function(node) { + var children = node.children; + if (children && children.length) { + node.x = d3_layout_clusterX(children); + node.y = d3_layout_clusterY(children); + } else { + node.x = previousNode ? x += separation(node, previousNode) : 0; + node.y = 0; + previousNode = node; + } + }); + var left = d3_layout_clusterLeft(root), right = d3_layout_clusterRight(root), x0 = left.x - separation(left, right) / 2, x1 = right.x + separation(right, left) / 2; + d3_layout_hierarchyVisitAfter(root, nodeSize ? function(node) { + node.x = (node.x - root.x) * size[0]; + node.y = (root.y - node.y) * size[1]; + } : function(node) { + node.x = (node.x - x0) / (x1 - x0) * size[0]; + node.y = (1 - (root.y ? node.y / root.y : 1)) * size[1]; + }); + return nodes; + } + cluster.separation = function(x) { + if (!arguments.length) return separation; + separation = x; + return cluster; + }; + cluster.size = function(x) { + if (!arguments.length) return nodeSize ? null : size; + nodeSize = (size = x) == null; + return cluster; + }; + cluster.nodeSize = function(x) { + if (!arguments.length) return nodeSize ? size : null; + nodeSize = (size = x) != null; + return cluster; + }; + return d3_layout_hierarchyRebind(cluster, hierarchy); + }; + function d3_layout_clusterY(children) { + return 1 + d3.max(children, function(child) { + return child.y; + }); + } + function d3_layout_clusterX(children) { + return children.reduce(function(x, child) { + return x + child.x; + }, 0) / children.length; + } + function d3_layout_clusterLeft(node) { + var children = node.children; + return children && children.length ? d3_layout_clusterLeft(children[0]) : node; + } + function d3_layout_clusterRight(node) { + var children = node.children, n; + return children && (n = children.length) ? d3_layout_clusterRight(children[n - 1]) : node; + } + d3.layout.treemap = function() { + var hierarchy = d3.layout.hierarchy(), round = Math.round, size = [ 1, 1 ], padding = null, pad = d3_layout_treemapPadNull, sticky = false, stickies, mode = "squarify", ratio = .5 * (1 + Math.sqrt(5)); + function scale(children, k) { + var i = -1, n = children.length, child, area; + while (++i < n) { + area = (child = children[i]).value * (k < 0 ? 0 : k); + child.area = isNaN(area) || area <= 0 ? 0 : area; + } + } + function squarify(node) { + var children = node.children; + if (children && children.length) { + var rect = pad(node), row = [], remaining = children.slice(), child, best = Infinity, score, u = mode === "slice" ? rect.dx : mode === "dice" ? rect.dy : mode === "slice-dice" ? node.depth & 1 ? rect.dy : rect.dx : Math.min(rect.dx, rect.dy), n; + scale(remaining, rect.dx * rect.dy / node.value); + row.area = 0; + while ((n = remaining.length) > 0) { + row.push(child = remaining[n - 1]); + row.area += child.area; + if (mode !== "squarify" || (score = worst(row, u)) <= best) { + remaining.pop(); + best = score; + } else { + row.area -= row.pop().area; + position(row, u, rect, false); + u = Math.min(rect.dx, rect.dy); + row.length = row.area = 0; + best = Infinity; + } + } + if (row.length) { + position(row, u, rect, true); + row.length = row.area = 0; + } + children.forEach(squarify); + } + } + function stickify(node) { + var children = node.children; + if (children && children.length) { + var rect = pad(node), remaining = children.slice(), child, row = []; + scale(remaining, rect.dx * rect.dy / node.value); + row.area = 0; + while (child = remaining.pop()) { + row.push(child); + row.area += child.area; + if (child.z != null) { + position(row, child.z ? rect.dx : rect.dy, rect, !remaining.length); + row.length = row.area = 0; + } + } + children.forEach(stickify); + } + } + function worst(row, u) { + var s = row.area, r, rmax = 0, rmin = Infinity, i = -1, n = row.length; + while (++i < n) { + if (!(r = row[i].area)) continue; + if (r < rmin) rmin = r; + if (r > rmax) rmax = r; + } + s *= s; + u *= u; + return s ? Math.max(u * rmax * ratio / s, s / (u * rmin * ratio)) : Infinity; + } + function position(row, u, rect, flush) { + var i = -1, n = row.length, x = rect.x, y = rect.y, v = u ? round(row.area / u) : 0, o; + if (u == rect.dx) { + if (flush || v > rect.dy) v = rect.dy; + while (++i < n) { + o = row[i]; + o.x = x; + o.y = y; + o.dy = v; + x += o.dx = Math.min(rect.x + rect.dx - x, v ? round(o.area / v) : 0); + } + o.z = true; + o.dx += rect.x + rect.dx - x; + rect.y += v; + rect.dy -= v; + } else { + if (flush || v > rect.dx) v = rect.dx; + while (++i < n) { + o = row[i]; + o.x = x; + o.y = y; + o.dx = v; + y += o.dy = Math.min(rect.y + rect.dy - y, v ? round(o.area / v) : 0); + } + o.z = false; + o.dy += rect.y + rect.dy - y; + rect.x += v; + rect.dx -= v; + } + } + function treemap(d) { + var nodes = stickies || hierarchy(d), root = nodes[0]; + root.x = 0; + root.y = 0; + root.dx = size[0]; + root.dy = size[1]; + if (stickies) hierarchy.revalue(root); + scale([ root ], root.dx * root.dy / root.value); + (stickies ? stickify : squarify)(root); + if (sticky) stickies = nodes; + return nodes; + } + treemap.size = function(x) { + if (!arguments.length) return size; + size = x; + return treemap; + }; + treemap.padding = function(x) { + if (!arguments.length) return padding; + function padFunction(node) { + var p = x.call(treemap, node, node.depth); + return p == null ? d3_layout_treemapPadNull(node) : d3_layout_treemapPad(node, typeof p === "number" ? [ p, p, p, p ] : p); + } + function padConstant(node) { + return d3_layout_treemapPad(node, x); + } + var type; + pad = (padding = x) == null ? d3_layout_treemapPadNull : (type = typeof x) === "function" ? padFunction : type === "number" ? (x = [ x, x, x, x ], + padConstant) : padConstant; + return treemap; + }; + treemap.round = function(x) { + if (!arguments.length) return round != Number; + round = x ? Math.round : Number; + return treemap; + }; + treemap.sticky = function(x) { + if (!arguments.length) return sticky; + sticky = x; + stickies = null; + return treemap; + }; + treemap.ratio = function(x) { + if (!arguments.length) return ratio; + ratio = x; + return treemap; + }; + treemap.mode = function(x) { + if (!arguments.length) return mode; + mode = x + ""; + return treemap; + }; + return d3_layout_hierarchyRebind(treemap, hierarchy); + }; + function d3_layout_treemapPadNull(node) { + return { + x: node.x, + y: node.y, + dx: node.dx, + dy: node.dy + }; + } + function d3_layout_treemapPad(node, padding) { + var x = node.x + padding[3], y = node.y + padding[0], dx = node.dx - padding[1] - padding[3], dy = node.dy - padding[0] - padding[2]; + if (dx < 0) { + x += dx / 2; + dx = 0; + } + if (dy < 0) { + y += dy / 2; + dy = 0; + } + return { + x: x, + y: y, + dx: dx, + dy: dy + }; + } + d3.random = { + normal: function(µ, σ) { + var n = arguments.length; + if (n < 2) σ = 1; + if (n < 1) µ = 0; + return function() { + var x, y, r; + do { + x = Math.random() * 2 - 1; + y = Math.random() * 2 - 1; + r = x * x + y * y; + } while (!r || r > 1); + return µ + σ * x * Math.sqrt(-2 * Math.log(r) / r); + }; + }, + logNormal: function() { + var random = d3.random.normal.apply(d3, arguments); + return function() { + return Math.exp(random()); + }; + }, + bates: function(m) { + var random = d3.random.irwinHall(m); + return function() { + return random() / m; + }; + }, + irwinHall: function(m) { + return function() { + for (var s = 0, j = 0; j < m; j++) s += Math.random(); + return s; + }; + } + }; + d3.scale = {}; + function d3_scaleExtent(domain) { + var start = domain[0], stop = domain[domain.length - 1]; + return start < stop ? [ start, stop ] : [ stop, start ]; + } + function d3_scaleRange(scale) { + return scale.rangeExtent ? scale.rangeExtent() : d3_scaleExtent(scale.range()); + } + function d3_scale_bilinear(domain, range, uninterpolate, interpolate) { + var u = uninterpolate(domain[0], domain[1]), i = interpolate(range[0], range[1]); + return function(x) { + return i(u(x)); + }; + } + function d3_scale_nice(domain, nice) { + var i0 = 0, i1 = domain.length - 1, x0 = domain[i0], x1 = domain[i1], dx; + if (x1 < x0) { + dx = i0, i0 = i1, i1 = dx; + dx = x0, x0 = x1, x1 = dx; + } + domain[i0] = nice.floor(x0); + domain[i1] = nice.ceil(x1); + return domain; + } + function d3_scale_niceStep(step) { + return step ? { + floor: function(x) { + return Math.floor(x / step) * step; + }, + ceil: function(x) { + return Math.ceil(x / step) * step; + } + } : d3_scale_niceIdentity; + } + var d3_scale_niceIdentity = { + floor: d3_identity, + ceil: d3_identity + }; + function d3_scale_polylinear(domain, range, uninterpolate, interpolate) { + var u = [], i = [], j = 0, k = Math.min(domain.length, range.length) - 1; + if (domain[k] < domain[0]) { + domain = domain.slice().reverse(); + range = range.slice().reverse(); + } + while (++j <= k) { + u.push(uninterpolate(domain[j - 1], domain[j])); + i.push(interpolate(range[j - 1], range[j])); + } + return function(x) { + var j = d3.bisect(domain, x, 1, k) - 1; + return i[j](u[j](x)); + }; + } + d3.scale.linear = function() { + return d3_scale_linear([ 0, 1 ], [ 0, 1 ], d3_interpolate, false); + }; + function d3_scale_linear(domain, range, interpolate, clamp) { + var output, input; + function rescale() { + var linear = Math.min(domain.length, range.length) > 2 ? d3_scale_polylinear : d3_scale_bilinear, uninterpolate = clamp ? d3_uninterpolateClamp : d3_uninterpolateNumber; + output = linear(domain, range, uninterpolate, interpolate); + input = linear(range, domain, uninterpolate, d3_interpolate); + return scale; + } + function scale(x) { + return output(x); + } + scale.invert = function(y) { + return input(y); + }; + scale.domain = function(x) { + if (!arguments.length) return domain; + domain = x.map(Number); + return rescale(); + }; + scale.range = function(x) { + if (!arguments.length) return range; + range = x; + return rescale(); + }; + scale.rangeRound = function(x) { + return scale.range(x).interpolate(d3_interpolateRound); + }; + scale.clamp = function(x) { + if (!arguments.length) return clamp; + clamp = x; + return rescale(); + }; + scale.interpolate = function(x) { + if (!arguments.length) return interpolate; + interpolate = x; + return rescale(); + }; + scale.ticks = function(m) { + return d3_scale_linearTicks(domain, m); + }; + scale.tickFormat = function(m, format) { + return d3_scale_linearTickFormat(domain, m, format); + }; + scale.nice = function(m) { + d3_scale_linearNice(domain, m); + return rescale(); + }; + scale.copy = function() { + return d3_scale_linear(domain, range, interpolate, clamp); + }; + return rescale(); + } + function d3_scale_linearRebind(scale, linear) { + return d3.rebind(scale, linear, "range", "rangeRound", "interpolate", "clamp"); + } + function d3_scale_linearNice(domain, m) { + return d3_scale_nice(domain, d3_scale_niceStep(d3_scale_linearTickRange(domain, m)[2])); + } + function d3_scale_linearTickRange(domain, m) { + if (m == null) m = 10; + var extent = d3_scaleExtent(domain), span = extent[1] - extent[0], step = Math.pow(10, Math.floor(Math.log(span / m) / Math.LN10)), err = m / span * step; + if (err <= .15) step *= 10; else if (err <= .35) step *= 5; else if (err <= .75) step *= 2; + extent[0] = Math.ceil(extent[0] / step) * step; + extent[1] = Math.floor(extent[1] / step) * step + step * .5; + extent[2] = step; + return extent; + } + function d3_scale_linearTicks(domain, m) { + return d3.range.apply(d3, d3_scale_linearTickRange(domain, m)); + } + function d3_scale_linearTickFormat(domain, m, format) { + var range = d3_scale_linearTickRange(domain, m); + if (format) { + var match = d3_format_re.exec(format); + match.shift(); + if (match[8] === "s") { + var prefix = d3.formatPrefix(Math.max(abs(range[0]), abs(range[1]))); + if (!match[7]) match[7] = "." + d3_scale_linearPrecision(prefix.scale(range[2])); + match[8] = "f"; + format = d3.format(match.join("")); + return function(d) { + return format(prefix.scale(d)) + prefix.symbol; + }; + } + if (!match[7]) match[7] = "." + d3_scale_linearFormatPrecision(match[8], range); + format = match.join(""); + } else { + format = ",." + d3_scale_linearPrecision(range[2]) + "f"; + } + return d3.format(format); + } + var d3_scale_linearFormatSignificant = { + s: 1, + g: 1, + p: 1, + r: 1, + e: 1 + }; + function d3_scale_linearPrecision(value) { + return -Math.floor(Math.log(value) / Math.LN10 + .01); + } + function d3_scale_linearFormatPrecision(type, range) { + var p = d3_scale_linearPrecision(range[2]); + return type in d3_scale_linearFormatSignificant ? Math.abs(p - d3_scale_linearPrecision(Math.max(abs(range[0]), abs(range[1])))) + +(type !== "e") : p - (type === "%") * 2; + } + d3.scale.log = function() { + return d3_scale_log(d3.scale.linear().domain([ 0, 1 ]), 10, true, [ 1, 10 ]); + }; + function d3_scale_log(linear, base, positive, domain) { + function log(x) { + return (positive ? Math.log(x < 0 ? 0 : x) : -Math.log(x > 0 ? 0 : -x)) / Math.log(base); + } + function pow(x) { + return positive ? Math.pow(base, x) : -Math.pow(base, -x); + } + function scale(x) { + return linear(log(x)); + } + scale.invert = function(x) { + return pow(linear.invert(x)); + }; + scale.domain = function(x) { + if (!arguments.length) return domain; + positive = x[0] >= 0; + linear.domain((domain = x.map(Number)).map(log)); + return scale; + }; + scale.base = function(_) { + if (!arguments.length) return base; + base = +_; + linear.domain(domain.map(log)); + return scale; + }; + scale.nice = function() { + var niced = d3_scale_nice(domain.map(log), positive ? Math : d3_scale_logNiceNegative); + linear.domain(niced); + domain = niced.map(pow); + return scale; + }; + scale.ticks = function() { + var extent = d3_scaleExtent(domain), ticks = [], u = extent[0], v = extent[1], i = Math.floor(log(u)), j = Math.ceil(log(v)), n = base % 1 ? 2 : base; + if (isFinite(j - i)) { + if (positive) { + for (;i < j; i++) for (var k = 1; k < n; k++) ticks.push(pow(i) * k); + ticks.push(pow(i)); + } else { + ticks.push(pow(i)); + for (;i++ < j; ) for (var k = n - 1; k > 0; k--) ticks.push(pow(i) * k); + } + for (i = 0; ticks[i] < u; i++) {} + for (j = ticks.length; ticks[j - 1] > v; j--) {} + ticks = ticks.slice(i, j); + } + return ticks; + }; + scale.tickFormat = function(n, format) { + if (!arguments.length) return d3_scale_logFormat; + if (arguments.length < 2) format = d3_scale_logFormat; else if (typeof format !== "function") format = d3.format(format); + var k = Math.max(.1, n / scale.ticks().length), f = positive ? (e = 1e-12, Math.ceil) : (e = -1e-12, + Math.floor), e; + return function(d) { + return d / pow(f(log(d) + e)) <= k ? format(d) : ""; + }; + }; + scale.copy = function() { + return d3_scale_log(linear.copy(), base, positive, domain); + }; + return d3_scale_linearRebind(scale, linear); + } + var d3_scale_logFormat = d3.format(".0e"), d3_scale_logNiceNegative = { + floor: function(x) { + return -Math.ceil(-x); + }, + ceil: function(x) { + return -Math.floor(-x); + } + }; + d3.scale.pow = function() { + return d3_scale_pow(d3.scale.linear(), 1, [ 0, 1 ]); + }; + function d3_scale_pow(linear, exponent, domain) { + var powp = d3_scale_powPow(exponent), powb = d3_scale_powPow(1 / exponent); + function scale(x) { + return linear(powp(x)); + } + scale.invert = function(x) { + return powb(linear.invert(x)); + }; + scale.domain = function(x) { + if (!arguments.length) return domain; + linear.domain((domain = x.map(Number)).map(powp)); + return scale; + }; + scale.ticks = function(m) { + return d3_scale_linearTicks(domain, m); + }; + scale.tickFormat = function(m, format) { + return d3_scale_linearTickFormat(domain, m, format); + }; + scale.nice = function(m) { + return scale.domain(d3_scale_linearNice(domain, m)); + }; + scale.exponent = function(x) { + if (!arguments.length) return exponent; + powp = d3_scale_powPow(exponent = x); + powb = d3_scale_powPow(1 / exponent); + linear.domain(domain.map(powp)); + return scale; + }; + scale.copy = function() { + return d3_scale_pow(linear.copy(), exponent, domain); + }; + return d3_scale_linearRebind(scale, linear); + } + function d3_scale_powPow(e) { + return function(x) { + return x < 0 ? -Math.pow(-x, e) : Math.pow(x, e); + }; + } + d3.scale.sqrt = function() { + return d3.scale.pow().exponent(.5); + }; + d3.scale.ordinal = function() { + return d3_scale_ordinal([], { + t: "range", + a: [ [] ] + }); + }; + function d3_scale_ordinal(domain, ranger) { + var index, range, rangeBand; + function scale(x) { + return range[((index.get(x) || (ranger.t === "range" ? index.set(x, domain.push(x)) : NaN)) - 1) % range.length]; + } + function steps(start, step) { + return d3.range(domain.length).map(function(i) { + return start + step * i; + }); + } + scale.domain = function(x) { + if (!arguments.length) return domain; + domain = []; + index = new d3_Map(); + var i = -1, n = x.length, xi; + while (++i < n) if (!index.has(xi = x[i])) index.set(xi, domain.push(xi)); + return scale[ranger.t].apply(scale, ranger.a); + }; + scale.range = function(x) { + if (!arguments.length) return range; + range = x; + rangeBand = 0; + ranger = { + t: "range", + a: arguments + }; + return scale; + }; + scale.rangePoints = function(x, padding) { + if (arguments.length < 2) padding = 0; + var start = x[0], stop = x[1], step = domain.length < 2 ? (start = (start + stop) / 2, + 0) : (stop - start) / (domain.length - 1 + padding); + range = steps(start + step * padding / 2, step); + rangeBand = 0; + ranger = { + t: "rangePoints", + a: arguments + }; + return scale; + }; + scale.rangeRoundPoints = function(x, padding) { + if (arguments.length < 2) padding = 0; + var start = x[0], stop = x[1], step = domain.length < 2 ? (start = stop = Math.round((start + stop) / 2), + 0) : (stop - start) / (domain.length - 1 + padding) | 0; + range = steps(start + Math.round(step * padding / 2 + (stop - start - (domain.length - 1 + padding) * step) / 2), step); + rangeBand = 0; + ranger = { + t: "rangeRoundPoints", + a: arguments + }; + return scale; + }; + scale.rangeBands = function(x, padding, outerPadding) { + if (arguments.length < 2) padding = 0; + if (arguments.length < 3) outerPadding = padding; + var reverse = x[1] < x[0], start = x[reverse - 0], stop = x[1 - reverse], step = (stop - start) / (domain.length - padding + 2 * outerPadding); + range = steps(start + step * outerPadding, step); + if (reverse) range.reverse(); + rangeBand = step * (1 - padding); + ranger = { + t: "rangeBands", + a: arguments + }; + return scale; + }; + scale.rangeRoundBands = function(x, padding, outerPadding) { + if (arguments.length < 2) padding = 0; + if (arguments.length < 3) outerPadding = padding; + var reverse = x[1] < x[0], start = x[reverse - 0], stop = x[1 - reverse], step = Math.floor((stop - start) / (domain.length - padding + 2 * outerPadding)); + range = steps(start + Math.round((stop - start - (domain.length - padding) * step) / 2), step); + if (reverse) range.reverse(); + rangeBand = Math.round(step * (1 - padding)); + ranger = { + t: "rangeRoundBands", + a: arguments + }; + return scale; + }; + scale.rangeBand = function() { + return rangeBand; + }; + scale.rangeExtent = function() { + return d3_scaleExtent(ranger.a[0]); + }; + scale.copy = function() { + return d3_scale_ordinal(domain, ranger); + }; + return scale.domain(domain); + } + d3.scale.category10 = function() { + return d3.scale.ordinal().range(d3_category10); + }; + d3.scale.category20 = function() { + return d3.scale.ordinal().range(d3_category20); + }; + d3.scale.category20b = function() { + return d3.scale.ordinal().range(d3_category20b); + }; + d3.scale.category20c = function() { + return d3.scale.ordinal().range(d3_category20c); + }; + var d3_category10 = [ 2062260, 16744206, 2924588, 14034728, 9725885, 9197131, 14907330, 8355711, 12369186, 1556175 ].map(d3_rgbString); + var d3_category20 = [ 2062260, 11454440, 16744206, 16759672, 2924588, 10018698, 14034728, 16750742, 9725885, 12955861, 9197131, 12885140, 14907330, 16234194, 8355711, 13092807, 12369186, 14408589, 1556175, 10410725 ].map(d3_rgbString); + var d3_category20b = [ 3750777, 5395619, 7040719, 10264286, 6519097, 9216594, 11915115, 13556636, 9202993, 12426809, 15186514, 15190932, 8666169, 11356490, 14049643, 15177372, 8077683, 10834324, 13528509, 14589654 ].map(d3_rgbString); + var d3_category20c = [ 3244733, 7057110, 10406625, 13032431, 15095053, 16616764, 16625259, 16634018, 3253076, 7652470, 10607003, 13101504, 7695281, 10394312, 12369372, 14342891, 6513507, 9868950, 12434877, 14277081 ].map(d3_rgbString); + d3.scale.quantile = function() { + return d3_scale_quantile([], []); + }; + function d3_scale_quantile(domain, range) { + var thresholds; + function rescale() { + var k = 0, q = range.length; + thresholds = []; + while (++k < q) thresholds[k - 1] = d3.quantile(domain, k / q); + return scale; + } + function scale(x) { + if (!isNaN(x = +x)) return range[d3.bisect(thresholds, x)]; + } + scale.domain = function(x) { + if (!arguments.length) return domain; + domain = x.map(d3_number).filter(d3_numeric).sort(d3_ascending); + return rescale(); + }; + scale.range = function(x) { + if (!arguments.length) return range; + range = x; + return rescale(); + }; + scale.quantiles = function() { + return thresholds; + }; + scale.invertExtent = function(y) { + y = range.indexOf(y); + return y < 0 ? [ NaN, NaN ] : [ y > 0 ? thresholds[y - 1] : domain[0], y < thresholds.length ? thresholds[y] : domain[domain.length - 1] ]; + }; + scale.copy = function() { + return d3_scale_quantile(domain, range); + }; + return rescale(); + } + d3.scale.quantize = function() { + return d3_scale_quantize(0, 1, [ 0, 1 ]); + }; + function d3_scale_quantize(x0, x1, range) { + var kx, i; + function scale(x) { + return range[Math.max(0, Math.min(i, Math.floor(kx * (x - x0))))]; + } + function rescale() { + kx = range.length / (x1 - x0); + i = range.length - 1; + return scale; + } + scale.domain = function(x) { + if (!arguments.length) return [ x0, x1 ]; + x0 = +x[0]; + x1 = +x[x.length - 1]; + return rescale(); + }; + scale.range = function(x) { + if (!arguments.length) return range; + range = x; + return rescale(); + }; + scale.invertExtent = function(y) { + y = range.indexOf(y); + y = y < 0 ? NaN : y / kx + x0; + return [ y, y + 1 / kx ]; + }; + scale.copy = function() { + return d3_scale_quantize(x0, x1, range); + }; + return rescale(); + } + d3.scale.threshold = function() { + return d3_scale_threshold([ .5 ], [ 0, 1 ]); + }; + function d3_scale_threshold(domain, range) { + function scale(x) { + if (x <= x) return range[d3.bisect(domain, x)]; + } + scale.domain = function(_) { + if (!arguments.length) return domain; + domain = _; + return scale; + }; + scale.range = function(_) { + if (!arguments.length) return range; + range = _; + return scale; + }; + scale.invertExtent = function(y) { + y = range.indexOf(y); + return [ domain[y - 1], domain[y] ]; + }; + scale.copy = function() { + return d3_scale_threshold(domain, range); + }; + return scale; + } + d3.scale.identity = function() { + return d3_scale_identity([ 0, 1 ]); + }; + function d3_scale_identity(domain) { + function identity(x) { + return +x; + } + identity.invert = identity; + identity.domain = identity.range = function(x) { + if (!arguments.length) return domain; + domain = x.map(identity); + return identity; + }; + identity.ticks = function(m) { + return d3_scale_linearTicks(domain, m); + }; + identity.tickFormat = function(m, format) { + return d3_scale_linearTickFormat(domain, m, format); + }; + identity.copy = function() { + return d3_scale_identity(domain); + }; + return identity; + } + d3.svg = {}; + function d3_zero() { + return 0; + } + d3.svg.arc = function() { + var innerRadius = d3_svg_arcInnerRadius, outerRadius = d3_svg_arcOuterRadius, cornerRadius = d3_zero, padRadius = d3_svg_arcAuto, startAngle = d3_svg_arcStartAngle, endAngle = d3_svg_arcEndAngle, padAngle = d3_svg_arcPadAngle; + function arc() { + var r0 = Math.max(0, +innerRadius.apply(this, arguments)), r1 = Math.max(0, +outerRadius.apply(this, arguments)), a0 = startAngle.apply(this, arguments) - halfπ, a1 = endAngle.apply(this, arguments) - halfπ, da = Math.abs(a1 - a0), cw = a0 > a1 ? 0 : 1; + if (r1 < r0) rc = r1, r1 = r0, r0 = rc; + if (da >= τε) return circleSegment(r1, cw) + (r0 ? circleSegment(r0, 1 - cw) : "") + "Z"; + var rc, cr, rp, ap, p0 = 0, p1 = 0, x0, y0, x1, y1, x2, y2, x3, y3, path = []; + if (ap = (+padAngle.apply(this, arguments) || 0) / 2) { + rp = padRadius === d3_svg_arcAuto ? Math.sqrt(r0 * r0 + r1 * r1) : +padRadius.apply(this, arguments); + if (!cw) p1 *= -1; + if (r1) p1 = d3_asin(rp / r1 * Math.sin(ap)); + if (r0) p0 = d3_asin(rp / r0 * Math.sin(ap)); + } + if (r1) { + x0 = r1 * Math.cos(a0 + p1); + y0 = r1 * Math.sin(a0 + p1); + x1 = r1 * Math.cos(a1 - p1); + y1 = r1 * Math.sin(a1 - p1); + var l1 = Math.abs(a1 - a0 - 2 * p1) <= π ? 0 : 1; + if (p1 && d3_svg_arcSweep(x0, y0, x1, y1) === cw ^ l1) { + var h1 = (a0 + a1) / 2; + x0 = r1 * Math.cos(h1); + y0 = r1 * Math.sin(h1); + x1 = y1 = null; + } + } else { + x0 = y0 = 0; + } + if (r0) { + x2 = r0 * Math.cos(a1 - p0); + y2 = r0 * Math.sin(a1 - p0); + x3 = r0 * Math.cos(a0 + p0); + y3 = r0 * Math.sin(a0 + p0); + var l0 = Math.abs(a0 - a1 + 2 * p0) <= π ? 0 : 1; + if (p0 && d3_svg_arcSweep(x2, y2, x3, y3) === 1 - cw ^ l0) { + var h0 = (a0 + a1) / 2; + x2 = r0 * Math.cos(h0); + y2 = r0 * Math.sin(h0); + x3 = y3 = null; + } + } else { + x2 = y2 = 0; + } + if ((rc = Math.min(Math.abs(r1 - r0) / 2, +cornerRadius.apply(this, arguments))) > .001) { + cr = r0 < r1 ^ cw ? 0 : 1; + var oc = x3 == null ? [ x2, y2 ] : x1 == null ? [ x0, y0 ] : d3_geom_polygonIntersect([ x0, y0 ], [ x3, y3 ], [ x1, y1 ], [ x2, y2 ]), ax = x0 - oc[0], ay = y0 - oc[1], bx = x1 - oc[0], by = y1 - oc[1], kc = 1 / Math.sin(Math.acos((ax * bx + ay * by) / (Math.sqrt(ax * ax + ay * ay) * Math.sqrt(bx * bx + by * by))) / 2), lc = Math.sqrt(oc[0] * oc[0] + oc[1] * oc[1]); + if (x1 != null) { + var rc1 = Math.min(rc, (r1 - lc) / (kc + 1)), t30 = d3_svg_arcCornerTangents(x3 == null ? [ x2, y2 ] : [ x3, y3 ], [ x0, y0 ], r1, rc1, cw), t12 = d3_svg_arcCornerTangents([ x1, y1 ], [ x2, y2 ], r1, rc1, cw); + if (rc === rc1) { + path.push("M", t30[0], "A", rc1, ",", rc1, " 0 0,", cr, " ", t30[1], "A", r1, ",", r1, " 0 ", 1 - cw ^ d3_svg_arcSweep(t30[1][0], t30[1][1], t12[1][0], t12[1][1]), ",", cw, " ", t12[1], "A", rc1, ",", rc1, " 0 0,", cr, " ", t12[0]); + } else { + path.push("M", t30[0], "A", rc1, ",", rc1, " 0 1,", cr, " ", t12[0]); + } + } else { + path.push("M", x0, ",", y0); + } + if (x3 != null) { + var rc0 = Math.min(rc, (r0 - lc) / (kc - 1)), t03 = d3_svg_arcCornerTangents([ x0, y0 ], [ x3, y3 ], r0, -rc0, cw), t21 = d3_svg_arcCornerTangents([ x2, y2 ], x1 == null ? [ x0, y0 ] : [ x1, y1 ], r0, -rc0, cw); + if (rc === rc0) { + path.push("L", t21[0], "A", rc0, ",", rc0, " 0 0,", cr, " ", t21[1], "A", r0, ",", r0, " 0 ", cw ^ d3_svg_arcSweep(t21[1][0], t21[1][1], t03[1][0], t03[1][1]), ",", 1 - cw, " ", t03[1], "A", rc0, ",", rc0, " 0 0,", cr, " ", t03[0]); + } else { + path.push("L", t21[0], "A", rc0, ",", rc0, " 0 0,", cr, " ", t03[0]); + } + } else { + path.push("L", x2, ",", y2); + } + } else { + path.push("M", x0, ",", y0); + if (x1 != null) path.push("A", r1, ",", r1, " 0 ", l1, ",", cw, " ", x1, ",", y1); + path.push("L", x2, ",", y2); + if (x3 != null) path.push("A", r0, ",", r0, " 0 ", l0, ",", 1 - cw, " ", x3, ",", y3); + } + path.push("Z"); + return path.join(""); + } + function circleSegment(r1, cw) { + return "M0," + r1 + "A" + r1 + "," + r1 + " 0 1," + cw + " 0," + -r1 + "A" + r1 + "," + r1 + " 0 1," + cw + " 0," + r1; + } + arc.innerRadius = function(v) { + if (!arguments.length) return innerRadius; + innerRadius = d3_functor(v); + return arc; + }; + arc.outerRadius = function(v) { + if (!arguments.length) return outerRadius; + outerRadius = d3_functor(v); + return arc; + }; + arc.cornerRadius = function(v) { + if (!arguments.length) return cornerRadius; + cornerRadius = d3_functor(v); + return arc; + }; + arc.padRadius = function(v) { + if (!arguments.length) return padRadius; + padRadius = v == d3_svg_arcAuto ? d3_svg_arcAuto : d3_functor(v); + return arc; + }; + arc.startAngle = function(v) { + if (!arguments.length) return startAngle; + startAngle = d3_functor(v); + return arc; + }; + arc.endAngle = function(v) { + if (!arguments.length) return endAngle; + endAngle = d3_functor(v); + return arc; + }; + arc.padAngle = function(v) { + if (!arguments.length) return padAngle; + padAngle = d3_functor(v); + return arc; + }; + arc.centroid = function() { + var r = (+innerRadius.apply(this, arguments) + +outerRadius.apply(this, arguments)) / 2, a = (+startAngle.apply(this, arguments) + +endAngle.apply(this, arguments)) / 2 - halfπ; + return [ Math.cos(a) * r, Math.sin(a) * r ]; + }; + return arc; + }; + var d3_svg_arcAuto = "auto"; + function d3_svg_arcInnerRadius(d) { + return d.innerRadius; + } + function d3_svg_arcOuterRadius(d) { + return d.outerRadius; + } + function d3_svg_arcStartAngle(d) { + return d.startAngle; + } + function d3_svg_arcEndAngle(d) { + return d.endAngle; + } + function d3_svg_arcPadAngle(d) { + return d && d.padAngle; + } + function d3_svg_arcSweep(x0, y0, x1, y1) { + return (x0 - x1) * y0 - (y0 - y1) * x0 > 0 ? 0 : 1; + } + function d3_svg_arcCornerTangents(p0, p1, r1, rc, cw) { + var x01 = p0[0] - p1[0], y01 = p0[1] - p1[1], lo = (cw ? rc : -rc) / Math.sqrt(x01 * x01 + y01 * y01), ox = lo * y01, oy = -lo * x01, x1 = p0[0] + ox, y1 = p0[1] + oy, x2 = p1[0] + ox, y2 = p1[1] + oy, x3 = (x1 + x2) / 2, y3 = (y1 + y2) / 2, dx = x2 - x1, dy = y2 - y1, d2 = dx * dx + dy * dy, r = r1 - rc, D = x1 * y2 - x2 * y1, d = (dy < 0 ? -1 : 1) * Math.sqrt(r * r * d2 - D * D), cx0 = (D * dy - dx * d) / d2, cy0 = (-D * dx - dy * d) / d2, cx1 = (D * dy + dx * d) / d2, cy1 = (-D * dx + dy * d) / d2, dx0 = cx0 - x3, dy0 = cy0 - y3, dx1 = cx1 - x3, dy1 = cy1 - y3; + if (dx0 * dx0 + dy0 * dy0 > dx1 * dx1 + dy1 * dy1) cx0 = cx1, cy0 = cy1; + return [ [ cx0 - ox, cy0 - oy ], [ cx0 * r1 / r, cy0 * r1 / r ] ]; + } + function d3_svg_line(projection) { + var x = d3_geom_pointX, y = d3_geom_pointY, defined = d3_true, interpolate = d3_svg_lineLinear, interpolateKey = interpolate.key, tension = .7; + function line(data) { + var segments = [], points = [], i = -1, n = data.length, d, fx = d3_functor(x), fy = d3_functor(y); + function segment() { + segments.push("M", interpolate(projection(points), tension)); + } + while (++i < n) { + if (defined.call(this, d = data[i], i)) { + points.push([ +fx.call(this, d, i), +fy.call(this, d, i) ]); + } else if (points.length) { + segment(); + points = []; + } + } + if (points.length) segment(); + return segments.length ? segments.join("") : null; + } + line.x = function(_) { + if (!arguments.length) return x; + x = _; + return line; + }; + line.y = function(_) { + if (!arguments.length) return y; + y = _; + return line; + }; + line.defined = function(_) { + if (!arguments.length) return defined; + defined = _; + return line; + }; + line.interpolate = function(_) { + if (!arguments.length) return interpolateKey; + if (typeof _ === "function") interpolateKey = interpolate = _; else interpolateKey = (interpolate = d3_svg_lineInterpolators.get(_) || d3_svg_lineLinear).key; + return line; + }; + line.tension = function(_) { + if (!arguments.length) return tension; + tension = _; + return line; + }; + return line; + } + d3.svg.line = function() { + return d3_svg_line(d3_identity); + }; + var d3_svg_lineInterpolators = d3.map({ + linear: d3_svg_lineLinear, + "linear-closed": d3_svg_lineLinearClosed, + step: d3_svg_lineStep, + "step-before": d3_svg_lineStepBefore, + "step-after": d3_svg_lineStepAfter, + basis: d3_svg_lineBasis, + "basis-open": d3_svg_lineBasisOpen, + "basis-closed": d3_svg_lineBasisClosed, + bundle: d3_svg_lineBundle, + cardinal: d3_svg_lineCardinal, + "cardinal-open": d3_svg_lineCardinalOpen, + "cardinal-closed": d3_svg_lineCardinalClosed, + monotone: d3_svg_lineMonotone + }); + d3_svg_lineInterpolators.forEach(function(key, value) { + value.key = key; + value.closed = /-closed$/.test(key); + }); + function d3_svg_lineLinear(points) { + return points.join("L"); + } + function d3_svg_lineLinearClosed(points) { + return d3_svg_lineLinear(points) + "Z"; + } + function d3_svg_lineStep(points) { + var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ]; + while (++i < n) path.push("H", (p[0] + (p = points[i])[0]) / 2, "V", p[1]); + if (n > 1) path.push("H", p[0]); + return path.join(""); + } + function d3_svg_lineStepBefore(points) { + var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ]; + while (++i < n) path.push("V", (p = points[i])[1], "H", p[0]); + return path.join(""); + } + function d3_svg_lineStepAfter(points) { + var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ]; + while (++i < n) path.push("H", (p = points[i])[0], "V", p[1]); + return path.join(""); + } + function d3_svg_lineCardinalOpen(points, tension) { + return points.length < 4 ? d3_svg_lineLinear(points) : points[1] + d3_svg_lineHermite(points.slice(1, -1), d3_svg_lineCardinalTangents(points, tension)); + } + function d3_svg_lineCardinalClosed(points, tension) { + return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite((points.push(points[0]), + points), d3_svg_lineCardinalTangents([ points[points.length - 2] ].concat(points, [ points[1] ]), tension)); + } + function d3_svg_lineCardinal(points, tension) { + return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite(points, d3_svg_lineCardinalTangents(points, tension)); + } + function d3_svg_lineHermite(points, tangents) { + if (tangents.length < 1 || points.length != tangents.length && points.length != tangents.length + 2) { + return d3_svg_lineLinear(points); + } + var quad = points.length != tangents.length, path = "", p0 = points[0], p = points[1], t0 = tangents[0], t = t0, pi = 1; + if (quad) { + path += "Q" + (p[0] - t0[0] * 2 / 3) + "," + (p[1] - t0[1] * 2 / 3) + "," + p[0] + "," + p[1]; + p0 = points[1]; + pi = 2; + } + if (tangents.length > 1) { + t = tangents[1]; + p = points[pi]; + pi++; + path += "C" + (p0[0] + t0[0]) + "," + (p0[1] + t0[1]) + "," + (p[0] - t[0]) + "," + (p[1] - t[1]) + "," + p[0] + "," + p[1]; + for (var i = 2; i < tangents.length; i++, pi++) { + p = points[pi]; + t = tangents[i]; + path += "S" + (p[0] - t[0]) + "," + (p[1] - t[1]) + "," + p[0] + "," + p[1]; + } + } + if (quad) { + var lp = points[pi]; + path += "Q" + (p[0] + t[0] * 2 / 3) + "," + (p[1] + t[1] * 2 / 3) + "," + lp[0] + "," + lp[1]; + } + return path; + } + function d3_svg_lineCardinalTangents(points, tension) { + var tangents = [], a = (1 - tension) / 2, p0, p1 = points[0], p2 = points[1], i = 1, n = points.length; + while (++i < n) { + p0 = p1; + p1 = p2; + p2 = points[i]; + tangents.push([ a * (p2[0] - p0[0]), a * (p2[1] - p0[1]) ]); + } + return tangents; + } + function d3_svg_lineBasis(points) { + if (points.length < 3) return d3_svg_lineLinear(points); + var i = 1, n = points.length, pi = points[0], x0 = pi[0], y0 = pi[1], px = [ x0, x0, x0, (pi = points[1])[0] ], py = [ y0, y0, y0, pi[1] ], path = [ x0, ",", y0, "L", d3_svg_lineDot4(d3_svg_lineBasisBezier3, px), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, py) ]; + points.push(points[n - 1]); + while (++i <= n) { + pi = points[i]; + px.shift(); + px.push(pi[0]); + py.shift(); + py.push(pi[1]); + d3_svg_lineBasisBezier(path, px, py); + } + points.pop(); + path.push("L", pi); + return path.join(""); + } + function d3_svg_lineBasisOpen(points) { + if (points.length < 4) return d3_svg_lineLinear(points); + var path = [], i = -1, n = points.length, pi, px = [ 0 ], py = [ 0 ]; + while (++i < 3) { + pi = points[i]; + px.push(pi[0]); + py.push(pi[1]); + } + path.push(d3_svg_lineDot4(d3_svg_lineBasisBezier3, px) + "," + d3_svg_lineDot4(d3_svg_lineBasisBezier3, py)); + --i; + while (++i < n) { + pi = points[i]; + px.shift(); + px.push(pi[0]); + py.shift(); + py.push(pi[1]); + d3_svg_lineBasisBezier(path, px, py); + } + return path.join(""); + } + function d3_svg_lineBasisClosed(points) { + var path, i = -1, n = points.length, m = n + 4, pi, px = [], py = []; + while (++i < 4) { + pi = points[i % n]; + px.push(pi[0]); + py.push(pi[1]); + } + path = [ d3_svg_lineDot4(d3_svg_lineBasisBezier3, px), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, py) ]; + --i; + while (++i < m) { + pi = points[i % n]; + px.shift(); + px.push(pi[0]); + py.shift(); + py.push(pi[1]); + d3_svg_lineBasisBezier(path, px, py); + } + return path.join(""); + } + function d3_svg_lineBundle(points, tension) { + var n = points.length - 1; + if (n) { + var x0 = points[0][0], y0 = points[0][1], dx = points[n][0] - x0, dy = points[n][1] - y0, i = -1, p, t; + while (++i <= n) { + p = points[i]; + t = i / n; + p[0] = tension * p[0] + (1 - tension) * (x0 + t * dx); + p[1] = tension * p[1] + (1 - tension) * (y0 + t * dy); + } + } + return d3_svg_lineBasis(points); + } + function d3_svg_lineDot4(a, b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]; + } + var d3_svg_lineBasisBezier1 = [ 0, 2 / 3, 1 / 3, 0 ], d3_svg_lineBasisBezier2 = [ 0, 1 / 3, 2 / 3, 0 ], d3_svg_lineBasisBezier3 = [ 0, 1 / 6, 2 / 3, 1 / 6 ]; + function d3_svg_lineBasisBezier(path, x, y) { + path.push("C", d3_svg_lineDot4(d3_svg_lineBasisBezier1, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier1, y), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier2, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier2, y), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, y)); + } + function d3_svg_lineSlope(p0, p1) { + return (p1[1] - p0[1]) / (p1[0] - p0[0]); + } + function d3_svg_lineFiniteDifferences(points) { + var i = 0, j = points.length - 1, m = [], p0 = points[0], p1 = points[1], d = m[0] = d3_svg_lineSlope(p0, p1); + while (++i < j) { + m[i] = (d + (d = d3_svg_lineSlope(p0 = p1, p1 = points[i + 1]))) / 2; + } + m[i] = d; + return m; + } + function d3_svg_lineMonotoneTangents(points) { + var tangents = [], d, a, b, s, m = d3_svg_lineFiniteDifferences(points), i = -1, j = points.length - 1; + while (++i < j) { + d = d3_svg_lineSlope(points[i], points[i + 1]); + if (abs(d) < ε) { + m[i] = m[i + 1] = 0; + } else { + a = m[i] / d; + b = m[i + 1] / d; + s = a * a + b * b; + if (s > 9) { + s = d * 3 / Math.sqrt(s); + m[i] = s * a; + m[i + 1] = s * b; + } + } + } + i = -1; + while (++i <= j) { + s = (points[Math.min(j, i + 1)][0] - points[Math.max(0, i - 1)][0]) / (6 * (1 + m[i] * m[i])); + tangents.push([ s || 0, m[i] * s || 0 ]); + } + return tangents; + } + function d3_svg_lineMonotone(points) { + return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite(points, d3_svg_lineMonotoneTangents(points)); + } + d3.svg.line.radial = function() { + var line = d3_svg_line(d3_svg_lineRadial); + line.radius = line.x, delete line.x; + line.angle = line.y, delete line.y; + return line; + }; + function d3_svg_lineRadial(points) { + var point, i = -1, n = points.length, r, a; + while (++i < n) { + point = points[i]; + r = point[0]; + a = point[1] - halfπ; + point[0] = r * Math.cos(a); + point[1] = r * Math.sin(a); + } + return points; + } + function d3_svg_area(projection) { + var x0 = d3_geom_pointX, x1 = d3_geom_pointX, y0 = 0, y1 = d3_geom_pointY, defined = d3_true, interpolate = d3_svg_lineLinear, interpolateKey = interpolate.key, interpolateReverse = interpolate, L = "L", tension = .7; + function area(data) { + var segments = [], points0 = [], points1 = [], i = -1, n = data.length, d, fx0 = d3_functor(x0), fy0 = d3_functor(y0), fx1 = x0 === x1 ? function() { + return x; + } : d3_functor(x1), fy1 = y0 === y1 ? function() { + return y; + } : d3_functor(y1), x, y; + function segment() { + segments.push("M", interpolate(projection(points1), tension), L, interpolateReverse(projection(points0.reverse()), tension), "Z"); + } + while (++i < n) { + if (defined.call(this, d = data[i], i)) { + points0.push([ x = +fx0.call(this, d, i), y = +fy0.call(this, d, i) ]); + points1.push([ +fx1.call(this, d, i), +fy1.call(this, d, i) ]); + } else if (points0.length) { + segment(); + points0 = []; + points1 = []; + } + } + if (points0.length) segment(); + return segments.length ? segments.join("") : null; + } + area.x = function(_) { + if (!arguments.length) return x1; + x0 = x1 = _; + return area; + }; + area.x0 = function(_) { + if (!arguments.length) return x0; + x0 = _; + return area; + }; + area.x1 = function(_) { + if (!arguments.length) return x1; + x1 = _; + return area; + }; + area.y = function(_) { + if (!arguments.length) return y1; + y0 = y1 = _; + return area; + }; + area.y0 = function(_) { + if (!arguments.length) return y0; + y0 = _; + return area; + }; + area.y1 = function(_) { + if (!arguments.length) return y1; + y1 = _; + return area; + }; + area.defined = function(_) { + if (!arguments.length) return defined; + defined = _; + return area; + }; + area.interpolate = function(_) { + if (!arguments.length) return interpolateKey; + if (typeof _ === "function") interpolateKey = interpolate = _; else interpolateKey = (interpolate = d3_svg_lineInterpolators.get(_) || d3_svg_lineLinear).key; + interpolateReverse = interpolate.reverse || interpolate; + L = interpolate.closed ? "M" : "L"; + return area; + }; + area.tension = function(_) { + if (!arguments.length) return tension; + tension = _; + return area; + }; + return area; + } + d3_svg_lineStepBefore.reverse = d3_svg_lineStepAfter; + d3_svg_lineStepAfter.reverse = d3_svg_lineStepBefore; + d3.svg.area = function() { + return d3_svg_area(d3_identity); + }; + d3.svg.area.radial = function() { + var area = d3_svg_area(d3_svg_lineRadial); + area.radius = area.x, delete area.x; + area.innerRadius = area.x0, delete area.x0; + area.outerRadius = area.x1, delete area.x1; + area.angle = area.y, delete area.y; + area.startAngle = area.y0, delete area.y0; + area.endAngle = area.y1, delete area.y1; + return area; + }; + d3.svg.chord = function() { + var source = d3_source, target = d3_target, radius = d3_svg_chordRadius, startAngle = d3_svg_arcStartAngle, endAngle = d3_svg_arcEndAngle; + function chord(d, i) { + var s = subgroup(this, source, d, i), t = subgroup(this, target, d, i); + return "M" + s.p0 + arc(s.r, s.p1, s.a1 - s.a0) + (equals(s, t) ? curve(s.r, s.p1, s.r, s.p0) : curve(s.r, s.p1, t.r, t.p0) + arc(t.r, t.p1, t.a1 - t.a0) + curve(t.r, t.p1, s.r, s.p0)) + "Z"; + } + function subgroup(self, f, d, i) { + var subgroup = f.call(self, d, i), r = radius.call(self, subgroup, i), a0 = startAngle.call(self, subgroup, i) - halfπ, a1 = endAngle.call(self, subgroup, i) - halfπ; + return { + r: r, + a0: a0, + a1: a1, + p0: [ r * Math.cos(a0), r * Math.sin(a0) ], + p1: [ r * Math.cos(a1), r * Math.sin(a1) ] + }; + } + function equals(a, b) { + return a.a0 == b.a0 && a.a1 == b.a1; + } + function arc(r, p, a) { + return "A" + r + "," + r + " 0 " + +(a > π) + ",1 " + p; + } + function curve(r0, p0, r1, p1) { + return "Q 0,0 " + p1; + } + chord.radius = function(v) { + if (!arguments.length) return radius; + radius = d3_functor(v); + return chord; + }; + chord.source = function(v) { + if (!arguments.length) return source; + source = d3_functor(v); + return chord; + }; + chord.target = function(v) { + if (!arguments.length) return target; + target = d3_functor(v); + return chord; + }; + chord.startAngle = function(v) { + if (!arguments.length) return startAngle; + startAngle = d3_functor(v); + return chord; + }; + chord.endAngle = function(v) { + if (!arguments.length) return endAngle; + endAngle = d3_functor(v); + return chord; + }; + return chord; + }; + function d3_svg_chordRadius(d) { + return d.radius; + } + d3.svg.diagonal = function() { + var source = d3_source, target = d3_target, projection = d3_svg_diagonalProjection; + function diagonal(d, i) { + var p0 = source.call(this, d, i), p3 = target.call(this, d, i), m = (p0.y + p3.y) / 2, p = [ p0, { + x: p0.x, + y: m + }, { + x: p3.x, + y: m + }, p3 ]; + p = p.map(projection); + return "M" + p[0] + "C" + p[1] + " " + p[2] + " " + p[3]; + } + diagonal.source = function(x) { + if (!arguments.length) return source; + source = d3_functor(x); + return diagonal; + }; + diagonal.target = function(x) { + if (!arguments.length) return target; + target = d3_functor(x); + return diagonal; + }; + diagonal.projection = function(x) { + if (!arguments.length) return projection; + projection = x; + return diagonal; + }; + return diagonal; + }; + function d3_svg_diagonalProjection(d) { + return [ d.x, d.y ]; + } + d3.svg.diagonal.radial = function() { + var diagonal = d3.svg.diagonal(), projection = d3_svg_diagonalProjection, projection_ = diagonal.projection; + diagonal.projection = function(x) { + return arguments.length ? projection_(d3_svg_diagonalRadialProjection(projection = x)) : projection; + }; + return diagonal; + }; + function d3_svg_diagonalRadialProjection(projection) { + return function() { + var d = projection.apply(this, arguments), r = d[0], a = d[1] - halfπ; + return [ r * Math.cos(a), r * Math.sin(a) ]; + }; + } + d3.svg.symbol = function() { + var type = d3_svg_symbolType, size = d3_svg_symbolSize; + function symbol(d, i) { + return (d3_svg_symbols.get(type.call(this, d, i)) || d3_svg_symbolCircle)(size.call(this, d, i)); + } + symbol.type = function(x) { + if (!arguments.length) return type; + type = d3_functor(x); + return symbol; + }; + symbol.size = function(x) { + if (!arguments.length) return size; + size = d3_functor(x); + return symbol; + }; + return symbol; + }; + function d3_svg_symbolSize() { + return 64; + } + function d3_svg_symbolType() { + return "circle"; + } + function d3_svg_symbolCircle(size) { + var r = Math.sqrt(size / π); + return "M0," + r + "A" + r + "," + r + " 0 1,1 0," + -r + "A" + r + "," + r + " 0 1,1 0," + r + "Z"; + } + var d3_svg_symbols = d3.map({ + circle: d3_svg_symbolCircle, + cross: function(size) { + var r = Math.sqrt(size / 5) / 2; + return "M" + -3 * r + "," + -r + "H" + -r + "V" + -3 * r + "H" + r + "V" + -r + "H" + 3 * r + "V" + r + "H" + r + "V" + 3 * r + "H" + -r + "V" + r + "H" + -3 * r + "Z"; + }, + diamond: function(size) { + var ry = Math.sqrt(size / (2 * d3_svg_symbolTan30)), rx = ry * d3_svg_symbolTan30; + return "M0," + -ry + "L" + rx + ",0" + " 0," + ry + " " + -rx + ",0" + "Z"; + }, + square: function(size) { + var r = Math.sqrt(size) / 2; + return "M" + -r + "," + -r + "L" + r + "," + -r + " " + r + "," + r + " " + -r + "," + r + "Z"; + }, + "triangle-down": function(size) { + var rx = Math.sqrt(size / d3_svg_symbolSqrt3), ry = rx * d3_svg_symbolSqrt3 / 2; + return "M0," + ry + "L" + rx + "," + -ry + " " + -rx + "," + -ry + "Z"; + }, + "triangle-up": function(size) { + var rx = Math.sqrt(size / d3_svg_symbolSqrt3), ry = rx * d3_svg_symbolSqrt3 / 2; + return "M0," + -ry + "L" + rx + "," + ry + " " + -rx + "," + ry + "Z"; + } + }); + d3.svg.symbolTypes = d3_svg_symbols.keys(); + var d3_svg_symbolSqrt3 = Math.sqrt(3), d3_svg_symbolTan30 = Math.tan(30 * d3_radians); + d3_selectionPrototype.transition = function(name) { + var id = d3_transitionInheritId || ++d3_transitionId, ns = d3_transitionNamespace(name), subgroups = [], subgroup, node, transition = d3_transitionInherit || { + time: Date.now(), + ease: d3_ease_cubicInOut, + delay: 0, + duration: 250 + }; + for (var j = -1, m = this.length; ++j < m; ) { + subgroups.push(subgroup = []); + for (var group = this[j], i = -1, n = group.length; ++i < n; ) { + if (node = group[i]) d3_transitionNode(node, i, ns, id, transition); + subgroup.push(node); + } + } + return d3_transition(subgroups, ns, id); + }; + d3_selectionPrototype.interrupt = function(name) { + return this.each(name == null ? d3_selection_interrupt : d3_selection_interruptNS(d3_transitionNamespace(name))); + }; + var d3_selection_interrupt = d3_selection_interruptNS(d3_transitionNamespace()); + function d3_selection_interruptNS(ns) { + return function() { + var lock, active; + if ((lock = this[ns]) && (active = lock[lock.active])) { + if (--lock.count) { + delete lock[lock.active]; + lock.active += .5; + } else { + delete this[ns]; + } + active.event && active.event.interrupt.call(this, this.__data__, active.index); + } + }; + } + function d3_transition(groups, ns, id) { + d3_subclass(groups, d3_transitionPrototype); + groups.namespace = ns; + groups.id = id; + return groups; + } + var d3_transitionPrototype = [], d3_transitionId = 0, d3_transitionInheritId, d3_transitionInherit; + d3_transitionPrototype.call = d3_selectionPrototype.call; + d3_transitionPrototype.empty = d3_selectionPrototype.empty; + d3_transitionPrototype.node = d3_selectionPrototype.node; + d3_transitionPrototype.size = d3_selectionPrototype.size; + d3.transition = function(selection, name) { + return selection && selection.transition ? d3_transitionInheritId ? selection.transition(name) : selection : d3_selectionRoot.transition(selection); + }; + d3.transition.prototype = d3_transitionPrototype; + d3_transitionPrototype.select = function(selector) { + var id = this.id, ns = this.namespace, subgroups = [], subgroup, subnode, node; + selector = d3_selection_selector(selector); + for (var j = -1, m = this.length; ++j < m; ) { + subgroups.push(subgroup = []); + for (var group = this[j], i = -1, n = group.length; ++i < n; ) { + if ((node = group[i]) && (subnode = selector.call(node, node.__data__, i, j))) { + if ("__data__" in node) subnode.__data__ = node.__data__; + d3_transitionNode(subnode, i, ns, id, node[ns][id]); + subgroup.push(subnode); + } else { + subgroup.push(null); + } + } + } + return d3_transition(subgroups, ns, id); + }; + d3_transitionPrototype.selectAll = function(selector) { + var id = this.id, ns = this.namespace, subgroups = [], subgroup, subnodes, node, subnode, transition; + selector = d3_selection_selectorAll(selector); + for (var j = -1, m = this.length; ++j < m; ) { + for (var group = this[j], i = -1, n = group.length; ++i < n; ) { + if (node = group[i]) { + transition = node[ns][id]; + subnodes = selector.call(node, node.__data__, i, j); + subgroups.push(subgroup = []); + for (var k = -1, o = subnodes.length; ++k < o; ) { + if (subnode = subnodes[k]) d3_transitionNode(subnode, k, ns, id, transition); + subgroup.push(subnode); + } + } + } + } + return d3_transition(subgroups, ns, id); + }; + d3_transitionPrototype.filter = function(filter) { + var subgroups = [], subgroup, group, node; + if (typeof filter !== "function") filter = d3_selection_filter(filter); + for (var j = 0, m = this.length; j < m; j++) { + subgroups.push(subgroup = []); + for (var group = this[j], i = 0, n = group.length; i < n; i++) { + if ((node = group[i]) && filter.call(node, node.__data__, i, j)) { + subgroup.push(node); + } + } + } + return d3_transition(subgroups, this.namespace, this.id); + }; + d3_transitionPrototype.tween = function(name, tween) { + var id = this.id, ns = this.namespace; + if (arguments.length < 2) return this.node()[ns][id].tween.get(name); + return d3_selection_each(this, tween == null ? function(node) { + node[ns][id].tween.remove(name); + } : function(node) { + node[ns][id].tween.set(name, tween); + }); + }; + function d3_transition_tween(groups, name, value, tween) { + var id = groups.id, ns = groups.namespace; + return d3_selection_each(groups, typeof value === "function" ? function(node, i, j) { + node[ns][id].tween.set(name, tween(value.call(node, node.__data__, i, j))); + } : (value = tween(value), function(node) { + node[ns][id].tween.set(name, value); + })); + } + d3_transitionPrototype.attr = function(nameNS, value) { + if (arguments.length < 2) { + for (value in nameNS) this.attr(value, nameNS[value]); + return this; + } + var interpolate = nameNS == "transform" ? d3_interpolateTransform : d3_interpolate, name = d3.ns.qualify(nameNS); + function attrNull() { + this.removeAttribute(name); + } + function attrNullNS() { + this.removeAttributeNS(name.space, name.local); + } + function attrTween(b) { + return b == null ? attrNull : (b += "", function() { + var a = this.getAttribute(name), i; + return a !== b && (i = interpolate(a, b), function(t) { + this.setAttribute(name, i(t)); + }); + }); + } + function attrTweenNS(b) { + return b == null ? attrNullNS : (b += "", function() { + var a = this.getAttributeNS(name.space, name.local), i; + return a !== b && (i = interpolate(a, b), function(t) { + this.setAttributeNS(name.space, name.local, i(t)); + }); + }); + } + return d3_transition_tween(this, "attr." + nameNS, value, name.local ? attrTweenNS : attrTween); + }; + d3_transitionPrototype.attrTween = function(nameNS, tween) { + var name = d3.ns.qualify(nameNS); + function attrTween(d, i) { + var f = tween.call(this, d, i, this.getAttribute(name)); + return f && function(t) { + this.setAttribute(name, f(t)); + }; + } + function attrTweenNS(d, i) { + var f = tween.call(this, d, i, this.getAttributeNS(name.space, name.local)); + return f && function(t) { + this.setAttributeNS(name.space, name.local, f(t)); + }; + } + return this.tween("attr." + nameNS, name.local ? attrTweenNS : attrTween); + }; + d3_transitionPrototype.style = function(name, value, priority) { + var n = arguments.length; + if (n < 3) { + if (typeof name !== "string") { + if (n < 2) value = ""; + for (priority in name) this.style(priority, name[priority], value); + return this; + } + priority = ""; + } + function styleNull() { + this.style.removeProperty(name); + } + function styleString(b) { + return b == null ? styleNull : (b += "", function() { + var a = d3_window.getComputedStyle(this, null).getPropertyValue(name), i; + return a !== b && (i = d3_interpolate(a, b), function(t) { + this.style.setProperty(name, i(t), priority); + }); + }); + } + return d3_transition_tween(this, "style." + name, value, styleString); + }; + d3_transitionPrototype.styleTween = function(name, tween, priority) { + if (arguments.length < 3) priority = ""; + function styleTween(d, i) { + var f = tween.call(this, d, i, d3_window.getComputedStyle(this, null).getPropertyValue(name)); + return f && function(t) { + this.style.setProperty(name, f(t), priority); + }; + } + return this.tween("style." + name, styleTween); + }; + d3_transitionPrototype.text = function(value) { + return d3_transition_tween(this, "text", value, d3_transition_text); + }; + function d3_transition_text(b) { + if (b == null) b = ""; + return function() { + this.textContent = b; + }; + } + d3_transitionPrototype.remove = function() { + var ns = this.namespace; + return this.each("end.transition", function() { + var p; + if (this[ns].count < 2 && (p = this.parentNode)) p.removeChild(this); + }); + }; + d3_transitionPrototype.ease = function(value) { + var id = this.id, ns = this.namespace; + if (arguments.length < 1) return this.node()[ns][id].ease; + if (typeof value !== "function") value = d3.ease.apply(d3, arguments); + return d3_selection_each(this, function(node) { + node[ns][id].ease = value; + }); + }; + d3_transitionPrototype.delay = function(value) { + var id = this.id, ns = this.namespace; + if (arguments.length < 1) return this.node()[ns][id].delay; + return d3_selection_each(this, typeof value === "function" ? function(node, i, j) { + node[ns][id].delay = +value.call(node, node.__data__, i, j); + } : (value = +value, function(node) { + node[ns][id].delay = value; + })); + }; + d3_transitionPrototype.duration = function(value) { + var id = this.id, ns = this.namespace; + if (arguments.length < 1) return this.node()[ns][id].duration; + return d3_selection_each(this, typeof value === "function" ? function(node, i, j) { + node[ns][id].duration = Math.max(1, value.call(node, node.__data__, i, j)); + } : (value = Math.max(1, value), function(node) { + node[ns][id].duration = value; + })); + }; + d3_transitionPrototype.each = function(type, listener) { + var id = this.id, ns = this.namespace; + if (arguments.length < 2) { + var inherit = d3_transitionInherit, inheritId = d3_transitionInheritId; + try { + d3_transitionInheritId = id; + d3_selection_each(this, function(node, i, j) { + d3_transitionInherit = node[ns][id]; + type.call(node, node.__data__, i, j); + }); + } finally { + d3_transitionInherit = inherit; + d3_transitionInheritId = inheritId; + } + } else { + d3_selection_each(this, function(node) { + var transition = node[ns][id]; + (transition.event || (transition.event = d3.dispatch("start", "end", "interrupt"))).on(type, listener); + }); + } + return this; + }; + d3_transitionPrototype.transition = function() { + var id0 = this.id, id1 = ++d3_transitionId, ns = this.namespace, subgroups = [], subgroup, group, node, transition; + for (var j = 0, m = this.length; j < m; j++) { + subgroups.push(subgroup = []); + for (var group = this[j], i = 0, n = group.length; i < n; i++) { + if (node = group[i]) { + transition = node[ns][id0]; + d3_transitionNode(node, i, ns, id1, { + time: transition.time, + ease: transition.ease, + delay: transition.delay + transition.duration, + duration: transition.duration + }); + } + subgroup.push(node); + } + } + return d3_transition(subgroups, ns, id1); + }; + function d3_transitionNamespace(name) { + return name == null ? "__transition__" : "__transition_" + name + "__"; + } + function d3_transitionNode(node, i, ns, id, inherit) { + var lock = node[ns] || (node[ns] = { + active: 0, + count: 0 + }), transition = lock[id]; + if (!transition) { + var time = inherit.time; + transition = lock[id] = { + tween: new d3_Map(), + time: time, + delay: inherit.delay, + duration: inherit.duration, + ease: inherit.ease, + index: i + }; + inherit = null; + ++lock.count; + d3.timer(function(elapsed) { + var delay = transition.delay, duration, ease, timer = d3_timer_active, tweened = []; + timer.t = delay + time; + if (delay <= elapsed) return start(elapsed - delay); + timer.c = start; + function start(elapsed) { + if (lock.active > id) return stop(); + var active = lock[lock.active]; + if (active) { + --lock.count; + delete lock[lock.active]; + active.event && active.event.interrupt.call(node, node.__data__, active.index); + } + lock.active = id; + transition.event && transition.event.start.call(node, node.__data__, i); + transition.tween.forEach(function(key, value) { + if (value = value.call(node, node.__data__, i)) { + tweened.push(value); + } + }); + ease = transition.ease; + duration = transition.duration; + d3.timer(function() { + timer.c = tick(elapsed || 1) ? d3_true : tick; + return 1; + }, 0, time); + } + function tick(elapsed) { + if (lock.active !== id) return 1; + var t = elapsed / duration, e = ease(t), n = tweened.length; + while (n > 0) { + tweened[--n].call(node, e); + } + if (t >= 1) { + transition.event && transition.event.end.call(node, node.__data__, i); + return stop(); + } + } + function stop() { + if (--lock.count) delete lock[id]; else delete node[ns]; + return 1; + } + }, 0, time); + } + } + d3.svg.axis = function() { + var scale = d3.scale.linear(), orient = d3_svg_axisDefaultOrient, innerTickSize = 6, outerTickSize = 6, tickPadding = 3, tickArguments_ = [ 10 ], tickValues = null, tickFormat_; + function axis(g) { + g.each(function() { + var g = d3.select(this); + var scale0 = this.__chart__ || scale, scale1 = this.__chart__ = scale.copy(); + var ticks = tickValues == null ? scale1.ticks ? scale1.ticks.apply(scale1, tickArguments_) : scale1.domain() : tickValues, tickFormat = tickFormat_ == null ? scale1.tickFormat ? scale1.tickFormat.apply(scale1, tickArguments_) : d3_identity : tickFormat_, tick = g.selectAll(".tick").data(ticks, scale1), tickEnter = tick.enter().insert("g", ".domain").attr("class", "tick").style("opacity", ε), tickExit = d3.transition(tick.exit()).style("opacity", ε).remove(), tickUpdate = d3.transition(tick.order()).style("opacity", 1), tickSpacing = Math.max(innerTickSize, 0) + tickPadding, tickTransform; + var range = d3_scaleRange(scale1), path = g.selectAll(".domain").data([ 0 ]), pathUpdate = (path.enter().append("path").attr("class", "domain"), + d3.transition(path)); + tickEnter.append("line"); + tickEnter.append("text"); + var lineEnter = tickEnter.select("line"), lineUpdate = tickUpdate.select("line"), text = tick.select("text").text(tickFormat), textEnter = tickEnter.select("text"), textUpdate = tickUpdate.select("text"), sign = orient === "top" || orient === "left" ? -1 : 1, x1, x2, y1, y2; + if (orient === "bottom" || orient === "top") { + tickTransform = d3_svg_axisX, x1 = "x", y1 = "y", x2 = "x2", y2 = "y2"; + text.attr("dy", sign < 0 ? "0em" : ".71em").style("text-anchor", "middle"); + pathUpdate.attr("d", "M" + range[0] + "," + sign * outerTickSize + "V0H" + range[1] + "V" + sign * outerTickSize); + } else { + tickTransform = d3_svg_axisY, x1 = "y", y1 = "x", x2 = "y2", y2 = "x2"; + text.attr("dy", ".32em").style("text-anchor", sign < 0 ? "end" : "start"); + pathUpdate.attr("d", "M" + sign * outerTickSize + "," + range[0] + "H0V" + range[1] + "H" + sign * outerTickSize); + } + lineEnter.attr(y2, sign * innerTickSize); + textEnter.attr(y1, sign * tickSpacing); + lineUpdate.attr(x2, 0).attr(y2, sign * innerTickSize); + textUpdate.attr(x1, 0).attr(y1, sign * tickSpacing); + if (scale1.rangeBand) { + var x = scale1, dx = x.rangeBand() / 2; + scale0 = scale1 = function(d) { + return x(d) + dx; + }; + } else if (scale0.rangeBand) { + scale0 = scale1; + } else { + tickExit.call(tickTransform, scale1, scale0); + } + tickEnter.call(tickTransform, scale0, scale1); + tickUpdate.call(tickTransform, scale1, scale1); + }); + } + axis.scale = function(x) { + if (!arguments.length) return scale; + scale = x; + return axis; + }; + axis.orient = function(x) { + if (!arguments.length) return orient; + orient = x in d3_svg_axisOrients ? x + "" : d3_svg_axisDefaultOrient; + return axis; + }; + axis.ticks = function() { + if (!arguments.length) return tickArguments_; + tickArguments_ = arguments; + return axis; + }; + axis.tickValues = function(x) { + if (!arguments.length) return tickValues; + tickValues = x; + return axis; + }; + axis.tickFormat = function(x) { + if (!arguments.length) return tickFormat_; + tickFormat_ = x; + return axis; + }; + axis.tickSize = function(x) { + var n = arguments.length; + if (!n) return innerTickSize; + innerTickSize = +x; + outerTickSize = +arguments[n - 1]; + return axis; + }; + axis.innerTickSize = function(x) { + if (!arguments.length) return innerTickSize; + innerTickSize = +x; + return axis; + }; + axis.outerTickSize = function(x) { + if (!arguments.length) return outerTickSize; + outerTickSize = +x; + return axis; + }; + axis.tickPadding = function(x) { + if (!arguments.length) return tickPadding; + tickPadding = +x; + return axis; + }; + axis.tickSubdivide = function() { + return arguments.length && axis; + }; + return axis; + }; + var d3_svg_axisDefaultOrient = "bottom", d3_svg_axisOrients = { + top: 1, + right: 1, + bottom: 1, + left: 1 + }; + function d3_svg_axisX(selection, x0, x1) { + selection.attr("transform", function(d) { + var v0 = x0(d); + return "translate(" + (isFinite(v0) ? v0 : x1(d)) + ",0)"; + }); + } + function d3_svg_axisY(selection, y0, y1) { + selection.attr("transform", function(d) { + var v0 = y0(d); + return "translate(0," + (isFinite(v0) ? v0 : y1(d)) + ")"; + }); + } + d3.svg.brush = function() { + var event = d3_eventDispatch(brush, "brushstart", "brush", "brushend"), x = null, y = null, xExtent = [ 0, 0 ], yExtent = [ 0, 0 ], xExtentDomain, yExtentDomain, xClamp = true, yClamp = true, resizes = d3_svg_brushResizes[0]; + function brush(g) { + g.each(function() { + var g = d3.select(this).style("pointer-events", "all").style("-webkit-tap-highlight-color", "rgba(0,0,0,0)").on("mousedown.brush", brushstart).on("touchstart.brush", brushstart); + var background = g.selectAll(".background").data([ 0 ]); + background.enter().append("rect").attr("class", "background").style("visibility", "hidden").style("cursor", "crosshair"); + g.selectAll(".extent").data([ 0 ]).enter().append("rect").attr("class", "extent").style("cursor", "move"); + var resize = g.selectAll(".resize").data(resizes, d3_identity); + resize.exit().remove(); + resize.enter().append("g").attr("class", function(d) { + return "resize " + d; + }).style("cursor", function(d) { + return d3_svg_brushCursor[d]; + }).append("rect").attr("x", function(d) { + return /[ew]$/.test(d) ? -3 : null; + }).attr("y", function(d) { + return /^[ns]/.test(d) ? -3 : null; + }).attr("width", 6).attr("height", 6).style("visibility", "hidden"); + resize.style("display", brush.empty() ? "none" : null); + var gUpdate = d3.transition(g), backgroundUpdate = d3.transition(background), range; + if (x) { + range = d3_scaleRange(x); + backgroundUpdate.attr("x", range[0]).attr("width", range[1] - range[0]); + redrawX(gUpdate); + } + if (y) { + range = d3_scaleRange(y); + backgroundUpdate.attr("y", range[0]).attr("height", range[1] - range[0]); + redrawY(gUpdate); + } + redraw(gUpdate); + }); + } + brush.event = function(g) { + g.each(function() { + var event_ = event.of(this, arguments), extent1 = { + x: xExtent, + y: yExtent, + i: xExtentDomain, + j: yExtentDomain + }, extent0 = this.__chart__ || extent1; + this.__chart__ = extent1; + if (d3_transitionInheritId) { + d3.select(this).transition().each("start.brush", function() { + xExtentDomain = extent0.i; + yExtentDomain = extent0.j; + xExtent = extent0.x; + yExtent = extent0.y; + event_({ + type: "brushstart" + }); + }).tween("brush:brush", function() { + var xi = d3_interpolateArray(xExtent, extent1.x), yi = d3_interpolateArray(yExtent, extent1.y); + xExtentDomain = yExtentDomain = null; + return function(t) { + xExtent = extent1.x = xi(t); + yExtent = extent1.y = yi(t); + event_({ + type: "brush", + mode: "resize" + }); + }; + }).each("end.brush", function() { + xExtentDomain = extent1.i; + yExtentDomain = extent1.j; + event_({ + type: "brush", + mode: "resize" + }); + event_({ + type: "brushend" + }); + }); + } else { + event_({ + type: "brushstart" + }); + event_({ + type: "brush", + mode: "resize" + }); + event_({ + type: "brushend" + }); + } + }); + }; + function redraw(g) { + g.selectAll(".resize").attr("transform", function(d) { + return "translate(" + xExtent[+/e$/.test(d)] + "," + yExtent[+/^s/.test(d)] + ")"; + }); + } + function redrawX(g) { + g.select(".extent").attr("x", xExtent[0]); + g.selectAll(".extent,.n>rect,.s>rect").attr("width", xExtent[1] - xExtent[0]); + } + function redrawY(g) { + g.select(".extent").attr("y", yExtent[0]); + g.selectAll(".extent,.e>rect,.w>rect").attr("height", yExtent[1] - yExtent[0]); + } + function brushstart() { + var target = this, eventTarget = d3.select(d3.event.target), event_ = event.of(target, arguments), g = d3.select(target), resizing = eventTarget.datum(), resizingX = !/^(n|s)$/.test(resizing) && x, resizingY = !/^(e|w)$/.test(resizing) && y, dragging = eventTarget.classed("extent"), dragRestore = d3_event_dragSuppress(), center, origin = d3.mouse(target), offset; + var w = d3.select(d3_window).on("keydown.brush", keydown).on("keyup.brush", keyup); + if (d3.event.changedTouches) { + w.on("touchmove.brush", brushmove).on("touchend.brush", brushend); + } else { + w.on("mousemove.brush", brushmove).on("mouseup.brush", brushend); + } + g.interrupt().selectAll("*").interrupt(); + if (dragging) { + origin[0] = xExtent[0] - origin[0]; + origin[1] = yExtent[0] - origin[1]; + } else if (resizing) { + var ex = +/w$/.test(resizing), ey = +/^n/.test(resizing); + offset = [ xExtent[1 - ex] - origin[0], yExtent[1 - ey] - origin[1] ]; + origin[0] = xExtent[ex]; + origin[1] = yExtent[ey]; + } else if (d3.event.altKey) center = origin.slice(); + g.style("pointer-events", "none").selectAll(".resize").style("display", null); + d3.select("body").style("cursor", eventTarget.style("cursor")); + event_({ + type: "brushstart" + }); + brushmove(); + function keydown() { + if (d3.event.keyCode == 32) { + if (!dragging) { + center = null; + origin[0] -= xExtent[1]; + origin[1] -= yExtent[1]; + dragging = 2; + } + d3_eventPreventDefault(); + } + } + function keyup() { + if (d3.event.keyCode == 32 && dragging == 2) { + origin[0] += xExtent[1]; + origin[1] += yExtent[1]; + dragging = 0; + d3_eventPreventDefault(); + } + } + function brushmove() { + var point = d3.mouse(target), moved = false; + if (offset) { + point[0] += offset[0]; + point[1] += offset[1]; + } + if (!dragging) { + if (d3.event.altKey) { + if (!center) center = [ (xExtent[0] + xExtent[1]) / 2, (yExtent[0] + yExtent[1]) / 2 ]; + origin[0] = xExtent[+(point[0] < center[0])]; + origin[1] = yExtent[+(point[1] < center[1])]; + } else center = null; + } + if (resizingX && move1(point, x, 0)) { + redrawX(g); + moved = true; + } + if (resizingY && move1(point, y, 1)) { + redrawY(g); + moved = true; + } + if (moved) { + redraw(g); + event_({ + type: "brush", + mode: dragging ? "move" : "resize" + }); + } + } + function move1(point, scale, i) { + var range = d3_scaleRange(scale), r0 = range[0], r1 = range[1], position = origin[i], extent = i ? yExtent : xExtent, size = extent[1] - extent[0], min, max; + if (dragging) { + r0 -= position; + r1 -= size + position; + } + min = (i ? yClamp : xClamp) ? Math.max(r0, Math.min(r1, point[i])) : point[i]; + if (dragging) { + max = (min += position) + size; + } else { + if (center) position = Math.max(r0, Math.min(r1, 2 * center[i] - min)); + if (position < min) { + max = min; + min = position; + } else { + max = position; + } + } + if (extent[0] != min || extent[1] != max) { + if (i) yExtentDomain = null; else xExtentDomain = null; + extent[0] = min; + extent[1] = max; + return true; + } + } + function brushend() { + brushmove(); + g.style("pointer-events", "all").selectAll(".resize").style("display", brush.empty() ? "none" : null); + d3.select("body").style("cursor", null); + w.on("mousemove.brush", null).on("mouseup.brush", null).on("touchmove.brush", null).on("touchend.brush", null).on("keydown.brush", null).on("keyup.brush", null); + dragRestore(); + event_({ + type: "brushend" + }); + } + } + brush.x = function(z) { + if (!arguments.length) return x; + x = z; + resizes = d3_svg_brushResizes[!x << 1 | !y]; + return brush; + }; + brush.y = function(z) { + if (!arguments.length) return y; + y = z; + resizes = d3_svg_brushResizes[!x << 1 | !y]; + return brush; + }; + brush.clamp = function(z) { + if (!arguments.length) return x && y ? [ xClamp, yClamp ] : x ? xClamp : y ? yClamp : null; + if (x && y) xClamp = !!z[0], yClamp = !!z[1]; else if (x) xClamp = !!z; else if (y) yClamp = !!z; + return brush; + }; + brush.extent = function(z) { + var x0, x1, y0, y1, t; + if (!arguments.length) { + if (x) { + if (xExtentDomain) { + x0 = xExtentDomain[0], x1 = xExtentDomain[1]; + } else { + x0 = xExtent[0], x1 = xExtent[1]; + if (x.invert) x0 = x.invert(x0), x1 = x.invert(x1); + if (x1 < x0) t = x0, x0 = x1, x1 = t; + } + } + if (y) { + if (yExtentDomain) { + y0 = yExtentDomain[0], y1 = yExtentDomain[1]; + } else { + y0 = yExtent[0], y1 = yExtent[1]; + if (y.invert) y0 = y.invert(y0), y1 = y.invert(y1); + if (y1 < y0) t = y0, y0 = y1, y1 = t; + } + } + return x && y ? [ [ x0, y0 ], [ x1, y1 ] ] : x ? [ x0, x1 ] : y && [ y0, y1 ]; + } + if (x) { + x0 = z[0], x1 = z[1]; + if (y) x0 = x0[0], x1 = x1[0]; + xExtentDomain = [ x0, x1 ]; + if (x.invert) x0 = x(x0), x1 = x(x1); + if (x1 < x0) t = x0, x0 = x1, x1 = t; + if (x0 != xExtent[0] || x1 != xExtent[1]) xExtent = [ x0, x1 ]; + } + if (y) { + y0 = z[0], y1 = z[1]; + if (x) y0 = y0[1], y1 = y1[1]; + yExtentDomain = [ y0, y1 ]; + if (y.invert) y0 = y(y0), y1 = y(y1); + if (y1 < y0) t = y0, y0 = y1, y1 = t; + if (y0 != yExtent[0] || y1 != yExtent[1]) yExtent = [ y0, y1 ]; + } + return brush; + }; + brush.clear = function() { + if (!brush.empty()) { + xExtent = [ 0, 0 ], yExtent = [ 0, 0 ]; + xExtentDomain = yExtentDomain = null; + } + return brush; + }; + brush.empty = function() { + return !!x && xExtent[0] == xExtent[1] || !!y && yExtent[0] == yExtent[1]; + }; + return d3.rebind(brush, event, "on"); + }; + var d3_svg_brushCursor = { + n: "ns-resize", + e: "ew-resize", + s: "ns-resize", + w: "ew-resize", + nw: "nwse-resize", + ne: "nesw-resize", + se: "nwse-resize", + sw: "nesw-resize" + }; + var d3_svg_brushResizes = [ [ "n", "e", "s", "w", "nw", "ne", "se", "sw" ], [ "e", "w" ], [ "n", "s" ], [] ]; + var d3_time_format = d3_time.format = d3_locale_enUS.timeFormat; + var d3_time_formatUtc = d3_time_format.utc; + var d3_time_formatIso = d3_time_formatUtc("%Y-%m-%dT%H:%M:%S.%LZ"); + d3_time_format.iso = Date.prototype.toISOString && +new Date("2000-01-01T00:00:00.000Z") ? d3_time_formatIsoNative : d3_time_formatIso; + function d3_time_formatIsoNative(date) { + return date.toISOString(); + } + d3_time_formatIsoNative.parse = function(string) { + var date = new Date(string); + return isNaN(date) ? null : date; + }; + d3_time_formatIsoNative.toString = d3_time_formatIso.toString; + d3_time.second = d3_time_interval(function(date) { + return new d3_date(Math.floor(date / 1e3) * 1e3); + }, function(date, offset) { + date.setTime(date.getTime() + Math.floor(offset) * 1e3); + }, function(date) { + return date.getSeconds(); + }); + d3_time.seconds = d3_time.second.range; + d3_time.seconds.utc = d3_time.second.utc.range; + d3_time.minute = d3_time_interval(function(date) { + return new d3_date(Math.floor(date / 6e4) * 6e4); + }, function(date, offset) { + date.setTime(date.getTime() + Math.floor(offset) * 6e4); + }, function(date) { + return date.getMinutes(); + }); + d3_time.minutes = d3_time.minute.range; + d3_time.minutes.utc = d3_time.minute.utc.range; + d3_time.hour = d3_time_interval(function(date) { + var timezone = date.getTimezoneOffset() / 60; + return new d3_date((Math.floor(date / 36e5 - timezone) + timezone) * 36e5); + }, function(date, offset) { + date.setTime(date.getTime() + Math.floor(offset) * 36e5); + }, function(date) { + return date.getHours(); + }); + d3_time.hours = d3_time.hour.range; + d3_time.hours.utc = d3_time.hour.utc.range; + d3_time.month = d3_time_interval(function(date) { + date = d3_time.day(date); + date.setDate(1); + return date; + }, function(date, offset) { + date.setMonth(date.getMonth() + offset); + }, function(date) { + return date.getMonth(); + }); + d3_time.months = d3_time.month.range; + d3_time.months.utc = d3_time.month.utc.range; + function d3_time_scale(linear, methods, format) { + function scale(x) { + return linear(x); + } + scale.invert = function(x) { + return d3_time_scaleDate(linear.invert(x)); + }; + scale.domain = function(x) { + if (!arguments.length) return linear.domain().map(d3_time_scaleDate); + linear.domain(x); + return scale; + }; + function tickMethod(extent, count) { + var span = extent[1] - extent[0], target = span / count, i = d3.bisect(d3_time_scaleSteps, target); + return i == d3_time_scaleSteps.length ? [ methods.year, d3_scale_linearTickRange(extent.map(function(d) { + return d / 31536e6; + }), count)[2] ] : !i ? [ d3_time_scaleMilliseconds, d3_scale_linearTickRange(extent, count)[2] ] : methods[target / d3_time_scaleSteps[i - 1] < d3_time_scaleSteps[i] / target ? i - 1 : i]; + } + scale.nice = function(interval, skip) { + var domain = scale.domain(), extent = d3_scaleExtent(domain), method = interval == null ? tickMethod(extent, 10) : typeof interval === "number" && tickMethod(extent, interval); + if (method) interval = method[0], skip = method[1]; + function skipped(date) { + return !isNaN(date) && !interval.range(date, d3_time_scaleDate(+date + 1), skip).length; + } + return scale.domain(d3_scale_nice(domain, skip > 1 ? { + floor: function(date) { + while (skipped(date = interval.floor(date))) date = d3_time_scaleDate(date - 1); + return date; + }, + ceil: function(date) { + while (skipped(date = interval.ceil(date))) date = d3_time_scaleDate(+date + 1); + return date; + } + } : interval)); + }; + scale.ticks = function(interval, skip) { + var extent = d3_scaleExtent(scale.domain()), method = interval == null ? tickMethod(extent, 10) : typeof interval === "number" ? tickMethod(extent, interval) : !interval.range && [ { + range: interval + }, skip ]; + if (method) interval = method[0], skip = method[1]; + return interval.range(extent[0], d3_time_scaleDate(+extent[1] + 1), skip < 1 ? 1 : skip); + }; + scale.tickFormat = function() { + return format; + }; + scale.copy = function() { + return d3_time_scale(linear.copy(), methods, format); + }; + return d3_scale_linearRebind(scale, linear); + } + function d3_time_scaleDate(t) { + return new Date(t); + } + var d3_time_scaleSteps = [ 1e3, 5e3, 15e3, 3e4, 6e4, 3e5, 9e5, 18e5, 36e5, 108e5, 216e5, 432e5, 864e5, 1728e5, 6048e5, 2592e6, 7776e6, 31536e6 ]; + var d3_time_scaleLocalMethods = [ [ d3_time.second, 1 ], [ d3_time.second, 5 ], [ d3_time.second, 15 ], [ d3_time.second, 30 ], [ d3_time.minute, 1 ], [ d3_time.minute, 5 ], [ d3_time.minute, 15 ], [ d3_time.minute, 30 ], [ d3_time.hour, 1 ], [ d3_time.hour, 3 ], [ d3_time.hour, 6 ], [ d3_time.hour, 12 ], [ d3_time.day, 1 ], [ d3_time.day, 2 ], [ d3_time.week, 1 ], [ d3_time.month, 1 ], [ d3_time.month, 3 ], [ d3_time.year, 1 ] ]; + var d3_time_scaleLocalFormat = d3_time_format.multi([ [ ".%L", function(d) { + return d.getMilliseconds(); + } ], [ ":%S", function(d) { + return d.getSeconds(); + } ], [ "%I:%M", function(d) { + return d.getMinutes(); + } ], [ "%I %p", function(d) { + return d.getHours(); + } ], [ "%a %d", function(d) { + return d.getDay() && d.getDate() != 1; + } ], [ "%b %d", function(d) { + return d.getDate() != 1; + } ], [ "%B", function(d) { + return d.getMonth(); + } ], [ "%Y", d3_true ] ]); + var d3_time_scaleMilliseconds = { + range: function(start, stop, step) { + return d3.range(Math.ceil(start / step) * step, +stop, step).map(d3_time_scaleDate); + }, + floor: d3_identity, + ceil: d3_identity + }; + d3_time_scaleLocalMethods.year = d3_time.year; + d3_time.scale = function() { + return d3_time_scale(d3.scale.linear(), d3_time_scaleLocalMethods, d3_time_scaleLocalFormat); + }; + var d3_time_scaleUtcMethods = d3_time_scaleLocalMethods.map(function(m) { + return [ m[0].utc, m[1] ]; + }); + var d3_time_scaleUtcFormat = d3_time_formatUtc.multi([ [ ".%L", function(d) { + return d.getUTCMilliseconds(); + } ], [ ":%S", function(d) { + return d.getUTCSeconds(); + } ], [ "%I:%M", function(d) { + return d.getUTCMinutes(); + } ], [ "%I %p", function(d) { + return d.getUTCHours(); + } ], [ "%a %d", function(d) { + return d.getUTCDay() && d.getUTCDate() != 1; + } ], [ "%b %d", function(d) { + return d.getUTCDate() != 1; + } ], [ "%B", function(d) { + return d.getUTCMonth(); + } ], [ "%Y", d3_true ] ]); + d3_time_scaleUtcMethods.year = d3_time.year.utc; + d3_time.scale.utc = function() { + return d3_time_scale(d3.scale.linear(), d3_time_scaleUtcMethods, d3_time_scaleUtcFormat); + }; + d3.text = d3_xhrType(function(request) { + return request.responseText; + }); + d3.json = function(url, callback) { + return d3_xhr(url, "application/json", d3_json, callback); + }; + function d3_json(request) { + return JSON.parse(request.responseText); + } + d3.html = function(url, callback) { + return d3_xhr(url, "text/html", d3_html, callback); + }; + function d3_html(request) { + var range = d3_document.createRange(); + range.selectNode(d3_document.body); + return range.createContextualFragment(request.responseText); + } + d3.xml = d3_xhrType(function(request) { + return request.responseXML; + }); + if (typeof define === "function" && define.amd) define(d3); else if (typeof module === "object" && module.exports) module.exports = d3; + this.d3 = d3; +}(); \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/js/device-stats.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/js/device-stats.js new file mode 100644 index 000000000..5104ab176 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/js/device-stats.js @@ -0,0 +1,247 @@ +/* + * 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. + */ + +var xAxis; + +var deviceType = $("#details").data("devicetype"); +var deviceId = $(".device-id").data("deviceid"); +var monitorOperations = $("#details").data("monitor"); +var appContext = $("#details").data("appcontext"); + +var marker1 = appContext + "/public/iot.unit.device.stats/images/map-marker-1.png"; +var marker2 = appContext + "/public/iot.unit.device.stats/images/map-marker-2.png"; + +var map, mapPoints = [], mapPaths = [], mapMarkers = []; +var palette = new Rickshaw.Color.Palette({scheme: "classic9"}); + +function initMap() { + if ($('#map').length) { + map = new google.maps.Map(document.getElementById("map"), { + center: {lat: 6.9344, lng: 79.8428}, + zoom: 12 + }); + } +} + +function formatDates() { + $(".formatDate").each(function () { + var timeStamp = $(this).html(); + $(this).html(getDateString(timeStamp)); + }); +} + +function getDateString(timeStamp) { + var monthNames = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec" + ]; + + var date = new Date(parseInt(timeStamp)); + var day = date.getDate(); + var monthIndex = date.getMonth() + 1; + if (monthIndex < 10) { + monthIndex = "0" + monthIndex; + } + + var year = date.getFullYear(); + var hours = date.getHours(); + var amPm = hours < 12 ? "AM" : "PM"; + + if (hours > 12) { + hours -= 12; + } + if (hours == 0) { + hours = 12; + } + + return day + '-' + monthNames[monthIndex - 1] + '-' + year + ' ' + hours + ':' + date.getMinutes() + amPm; +} + +$(window).on("resize", function () { + location.reload(false); +}); + +$(document).ready(function () { + formatDates(); + updateGraphs(); +}); + +function updateGraphs() { + var tv = 5000; + var graphs = {}; + for (var op in monitorOperations) { + var opName = monitorOperations[op].name; + if (opName == "gps") { + $("#map").removeClass("hidden"); + } else { + var xLabel = "", yLabel = ""; + if (monitorOperations[op].ui_unit) { + var graph_data = monitorOperations[op].ui_unit.data; + for (var d in graph_data) { + if (graph_data[d].hasOwnProperty("column")) { + if (graph_data[d]["column"]["ui-mapping"] == 'x-axis') { + xLabel = graph_data[d]["column"]["label"]; + } else if (graph_data[d]["column"]["ui-mapping"] == 'y-axis') { + yLabel = graph_data[d]["column"]["label"]; + } + } + } + } + var graphHtml = '
      ' + + '
      ' + yLabel + '
      ' + + '
      ' + + '
      ' + + '
      ' + + '
      ' + + '
      ' + + '
      ' + xLabel + '
      ' + + '
      '; + $("#div-chart").append(graphHtml); + + var graph = new Rickshaw.Graph({ + element: document.getElementById("chart-" + opName), + width: $("#chartWrapper").width() - 50, + height: 300, + renderer: "line", + padding: {top: 0.2, left: 0.0, right: 0.0, bottom: 0.2}, + series: new Rickshaw.Series.FixedDuration([{name: monitorOperations[op].name}], undefined, { + timeInterval: 10000, + maxDataPoints: 20, + color: palette.color(), + timeBase: new Date().getTime() / 1000 + }) + }); + + graph.render(); + + xAxis = new Rickshaw.Graph.Axis.Time({ + graph: graph + }); + + xAxis.render(); + + new Rickshaw.Graph.Axis.Y({ + graph: graph, + orientation: 'left', + height: 300, + tickFormat: Rickshaw.Fixtures.Number.formatKMBT, + element: document.getElementById('y_axis-' + opName) + }); + + new Rickshaw.Graph.Legend({ + graph: graph, + element: document.getElementById('legend-' + opName) + }); + + new Rickshaw.Graph.HoverDetail({ + graph: graph, + formatter: function (series, x, y) { + var date = '' + moment(x * 1000).format('Do MMM YYYY h:mm:ss a') + ''; + var swatch = ''; + return swatch + series.name + ": " + parseInt(y) + '
      ' + date; + } + }); + + graphs[opName] = graph; + } + } + + setInterval(function () { + + var getStatsRequest = $.ajax({ + url: appContext + "/api/operations/" + deviceType + "/stats?deviceId=" + + deviceId, + method: "get" + }); + + getStatsRequest.done(function (data) { + var stats = data.data; + var lastUpdate = -1; + for (var s in stats) { + var val = stats[s]; + if (!val) { + continue; + } + if (val.time > lastUpdate) { + lastUpdate = val.time; + } + delete val["time"]; + if (val.map) { + mapPoints.push(val.map); + var marker = new google.maps.Marker({ + position: val.map, + map: map, + icon: marker1, + title: "Seen at " + getDateString(lastUpdate) + }); + marker.setMap(map); + map.panTo(val.map); + mapMarkers.push(marker); + + if (mapPoints.length > 1) { + var l = mapPoints.length; + var path = new google.maps.Polyline({ + path: [mapPoints[l - 1], mapPoints[l - 2]], + geodesic: true, + strokeColor: "#FF0000", + strokeOpacity: 1.0, + strokeWeight: 2 + }); + + path.setMap(map); + mapPaths.push(path); + + mapMarkers[l - 2].setIcon(marker2); + } + + if (mapPoints.length >= 10) { + mapMarkers[0].setMap(null); + mapMarkers.splice(0, 1); + + mapPaths[0].setMap(null); + mapPaths.splice(0, 1); + + mapPoints.splice(0, 1); + } + } else { + var graphVals = {}; + for (var key in val) { + graphVals[key] = val[key]; + graphs[key].series.addData(graphVals); + graphs[key].render(); + } + } + } + + if (lastUpdate == -1) { + $('#last_seen').text("Not seen recently"); + } + + var timeDiff = new Date().getTime() - lastUpdate; + if (timeDiff < tv * 2) { + $('#last_seen').text("Last seen: A while ago"); + } else if (timeDiff < 60 * 1000) { + $('#last_seen').text("Last seen: Less than a minute ago"); + } else if (timeDiff < 60 * 60 * 1000) { + $('#last_seen').text("Last seen: " + Math.round(timeDiff / (60 * 1000)) + + " minutes ago"); + } else { + $('#last_seen').text("Last seen: " + getDateString(lastUpdate)); + } + }); + }, tv); +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/js/moment.min.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/js/moment.min.js new file mode 100644 index 000000000..78e5aaadc --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/js/moment.min.js @@ -0,0 +1,25 @@ +/* + * 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. + */ + +//! moment.js +//! version : 2.10.2 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com +!function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):a.moment=b()}(this,function(){"use strict";function a(){return Ac.apply(null,arguments)}function b(a){Ac=a}function c(){return{empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function d(a){return"[object Array]"===Object.prototype.toString.call(a)}function e(a){return"[object Date]"===Object.prototype.toString.call(a)||a instanceof Date}function f(a,b){var c,d=[];for(c=0;c0)for(c in Cc)d=Cc[c],e=b[d],"undefined"!=typeof e&&(a[d]=e);return a}function m(b){l(this,b),this._d=new Date(+b._d),Dc===!1&&(Dc=!0,a.updateOffset(this),Dc=!1)}function n(a){return a instanceof m||null!=a&&g(a,"_isAMomentObject")}function o(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=b>=0?Math.floor(b):Math.ceil(b)),c}function p(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;e>d;d++)(c&&a[d]!==b[d]||!c&&o(a[d])!==o(b[d]))&&g++;return g+f}function q(){}function r(a){return a?a.toLowerCase().replace("_","-"):a}function s(a){for(var b,c,d,e,f=0;f0;){if(d=t(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&p(e,c,!0)>=b-1)break;b--}f++}return null}function t(a){var b=null;if(!Ec[a]&&"undefined"!=typeof module&&module&&module.exports)try{b=Bc._abbr,require("./locale/"+a),u(b)}catch(c){}return Ec[a]}function u(a,b){var c;return a&&(c="undefined"==typeof b?w(a):v(a,b),c&&(Bc=c)),Bc._abbr}function v(a,b){return null!==b?(b.abbr=a,Ec[a]||(Ec[a]=new q),Ec[a].set(b),u(a),Ec[a]):(delete Ec[a],null)}function w(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return Bc;if(!d(a)){if(b=t(a))return b;a=[a]}return s(a)}function x(a,b){var c=a.toLowerCase();Fc[c]=Fc[c+"s"]=Fc[b]=a}function y(a){return"string"==typeof a?Fc[a]||Fc[a.toLowerCase()]:void 0}function z(a){var b,c,d={};for(c in a)g(a,c)&&(b=y(c),b&&(d[b]=a[c]));return d}function A(b,c){return function(d){return null!=d?(C(this,b,d),a.updateOffset(this,c),this):B(this,b)}}function B(a,b){return a._d["get"+(a._isUTC?"UTC":"")+b]()}function C(a,b,c){return a._d["set"+(a._isUTC?"UTC":"")+b](c)}function D(a,b){var c;if("object"==typeof a)for(c in a)this.set(c,a[c]);else if(a=y(a),"function"==typeof this[a])return this[a](b);return this}function E(a,b,c){for(var d=""+Math.abs(a),e=a>=0;d.lengthb;b++)d[b]=Jc[d[b]]?Jc[d[b]]:G(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function I(a,b){return a.isValid()?(b=J(b,a.localeData()),Ic[b]||(Ic[b]=H(b)),Ic[b](a)):a.localeData().invalidDate()}function J(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(Hc.lastIndex=0;d>=0&&Hc.test(a);)a=a.replace(Hc,c),Hc.lastIndex=0,d-=1;return a}function K(a,b,c){Yc[a]="function"==typeof b?b:function(a){return a&&c?c:b}}function L(a,b){return g(Yc,a)?Yc[a](b._strict,b._locale):new RegExp(M(a))}function M(a){return a.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e}).replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function N(a,b){var c,d=b;for("string"==typeof a&&(a=[a]),"number"==typeof b&&(d=function(a,c){c[b]=o(a)}),c=0;cd;d++){if(e=i([2e3,d]),c&&!this._longMonthsParse[d]&&(this._longMonthsParse[d]=new RegExp("^"+this.months(e,"").replace(".","")+"$","i"),this._shortMonthsParse[d]=new RegExp("^"+this.monthsShort(e,"").replace(".","")+"$","i")),c||this._monthsParse[d]||(f="^"+this.months(e,"")+"|^"+this.monthsShort(e,""),this._monthsParse[d]=new RegExp(f.replace(".",""),"i")),c&&"MMMM"===b&&this._longMonthsParse[d].test(a))return d;if(c&&"MMM"===b&&this._shortMonthsParse[d].test(a))return d;if(!c&&this._monthsParse[d].test(a))return d}}function U(a,b){var c;return"string"==typeof b&&(b=a.localeData().monthsParse(b),"number"!=typeof b)?a:(c=Math.min(a.date(),Q(a.year(),b)),a._d["set"+(a._isUTC?"UTC":"")+"Month"](b,c),a)}function V(b){return null!=b?(U(this,b),a.updateOffset(this,!0),this):B(this,"Month")}function W(){return Q(this.year(),this.month())}function X(a){var b,c=a._a;return c&&-2===a._pf.overflow&&(b=c[_c]<0||c[_c]>11?_c:c[ad]<1||c[ad]>Q(c[$c],c[_c])?ad:c[bd]<0||c[bd]>24||24===c[bd]&&(0!==c[cd]||0!==c[dd]||0!==c[ed])?bd:c[cd]<0||c[cd]>59?cd:c[dd]<0||c[dd]>59?dd:c[ed]<0||c[ed]>999?ed:-1,a._pf._overflowDayOfYear&&($c>b||b>ad)&&(b=ad),a._pf.overflow=b),a}function Y(b){a.suppressDeprecationWarnings===!1&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+b)}function Z(a,b){var c=!0;return h(function(){return c&&(Y(a),c=!1),b.apply(this,arguments)},b)}function $(a,b){hd[a]||(Y(b),hd[a]=!0)}function _(a){var b,c,d=a._i,e=id.exec(d);if(e){for(a._pf.iso=!0,b=0,c=jd.length;c>b;b++)if(jd[b][1].exec(d)){a._f=jd[b][0]+(e[6]||" ");break}for(b=0,c=kd.length;c>b;b++)if(kd[b][1].exec(d)){a._f+=kd[b][0];break}d.match(Vc)&&(a._f+="Z"),sa(a)}else a._isValid=!1}function aa(b){var c=ld.exec(b._i);return null!==c?void(b._d=new Date(+c[1])):(_(b),void(b._isValid===!1&&(delete b._isValid,a.createFromInputFallback(b))))}function ba(a,b,c,d,e,f,g){var h=new Date(a,b,c,d,e,f,g);return 1970>a&&h.setFullYear(a),h}function ca(a){var b=new Date(Date.UTC.apply(null,arguments));return 1970>a&&b.setUTCFullYear(a),b}function da(a){return ea(a)?366:365}function ea(a){return a%4===0&&a%100!==0||a%400===0}function fa(){return ea(this.year())}function ga(a,b,c){var d,e=c-b,f=c-a.day();return f>e&&(f-=7),e-7>f&&(f+=7),d=za(a).add(f,"d"),{week:Math.ceil(d.dayOfYear()/7),year:d.year()}}function ha(a){return ga(a,this._week.dow,this._week.doy).week}function ia(){return this._week.dow}function ja(){return this._week.doy}function ka(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")}function la(a){var b=ga(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")}function ma(a,b,c,d,e){var f,g,h=ca(a,0,1).getUTCDay();return h=0===h?7:h,c=null!=c?c:e,f=e-h+(h>d?7:0)-(e>h?7:0),g=7*(b-1)+(c-e)+f+1,{year:g>0?a:a-1,dayOfYear:g>0?g:da(a-1)+g}}function na(a){var b=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")}function oa(a,b,c){return null!=a?a:null!=b?b:c}function pa(a){var b=new Date;return a._useUTC?[b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate()]:[b.getFullYear(),b.getMonth(),b.getDate()]}function qa(a){var b,c,d,e,f=[];if(!a._d){for(d=pa(a),a._w&&null==a._a[ad]&&null==a._a[_c]&&ra(a),a._dayOfYear&&(e=oa(a._a[$c],d[$c]),a._dayOfYear>da(e)&&(a._pf._overflowDayOfYear=!0),c=ca(e,0,a._dayOfYear),a._a[_c]=c.getUTCMonth(),a._a[ad]=c.getUTCDate()),b=0;3>b&&null==a._a[b];++b)a._a[b]=f[b]=d[b];for(;7>b;b++)a._a[b]=f[b]=null==a._a[b]?2===b?1:0:a._a[b];24===a._a[bd]&&0===a._a[cd]&&0===a._a[dd]&&0===a._a[ed]&&(a._nextDay=!0,a._a[bd]=0),a._d=(a._useUTC?ca:ba).apply(null,f),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[bd]=24)}}function ra(a){var b,c,d,e,f,g,h;b=a._w,null!=b.GG||null!=b.W||null!=b.E?(f=1,g=4,c=oa(b.GG,a._a[$c],ga(za(),1,4).year),d=oa(b.W,1),e=oa(b.E,1)):(f=a._locale._week.dow,g=a._locale._week.doy,c=oa(b.gg,a._a[$c],ga(za(),f,g).year),d=oa(b.w,1),null!=b.d?(e=b.d,f>e&&++d):e=null!=b.e?b.e+f:f),h=ma(c,d,e,g,f),a._a[$c]=h.year,a._dayOfYear=h.dayOfYear}function sa(b){if(b._f===a.ISO_8601)return void _(b);b._a=[],b._pf.empty=!0;var c,d,e,f,g,h=""+b._i,i=h.length,j=0;for(e=J(b._f,b._locale).match(Gc)||[],c=0;c0&&b._pf.unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),j+=d.length),Jc[f]?(d?b._pf.empty=!1:b._pf.unusedTokens.push(f),P(f,d,b)):b._strict&&!d&&b._pf.unusedTokens.push(f);b._pf.charsLeftOver=i-j,h.length>0&&b._pf.unusedInput.push(h),b._pf.bigHour===!0&&b._a[bd]<=12&&(b._pf.bigHour=void 0),b._a[bd]=ta(b._locale,b._a[bd],b._meridiem),qa(b),X(b)}function ta(a,b,c){var d;return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&12>b&&(b+=12),d||12!==b||(b=0),b):b}function ua(a){var b,d,e,f,g;if(0===a._f.length)return a._pf.invalidFormat=!0,void(a._d=new Date(0/0));for(f=0;fg)&&(e=g,d=b));h(a,d||b)}function va(a){if(!a._d){var b=z(a._i);a._a=[b.year,b.month,b.day||b.date,b.hour,b.minute,b.second,b.millisecond],qa(a)}}function wa(a){var b,c=a._i,e=a._f;return a._locale=a._locale||w(a._l),null===c||void 0===e&&""===c?k({nullInput:!0}):("string"==typeof c&&(a._i=c=a._locale.preparse(c)),n(c)?new m(X(c)):(d(e)?ua(a):e?sa(a):xa(a),b=new m(X(a)),b._nextDay&&(b.add(1,"d"),b._nextDay=void 0),b))}function xa(b){var c=b._i;void 0===c?b._d=new Date:e(c)?b._d=new Date(+c):"string"==typeof c?aa(b):d(c)?(b._a=f(c.slice(0),function(a){return parseInt(a,10)}),qa(b)):"object"==typeof c?va(b):"number"==typeof c?b._d=new Date(c):a.createFromInputFallback(b)}function ya(a,b,d,e,f){var g={};return"boolean"==typeof d&&(e=d,d=void 0),g._isAMomentObject=!0,g._useUTC=g._isUTC=f,g._l=d,g._i=a,g._f=b,g._strict=e,g._pf=c(),wa(g)}function za(a,b,c,d){return ya(a,b,c,d,!1)}function Aa(a,b){var c,e;if(1===b.length&&d(b[0])&&(b=b[0]),!b.length)return za();for(c=b[0],e=1;ea&&(a=-a,c="-"),c+E(~~(a/60),2)+b+E(~~a%60,2)})}function Ga(a){var b=(a||"").match(Vc)||[],c=b[b.length-1]||[],d=(c+"").match(qd)||["-",0,0],e=+(60*d[1])+o(d[2]);return"+"===d[0]?e:-e}function Ha(b,c){var d,f;return c._isUTC?(d=c.clone(),f=(n(b)||e(b)?+b:+za(b))-+d,d._d.setTime(+d._d+f),a.updateOffset(d,!1),d):za(b).local();return c._isUTC?za(b).zone(c._offset||0):za(b).local()}function Ia(a){return 15*-Math.round(a._d.getTimezoneOffset()/15)}function Ja(b,c){var d,e=this._offset||0;return null!=b?("string"==typeof b&&(b=Ga(b)),Math.abs(b)<16&&(b=60*b),!this._isUTC&&c&&(d=Ia(this)),this._offset=b,this._isUTC=!0,null!=d&&this.add(d,"m"),e!==b&&(!c||this._changeInProgress?Za(this,Ua(b-e,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,a.updateOffset(this,!0),this._changeInProgress=null)),this):this._isUTC?e:Ia(this)}function Ka(a,b){return null!=a?("string"!=typeof a&&(a=-a),this.utcOffset(a,b),this):-this.utcOffset()}function La(a){return this.utcOffset(0,a)}function Ma(a){return this._isUTC&&(this.utcOffset(0,a),this._isUTC=!1,a&&this.subtract(Ia(this),"m")),this}function Na(){return this._tzm?this.utcOffset(this._tzm):"string"==typeof this._i&&this.utcOffset(Ga(this._i)),this}function Oa(a){return a=a?za(a).utcOffset():0,(this.utcOffset()-a)%60===0}function Pa(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Qa(){if(this._a){var a=this._isUTC?i(this._a):za(this._a);return this.isValid()&&p(this._a,a.toArray())>0}return!1}function Ra(){return!this._isUTC}function Sa(){return this._isUTC}function Ta(){return this._isUTC&&0===this._offset}function Ua(a,b){var c,d,e,f=a,h=null;return Ea(a)?f={ms:a._milliseconds,d:a._days,M:a._months}:"number"==typeof a?(f={},b?f[b]=a:f.milliseconds=a):(h=rd.exec(a))?(c="-"===h[1]?-1:1,f={y:0,d:o(h[ad])*c,h:o(h[bd])*c,m:o(h[cd])*c,s:o(h[dd])*c,ms:o(h[ed])*c}):(h=sd.exec(a))?(c="-"===h[1]?-1:1,f={y:Va(h[2],c),M:Va(h[3],c),d:Va(h[4],c),h:Va(h[5],c),m:Va(h[6],c),s:Va(h[7],c),w:Va(h[8],c)}):null==f?f={}:"object"==typeof f&&("from"in f||"to"in f)&&(e=Xa(za(f.from),za(f.to)),f={},f.ms=e.milliseconds,f.M=e.months),d=new Da(f),Ea(a)&&g(a,"_locale")&&(d._locale=a._locale),d}function Va(a,b){var c=a&&parseFloat(a.replace(",","."));return(isNaN(c)?0:c)*b}function Wa(a,b){var c={milliseconds:0,months:0};return c.months=b.month()-a.month()+12*(b.year()-a.year()),a.clone().add(c.months,"M").isAfter(b)&&--c.months,c.milliseconds=+b-+a.clone().add(c.months,"M"),c}function Xa(a,b){var c;return b=Ha(b,a),a.isBefore(b)?c=Wa(a,b):(c=Wa(b,a),c.milliseconds=-c.milliseconds,c.months=-c.months),c}function Ya(a,b){return function(c,d){var e,f;return null===d||isNaN(+d)||($(b,"moment()."+b+"(period, number) is deprecated. Please use moment()."+b+"(number, period)."),f=c,c=d,d=f),c="string"==typeof c?+c:c,e=Ua(c,d),Za(this,e,a),this}}function Za(b,c,d,e){var f=c._milliseconds,g=c._days,h=c._months;e=null==e?!0:e,f&&b._d.setTime(+b._d+f*d),g&&C(b,"Date",B(b,"Date")+g*d),h&&U(b,B(b,"Month")+h*d),e&&a.updateOffset(b,g||h)}function $a(a){var b=a||za(),c=Ha(b,this).startOf("day"),d=this.diff(c,"days",!0),e=-6>d?"sameElse":-1>d?"lastWeek":0>d?"lastDay":1>d?"sameDay":2>d?"nextDay":7>d?"nextWeek":"sameElse";return this.format(this.localeData().calendar(e,this,za(b)))}function _a(){return new m(this)}function ab(a,b){var c;return b=y("undefined"!=typeof b?b:"millisecond"),"millisecond"===b?(a=n(a)?a:za(a),+this>+a):(c=n(a)?+a:+za(a),c<+this.clone().startOf(b))}function bb(a,b){var c;return b=y("undefined"!=typeof b?b:"millisecond"),"millisecond"===b?(a=n(a)?a:za(a),+a>+this):(c=n(a)?+a:+za(a),+this.clone().endOf(b)a?Math.ceil(a):Math.floor(a)}function fb(a,b,c){var d,e,f=Ha(a,this),g=6e4*(f.utcOffset()-this.utcOffset());return b=y(b),"year"===b||"month"===b||"quarter"===b?(e=gb(this,f),"quarter"===b?e/=3:"year"===b&&(e/=12)):(d=this-f,e="second"===b?d/1e3:"minute"===b?d/6e4:"hour"===b?d/36e5:"day"===b?(d-g)/864e5:"week"===b?(d-g)/6048e5:d),c?e:eb(e)}function gb(a,b){var c,d,e=12*(b.year()-a.year())+(b.month()-a.month()),f=a.clone().add(e,"months");return 0>b-f?(c=a.clone().add(e-1,"months"),d=(b-f)/(f-c)):(c=a.clone().add(e+1,"months"),d=(b-f)/(c-f)),-(e+d)}function hb(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")}function ib(){var a=this.clone().utc();return 0b;b++)if(this._weekdaysParse[b]||(c=za([2e3,1]).day(b),d="^"+this.weekdays(c,"")+"|^"+this.weekdaysShort(c,"")+"|^"+this.weekdaysMin(c,""),this._weekdaysParse[b]=new RegExp(d.replace(".",""),"i")),this._weekdaysParse[b].test(a))return b}function Jb(a){var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=Eb(a,this.localeData()),this.add(a-b,"d")):b}function Kb(a){var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")}function Lb(a){return null==a?this.day()||7:this.day(this.day()%7?a:a-7)}function Mb(a,b){F(a,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),b)})}function Nb(a,b){return b._meridiemParse}function Ob(a){return"p"===(a+"").toLowerCase().charAt(0)}function Pb(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"}function Qb(a){F(0,[a,3],0,"millisecond")}function Rb(){return this._isUTC?"UTC":""}function Sb(){return this._isUTC?"Coordinated Universal Time":""}function Tb(a){return za(1e3*a)}function Ub(){return za.apply(null,arguments).parseZone()}function Vb(a,b,c){var d=this._calendar[a];return"function"==typeof d?d.call(b,c):d}function Wb(a){var b=this._longDateFormat[a];return!b&&this._longDateFormat[a.toUpperCase()]&&(b=this._longDateFormat[a.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a]=b),b}function Xb(){return this._invalidDate}function Yb(a){return this._ordinal.replace("%d",a)}function Zb(a){return a}function $b(a,b,c,d){var e=this._relativeTime[c];return"function"==typeof e?e(a,b,c,d):e.replace(/%d/i,a)}function _b(a,b){var c=this._relativeTime[a>0?"future":"past"];return"function"==typeof c?c(b):c.replace(/%s/i,b)}function ac(a){var b,c;for(c in a)b=a[c],"function"==typeof b?this[c]=b:this["_"+c]=b;this._ordinalParseLenient=new RegExp(this._ordinalParse.source+"|"+/\d{1,2}/.source)}function bc(a,b,c,d){var e=w(),f=i().set(d,b);return e[c](f,a)}function cc(a,b,c,d,e){if("number"==typeof a&&(b=a,a=void 0),a=a||"",null!=b)return bc(a,b,c,e);var f,g=[];for(f=0;d>f;f++)g[f]=bc(a,f,c,e);return g}function dc(a,b){return cc(a,b,"months",12,"month")}function ec(a,b){return cc(a,b,"monthsShort",12,"month")}function fc(a,b){return cc(a,b,"weekdays",7,"day")}function gc(a,b){return cc(a,b,"weekdaysShort",7,"day")}function hc(a,b){return cc(a,b,"weekdaysMin",7,"day")}function ic(){var a=this._data;return this._milliseconds=Od(this._milliseconds),this._days=Od(this._days),this._months=Od(this._months),a.milliseconds=Od(a.milliseconds),a.seconds=Od(a.seconds),a.minutes=Od(a.minutes),a.hours=Od(a.hours),a.months=Od(a.months),a.years=Od(a.years),this}function jc(a,b,c,d){var e=Ua(b,c);return a._milliseconds+=d*e._milliseconds,a._days+=d*e._days,a._months+=d*e._months,a._bubble()}function kc(a,b){return jc(this,a,b,1)}function lc(a,b){return jc(this,a,b,-1)}function mc(){var a,b,c,d=this._milliseconds,e=this._days,f=this._months,g=this._data,h=0;return g.milliseconds=d%1e3,a=eb(d/1e3),g.seconds=a%60,b=eb(a/60),g.minutes=b%60,c=eb(b/60),g.hours=c%24,e+=eb(c/24),h=eb(nc(e)),e-=eb(oc(h)),f+=eb(e/30),e%=30,h+=eb(f/12),f%=12,g.days=e,g.months=f,g.years=h,this}function nc(a){return 400*a/146097}function oc(a){return 146097*a/400}function pc(a){var b,c,d=this._milliseconds;if(a=y(a),"month"===a||"year"===a)return b=this._days+d/864e5,c=this._months+12*nc(b),"month"===a?c:c/12;switch(b=this._days+Math.round(oc(this._months/12)),a){case"week":return b/7+d/6048e5;case"day":return b+d/864e5;case"hour":return 24*b+d/36e5;case"minute":return 24*b*60+d/6e4;case"second":return 24*b*60*60+d/1e3;case"millisecond":return Math.floor(24*b*60*60*1e3)+d;default:throw new Error("Unknown unit "+a)}}function qc(){return this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*o(this._months/12)}function rc(a){return function(){return this.as(a)}}function sc(a){return a=y(a),this[a+"s"]()}function tc(a){return function(){return this._data[a]}}function uc(){return eb(this.days()/7)}function vc(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function wc(a,b,c){var d=Ua(a).abs(),e=ce(d.as("s")),f=ce(d.as("m")),g=ce(d.as("h")),h=ce(d.as("d")),i=ce(d.as("M")),j=ce(d.as("y")),k=e0,k[4]=c,vc.apply(null,k)}function xc(a,b){return void 0===de[a]?!1:void 0===b?de[a]:(de[a]=b,!0)}function yc(a){var b=this.localeData(),c=wc(this,!a,b);return a&&(c=b.pastFuture(+this,c)),b.postformat(c)}function zc(){var a=ee(this.years()),b=ee(this.months()),c=ee(this.days()),d=ee(this.hours()),e=ee(this.minutes()),f=ee(this.seconds()+this.milliseconds()/1e3),g=this.asSeconds();return g?(0>g?"-":"")+"P"+(a?a+"Y":"")+(b?b+"M":"")+(c?c+"D":"")+(d||e||f?"T":"")+(d?d+"H":"")+(e?e+"M":"")+(f?f+"S":""):"P0D"}var Ac,Bc,Cc=a.momentProperties=[],Dc=!1,Ec={},Fc={},Gc=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|x|X|zz?|ZZ?|.)/g,Hc=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,Ic={},Jc={},Kc=/\d/,Lc=/\d\d/,Mc=/\d{3}/,Nc=/\d{4}/,Oc=/[+-]?\d{6}/,Pc=/\d\d?/,Qc=/\d{1,3}/,Rc=/\d{1,4}/,Sc=/[+-]?\d{1,6}/,Tc=/\d+/,Uc=/[+-]?\d+/,Vc=/Z|[+-]\d\d:?\d\d/gi,Wc=/[+-]?\d+(\.\d{1,3})?/,Xc=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Yc={},Zc={},$c=0,_c=1,ad=2,bd=3,cd=4,dd=5,ed=6;F("M",["MM",2],"Mo",function(){return this.month()+1}),F("MMM",0,0,function(a){return this.localeData().monthsShort(this,a)}),F("MMMM",0,0,function(a){return this.localeData().months(this,a)}),x("month","M"),K("M",Pc),K("MM",Pc,Lc),K("MMM",Xc),K("MMMM",Xc),N(["M","MM"],function(a,b){b[_c]=o(a)-1}),N(["MMM","MMMM"],function(a,b,c,d){var e=c._locale.monthsParse(a,d,c._strict);null!=e?b[_c]=e:c._pf.invalidMonth=a});var fd="January_February_March_April_May_June_July_August_September_October_November_December".split("_"),gd="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),hd={};a.suppressDeprecationWarnings=!1;var id=/^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,jd=[["YYYYYY-MM-DD",/[+-]\d{6}-\d{2}-\d{2}/],["YYYY-MM-DD",/\d{4}-\d{2}-\d{2}/],["GGGG-[W]WW-E",/\d{4}-W\d{2}-\d/],["GGGG-[W]WW",/\d{4}-W\d{2}/],["YYYY-DDD",/\d{4}-\d{3}/]],kd=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],ld=/^\/?Date\((\-?\d+)/i;a.createFromInputFallback=Z("moment construction falls back to js Date. This is discouraged and will be removed in upcoming major release. Please refer to https://github.com/moment/moment/issues/1407 for more info.",function(a){a._d=new Date(a._i+(a._useUTC?" UTC":""))}),F(0,["YY",2],0,function(){return this.year()%100}),F(0,["YYYY",4],0,"year"),F(0,["YYYYY",5],0,"year"),F(0,["YYYYYY",6,!0],0,"year"),x("year","y"),K("Y",Uc),K("YY",Pc,Lc),K("YYYY",Rc,Nc),K("YYYYY",Sc,Oc),K("YYYYYY",Sc,Oc),N(["YYYY","YYYYY","YYYYYY"],$c),N("YY",function(b,c){c[$c]=a.parseTwoDigitYear(b)}),a.parseTwoDigitYear=function(a){return o(a)+(o(a)>68?1900:2e3)};var md=A("FullYear",!1);F("w",["ww",2],"wo","week"),F("W",["WW",2],"Wo","isoWeek"),x("week","w"),x("isoWeek","W"),K("w",Pc),K("ww",Pc,Lc),K("W",Pc),K("WW",Pc,Lc),O(["w","ww","W","WW"],function(a,b,c,d){b[d.substr(0,1)]=o(a)});var nd={dow:0,doy:6};F("DDD",["DDDD",3],"DDDo","dayOfYear"),x("dayOfYear","DDD"),K("DDD",Qc),K("DDDD",Mc),N(["DDD","DDDD"],function(a,b,c){c._dayOfYear=o(a)}),a.ISO_8601=function(){};var od=Z("moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548",function(){var a=za.apply(null,arguments);return this>a?this:a}),pd=Z("moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548",function(){var a=za.apply(null,arguments);return a>this?this:a});Fa("Z",":"),Fa("ZZ",""),K("Z",Vc),K("ZZ",Vc),N(["Z","ZZ"],function(a,b,c){c._useUTC=!0,c._tzm=Ga(a)});var qd=/([\+\-]|\d\d)/gi;a.updateOffset=function(){};var rd=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,sd=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/;Ua.fn=Da.prototype;var td=Ya(1,"add"),ud=Ya(-1,"subtract");a.defaultFormat="YYYY-MM-DDTHH:mm:ssZ";var vd=Z("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(a){return void 0===a?this.localeData():this.locale(a)});F(0,["gg",2],0,function(){return this.weekYear()%100}),F(0,["GG",2],0,function(){return this.isoWeekYear()%100}),xb("gggg","weekYear"),xb("ggggg","weekYear"),xb("GGGG","isoWeekYear"),xb("GGGGG","isoWeekYear"),x("weekYear","gg"),x("isoWeekYear","GG"),K("G",Uc),K("g",Uc),K("GG",Pc,Lc),K("gg",Pc,Lc),K("GGGG",Rc,Nc),K("gggg",Rc,Nc),K("GGGGG",Sc,Oc),K("ggggg",Sc,Oc),O(["gggg","ggggg","GGGG","GGGGG"],function(a,b,c,d){b[d.substr(0,2)]=o(a)}),O(["gg","GG"],function(b,c,d,e){c[e]=a.parseTwoDigitYear(b)}),F("Q",0,0,"quarter"),x("quarter","Q"),K("Q",Kc),N("Q",function(a,b){b[_c]=3*(o(a)-1)}),F("D",["DD",2],"Do","date"),x("date","D"),K("D",Pc),K("DD",Pc,Lc),K("Do",function(a,b){return a?b._ordinalParse:b._ordinalParseLenient}),N(["D","DD"],ad),N("Do",function(a,b){b[ad]=o(a.match(Pc)[0],10)});var wd=A("Date",!0);F("d",0,"do","day"),F("dd",0,0,function(a){return this.localeData().weekdaysMin(this,a)}),F("ddd",0,0,function(a){return this.localeData().weekdaysShort(this,a)}),F("dddd",0,0,function(a){return this.localeData().weekdays(this,a)}),F("e",0,0,"weekday"),F("E",0,0,"isoWeekday"),x("day","d"),x("weekday","e"),x("isoWeekday","E"),K("d",Pc),K("e",Pc),K("E",Pc),K("dd",Xc),K("ddd",Xc),K("dddd",Xc),O(["dd","ddd","dddd"],function(a,b,c){var d=c._locale.weekdaysParse(a);null!=d?b.d=d:c._pf.invalidWeekday=a}),O(["d","e","E"],function(a,b,c,d){b[d]=o(a)});var xd="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),yd="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),zd="Su_Mo_Tu_We_Th_Fr_Sa".split("_");F("H",["HH",2],0,"hour"),F("h",["hh",2],0,function(){return this.hours()%12||12}),Mb("a",!0),Mb("A",!1),x("hour","h"),K("a",Nb),K("A",Nb),K("H",Pc),K("h",Pc),K("HH",Pc,Lc),K("hh",Pc,Lc),N(["H","HH"],bd),N(["a","A"],function(a,b,c){c._isPm=c._locale.isPM(a),c._meridiem=a}),N(["h","hh"],function(a,b,c){b[bd]=o(a),c._pf.bigHour=!0});var Ad=/[ap]\.?m?\.?/i,Bd=A("Hours",!0);F("m",["mm",2],0,"minute"),x("minute","m"),K("m",Pc),K("mm",Pc,Lc),N(["m","mm"],cd);var Cd=A("Minutes",!1);F("s",["ss",2],0,"second"),x("second","s"),K("s",Pc),K("ss",Pc,Lc),N(["s","ss"],dd);var Dd=A("Seconds",!1);F("S",0,0,function(){return~~(this.millisecond()/100)}),F(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),Qb("SSS"),Qb("SSSS"),x("millisecond","ms"),K("S",Qc,Kc),K("SS",Qc,Lc),K("SSS",Qc,Mc),K("SSSS",Tc),N(["S","SS","SSS","SSSS"],function(a,b){b[ed]=o(1e3*("0."+a))});var Ed=A("Milliseconds",!1);F("z",0,0,"zoneAbbr"),F("zz",0,0,"zoneName");var Fd=m.prototype;Fd.add=td,Fd.calendar=$a,Fd.clone=_a,Fd.diff=fb,Fd.endOf=pb,Fd.format=jb,Fd.from=kb,Fd.fromNow=lb,Fd.get=D,Fd.invalidAt=wb,Fd.isAfter=ab,Fd.isBefore=bb,Fd.isBetween=cb,Fd.isSame=db,Fd.isValid=ub,Fd.lang=vd,Fd.locale=mb,Fd.localeData=nb,Fd.max=pd,Fd.min=od,Fd.parsingFlags=vb,Fd.set=D,Fd.startOf=ob,Fd.subtract=ud,Fd.toArray=tb,Fd.toDate=sb,Fd.toISOString=ib,Fd.toJSON=ib,Fd.toString=hb,Fd.unix=rb,Fd.valueOf=qb,Fd.year=md,Fd.isLeapYear=fa,Fd.weekYear=zb,Fd.isoWeekYear=Ab,Fd.quarter=Fd.quarters=Db,Fd.month=V,Fd.daysInMonth=W,Fd.week=Fd.weeks=ka,Fd.isoWeek=Fd.isoWeeks=la,Fd.weeksInYear=Cb,Fd.isoWeeksInYear=Bb,Fd.date=wd,Fd.day=Fd.days=Jb,Fd.weekday=Kb,Fd.isoWeekday=Lb,Fd.dayOfYear=na,Fd.hour=Fd.hours=Bd,Fd.minute=Fd.minutes=Cd,Fd.second=Fd.seconds=Dd,Fd.millisecond=Fd.milliseconds=Ed,Fd.utcOffset=Ja,Fd.utc=La,Fd.local=Ma,Fd.parseZone=Na,Fd.hasAlignedHourOffset=Oa,Fd.isDST=Pa,Fd.isDSTShifted=Qa,Fd.isLocal=Ra,Fd.isUtcOffset=Sa,Fd.isUtc=Ta,Fd.isUTC=Ta,Fd.zoneAbbr=Rb,Fd.zoneName=Sb,Fd.dates=Z("dates accessor is deprecated. Use date instead.",wd),Fd.months=Z("months accessor is deprecated. Use month instead",V),Fd.years=Z("years accessor is deprecated. Use year instead",md),Fd.zone=Z("moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779",Ka);var Gd=Fd,Hd={sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},Id={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY LT",LLLL:"dddd, MMMM D, YYYY LT"},Jd="Invalid date",Kd="%d",Ld=/\d{1,2}/,Md={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},Nd=q.prototype;Nd._calendar=Hd,Nd.calendar=Vb,Nd._longDateFormat=Id,Nd.longDateFormat=Wb,Nd._invalidDate=Jd,Nd.invalidDate=Xb,Nd._ordinal=Kd,Nd.ordinal=Yb,Nd._ordinalParse=Ld, +Nd.preparse=Zb,Nd.postformat=Zb,Nd._relativeTime=Md,Nd.relativeTime=$b,Nd.pastFuture=_b,Nd.set=ac,Nd.months=R,Nd._months=fd,Nd.monthsShort=S,Nd._monthsShort=gd,Nd.monthsParse=T,Nd.week=ha,Nd._week=nd,Nd.firstDayOfYear=ja,Nd.firstDayOfWeek=ia,Nd.weekdays=Fb,Nd._weekdays=xd,Nd.weekdaysMin=Hb,Nd._weekdaysMin=zd,Nd.weekdaysShort=Gb,Nd._weekdaysShort=yd,Nd.weekdaysParse=Ib,Nd.isPM=Ob,Nd._meridiemParse=Ad,Nd.meridiem=Pb,u("en",{ordinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(a){var b=a%10,c=1===o(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),a.lang=Z("moment.lang is deprecated. Use moment.locale instead.",u),a.langData=Z("moment.langData is deprecated. Use moment.localeData instead.",w);var Od=Math.abs,Pd=rc("ms"),Qd=rc("s"),Rd=rc("m"),Sd=rc("h"),Td=rc("d"),Ud=rc("w"),Vd=rc("M"),Wd=rc("y"),Xd=tc("milliseconds"),Yd=tc("seconds"),Zd=tc("minutes"),$d=tc("hours"),_d=tc("days"),ae=tc("months"),be=tc("years"),ce=Math.round,de={s:45,m:45,h:22,d:26,M:11},ee=Math.abs,fe=Da.prototype;fe.abs=ic,fe.add=kc,fe.subtract=lc,fe.as=pc,fe.asMilliseconds=Pd,fe.asSeconds=Qd,fe.asMinutes=Rd,fe.asHours=Sd,fe.asDays=Td,fe.asWeeks=Ud,fe.asMonths=Vd,fe.asYears=Wd,fe.valueOf=qc,fe._bubble=mc,fe.get=sc,fe.milliseconds=Xd,fe.seconds=Yd,fe.minutes=Zd,fe.hours=$d,fe.days=_d,fe.weeks=uc,fe.months=ae,fe.years=be,fe.humanize=yc,fe.toISOString=zc,fe.toString=zc,fe.toJSON=zc,fe.locale=mb,fe.localeData=nb,fe.toIsoString=Z("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",zc),fe.lang=vd,F("X",0,0,"unix"),F("x",0,0,"valueOf"),K("x",Uc),K("X",Wc),N("X",function(a,b,c){c._d=new Date(1e3*parseFloat(a,10))}),N("x",function(a,b,c){c._d=new Date(o(a))}),a.version="2.10.2",b(za),a.fn=Gd,a.min=Ba,a.max=Ca,a.utc=i,a.unix=Tb,a.months=dc,a.isDate=e,a.locale=u,a.invalid=k,a.duration=Ua,a.isMoment=n,a.weekdays=fc,a.parseZone=Ub,a.localeData=w,a.isDuration=Ea,a.monthsShort=ec,a.weekdaysMin=hc,a.defineLocale=v,a.weekdaysShort=gc,a.normalizeUnits=y,a.relativeTimeThreshold=xc;var ge=a;return ge}); \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/js/rickshaw.min.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/js/rickshaw.min.js new file mode 100644 index 000000000..be060e0b8 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/public/js/rickshaw.min.js @@ -0,0 +1,21 @@ +/* + * 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. + */ + +(function(root,factory){if(typeof define==="function"&&define.amd){define(["d3.min"],function(d3){return root.Rickshaw=factory(d3)})}else if(typeof exports==="object"){module.exports=factory(require("d3"))}else{root.Rickshaw=factory(d3)}})(this,function(d3){var Rickshaw={namespace:function(namespace,obj){var parts=namespace.split(".");var parent=Rickshaw;for(var i=1,length=parts.length;i0){var x=s.data[0].x;var y=s.data[0].y;if(typeof x!="number"||typeof y!="number"&&y!==null){throw"x and y properties of points should be numbers instead of "+typeof x+" and "+typeof y}}if(s.data.length>=3){if(s.data[2].xthis.window.xMax)isInRange=false;return isInRange}return true};this.onUpdate=function(callback){this.updateCallbacks.push(callback)};this.onConfigure=function(callback){this.configureCallbacks.push(callback)};this.registerRenderer=function(renderer){this._renderers=this._renderers||{};this._renderers[renderer.name]=renderer};this.configure=function(args){this.config=this.config||{};if(args.width||args.height){this.setSize(args)}Rickshaw.keys(this.defaults).forEach(function(k){this.config[k]=k in args?args[k]:k in this?this[k]:this.defaults[k]},this);Rickshaw.keys(this.config).forEach(function(k){this[k]=this.config[k]},this);if("stack"in args)args.unstack=!args.stack;var renderer=args.renderer||this.renderer&&this.renderer.name||"stack";this.setRenderer(renderer,args);this.configureCallbacks.forEach(function(callback){callback(args)})};this.setRenderer=function(r,args){if(typeof r=="function"){this.renderer=new r({graph:self});this.registerRenderer(this.renderer)}else{if(!this._renderers[r]){throw"couldn't find renderer "+r}this.renderer=this._renderers[r]}if(typeof args=="object"){this.renderer.configure(args)}};this.setSize=function(args){args=args||{};if(typeof window!==undefined){var style=window.getComputedStyle(this.element,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);var elementHeight=parseInt(style.getPropertyValue("height"),10)}this.width=args.width||elementWidth||400;this.height=args.height||elementHeight||250;this.vis&&this.vis.attr("width",this.width).attr("height",this.height)};this.initialize(args)};Rickshaw.namespace("Rickshaw.Fixtures.Color");Rickshaw.Fixtures.Color=function(){this.schemes={};this.schemes.spectrum14=["#ecb796","#dc8f70","#b2a470","#92875a","#716c49","#d2ed82","#bbe468","#a1d05d","#e7cbe6","#d8aad6","#a888c2","#9dc2d3","#649eb9","#387aa3"].reverse();this.schemes.spectrum2000=["#57306f","#514c76","#646583","#738394","#6b9c7d","#84b665","#a7ca50","#bfe746","#e2f528","#fff726","#ecdd00","#d4b11d","#de8800","#de4800","#c91515","#9a0000","#7b0429","#580839","#31082b"];this.schemes.spectrum2001=["#2f243f","#3c2c55","#4a3768","#565270","#6b6b7c","#72957f","#86ad6e","#a1bc5e","#b8d954","#d3e04e","#ccad2a","#cc8412","#c1521d","#ad3821","#8a1010","#681717","#531e1e","#3d1818","#320a1b"];this.schemes.classic9=["#423d4f","#4a6860","#848f39","#a2b73c","#ddcb53","#c5a32f","#7d5836","#963b20","#7c2626","#491d37","#2f254a"].reverse();this.schemes.httpStatus={503:"#ea5029",502:"#d23f14",500:"#bf3613",410:"#efacea",409:"#e291dc",403:"#f457e8",408:"#e121d2",401:"#b92dae",405:"#f47ceb",404:"#a82a9f",400:"#b263c6",301:"#6fa024",302:"#87c32b",307:"#a0d84c",304:"#28b55c",200:"#1a4f74",206:"#27839f",201:"#52adc9",202:"#7c979f",203:"#a5b8bd",204:"#c1cdd1"};this.schemes.colorwheel=["#b5b6a9","#858772","#785f43","#96557e","#4682b4","#65b9ac","#73c03a","#cb513a"].reverse();this.schemes.cool=["#5e9d2f","#73c03a","#4682b4","#7bc3b8","#a9884e","#c1b266","#a47493","#c09fb5"];this.schemes.munin=["#00cc00","#0066b3","#ff8000","#ffcc00","#330099","#990099","#ccff00","#ff0000","#808080","#008f00","#00487d","#b35a00","#b38f00","#6b006b","#8fb300","#b30000","#bebebe","#80ff80","#80c9ff","#ffc080","#ffe680","#aa80ff","#ee00cc","#ff8080","#666600","#ffbfff","#00ffcc","#cc6699","#999900"]};Rickshaw.namespace("Rickshaw.Fixtures.RandomData");Rickshaw.Fixtures.RandomData=function(timeInterval){var addData;timeInterval=timeInterval||1;var lastRandomValue=200;var timeBase=Math.floor((new Date).getTime()/1e3);this.addData=function(data){var randomValue=Math.random()*100+15+lastRandomValue;var index=data[0].length;var counter=1;data.forEach(function(series){var randomVariance=Math.random()*20;var v=randomValue/25+counter++ +(Math.cos(index*counter*11/960)+2)*15+(Math.cos(index/7)+2)*7+(Math.cos(index/17)+2)*1;series.push({x:index*timeInterval+timeBase,y:v+randomVariance})});lastRandomValue=randomValue*.85};this.removeData=function(data){data.forEach(function(series){series.shift()});timeBase+=timeInterval}};Rickshaw.namespace("Rickshaw.Fixtures.Time");Rickshaw.Fixtures.Time=function(){var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getUTCFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getUTCFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getUTCMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getUTCDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getUTCMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getUTCSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getUTCSeconds()+"s"}},{name:"decisecond",seconds:1/10,formatter:function(d){return d.getUTCMilliseconds()+"ms"}},{name:"centisecond",seconds:1/100,formatter:function(d){return d.getUTCMilliseconds()+"ms"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toUTCString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var date,floor,year;if(unit.name=="month"){date=new Date(time*1e3);floor=Date.UTC(date.getUTCFullYear(),date.getUTCMonth())/1e3;if(floor==time)return time;year=date.getUTCFullYear();var month=date.getUTCMonth();if(month==11){month=0;year=year+1}else{month+=1}return Date.UTC(year,month)/1e3}if(unit.name=="year"){date=new Date(time*1e3);floor=Date.UTC(date.getUTCFullYear(),0)/1e3;if(floor==time)return time;year=date.getUTCFullYear()+1;return Date.UTC(year,0)/1e3}return Math.ceil(time/unit.seconds)*unit.seconds}};Rickshaw.namespace("Rickshaw.Fixtures.Time.Local");Rickshaw.Fixtures.Time.Local=function(){var self=this;this.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];this.units=[{name:"decade",seconds:86400*365.25*10,formatter:function(d){return parseInt(d.getFullYear()/10,10)*10}},{name:"year",seconds:86400*365.25,formatter:function(d){return d.getFullYear()}},{name:"month",seconds:86400*30.5,formatter:function(d){return self.months[d.getMonth()]}},{name:"week",seconds:86400*7,formatter:function(d){return self.formatDate(d)}},{name:"day",seconds:86400,formatter:function(d){return d.getDate()}},{name:"6 hour",seconds:3600*6,formatter:function(d){return self.formatTime(d)}},{name:"hour",seconds:3600,formatter:function(d){return self.formatTime(d)}},{name:"15 minute",seconds:60*15,formatter:function(d){return self.formatTime(d)}},{name:"minute",seconds:60,formatter:function(d){return d.getMinutes()}},{name:"15 second",seconds:15,formatter:function(d){return d.getSeconds()+"s"}},{name:"second",seconds:1,formatter:function(d){return d.getSeconds()+"s"}},{name:"decisecond",seconds:1/10,formatter:function(d){return d.getMilliseconds()+"ms"}},{name:"centisecond",seconds:1/100,formatter:function(d){return d.getMilliseconds()+"ms"}}];this.unit=function(unitName){return this.units.filter(function(unit){return unitName==unit.name}).shift()};this.formatDate=function(d){return d3.time.format("%b %e")(d)};this.formatTime=function(d){return d.toString().match(/(\d+:\d+):/)[1]};this.ceil=function(time,unit){var date,floor,year;if(unit.name=="day"){var nearFuture=new Date((time+unit.seconds-1)*1e3);var rounded=new Date(0);rounded.setMilliseconds(0);rounded.setSeconds(0);rounded.setMinutes(0);rounded.setHours(0);rounded.setDate(nearFuture.getDate());rounded.setMonth(nearFuture.getMonth());rounded.setFullYear(nearFuture.getFullYear());return rounded.getTime()/1e3}if(unit.name=="month"){date=new Date(time*1e3);floor=new Date(date.getFullYear(),date.getMonth()).getTime()/1e3;if(floor==time)return time;year=date.getFullYear();var month=date.getMonth();if(month==11){month=0;year=year+1}else{month+=1}return new Date(year,month).getTime()/1e3}if(unit.name=="year"){date=new Date(time*1e3);floor=new Date(date.getUTCFullYear(),0).getTime()/1e3;if(floor==time)return time;year=date.getFullYear()+1;return new Date(year,0).getTime()/1e3}return Math.ceil(time/unit.seconds)*unit.seconds}};Rickshaw.namespace("Rickshaw.Fixtures.Number");Rickshaw.Fixtures.Number.formatKMBT=function(y){var abs_y=Math.abs(y);if(abs_y>=1e12){return y/1e12+"T"}else if(abs_y>=1e9){return y/1e9+"B"}else if(abs_y>=1e6){return y/1e6+"M"}else if(abs_y>=1e3){return y/1e3+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.Fixtures.Number.formatBase1024KMGTP=function(y){var abs_y=Math.abs(y);if(abs_y>=0x4000000000000){return y/0x4000000000000+"P"}else if(abs_y>=1099511627776){return y/1099511627776+"T"}else if(abs_y>=1073741824){return y/1073741824+"G"}else if(abs_y>=1048576){return y/1048576+"M"}else if(abs_y>=1024){return y/1024+"K"}else if(abs_y<1&&y>0){return y.toFixed(2)}else if(abs_y===0){return""}else{return y}};Rickshaw.namespace("Rickshaw.Color.Palette");Rickshaw.Color.Palette=function(args){var color=new Rickshaw.Fixtures.Color;args=args||{};this.schemes={};this.scheme=color.schemes[args.scheme]||args.scheme||color.schemes.colorwheel;this.runningIndex=0;this.generatorIndex=0;if(args.interpolatedStopCount){var schemeCount=this.scheme.length-1;var i,j,scheme=[];for(i=0;iself.graph.x.range()[1]){if(annotation.element){annotation.line.classList.add("offscreen");annotation.element.style.display="none"}annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.add("offscreen")});return}if(!annotation.element){var element=annotation.element=document.createElement("div");element.classList.add("annotation");this.elements.timeline.appendChild(element);element.addEventListener("click",function(e){element.classList.toggle("active");annotation.line.classList.toggle("active");annotation.boxes.forEach(function(box){if(box.rangeElement)box.rangeElement.classList.toggle("active")})},false)}annotation.element.style.left=left+"px";annotation.element.style.display="block";annotation.boxes.forEach(function(box){var element=box.element;if(!element){element=box.element=document.createElement("div");element.classList.add("content");element.innerHTML=box.content;annotation.element.appendChild(element);annotation.line=document.createElement("div");annotation.line.classList.add("annotation_line");self.graph.element.appendChild(annotation.line);if(box.end){box.rangeElement=document.createElement("div");box.rangeElement.classList.add("annotation_range");self.graph.element.appendChild(box.rangeElement)}}if(box.end){var annotationRangeStart=left;var annotationRangeEnd=Math.min(self.graph.x(box.end),self.graph.x.range()[1]);if(annotationRangeStart>annotationRangeEnd){annotationRangeEnd=left;annotationRangeStart=Math.max(self.graph.x(box.end),self.graph.x.range()[0])}var annotationRangeWidth=annotationRangeEnd-annotationRangeStart;box.rangeElement.style.left=annotationRangeStart+"px";box.rangeElement.style.width=annotationRangeWidth+"px";box.rangeElement.classList.remove("offscreen")}annotation.line.classList.remove("offscreen");annotation.line.style.left=left+"px"})},this)};this.graph.onUpdate(function(){self.update()})};Rickshaw.namespace("Rickshaw.Graph.Axis.Time");Rickshaw.Graph.Axis.Time=function(args){var self=this;this.graph=args.graph;this.elements=[];this.ticksTreatment=args.ticksTreatment||"plain";this.fixedTimeUnit=args.timeUnit;var time=args.timeFixture||new Rickshaw.Fixtures.Time;this.appropriateTimeUnit=function(){var unit;var units=time.units;var domain=this.graph.x.domain();var rangeSeconds=domain[1]-domain[0];units.forEach(function(u){if(Math.floor(rangeSeconds/u.seconds)>=2){unit=unit||u}});return unit||time.units[time.units.length-1]};this.tickOffsets=function(){var domain=this.graph.x.domain();var unit=this.fixedTimeUnit||this.appropriateTimeUnit();var count=Math.ceil((domain[1]-domain[0])/unit.seconds);var runningTick=domain[0];var offsets=[];for(var i=0;iself.graph.x.range()[1])return;var element=document.createElement("div");element.style.left=self.graph.x(o.value)+"px";element.classList.add("x_tick");element.classList.add(self.ticksTreatment);var title=document.createElement("div");title.classList.add("title");title.innerHTML=o.unit.formatter(new Date(o.value*1e3));element.appendChild(title);self.graph.element.appendChild(element);self.elements.push(element)})};this.graph.onUpdate(function(){self.render()})};Rickshaw.namespace("Rickshaw.Graph.Axis.X");Rickshaw.Graph.Axis.X=function(args){var self=this;var berthRate=.1;this.initialize=function(args){this.graph=args.graph;this.orientation=args.orientation||"top";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";if(args.element){this.element=args.element;this._discoverSize(args.element,args);this.vis=d3.select(args.element).append("svg:svg").attr("height",this.height).attr("width",this.width).attr("class","rickshaw_graph x_axis_d3");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}this.graph.onUpdate(function(){self.render()})};this.setSize=function(args){args=args||{};if(!this.element)return;this._discoverSize(this.element.parentNode,args);this.vis.attr("height",this.height).attr("width",this.width*(1+berthRate));var berth=Math.floor(this.width*berthRate/2);this.element.style.left=-1*berth+"px"};this.render=function(){if(this._renderWidth!==undefined&&this.graph.width!==this._renderWidth)this.setSize({auto:true});var axis=d3.svg.axis().scale(this.graph.x).orient(this.orientation);axis.tickFormat(args.tickFormat||function(x){return x});if(this.tickValues)axis.tickValues(this.tickValues);this.ticks=this.staticTicks||Math.floor(this.graph.width/this.pixelsPerTick);var berth=Math.floor(this.width*berthRate/2)||0;var transform;if(this.orientation=="top"){var yOffset=this.height||this.graph.height;transform="translate("+berth+","+yOffset+")"}else{transform="translate("+berth+", 0)"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["x_ticks_d3",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));var gridSize=(this.orientation=="bottom"?1:-1)*this.graph.height;this.graph.vis.append("svg:g").attr("class","x_grid_d3").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize)).selectAll("text").each(function(){this.parentNode.setAttribute("data-x-value",this.textContent)});this._renderHeight=this.graph.height};this._discoverSize=function(element,args){if(typeof window!=="undefined"){var style=window.getComputedStyle(element,null);var elementHeight=parseInt(style.getPropertyValue("height"),10);if(!args.auto){var elementWidth=parseInt(style.getPropertyValue("width"),10)}}this.width=(args.width||elementWidth||this.graph.width)*(1+berthRate);this.height=args.height||elementHeight||40};this.initialize(args)};Rickshaw.namespace("Rickshaw.Graph.Axis.Y");Rickshaw.Graph.Axis.Y=Rickshaw.Class.create({initialize:function(args){this.graph=args.graph;this.orientation=args.orientation||"right";this.pixelsPerTick=args.pixelsPerTick||75;if(args.ticks)this.staticTicks=args.ticks;if(args.tickValues)this.tickValues=args.tickValues;this.tickSize=args.tickSize||4;this.ticksTreatment=args.ticksTreatment||"plain";this.tickFormat=args.tickFormat||function(y){return y};this.berthRate=.1;if(args.element){this.element=args.element;this.vis=d3.select(args.element).append("svg:svg").attr("class","rickshaw_graph y_axis");this.element=this.vis[0][0];this.element.style.position="relative";this.setSize({width:args.width,height:args.height})}else{this.vis=this.graph.vis}var self=this;this.graph.onUpdate(function(){self.render()})},setSize:function(args){args=args||{};if(!this.element)return;if(typeof window!=="undefined"){var style=window.getComputedStyle(this.element.parentNode,null);var elementWidth=parseInt(style.getPropertyValue("width"),10);if(!args.auto){var elementHeight=parseInt(style.getPropertyValue("height"),10)}}this.width=args.width||elementWidth||this.graph.width*this.berthRate;this.height=args.height||elementHeight||this.graph.height;this.vis.attr("width",this.width).attr("height",this.height*(1+this.berthRate));var berth=this.height*this.berthRate;if(this.orientation=="left"){this.element.style.top=-1*berth+"px"}},render:function(){if(this._renderHeight!==undefined&&this.graph.height!==this._renderHeight)this.setSize({auto:true});this.ticks=this.staticTicks||Math.floor(this.graph.height/this.pixelsPerTick);var axis=this._drawAxis(this.graph.y);this._drawGrid(axis);this._renderHeight=this.graph.height},_drawAxis:function(scale){var axis=d3.svg.axis().scale(scale).orient(this.orientation);axis.tickFormat(this.tickFormat);if(this.tickValues)axis.tickValues(this.tickValues);if(this.orientation=="left"){var berth=this.height*this.berthRate;var transform="translate("+this.width+", "+berth+")"}if(this.element){this.vis.selectAll("*").remove()}this.vis.append("svg:g").attr("class",["y_ticks",this.ticksTreatment].join(" ")).attr("transform",transform).call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(this.tickSize));return axis},_drawGrid:function(axis){var gridSize=(this.orientation=="right"?1:-1)*this.graph.width;this.graph.vis.append("svg:g").attr("class","y_grid").call(axis.ticks(this.ticks).tickSubdivide(0).tickSize(gridSize)).selectAll("text").each(function(){this.parentNode.setAttribute("data-y-value",this.textContent) +})}});Rickshaw.namespace("Rickshaw.Graph.Axis.Y.Scaled");Rickshaw.Graph.Axis.Y.Scaled=Rickshaw.Class.create(Rickshaw.Graph.Axis.Y,{initialize:function($super,args){if(typeof args.scale==="undefined"){throw new Error("Scaled requires scale")}this.scale=args.scale;if(typeof args.grid==="undefined"){this.grid=true}else{this.grid=args.grid}$super(args)},_drawAxis:function($super,scale){var domain=this.scale.domain();var renderDomain=this.graph.renderer.domain().y;var extents=[Math.min.apply(Math,domain),Math.max.apply(Math,domain)];var extentMap=d3.scale.linear().domain([0,1]).range(extents);var adjExtents=[extentMap(renderDomain[0]),extentMap(renderDomain[1])];var adjustment=d3.scale.linear().domain(extents).range(adjExtents);var adjustedScale=this.scale.copy().domain(domain.map(adjustment)).range(scale.range());return $super(adjustedScale)},_drawGrid:function($super,axis){if(this.grid){$super(axis)}}});Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Highlight");Rickshaw.Graph.Behavior.Series.Highlight=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;var colorSafe={};var activeLine=null;var disabledColor=args.disabledColor||function(seriesColor){return d3.interpolateRgb(seriesColor,d3.rgb("#d8d8d8"))(.8).toString()};this.addHighlightEvents=function(l){l.element.addEventListener("mouseover",function(e){if(activeLine)return;else activeLine=l;self.legend.lines.forEach(function(line){if(l===line){if(self.graph.renderer.unstack&&(line.series.renderer?line.series.renderer.unstack:true)){var seriesIndex=self.graph.series.indexOf(line.series);line.originalIndex=seriesIndex;var series=self.graph.series.splice(seriesIndex,1)[0];self.graph.series.push(series)}return}colorSafe[line.series.name]=colorSafe[line.series.name]||line.series.color;line.series.color=disabledColor(line.series.color)});self.graph.update()},false);l.element.addEventListener("mouseout",function(e){if(!activeLine)return;else activeLine=null;self.legend.lines.forEach(function(line){if(l===line&&line.hasOwnProperty("originalIndex")){var series=self.graph.series.pop();self.graph.series.splice(line.originalIndex,0,series);delete line.originalIndex}if(colorSafe[line.series.name]){line.series.color=colorSafe[line.series.name]}});self.graph.update()},false)};if(this.legend){this.legend.lines.forEach(function(l){self.addHighlightEvents(l)})}};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Order");Rickshaw.Graph.Behavior.Series.Order=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;if(typeof window.jQuery=="undefined"){throw"couldn't find jQuery at window.jQuery"}if(typeof window.jQuery.ui=="undefined"){throw"couldn't find jQuery UI at window.jQuery.ui"}jQuery(function(){jQuery(self.legend.list).sortable({containment:"parent",tolerance:"pointer",update:function(event,ui){var series=[];jQuery(self.legend.list).find("li").each(function(index,item){if(!item.series)return;series.push(item.series)});for(var i=self.graph.series.length-1;i>=0;i--){self.graph.series[i]=series.shift()}self.graph.update()}});jQuery(self.legend.list).disableSelection()});this.graph.onUpdate(function(){var h=window.getComputedStyle(self.legend.element).height;self.legend.element.style.height=h})};Rickshaw.namespace("Rickshaw.Graph.Behavior.Series.Toggle");Rickshaw.Graph.Behavior.Series.Toggle=function(args){this.graph=args.graph;this.legend=args.legend;var self=this;this.addAnchor=function(line){var anchor=document.createElement("a");anchor.innerHTML="✔";anchor.classList.add("action");line.element.insertBefore(anchor,line.element.firstChild);anchor.onclick=function(e){if(line.series.disabled){line.series.enable();line.element.classList.remove("disabled")}else{if(this.graph.series.filter(function(s){return!s.disabled}).length<=1)return;line.series.disable();line.element.classList.add("disabled")}self.graph.update()}.bind(this);var label=line.element.getElementsByTagName("span")[0];label.onclick=function(e){var disableAllOtherLines=line.series.disabled;if(!disableAllOtherLines){for(var i=0;idomainX){dataIndex=Math.abs(domainX-data[i].x)0){alignables.forEach(function(el){el.classList.remove("left");el.classList.add("right")});var rightAlignError=this._calcLayoutError(alignables);if(rightAlignError>leftAlignError){alignables.forEach(function(el){el.classList.remove("right");el.classList.add("left")})}}if(typeof this.onRender=="function"){this.onRender(args)}},_calcLayoutError:function(alignables){var parentRect=this.element.parentNode.getBoundingClientRect();var error=0;var alignRight=alignables.forEach(function(el){var rect=el.getBoundingClientRect();if(!rect.width){return}if(rect.right>parentRect.right){error+=rect.right-parentRect.right}if(rect.left=self.previewWidth){frameAfterDrag[0]-=frameAfterDrag[1]-self.previewWidth;frameAfterDrag[1]=self.previewWidth}}self.graphs.forEach(function(graph){var domainScale=d3.scale.linear().interpolate(d3.interpolateNumber).domain([0,self.previewWidth]).range(graph.dataDomain());var windowAfterDrag=[domainScale(frameAfterDrag[0]),domainScale(frameAfterDrag[1])];self.slideCallbacks.forEach(function(callback){callback(graph,windowAfterDrag[0],windowAfterDrag[1])});if(frameAfterDrag[0]===0){windowAfterDrag[0]=undefined}if(frameAfterDrag[1]===self.previewWidth){windowAfterDrag[1]=undefined}graph.window.xMin=windowAfterDrag[0];graph.window.xMax=windowAfterDrag[1];graph.update()})}function onMousedown(){drag.target=d3.event.target;drag.start=self._getClientXFromEvent(d3.event,drag);self.frameBeforeDrag=self.currentFrame.slice();d3.event.preventDefault?d3.event.preventDefault():d3.event.returnValue=false;d3.select(document).on("mousemove.rickshaw_range_slider_preview",onMousemove);d3.select(document).on("mouseup.rickshaw_range_slider_preview",onMouseup);d3.select(document).on("touchmove.rickshaw_range_slider_preview",onMousemove);d3.select(document).on("touchend.rickshaw_range_slider_preview",onMouseup);d3.select(document).on("touchcancel.rickshaw_range_slider_preview",onMouseup)}function onMousedownLeftHandle(datum,index){drag.left=true;onMousedown()}function onMousedownRightHandle(datum,index){drag.right=true;onMousedown()}function onMousedownMiddleHandle(datum,index){drag.left=true;drag.right=true;drag.rigid=true;onMousedown()}function onMouseup(datum,index){d3.select(document).on("mousemove.rickshaw_range_slider_preview",null);d3.select(document).on("mouseup.rickshaw_range_slider_preview",null);d3.select(document).on("touchmove.rickshaw_range_slider_preview",null);d3.select(document).on("touchend.rickshaw_range_slider_preview",null);d3.select(document).on("touchcancel.rickshaw_range_slider_preview",null);delete self.frameBeforeDrag;drag.left=false;drag.right=false;drag.rigid=false}element.select("rect.left_handle").on("mousedown",onMousedownLeftHandle);element.select("rect.right_handle").on("mousedown",onMousedownRightHandle);element.select("rect.middle_handle").on("mousedown",onMousedownMiddleHandle);element.select("rect.left_handle").on("touchstart",onMousedownLeftHandle);element.select("rect.right_handle").on("touchstart",onMousedownRightHandle);element.select("rect.middle_handle").on("touchstart",onMousedownMiddleHandle)},_getClientXFromEvent:function(event,drag){switch(event.type){case"touchstart":case"touchmove":var touchList=event.changedTouches;var touch=null;for(var touchIndex=0;touchIndexyMax)yMax=y});if(!series.length)return;if(series[0].xxMax)xMax=series[series.length-1].x});xMin-=(xMax-xMin)*this.padding.left;xMax+=(xMax-xMin)*this.padding.right;yMin=this.graph.min==="auto"?yMin:this.graph.min||0;yMax=this.graph.max===undefined?yMax:this.graph.max;if(this.graph.min==="auto"||yMin<0){yMin-=(yMax-yMin)*this.padding.bottom}if(this.graph.max===undefined){yMax+=(yMax-yMin)*this.padding.top}return{x:[xMin,xMax],y:[yMin,yMax]}},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var data=series.filter(function(s){return!s.disabled}).map(function(s){return s.stack});var pathNodes=vis.selectAll("path.path").data(data).enter().append("svg:path").classed("path",true).attr("d",this.seriesPathFactory());if(this.stroke){var strokeNodes=vis.selectAll("path.stroke").data(data).enter().append("svg:path").classed("stroke",true).attr("d",this.seriesStrokeFactory())}var i=0;series.forEach(function(series){if(series.disabled)return;series.path=pathNodes[0][i];if(this.stroke)series.stroke=strokeNodes[0][i];this._styleSeries(series);i++},this)},_styleSeries:function(series){var fill=this.fill?series.color:"none";var stroke=this.stroke?series.color:"none";series.path.setAttribute("fill",fill);series.path.setAttribute("stroke",stroke);series.path.setAttribute("stroke-width",this.strokeWidth);if(series.className){d3.select(series.path).classed(series.className,true)}if(series.className&&this.stroke){d3.select(series.stroke).classed(series.className,true)}},configure:function(args){args=args||{};Rickshaw.keys(this.defaults()).forEach(function(key){if(!args.hasOwnProperty(key)){this[key]=this[key]||this.graph[key]||this.defaults()[key];return}if(typeof this.defaults()[key]=="object"){Rickshaw.keys(this.defaults()[key]).forEach(function(k){this[key][k]=args[key][k]!==undefined?args[key][k]:this[key][k]!==undefined?this[key][k]:this.defaults()[key][k]},this)}else{this[key]=args[key]!==undefined?args[key]:this[key]!==undefined?this[key]:this.graph[key]!==undefined?this.graph[key]:this.defaults()[key]}},this)},setStrokeWidth:function(strokeWidth){if(strokeWidth!==undefined){this.strokeWidth=strokeWidth}},setTension:function(tension){if(tension!==undefined){this.tension=tension}}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Line");Rickshaw.Graph.Renderer.Line=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"line",defaults:function($super){return Rickshaw.extend($super(),{unstack:true,fill:false,stroke:true})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.line().x(function(d){return graph.x(d.x)}).y(function(d){return graph.y(d.y)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Stack");Rickshaw.Graph.Renderer.Stack=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"stack",defaults:function($super){return Rickshaw.extend($super(),{fill:true,stroke:false,unstack:false})},seriesPathFactory:function(){var graph=this.graph;var factory=d3.svg.area().x(function(d){return graph.x(d.x)}).y0(function(d){return graph.y(d.y0)}).y1(function(d){return graph.y(d.y+d.y0)}).interpolate(this.graph.interpolation).tension(this.tension);factory.defined&&factory.defined(function(d){return d.y!==null});return factory}});Rickshaw.namespace("Rickshaw.Graph.Renderer.Bar");Rickshaw.Graph.Renderer.Bar=Rickshaw.Class.create(Rickshaw.Graph.Renderer,{name:"bar",defaults:function($super){var defaults=Rickshaw.extend($super(),{gapSize:.05,unstack:false});delete defaults.tension;return defaults},initialize:function($super,args){args=args||{};this.gapSize=args.gapSize||this.gapSize;$super(args)},domain:function($super){var domain=$super();var frequentInterval=this._frequentInterval(this.graph.stackedData.slice(-1).shift());domain.x[1]+=Number(frequentInterval.magnitude);return domain},barWidth:function(series){var frequentInterval=this._frequentInterval(series.stack);var barWidth=this.graph.x.magnitude(frequentInterval.magnitude)*(1-this.gapSize);return barWidth},render:function(args){args=args||{};var graph=this.graph;var series=args.series||graph.series;var vis=args.vis||graph.vis;vis.selectAll("*").remove();var barWidth=this.barWidth(series.active()[0]);var barXOffset=0;var activeSeriesCount=series.filter(function(s){return!s.disabled}).length;var seriesBarWidth=this.unstack?barWidth/activeSeriesCount:barWidth;var transform=function(d){var matrix=[1,0,0,d.y<0?-1:1,0,d.y<0?graph.y.magnitude(Math.abs(d.y))*2:0];return"matrix("+matrix.join(",")+")"};series.forEach(function(series){if(series.disabled)return;var barWidth=this.barWidth(series);var nodes=vis.selectAll("path").data(series.stack.filter(function(d){return d.y!==null})).enter().append("svg:rect").attr("x",function(d){return graph.x(d.x)+barXOffset}).attr("y",function(d){return graph.y(d.y0+Math.abs(d.y))*(d.y<0?-1:1)}).attr("width",seriesBarWidth).attr("height",function(d){return graph.y.magnitude(Math.abs(d.y))}).attr("transform",transform);Array.prototype.forEach.call(nodes[0],function(n){n.setAttribute("fill",series.color)});if(this.unstack)barXOffset+=seriesBarWidth},this)},_frequentInterval:function(data){var intervalCounts={};for(var i=0;i0){this[0].data.forEach(function(plot){item.data.push({x:plot.x,y:0})})}else if(item.data.length===0){item.data.push({x:this.timeBase-(this.timeInterval||0),y:0})}this.push(item);if(this.legend){this.legend.addLine(this.itemByName(item.name))}},addData:function(data,x){var index=this.getIndex();Rickshaw.keys(data).forEach(function(name){if(!this.itemByName(name)){this.addItem({name:name})}},this);this.forEach(function(item){item.data.push({x:x||(index*this.timeInterval||1)+this.timeBase,y:data[item.name]||0})},this)},getIndex:function(){return this[0]&&this[0].data&&this[0].data.length?this[0].data.length:0},itemByName:function(name){for(var i=0;i1;i--){this.currentSize+=1;this.currentIndex+=1;this.forEach(function(item){item.data.unshift({x:((i-1)*this.timeInterval||1)+this.timeBase,y:0,i:i})},this)}}},addData:function($super,data,x){$super(data,x);this.currentSize+=1;this.currentIndex+=1;if(this.maxDataPoints!==undefined){while(this.currentSize>this.maxDataPoints){this.dropData()}}},dropData:function(){this.forEach(function(item){item.data.splice(0,1)});this.currentSize-=1},getIndex:function(){return this.currentIndex}});return Rickshaw}); \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/stats.hbs b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/stats.hbs new file mode 100644 index 000000000..ff0fb6023 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/stats.hbs @@ -0,0 +1,29 @@ +{{#if monitor_operations}} + {{#zone "topCss"}} + {{css "css/graph.css"}} + {{/zone}} + +
      +
      +
      +
      + + + + + View Device Analytics + + {{#zone "bottomJs"}} + {{js "js/d3.min.js"}} + {{js "js/rickshaw.min.js"}} + {{js "js/moment.min.js"}} + {{js "js/device-stats.js"}} + {{/zone}} +{{else}} +
      +

      + Stats Loading Failed!

      +
      +{{/if}} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/stats.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/stats.js new file mode 100644 index 000000000..e7f862207 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/stats.js @@ -0,0 +1,32 @@ +/* + * 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. + */ + +function onRequest(context) { + var log = new Log("stats.js"); + var operationModule = require("/app/modules/operation.js").operationModule; + var device = context.unit.params.device; + var monitor_operations; + try { + monitor_operations = JSON.stringify(operationModule.getMonitorOperations(device.type)); + } catch (e) { + log.error("Monitor operation loading failed."); + monitor_operations = null; + } + + return {"monitor_operations": monitor_operations, "device": device}; +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/stats.json b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/stats.json new file mode 100644 index 000000000..688e93980 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.device.stats/stats.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0" +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.platform.configuration/configuration.hbs b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.platform.configuration/configuration.hbs new file mode 100644 index 000000000..94522b0bb --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.platform.configuration/configuration.hbs @@ -0,0 +1,492 @@ +
      +
      + +
      +
      +

      + Platform Configurations +

      +
      + General and Platform Specific Server Settings for the Tenant +
      +
      +
      +
      + + +
      + +
      +
      + +
      + +
      + + +
      +
      + +
      +
      + + +
      +
      + + + +
      +
      +
      + + +
      + + +
      + +
      +
      + + +
      +
      + +
      +
      + + +
      + +
      + + +
      +
      +
      + + +
      +
      + +
      +
      + +
      +
      +
      +
      + + + +
      +
      + + +
      + + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      +
      + + + +
      + +
      + + +
      +
      + + + +
      + +
      +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      +
      + + + +
      + +
      +
      + + +
      + +
      + + +
      + +
      + + +
      +
      + + +
      +
      + +
      +
      + +
      +
      +
      +
      + + + +
      +
      +
      + + +
      +
      + + +
      +
      +
      + + +
      +
      + +
      +
      + +
      +
      +
      +
      + +
      +
      +
      +
      + + + +
      +
      +{{#zone "bottomJs"}} + {{js "js/platform-configuration.js"}} +{{/zone}} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.platform.configuration/configuration.json b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.platform.configuration/configuration.json new file mode 100644 index 000000000..be0496bf6 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.platform.configuration/configuration.json @@ -0,0 +1,4 @@ +{ + "version" : "1.0.0", + "extends": "cdmf.unit.platform.configuration" +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.platform.configuration/public/js/platform-configuration.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.platform.configuration/public/js/platform-configuration.js new file mode 100644 index 000000000..41123e12a --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.platform.configuration/public/js/platform-configuration.js @@ -0,0 +1,856 @@ +/* + * 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. + */ + +/** + * Checks if provided input is valid against RegEx input. + * + * @param regExp Regular expression + * @param inputString Input string to check + * @returns {boolean} Returns true if input matches RegEx + */ +function inputIsValid(regExp, inputString) { + return regExp.test(inputString); +} + +/** + * Checks if an email address has the valid format or not. + * + * @param email Email address + * @returns {boolean} true if email has the valid format, otherwise false. + */ +function emailIsValid(email) { + var regExp = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/; + return regExp.test(email); +} + +var notifierTypeConstants = { + "LOCAL": "1", + "GCM": "2" +}; +// Constants to define platform types available +var platformTypeConstants = { + "ANDROID": "android", + "IOS": "ios", + "WINDOWS": "windows" +}; + +var responseCodes = { + "CREATED": "Created", + "SUCCESS": "201", + "INTERNAL_SERVER_ERROR": "Internal Server Error" +}; + +var configParams = { + "NOTIFIER_TYPE": "notifierType", + "NOTIFIER_FREQUENCY": "notifierFrequency", + "GCM_API_KEY": "gcmAPIKey", + "GCM_SENDER_ID": "gcmSenderId", + "ANDROID_EULA": "androidEula", + "IOS_EULA": "iosEula", + "CONFIG_COUNTRY": "configCountry", + "CONFIG_STATE": "configState", + "CONFIG_LOCALITY": "configLocality", + "CONFIG_ORGANIZATION": "configOrganization", + "CONFIG_ORGANIZATION_UNIT": "configOrganizationUnit", + "MDM_CERT_PASSWORD": "MDMCertPassword", + "MDM_CERT_TOPIC_ID": "MDMCertTopicID", + "APNS_CERT_PASSWORD": "APNSCertPassword", + "MDM_CERT": "MDMCert", + "MDM_CERT_NAME": "MDMCertName", + "APNS_CERT": "APNSCert", + "APNS_CERT_NAME": "APNSCertName", + "ORG_DISPLAY_NAME": "organizationDisplayName", + "GENERAL_EMAIL_HOST": "emailHost", + "GENERAL_EMAIL_PORT": "emailPort", + "GENERAL_EMAIL_USERNAME": "emailUsername", + "GENERAL_EMAIL_PASSWORD": "emailPassword", + "GENERAL_EMAIL_SENDER_ADDRESS": "emailSender", + "GENERAL_EMAIL_TEMPLATE": "emailTemplate", + "COMMON_NAME": "commonName", + "KEYSTORE_PASSWORD": "keystorePassword", + "PRIVATE_KEY_PASSWORD": "privateKeyPassword", + "BEFORE_EXPIRE": "beforeExpire", + "AFTER_EXPIRE": "afterExpire", + "WINDOWS_EULA": "windowsLicense" +}; + +$(document).ready(function () { + $("#gcm-inputs").hide(); + tinymce.init({ + selector: "textarea", + theme: "modern", + plugins: [ + "advlist autolink lists link image charmap print preview anchor", + "searchreplace visualblocks code fullscreen", + "insertdatetime image table contextmenu paste" + ], + toolbar: "undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image" + }); + + var getAndroidConfigAPI = "/mdm-android-agent/configuration"; + var getGeneralConfigAPI = "/devicemgt_admin/configuration"; + var getIosConfigAPI = "/ios/configuration"; + var getWindowsConfigAPI = "/mdm-windows-agent/services/configuration"; + + /** + * Following requests would execute + * on page load event of platform configuration page in WSO2 EMM Console. + * Upon receiving the response, the parameters will be set to the fields, + * in case those configurations are already set. + */ + invokerUtil.get( + getAndroidConfigAPI, + + function (data) { + data = JSON.parse(data); + if (data != null && data.configuration != null) { + for (var i = 0; i < data.configuration.length; i++) { + var config = data.configuration[i]; + if(config.name == configParams["NOTIFIER_TYPE"]){ + $("#android-config-notifier").val(config.value); + if(config.value != notifierTypeConstants["GCM"] ) { + $("#gcm-inputs").hide(); + }else{ + $("#gcm-inputs").show(); + } + } else if(config.name == configParams["NOTIFIER_FREQUENCY"]){ + $("input#android-config-notifier-frequency").val(config.value); + } else if(config.name == configParams["GCM_API_KEY"]){ + $("input#android-config-gcm-api-key").val(config.value); + } else if(config.name == configParams["GCM_SENDER_ID"]){ + $("input#android-config-gcm-sender-id").val(config.value); + } else if(config.name == configParams["ANDROID_EULA"]){ + $("#android-eula").val(config.value); + } + } + } + + }, function () { + + } + ); + + invokerUtil.get( + getGeneralConfigAPI, + + function (data) { + data = JSON.parse(data); + if (data != null && data.configuration != null) { + for (var i = 0; i < data.configuration.length; i++) { + var config = data.configuration[i]; + if(config.name == configParams["NOTIFIER_FREQUENCY"]){ + $("input#monitoring-config-frequency").val(config.value); + } + /*if(config.name == configParams["GENERAL_EMAIL_HOST"]){ + $("input#email-config-host").val(config.value); + } else if(config.name == configParams["GENERAL_EMAIL_PORT"]){ + $("input#email-config-port").val(config.value); + } else if(config.name == configParams["GENERAL_EMAIL_USERNAME"]){ + $("input#email-config-username").val(config.value); + } else if(config.name == configParams["GENERAL_EMAIL_PASSWORD"]){ + $("input#email-config-password").val(config.value); + } else if(config.name == configParams["GENERAL_EMAIL_SENDER_ADDRESS"]){ + $("input#email-config-sender-email").val(config.value); + } else if(config.name == configParams["GENERAL_EMAIL_TEMPLATE"]){ + $("input#email-config-template").val(config.value); + }*/ + } + } + + }, function () { + + } + ); + + invokerUtil.get( + getIosConfigAPI, + + function (data) { + data = JSON.parse(data); + if (data != null && data.configuration != null) { + for (var i = 0; i < data.configuration.length; i++) { + var config = data.configuration[i]; + if(config.name == configParams["CONFIG_COUNTRY"]){ + $("input#ios-config-country").val(config.value); + } else if(config.name == configParams["CONFIG_STATE"]){ + $("input#ios-config-state").val(config.value); + } else if(config.name == configParams["CONFIG_LOCALITY"]){ + $("input#ios-config-locality").val(config.value); + } else if(config.name == configParams["CONFIG_ORGANIZATION"]){ + $("input#ios-config-organization").val(config.value); + } else if(config.name == configParams["CONFIG_ORGANIZATION_UNIT"]){ + $("input#ios-config-organization-unit").val(config.value); + } else if(config.name == configParams["MDM_CERT_PASSWORD"]){ + $("input#ios-config-mdm-certificate-password").val(config.value); + } else if(config.name == configParams["MDM_CERT_TOPIC_ID"]){ + $("input#ios-config-mdm-certificate-topic-id").val(config.value); + } else if(config.name == configParams["APNS_CERT_PASSWORD"]){ + $("input#ios-config-apns-certificate-password").val(config.value); + } else if(config.name == configParams["MDM_CERT_NAME"]){ + $("#mdm-cert-file-name").html(config.value); + } else if(config.name == configParams["APNS_CERT_NAME"]){ + $("#apns-cert-file-name").html(config.value); + } else if(config.name == configParams["ORG_DISPLAY_NAME"]){ + $("input#ios-org-display-name").val(config.value); + } else if(config.name == configParams["IOS_EULA"]){ + $("#ios-eula").val(config.value); + } + } + } + + }, function () { + + } + ); + + invokerUtil.get( + getWindowsConfigAPI, + + function (data) { + data = JSON.parse(data); + if (data != null && data.configuration != null) { + for (var i = 0; i < data.configuration.length; i++) { + var config = data.configuration[i]; + if(config.name == configParams["NOTIFIER_FREQUENCY"]) { + $("input#windows-config-notifier-frequency").val(config.value); + } else if(config.name == configParams["WINDOWS_EULA"]) { + $("#windows-eula").val(config.value); + } + } + } + + }, function () { + + } + ); + + + $("select.select2[multiple=multiple]").select2({ + tags : true + }); + + $("#android-config-notifier").change(function() { + var notifierType = $("#android-config-notifier").find("option:selected").attr("value"); + if(notifierType != notifierTypeConstants["GCM"] ) { + $("#gcm-inputs").hide(); + $("#local-inputs").show(); + }else{ + $("#local-inputs").hide(); + $("#gcm-inputs").show(); + } + }); + /** + * Following click function would execute + * when a user clicks on "Save" button + * on Android platform configuration page in WSO2 EMM Console. + */ + $("button#save-android-btn").click(function() { + var notifierType = $("#android-config-notifier").find("option:selected").attr("value"); + var notifierFrequency = $("input#android-config-notifier-frequency").val(); + var gcmAPIKey = $("input#android-config-gcm-api-key").val(); + var gcmSenderId = $("input#android-config-gcm-sender-id").val(); + var androidLicense = tinymce.get('android-eula').getContent(); + + var errorMsgWrapper = "#android-config-error-msg"; + var errorMsg = "#android-config-error-msg span"; + if (!notifierFrequency) { + $(errorMsg).text("Notifier frequency is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!$.isNumeric(notifierFrequency)) { + $(errorMsg).text("Provided notifier frequency is invalid. Please check."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (notifierType == notifierTypeConstants["GCM"] && !gcmAPIKey) { + $(errorMsg).text("GCM API Key is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (notifierType == notifierTypeConstants["GCM"] && !gcmSenderId) { + $(errorMsg).text("GCM Sender ID is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else { + + var addConfigFormData = {}; + var configList = new Array(); + + var type = { + "name": configParams["NOTIFIER_TYPE"], + "value": notifierType, + "contentType": "text" + }; + + var frequency = { + "name": configParams["NOTIFIER_FREQUENCY"], + "value": notifierFrequency, + "contentType": "text" + }; + + var gcmKey = { + "name": configParams["GCM_API_KEY"], + "value": gcmAPIKey, + "contentType": "text" + }; + + var gcmId = { + "name": configParams["GCM_SENDER_ID"], + "value": gcmSenderId, + "contentType": "text" + }; + + var androidEula = { + "name": configParams["ANDROID_EULA"], + "value": androidLicense, + "contentType": "text" + }; + + configList.push(type); + configList.push(frequency); + configList.push(androidEula); + if (notifierType == notifierTypeConstants["GCM"]) { + configList.push(gcmKey); + configList.push(gcmId); + } + + addConfigFormData.type = platformTypeConstants["ANDROID"]; + addConfigFormData.configuration = configList; + + var addConfigAPI = "/mdm-android-agent/configuration"; + + invokerUtil.post( + addConfigAPI, + addConfigFormData, + function (data) { + data = JSON.parse(data); + if (data.responseCode == responseCodes["CREATED"]) { + $("#config-save-form").addClass("hidden"); + $("#record-created-msg").removeClass("hidden"); + } else if (data == 500) { + $(errorMsg).text("Exception occurred at backend."); + } else if (data == 403) { + $(errorMsg).text("Action was not permitted."); + } else { + $(errorMsg).text("An unexpected error occurred."); + } + + $(errorMsgWrapper).removeClass("hidden"); + }, function () { + $(errorMsg).text("An unexpected error occurred."); + $(errorMsgWrapper).removeClass("hidden"); + } + ); + } + }); + + /** + * Following click function would execute + * when a user clicks on "Save" button + * on General platform configuration page in WSO2 EMM Console. + */ + $("button#save-general-btn").click(function() { + var notifierFrequency = $("input#monitoring-config-frequency").val(); + /*var emailHost = $("input#email-config-host").val(); + var emailPort = $("input#email-config-port").val(); + var emailUsername = $("input#email-config-username").val(); + var emailPassword = $("input#email-config-password").val(); + var emailSenderAddress = $("input#email-config-sender-email").val(); + var emailTemplate = $("input#email-config-template").val();*/ + + var errorMsgWrapper = "#email-config-error-msg"; + var errorMsg = "#email-config-error-msg span"; + + if (!notifierFrequency) { + $(errorMsg).text("Monitoring frequency is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!$.isNumeric(notifierFrequency)) { + $(errorMsg).text("Provided monitoring frequency is invalid. It must be a number."); + $(errorMsgWrapper).removeClass("hidden"); + } else { + + var addConfigFormData = {}; + var configList = new Array(); + + var monitorFrequency = { + "name": configParams["NOTIFIER_FREQUENCY"], + "value": notifierFrequency, + "contentType": "text" + }; + + /*if (!emailHost) { + $(errorMsg).text("Email Host is a required field. It cannot be empty."); + //$(errorMsgWrapper).removeClass("hidden"); + } else if (!notifierFrequency) { + $(errorMsg).text("Monitoring frequency is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!$.isNumeric(notifierFrequency)) { + $(errorMsg).text("Provided monitoring frequency is invalid. It must be a number."); + $(errorMsgWrapper).removeClass("hidden"); + }else if (!emailPort) { + $(errorMsg).text("Email Port is a required field. It cannot be empty."); + //$(errorMsgWrapper).removeClass("hidden"); + } else if (!emailUsername) { + $(errorMsg).text("Username is a required field. It cannot be empty."); + //$(errorMsgWrapper).removeClass("hidden"); + } else if (!emailPassword) { + $(errorMsg).text("Password is a required field. It cannot be empty."); + //$(errorMsgWrapper).removeClass("hidden"); + } else if (!emailSenderAddress) { + $(errorMsg).text("Sender Email Address is a required field. It cannot be empty."); + //$(errorMsgWrapper).removeClass("hidden"); + } else if (!emailIsValid(emailSenderAddress)) { + $(errorMsg).text("Provided sender email is invalid. Please check."); + //$(errorMsgWrapper).removeClass("hidden"); + } */ + + /*var host = { + "name": configParams["GENERAL_EMAIL_HOST"], + "value": emailHost, + "contentType": "text" + }; + + var port = { + "name": configParams["GENERAL_EMAIL_PORT"], + "value": emailPort, + "contentType": "text" + }; + + var username = { + "name": configParams["GENERAL_EMAIL_USERNAME"], + "value": emailUsername, + "contentType": "text" + }; + + var password = { + "name": configParams["GENERAL_EMAIL_PASSWORD"], + "value": emailPassword, + "contentType": "text" + }; + + var sender = { + "name": configParams["GENERAL_EMAIL_SENDER_ADDRESS"], + "value": emailSenderAddress, + "contentType": "text" + }; + + var template = { + "name": configParams["GENERAL_EMAIL_TEMPLATE"], + "value": emailTemplate, + "contentType": "text" + };*/ + + configList.push(monitorFrequency); + /*configList.push(host); + configList.push(port); + configList.push(username); + configList.push(password); + configList.push(sender); + configList.push(template);*/ + + addConfigFormData.configuration = configList; + + var addConfigAPI = "/devicemgt_admin/configuration"; + + invokerUtil.post( + addConfigAPI, + addConfigFormData, + function (data) { + data = JSON.parse(data); + if (data.statusCode == responseCodes["SUCCESS"]) { + $("#config-save-form").addClass("hidden"); + $("#record-created-msg").removeClass("hidden"); + } else if (data == 500) { + $(errorMsg).text("Exception occurred at backend."); + } else if (data == 403) { + $(errorMsg).text("Action was not permitted."); + } else { + $(errorMsg).text("An unexpected error occurred."); + } + + $(errorMsgWrapper).removeClass("hidden"); + }, function () { + $(errorMsg).text("An unexpected error occurred."); + $(errorMsgWrapper).removeClass("hidden"); + } + ); + } + }); + + var errorMsgWrapper = "#ios-config-error-msg"; + var errorMsg = "#ios-config-error-msg span"; + var fileTypes = ['pfx']; + var notSupportedError = false; + + var base64MDMCert = ""; + var fileInputMDMCert = $('#ios-config-mdm-certificate'); + var fileNameMDMCert = ""; + var invalidFormatMDMCert = false; + + var base64APNSCert = ""; + var fileInputAPNSCert = $('#ios-config-apns-certificate'); + var fileNameAPNSCert = ""; + var invalidFormatAPNSCert = false; + + $(fileInputMDMCert).change(function() { + + if (!window.File || !window.FileReader || !window.FileList || !window.Blob) { + $(errorMsg).text("The File APIs are not fully supported in this browser."); + $(errorMsgWrapper).removeClass("hidden"); + notSupportedError = true; + return; + } + + var file = fileInputMDMCert[0].files[0]; + fileNameMDMCert = file.name; + var extension = file.name.split('.').pop().toLowerCase(), + isSuccess = fileTypes.indexOf(extension) > -1; + + if (isSuccess) { + var fileReader = new FileReader(); + fileReader.onload = function(event) { + base64MDMCert = event.target.result; + }; + fileReader.readAsDataURL(file); + invalidFormatMDMCert = false; + } else { + base64MDMCert = ""; + invalidFormatMDMCert = true; + } + }); + + $(fileInputAPNSCert).change(function() { + + if (!window.File || !window.FileReader || !window.FileList || !window.Blob) { + $(errorMsg).text("The File APIs are not fully supported in this browser."); + $(errorMsgWrapper).removeClass("hidden"); + notSupportedError = true; + return; + } + + var file = fileInputAPNSCert[0].files[0]; + fileNameAPNSCert = file.name; + var extension = file.name.split('.').pop().toLowerCase(), + isSuccess = fileTypes.indexOf(extension) > -1; + + if (isSuccess) { + var fileReader = new FileReader(); + fileReader.onload = function(event) { + base64APNSCert = event.target.result; + }; + fileReader.readAsDataURL(file); + invalidFormatAPNSCert = false; + } else { + base64MDMCert = ""; + invalidFormatAPNSCert = true; + } + }); + + $("button#save-ios-btn").click(function() { + + var configCountry = $("#ios-config-country").val(); + var configState = $("#ios-config-state").val(); + var configLocality = $("#ios-config-locality").val(); + var configOrganization = $("#ios-config-organization").val(); + var configOrganizationUnit = $("#ios-config-organization-unit").val(); + var MDMCertPassword = $("#ios-config-mdm-certificate-password").val(); + var MDMCertTopicID = $("#ios-config-mdm-certificate-topic-id").val(); + var APNSCertPassword = $("#ios-config-apns-certificate-password").val(); + var configOrgDisplayName = $("#ios-org-display-name").val(); + var iosLicense = tinymce.get('ios-eula').getContent(); + + if (!configCountry) { + $(errorMsg).text("SCEP country is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!configState) { + $(errorMsg).text("SCEP state is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!configLocality) { + $(errorMsg).text("SCEP locality is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!configOrganization) { + $(errorMsg).text("SCEP organization is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!configOrganizationUnit) { + $(errorMsg).text("SCEP organization unit is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!MDMCertPassword) { + $(errorMsg).text("MDM certificate password is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!MDMCertTopicID) { + $(errorMsg).text("MDM certificate topic ID is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!APNSCertPassword) { + $(errorMsg).text("APNS certificate password is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if(notSupportedError) { + $(errorMsg).text("The File APIs are not fully supported in this browser."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (invalidFormatMDMCert) { + $(errorMsg).text("MDM certificate needs to be in pfx format."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (base64MDMCert == '') { + $(errorMsg).text("MDM certificate is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (invalidFormatAPNSCert) { + $(errorMsg).text("APNS certificate needs to be in pfx format."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (base64APNSCert == '') { + $(errorMsg).text("APNS certificate is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!configOrgDisplayName) { + $(errorMsg).text("Organization display name is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } + + var addConfigFormData = {}; + var configList = new Array(); + + var configCountry = { + "name": configParams["CONFIG_COUNTRY"], + "value": configCountry, + "contentType": "text" + }; + + var configState = { + "name": configParams["CONFIG_STATE"], + "value": configState, + "contentType": "text" + }; + + var configLocality = { + "name": configParams["CONFIG_LOCALITY"], + "value": configLocality, + "contentType": "text" + }; + + var configOrganization = { + "name": configParams["CONFIG_ORGANIZATION"], + "value": configOrganization, + "contentType": "text" + }; + + var configOrganizationUnit = { + "name": configParams["CONFIG_ORGANIZATION_UNIT"], + "value": configOrganizationUnit, + "contentType": "text" + }; + + var MDMCertPassword = { + "name": configParams["MDM_CERT_PASSWORD"], + "value": MDMCertPassword, + "contentType": "text" + }; + + var MDMCertTopicID = { + "name": configParams["MDM_CERT_TOPIC_ID"], + "value": MDMCertTopicID, + "contentType": "text" + }; + + var APNSCertPassword = { + "name": configParams["APNS_CERT_PASSWORD"], + "value": APNSCertPassword, + "contentType": "text" + }; + + var paramBase64MDMCert = { + "name": configParams["MDM_CERT"], + "value": base64MDMCert, + "contentType": "text" + }; + + var MDMCertName = { + "name": configParams["MDM_CERT_NAME"], + "value": fileNameMDMCert, + "contentType": "text" + }; + + var paramBase64APNSCert = { + "name": configParams["APNS_CERT"], + "value": base64APNSCert, + "contentType": "text" + }; + + var APNSCertName = { + "name": configParams["APNS_CERT_NAME"], + "value": fileNameAPNSCert, + "contentType": "text" + }; + + var paramOrganizationDisplayName = { + "name": configParams["ORG_DISPLAY_NAME"], + "value": configOrgDisplayName, + "contentType": "text" + }; + + var iosEula = { + "name": configParams["IOS_EULA"], + "value": iosLicense, + "contentType": "text" + }; + + configList.push(configCountry); + configList.push(configState); + configList.push(configLocality); + configList.push(configOrganization); + configList.push(configOrganizationUnit); + configList.push(MDMCertPassword); + configList.push(MDMCertTopicID); + configList.push(APNSCertPassword); + configList.push(paramBase64MDMCert); + configList.push(MDMCertName); + configList.push(paramBase64APNSCert); + configList.push(APNSCertName); + configList.push(paramOrganizationDisplayName); + configList.push(iosEula); + + addConfigFormData.type = platformTypeConstants["IOS"]; + addConfigFormData.configuration = configList; + + var addConfigAPI = "/ios/configuration"; + + invokerUtil.post( + addConfigAPI, + addConfigFormData, + function (data) { + data = JSON.parse(data); + if (data.responseCode == responseCodes["CREATED"]) { + $("#config-save-form").addClass("hidden"); + $("#record-created-msg").removeClass("hidden"); + } else if (data == 500) { + $(errorMsg).text("Exception occurred at backend."); + } else if (data == 400) { + $(errorMsg).text("Configurations cannot be empty."); + } else { + $(errorMsg).text("An unexpected error occurred."); + } + + $(errorMsgWrapper).removeClass("hidden"); + }, function () { + $(errorMsg).text("An unexpected error occurred."); + $(errorMsgWrapper).removeClass("hidden"); + } + ); + + }); + + var errorMsgWrapper = "#windows-config-error-msg"; + var errorMsg = "#windows-config-error-msg span"; + var fileTypes = ['jks']; + var notSupportedError = false; + + var base64WindowsMDMCert = ""; + var fileInputWindowsMDMCert = $('#windows-config-mdm-certificate'); + var fileNameWindowsMDMCert = ""; + var invalidFormatWindowsMDMCert = false; + + $(fileInputWindowsMDMCert).change(function() { + + if (!window.File || !window.FileReader || !window.FileList || !window.Blob) { + $(errorMsg).text("The File APIs are not fully supported in this browser."); + $(errorMsgWrapper).removeClass("hidden"); + notSupportedError = true; + return; + } + + var file = fileInputWindowsMDMCert[0].files[0]; + fileNameWindowsMDMCert = file.name; + var extension = file.name.split('.').pop().toLowerCase(), + isSuccess = fileTypes.indexOf(extension) > -1; + + if (isSuccess) { + var fileReader = new FileReader(); + fileReader.onload = function(event) { + base64WindowsMDMCert = event.target.result; + }; + fileReader.readAsDataURL(file); + invalidFormatWindowsMDMCert = false; + } else { + base64MDMCert = ""; + invalidFormatWindowsMDMCert = true; + } + }); + + $("button#save-windows-btn").click(function() { + + var notifierFrequency = $("#windows-config-notifier-frequency").val(); + var windowsLicense = tinymce.get('windows-eula').getContent(); + + if (!notifierFrequency) { + $(errorMsg).text("Notifier Frequency is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!windowsLicense) { + $(errorMsg).text("License is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if(!$.isNumeric(notifierFrequency)){ + $(errorMsg).text("Provided Notifier frequency is invalid. It must be a number."); + $(errorMsgWrapper).removeClass("hidden"); + } + + var addConfigFormData = {}; + var configList = new Array(); + + var paramNotifierFrequency = { + "name": configParams["NOTIFIER_FREQUENCY"], + "value": notifierFrequency, + "contentType": "text" + }; + + var windowsEula = { + "name": configParams["WINDOWS_EULA"], + "value": windowsLicense, + "contentType": "text" + }; + + configList.push(paramNotifierFrequency); + configList.push(windowsEula); + + addConfigFormData.type = platformTypeConstants["WINDOWS"]; + addConfigFormData.configuration = configList; + + var addConfigAPI = "/mdm-windows-agent/services/configuration"; + + invokerUtil.post( + addConfigAPI, + addConfigFormData, + function (data) { + data = JSON.parse(data); + if (data.responseCode == responseCodes["CREATED"]) { + $("#config-save-form").addClass("hidden"); + $("#record-created-msg").removeClass("hidden"); + } else if (data == 500) { + $(errorMsg).text("Exception occurred at backend."); + } else if (data == 400) { + $(errorMsg).text("Configurations cannot be empty."); + } else { + $(errorMsg).text("An unexpected error occurred."); + } + + $(errorMsgWrapper).removeClass("hidden"); + }, function () { + $(errorMsg).text("An unexpected error occurred."); + $(errorMsgWrapper).removeClass("hidden"); + } + ); + + }); +}); + +// Start of HTML embedded invoke methods +var showAdvanceOperation = function (operation, button) { + $(button).addClass('selected'); + $(button).siblings().removeClass('selected'); + var hiddenOperation = ".wr-hidden-operations-content > div"; + $(hiddenOperation + '[data-operation="' + operation + '"]').show(); + $(hiddenOperation + '[data-operation="' + operation + '"]').siblings().hide(); +}; diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/edit.hbs b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/edit.hbs new file mode 100644 index 000000000..441828918 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/edit.hbs @@ -0,0 +1,246 @@ +{{#zone "topCss"}} + {{css "css/codemirror.css"}} +{{/zone}} +
      +
      + + + + + + + + + +
      +
      +

      EDIT POLICY

      +
      +
      +
      +
      +
      +

      Step 1: Edit current profile

      +
      +
      + +
      +
      +
      + +
      +
      +
      +
      +
      + +
      +
      +
      +
      + + + +
      +
      +{{#zone "bottomJs"}} + {{js "js/codemirror.js"}} + {{js "js/sql.js"}} + {{js "js/policy-edit.js"}} +{{/zone}} + diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/edit.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/edit.js new file mode 100644 index 000000000..36b130ca0 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/edit.js @@ -0,0 +1,25 @@ +/* + * 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. + */ + +function onRequest(context) { + var log = new Log("policy-view-edit-unit backend js"); + log.debug("calling policy-view-edit-unit"); + var userModule = require("/app/modules/user.js").userModule; + context.roles = userModule.getRoles().content; + return context; +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/edit.json b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/edit.json new file mode 100644 index 000000000..fd2590129 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/edit.json @@ -0,0 +1,3 @@ +{ + "version" : "1.0.0" +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/public/css/codemirror.css b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/public/css/codemirror.css new file mode 100644 index 000000000..e749a5211 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/public/css/codemirror.css @@ -0,0 +1,342 @@ +/* + * 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. + */ + +/* BASICS */ + +.CodeMirror { + /* Set height, width, borders, and global font properties here */ + font-family: monospace; + height: 300px; + color: black; +} + +/* PADDING */ + +.CodeMirror-lines { + padding: 4px 0; /* Vertical padding around content */ +} +.CodeMirror pre { + padding: 0 4px; /* Horizontal padding of content */ +} + +.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + background-color: white; /* The little square between H and V scrollbars */ +} + +/* GUTTER */ + +.CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; +} +.CodeMirror-linenumbers {} +.CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; + white-space: nowrap; + left: -30px; +} + +.CodeMirror-guttermarker { color: black; } +.CodeMirror-guttermarker-subtle { color: #999; } + +/* CURSOR */ + +.CodeMirror div.CodeMirror-cursor { + border-left: 1px solid black; +} +/* Shown when moving in bi-directional text */ +.CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; +} +.CodeMirror.cm-fat-cursor div.CodeMirror-cursor { + width: auto; + border: 0; + background: #7e7; +} +.CodeMirror.cm-fat-cursor div.CodeMirror-cursors { + z-index: 1; +} + +.cm-animate-fat-cursor { + width: auto; + border: 0; + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; +} +@-moz-keyframes blink { + 0% { background: #7e7; } + 50% { background: none; } + 100% { background: #7e7; } +} +@-webkit-keyframes blink { + 0% { background: #7e7; } + 50% { background: none; } + 100% { background: #7e7; } +} +@keyframes blink { + 0% { background: #7e7; } + 50% { background: none; } + 100% { background: #7e7; } +} + +/* Can style cursor different in overwrite (non-insert) mode */ +div.CodeMirror-overwrite div.CodeMirror-cursor {} + +.cm-tab { display: inline-block; text-decoration: inherit; } + +.CodeMirror-ruler { + border-left: 1px solid #ccc; + position: absolute; +} + +/* DEFAULT THEME */ + +.cm-s-default .cm-keyword {color: #708;} +.cm-s-default .cm-atom {color: #219;} +.cm-s-default .cm-number {color: #164;} +.cm-s-default .cm-def {color: #00f;} +.cm-s-default .cm-variable, +.cm-s-default .cm-punctuation, +.cm-s-default .cm-property, +.cm-s-default .cm-operator {} +.cm-s-default .cm-variable-2 {color: #05a;} +.cm-s-default .cm-variable-3 {color: #085;} +.cm-s-default .cm-comment {color: #a50;} +.cm-s-default .cm-string {color: #a11;} +.cm-s-default .cm-string-2 {color: #f50;} +.cm-s-default .cm-meta {color: #555;} +.cm-s-default .cm-qualifier {color: #555;} +.cm-s-default .cm-builtin {color: #30a;} +.cm-s-default .cm-bracket {color: #997;} +.cm-s-default .cm-tag {color: #170;} +.cm-s-default .cm-attribute {color: #00c;} +.cm-s-default .cm-header {color: blue;} +.cm-s-default .cm-quote {color: #090;} +.cm-s-default .cm-hr {color: #999;} +.cm-s-default .cm-link {color: #00c;} + +.cm-negative {color: #d44;} +.cm-positive {color: #292;} +.cm-header, .cm-strong {font-weight: bold;} +.cm-em {font-style: italic;} +.cm-link {text-decoration: underline;} +.cm-strikethrough {text-decoration: line-through;} + +.cm-s-default .cm-error {color: #f00;} +.cm-invalidchar {color: #f00;} + +/* Default styles for common addons */ + +div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} +.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } +.CodeMirror-activeline-background {background: #e8f2ff;} + +/* STOP */ + +/* The rest of this file contains styles related to the mechanics of + the editor. You probably shouldn't touch them. */ + +.CodeMirror { + position: relative; + overflow: hidden; + background: white; +} + +.CodeMirror-scroll { + overflow: scroll !important; /* Things will break if this is overridden */ + /* 30px is the magic margin used to hide the element's real scrollbars */ + /* See overflow: hidden in .CodeMirror */ + margin-bottom: -30px; margin-right: -30px; + padding-bottom: 30px; + height: 100%; + outline: none; /* Prevent dragging from highlighting the element */ + position: relative; +} +.CodeMirror-sizer { + position: relative; + border-right: 30px solid transparent; +} + +/* The fake, visible scrollbars. Used to force redraw during scrolling + before actuall scrolling happens, thus preventing shaking and + flickering artifacts. */ +.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + position: absolute; + z-index: 6; + display: none; +} +.CodeMirror-vscrollbar { + right: 0; top: 0; + overflow-x: hidden; + overflow-y: scroll; +} +.CodeMirror-hscrollbar { + bottom: 0; left: 0; + overflow-y: hidden; + overflow-x: scroll; +} +.CodeMirror-scrollbar-filler { + right: 0; bottom: 0; +} +.CodeMirror-gutter-filler { + left: 0; bottom: 0; +} + +.CodeMirror-gutters { + position: absolute; left: 0; top: 0; + z-index: 3; +} +.CodeMirror-gutter { + white-space: normal; + height: 100%; + display: inline-block; + /* Hack to make IE7 behave */ + *zoom:1; + *display:inline; +} +.CodeMirror-gutter-wrapper { + position: absolute; + z-index: 4; + height: 100%; +} +.CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4; + left: -30px; +} +.CodeMirror-gutter-wrapper { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.CodeMirror-lines { + cursor: text; + min-height: 1px; /* prevents collapsing before first draw */ +} +.CodeMirror pre { + /* Reset some styles that the rest of the page might have set */ + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; + -webkit-tap-highlight-color: transparent; +} +.CodeMirror-wrap pre { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} + +.CodeMirror-linebackground { + position: absolute; + left: 0; right: 0; top: 0; bottom: 0; + z-index: 0; +} + +.CodeMirror-linewidget { + position: relative; + z-index: 2; + overflow: auto; +} + +.CodeMirror-widget {} + +.CodeMirror-code { + outline: none; +} + +/* Force content-box sizing for the elements where we expect it */ +.CodeMirror-scroll, +.CodeMirror-sizer, +.CodeMirror-gutter, +.CodeMirror-gutters, +.CodeMirror-linenumber { + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +.CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; +} +.CodeMirror-measure pre { position: static; } + +.CodeMirror div.CodeMirror-cursor { + position: absolute; + border-right: none; + width: 0; +} + +div.CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 3; +} +.CodeMirror-focused div.CodeMirror-cursors { + visibility: visible; +} + +.CodeMirror-selected { background: #d9d9d9; } +.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } +.CodeMirror-crosshair { cursor: crosshair; } +.CodeMirror ::selection { background: #d7d4f0; } +.CodeMirror ::-moz-selection { background: #d7d4f0; } + +.cm-searching { + background: #ffa; + background: rgba(255, 255, 0, .4); +} + +/* IE7 hack to prevent it from returning funny offsetTops on the spans */ +.CodeMirror span { *vertical-align: text-bottom; } + +/* Used to force a border model for a node */ +.cm-force-border { padding-right: .1px; } + +@media print { + /* Hide the cursor when printing */ + .CodeMirror div.CodeMirror-cursors { + visibility: hidden; + } +} + +/* See issue #2901 */ +.cm-tab-wrap-hack:after { content: ''; } + +/* Help users use markselection to safely style text background */ +span.CodeMirror-selectedtext { background: none; } \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/public/js/codemirror.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/public/js/codemirror.js new file mode 100644 index 000000000..20f3f95ed --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/public/js/codemirror.js @@ -0,0 +1,8720 @@ +/* + * 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. + */ + +// This is CodeMirror (http://codemirror.net), a code editor +// implemented in JavaScript on top of the browser's DOM. +// +// You can find some technical background for some of the code below +// at http://marijnhaverbeke.nl/blog/#cm-internals . + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + module.exports = mod(); + else if (typeof define == "function" && define.amd) // AMD + return define([], mod); + else // Plain browser env + this.CodeMirror = mod(); +})(function() { + "use strict"; + + // BROWSER SNIFFING + + // Kludges for bugs and behavior differences that can't be feature + // detected are enabled based on userAgent etc sniffing. + + var gecko = /gecko\/\d/i.test(navigator.userAgent); + var ie_upto10 = /MSIE \d/.test(navigator.userAgent); + var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent); + var ie = ie_upto10 || ie_11up; + var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]); + var webkit = /WebKit\//.test(navigator.userAgent); + var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent); + var chrome = /Chrome\//.test(navigator.userAgent); + var presto = /Opera\//.test(navigator.userAgent); + var safari = /Apple Computer/.test(navigator.vendor); + var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent); + var phantom = /PhantomJS/.test(navigator.userAgent); + + var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent); + // This is woefully incomplete. Suggestions for alternative methods welcome. + var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent); + var mac = ios || /Mac/.test(navigator.platform); + var windows = /win/i.test(navigator.platform); + + var presto_version = presto && navigator.userAgent.match(/Version\/(\d*\.\d*)/); + if (presto_version) presto_version = Number(presto_version[1]); + if (presto_version && presto_version >= 15) { presto = false; webkit = true; } + // Some browsers use the wrong event properties to signal cmd/ctrl on OS X + var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11)); + var captureRightClick = gecko || (ie && ie_version >= 9); + + // Optimize some code when these features are not used. + var sawReadOnlySpans = false, sawCollapsedSpans = false; + + // EDITOR CONSTRUCTOR + + // A CodeMirror instance represents an editor. This is the object + // that user code is usually dealing with. + + function CodeMirror(place, options) { + if (!(this instanceof CodeMirror)) return new CodeMirror(place, options); + + this.options = options = options ? copyObj(options) : {}; + // Determine effective options based on given values and defaults. + copyObj(defaults, options, false); + setGuttersForLineNumbers(options); + + var doc = options.value; + if (typeof doc == "string") doc = new Doc(doc, options.mode); + this.doc = doc; + + var input = new CodeMirror.inputStyles[options.inputStyle](this); + var display = this.display = new Display(place, doc, input); + display.wrapper.CodeMirror = this; + updateGutters(this); + themeChanged(this); + if (options.lineWrapping) + this.display.wrapper.className += " CodeMirror-wrap"; + if (options.autofocus && !mobile) display.input.focus(); + initScrollbars(this); + + this.state = { + keyMaps: [], // stores maps added by addKeyMap + overlays: [], // highlighting overlays, as added by addOverlay + modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info + overwrite: false, + delayingBlurEvent: false, + focused: false, + suppressEdits: false, // used to disable editing during key handlers when in readOnly mode + pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in input.poll + draggingText: false, + highlight: new Delayed(), // stores highlight worker timeout + keySeq: null, // Unfinished key sequence + specialChars: null + }; + + var cm = this; + + // Override magic textarea content restore that IE sometimes does + // on our hidden textarea on reload + if (ie && ie_version < 11) setTimeout(function() { cm.display.input.reset(true); }, 20); + + registerEventHandlers(this); + ensureGlobalHandlers(); + + startOperation(this); + this.curOp.forceUpdate = true; + attachDoc(this, doc); + + if ((options.autofocus && !mobile) || cm.hasFocus()) + setTimeout(bind(onFocus, this), 20); + else + onBlur(this); + + for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt)) + optionHandlers[opt](this, options[opt], Init); + maybeUpdateLineNumberWidth(this); + if (options.finishInit) options.finishInit(this); + for (var i = 0; i < initHooks.length; ++i) initHooks[i](this); + endOperation(this); + // Suppress optimizelegibility in Webkit, since it breaks text + // measuring on line wrapping boundaries. + if (webkit && options.lineWrapping && + getComputedStyle(display.lineDiv).textRendering == "optimizelegibility") + display.lineDiv.style.textRendering = "auto"; + } + + // DISPLAY CONSTRUCTOR + + // The display handles the DOM integration, both for input reading + // and content drawing. It holds references to DOM nodes and + // display-related state. + + function Display(place, doc, input) { + var d = this; + this.input = input; + + // Covers bottom-right square when both scrollbars are present. + d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); + d.scrollbarFiller.setAttribute("cm-not-content", "true"); + // Covers bottom of gutter when coverGutterNextToScrollbar is on + // and h scrollbar is present. + d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); + d.gutterFiller.setAttribute("cm-not-content", "true"); + // Will contain the actual code, positioned to cover the viewport. + d.lineDiv = elt("div", null, "CodeMirror-code"); + // Elements are added to these to represent selection and cursors. + d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); + d.cursorDiv = elt("div", null, "CodeMirror-cursors"); + // A visibility: hidden element used to find the size of things. + d.measure = elt("div", null, "CodeMirror-measure"); + // When lines outside of the viewport are measured, they are drawn in this. + d.lineMeasure = elt("div", null, "CodeMirror-measure"); + // Wraps everything that needs to exist inside the vertically-padded coordinate system + d.lineSpace = elt("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], + null, "position: relative; outline: none"); + // Moved around its parent to cover visible view. + d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative"); + // Set to the height of the document, allowing scrolling. + d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); + d.sizerWidth = null; + // Behavior of elts with overflow: auto and padding is + // inconsistent across browsers. This is used to ensure the + // scrollable area is big enough. + d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;"); + // Will contain the gutters, if any. + d.gutters = elt("div", null, "CodeMirror-gutters"); + d.lineGutter = null; + // Actual scrollable element. + d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); + d.scroller.setAttribute("tabIndex", "-1"); + // The element in which the editor lives. + d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); + + // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) + if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } + if (!webkit && !(gecko && mobile)) d.scroller.draggable = true; + + if (place) { + if (place.appendChild) place.appendChild(d.wrapper); + else place(d.wrapper); + } + + // Current rendered range (may be bigger than the view window). + d.viewFrom = d.viewTo = doc.first; + d.reportedViewFrom = d.reportedViewTo = doc.first; + // Information about the rendered lines. + d.view = []; + d.renderedView = null; + // Holds info about a single rendered line when it was rendered + // for measurement, while not in view. + d.externalMeasured = null; + // Empty space (in pixels) above the view + d.viewOffset = 0; + d.lastWrapHeight = d.lastWrapWidth = 0; + d.updateLineNumbers = null; + + d.nativeBarWidth = d.barHeight = d.barWidth = 0; + d.scrollbarsClipped = false; + + // Used to only resize the line number gutter when necessary (when + // the amount of lines crosses a boundary that makes its width change) + d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; + // Set to true when a non-horizontal-scrolling line widget is + // added. As an optimization, line widget aligning is skipped when + // this is false. + d.alignWidgets = false; + + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + + // Tracks the maximum line length so that the horizontal scrollbar + // can be kept static when scrolling. + d.maxLine = null; + d.maxLineLength = 0; + d.maxLineChanged = false; + + // Used for measuring wheel scrolling granularity + d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; + + // True when shift is held down. + d.shift = false; + + // Used to track whether anything happened since the context menu + // was opened. + d.selForContextMenu = null; + + d.activeTouch = null; + + input.init(d); + } + + // STATE UPDATES + + // Used to get the editor into a consistent state again when options change. + + function loadMode(cm) { + cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption); + resetModeState(cm); + } + + function resetModeState(cm) { + cm.doc.iter(function(line) { + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + }); + cm.doc.frontier = cm.doc.first; + startWorker(cm, 100); + cm.state.modeGen++; + if (cm.curOp) regChange(cm); + } + + function wrappingChanged(cm) { + if (cm.options.lineWrapping) { + addClass(cm.display.wrapper, "CodeMirror-wrap"); + cm.display.sizer.style.minWidth = ""; + cm.display.sizerWidth = null; + } else { + rmClass(cm.display.wrapper, "CodeMirror-wrap"); + findMaxLine(cm); + } + estimateLineHeights(cm); + regChange(cm); + clearCaches(cm); + setTimeout(function(){updateScrollbars(cm);}, 100); + } + + // Returns a function that estimates the height of a line, to use as + // first approximation until the line becomes visible (and is thus + // properly measurable). + function estimateHeight(cm) { + var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; + var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); + return function(line) { + if (lineIsHidden(cm.doc, line)) return 0; + + var widgetsHeight = 0; + if (line.widgets) for (var i = 0; i < line.widgets.length; i++) { + if (line.widgets[i].height) widgetsHeight += line.widgets[i].height; + } + + if (wrapping) + return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th; + else + return widgetsHeight + th; + }; + } + + function estimateLineHeights(cm) { + var doc = cm.doc, est = estimateHeight(cm); + doc.iter(function(line) { + var estHeight = est(line); + if (estHeight != line.height) updateLineHeight(line, estHeight); + }); + } + + function themeChanged(cm) { + cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + + cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-"); + clearCaches(cm); + } + + function guttersChanged(cm) { + updateGutters(cm); + regChange(cm); + setTimeout(function(){alignHorizontally(cm);}, 20); + } + + // Rebuild the gutter elements, ensure the margin to the left of the + // code matches their width. + function updateGutters(cm) { + var gutters = cm.display.gutters, specs = cm.options.gutters; + removeChildren(gutters); + for (var i = 0; i < specs.length; ++i) { + var gutterClass = specs[i]; + var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass)); + if (gutterClass == "CodeMirror-linenumbers") { + cm.display.lineGutter = gElt; + gElt.style.width = (cm.display.lineNumWidth || 1) + "px"; + } + } + gutters.style.display = i ? "" : "none"; + updateGutterSpace(cm); + } + + function updateGutterSpace(cm) { + var width = cm.display.gutters.offsetWidth; + cm.display.sizer.style.marginLeft = width + "px"; + } + + // Compute the character length of a line, taking into account + // collapsed ranges (see markText) that might hide parts, and join + // other lines onto it. + function lineLength(line) { + if (line.height == 0) return 0; + var len = line.text.length, merged, cur = line; + while (merged = collapsedSpanAtStart(cur)) { + var found = merged.find(0, true); + cur = found.from.line; + len += found.from.ch - found.to.ch; + } + cur = line; + while (merged = collapsedSpanAtEnd(cur)) { + var found = merged.find(0, true); + len -= cur.text.length - found.from.ch; + cur = found.to.line; + len += cur.text.length - found.to.ch; + } + return len; + } + + // Find the longest line in the document. + function findMaxLine(cm) { + var d = cm.display, doc = cm.doc; + d.maxLine = getLine(doc, doc.first); + d.maxLineLength = lineLength(d.maxLine); + d.maxLineChanged = true; + doc.iter(function(line) { + var len = lineLength(line); + if (len > d.maxLineLength) { + d.maxLineLength = len; + d.maxLine = line; + } + }); + } + + // Make sure the gutters options contains the element + // "CodeMirror-linenumbers" when the lineNumbers option is true. + function setGuttersForLineNumbers(options) { + var found = indexOf(options.gutters, "CodeMirror-linenumbers"); + if (found == -1 && options.lineNumbers) { + options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]); + } else if (found > -1 && !options.lineNumbers) { + options.gutters = options.gutters.slice(0); + options.gutters.splice(found, 1); + } + } + + // SCROLLBARS + + // Prepare DOM reads needed to update the scrollbars. Done in one + // shot to minimize update/measure roundtrips. + function measureForScrollbars(cm) { + var d = cm.display, gutterW = d.gutters.offsetWidth; + var docH = Math.round(cm.doc.height + paddingVert(cm.display)); + return { + clientHeight: d.scroller.clientHeight, + viewHeight: d.wrapper.clientHeight, + scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth, + viewWidth: d.wrapper.clientWidth, + barLeft: cm.options.fixedGutter ? gutterW : 0, + docHeight: docH, + scrollHeight: docH + scrollGap(cm) + d.barHeight, + nativeBarWidth: d.nativeBarWidth, + gutterWidth: gutterW + }; + } + + function NativeScrollbars(place, scroll, cm) { + this.cm = cm; + var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); + var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); + place(vert); place(horiz); + + on(vert, "scroll", function() { + if (vert.clientHeight) scroll(vert.scrollTop, "vertical"); + }); + on(horiz, "scroll", function() { + if (horiz.clientWidth) scroll(horiz.scrollLeft, "horizontal"); + }); + + this.checkedOverlay = false; + // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). + if (ie && ie_version < 8) this.horiz.style.minHeight = this.vert.style.minWidth = "18px"; + } + + NativeScrollbars.prototype = copyObj({ + update: function(measure) { + var needsH = measure.scrollWidth > measure.clientWidth + 1; + var needsV = measure.scrollHeight > measure.clientHeight + 1; + var sWidth = measure.nativeBarWidth; + + if (needsV) { + this.vert.style.display = "block"; + this.vert.style.bottom = needsH ? sWidth + "px" : "0"; + var totalHeight = measure.viewHeight - (needsH ? sWidth : 0); + // A bug in IE8 can cause this value to be negative, so guard it. + this.vert.firstChild.style.height = + Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px"; + } else { + this.vert.style.display = ""; + this.vert.firstChild.style.height = "0"; + } + + if (needsH) { + this.horiz.style.display = "block"; + this.horiz.style.right = needsV ? sWidth + "px" : "0"; + this.horiz.style.left = measure.barLeft + "px"; + var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0); + this.horiz.firstChild.style.width = + (measure.scrollWidth - measure.clientWidth + totalWidth) + "px"; + } else { + this.horiz.style.display = ""; + this.horiz.firstChild.style.width = "0"; + } + + if (!this.checkedOverlay && measure.clientHeight > 0) { + if (sWidth == 0) this.overlayHack(); + this.checkedOverlay = true; + } + + return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0}; + }, + setScrollLeft: function(pos) { + if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos; + }, + setScrollTop: function(pos) { + if (this.vert.scrollTop != pos) this.vert.scrollTop = pos; + }, + overlayHack: function() { + var w = mac && !mac_geMountainLion ? "12px" : "18px"; + this.horiz.style.minHeight = this.vert.style.minWidth = w; + var self = this; + var barMouseDown = function(e) { + if (e_target(e) != self.vert && e_target(e) != self.horiz) + operation(self.cm, onMouseDown)(e); + }; + on(this.vert, "mousedown", barMouseDown); + on(this.horiz, "mousedown", barMouseDown); + }, + clear: function() { + var parent = this.horiz.parentNode; + parent.removeChild(this.horiz); + parent.removeChild(this.vert); + } + }, NativeScrollbars.prototype); + + function NullScrollbars() {} + + NullScrollbars.prototype = copyObj({ + update: function() { return {bottom: 0, right: 0}; }, + setScrollLeft: function() {}, + setScrollTop: function() {}, + clear: function() {} + }, NullScrollbars.prototype); + + CodeMirror.scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars}; + + function initScrollbars(cm) { + if (cm.display.scrollbars) { + cm.display.scrollbars.clear(); + if (cm.display.scrollbars.addClass) + rmClass(cm.display.wrapper, cm.display.scrollbars.addClass); + } + + cm.display.scrollbars = new CodeMirror.scrollbarModel[cm.options.scrollbarStyle](function(node) { + cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller); + // Prevent clicks in the scrollbars from killing focus + on(node, "mousedown", function() { + if (cm.state.focused) setTimeout(function() { cm.display.input.focus(); }, 0); + }); + node.setAttribute("cm-not-content", "true"); + }, function(pos, axis) { + if (axis == "horizontal") setScrollLeft(cm, pos); + else setScrollTop(cm, pos); + }, cm); + if (cm.display.scrollbars.addClass) + addClass(cm.display.wrapper, cm.display.scrollbars.addClass); + } + + function updateScrollbars(cm, measure) { + if (!measure) measure = measureForScrollbars(cm); + var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight; + updateScrollbarsInner(cm, measure); + for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) { + if (startWidth != cm.display.barWidth && cm.options.lineWrapping) + updateHeightsInViewport(cm); + updateScrollbarsInner(cm, measureForScrollbars(cm)); + startWidth = cm.display.barWidth; startHeight = cm.display.barHeight; + } + } + + // Re-synchronize the fake scrollbars with the actual size of the + // content. + function updateScrollbarsInner(cm, measure) { + var d = cm.display; + var sizes = d.scrollbars.update(measure); + + d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px"; + d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px"; + + if (sizes.right && sizes.bottom) { + d.scrollbarFiller.style.display = "block"; + d.scrollbarFiller.style.height = sizes.bottom + "px"; + d.scrollbarFiller.style.width = sizes.right + "px"; + } else d.scrollbarFiller.style.display = ""; + if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { + d.gutterFiller.style.display = "block"; + d.gutterFiller.style.height = sizes.bottom + "px"; + d.gutterFiller.style.width = measure.gutterWidth + "px"; + } else d.gutterFiller.style.display = ""; + } + + // Compute the lines that are visible in a given viewport (defaults + // the the current scroll position). viewport may contain top, + // height, and ensure (see op.scrollToPos) properties. + function visibleLines(display, doc, viewport) { + var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop; + top = Math.floor(top - paddingTop(display)); + var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight; + + var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom); + // Ensure is a {from: {line, ch}, to: {line, ch}} object, and + // forces those lines into the viewport (if possible). + if (viewport && viewport.ensure) { + var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line; + if (ensureFrom < from) { + from = ensureFrom; + to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight); + } else if (Math.min(ensureTo, doc.lastLine()) >= to) { + from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight); + to = ensureTo; + } + } + return {from: from, to: Math.max(to, from + 1)}; + } + + // LINE NUMBERS + + // Re-align line numbers and gutter marks to compensate for + // horizontal scrolling. + function alignHorizontally(cm) { + var display = cm.display, view = display.view; + if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return; + var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; + var gutterW = display.gutters.offsetWidth, left = comp + "px"; + for (var i = 0; i < view.length; i++) if (!view[i].hidden) { + if (cm.options.fixedGutter && view[i].gutter) + view[i].gutter.style.left = left; + var align = view[i].alignable; + if (align) for (var j = 0; j < align.length; j++) + align[j].style.left = left; + } + if (cm.options.fixedGutter) + display.gutters.style.left = (comp + gutterW) + "px"; + } + + // Used to ensure that the line number gutter is still the right + // size for the current document size. Returns true when an update + // is needed. + function maybeUpdateLineNumberWidth(cm) { + if (!cm.options.lineNumbers) return false; + var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; + if (last.length != display.lineNumChars) { + var test = display.measure.appendChild(elt("div", [elt("div", last)], + "CodeMirror-linenumber CodeMirror-gutter-elt")); + var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; + display.lineGutter.style.width = ""; + display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1; + display.lineNumWidth = display.lineNumInnerWidth + padding; + display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; + display.lineGutter.style.width = display.lineNumWidth + "px"; + updateGutterSpace(cm); + return true; + } + return false; + } + + function lineNumberFor(options, i) { + return String(options.lineNumberFormatter(i + options.firstLineNumber)); + } + + // Computes display.scroller.scrollLeft + display.gutters.offsetWidth, + // but using getBoundingClientRect to get a sub-pixel-accurate + // result. + function compensateForHScroll(display) { + return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left; + } + + // DISPLAY DRAWING + + function DisplayUpdate(cm, viewport, force) { + var display = cm.display; + + this.viewport = viewport; + // Store some values that we'll need later (but don't want to force a relayout for) + this.visible = visibleLines(display, cm.doc, viewport); + this.editorIsHidden = !display.wrapper.offsetWidth; + this.wrapperHeight = display.wrapper.clientHeight; + this.wrapperWidth = display.wrapper.clientWidth; + this.oldDisplayWidth = displayWidth(cm); + this.force = force; + this.dims = getDimensions(cm); + this.events = []; + } + + DisplayUpdate.prototype.signal = function(emitter, type) { + if (hasHandler(emitter, type)) + this.events.push(arguments); + }; + DisplayUpdate.prototype.finish = function() { + for (var i = 0; i < this.events.length; i++) + signal.apply(null, this.events[i]); + }; + + function maybeClipScrollbars(cm) { + var display = cm.display; + if (!display.scrollbarsClipped && display.scroller.offsetWidth) { + display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth; + display.heightForcer.style.height = scrollGap(cm) + "px"; + display.sizer.style.marginBottom = -display.nativeBarWidth + "px"; + display.sizer.style.borderRightWidth = scrollGap(cm) + "px"; + display.scrollbarsClipped = true; + } + } + + // Does the actual updating of the line display. Bails out + // (returning false) when there is nothing to be done and forced is + // false. + function updateDisplayIfNeeded(cm, update) { + var display = cm.display, doc = cm.doc; + + if (update.editorIsHidden) { + resetView(cm); + return false; + } + + // Bail out if the visible area is already rendered and nothing changed. + if (!update.force && + update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) && + display.renderedView == display.view && countDirtyView(cm) == 0) + return false; + + if (maybeUpdateLineNumberWidth(cm)) { + resetView(cm); + update.dims = getDimensions(cm); + } + + // Compute a suitable new viewport (from & to) + var end = doc.first + doc.size; + var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first); + var to = Math.min(end, update.visible.to + cm.options.viewportMargin); + if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom); + if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo); + if (sawCollapsedSpans) { + from = visualLineNo(cm.doc, from); + to = visualLineEndNo(cm.doc, to); + } + + var different = from != display.viewFrom || to != display.viewTo || + display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth; + adjustView(cm, from, to); + + display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)); + // Position the mover div to align with the current scroll position + cm.display.mover.style.top = display.viewOffset + "px"; + + var toUpdate = countDirtyView(cm); + if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo)) + return false; + + // For big changes, we hide the enclosing element during the + // update, since that speeds up the operations on most browsers. + var focused = activeElt(); + if (toUpdate > 4) display.lineDiv.style.display = "none"; + patchDisplay(cm, display.updateLineNumbers, update.dims); + if (toUpdate > 4) display.lineDiv.style.display = ""; + display.renderedView = display.view; + // There might have been a widget with a focused element that got + // hidden or updated, if so re-focus it. + if (focused && activeElt() != focused && focused.offsetHeight) focused.focus(); + + // Prevent selection and cursors from interfering with the scroll + // width and height. + removeChildren(display.cursorDiv); + removeChildren(display.selectionDiv); + display.gutters.style.height = 0; + + if (different) { + display.lastWrapHeight = update.wrapperHeight; + display.lastWrapWidth = update.wrapperWidth; + startWorker(cm, 400); + } + + display.updateLineNumbers = null; + + return true; + } + + function postUpdateDisplay(cm, update) { + var force = update.force, viewport = update.viewport; + for (var first = true;; first = false) { + if (first && cm.options.lineWrapping && update.oldDisplayWidth != displayWidth(cm)) { + force = true; + } else { + force = false; + // Clip forced viewport to actual scrollable area. + if (viewport && viewport.top != null) + viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)}; + // Updated line heights might result in the drawn area not + // actually covering the viewport. Keep looping until it does. + update.visible = visibleLines(cm.display, cm.doc, viewport); + if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo) + break; + } + if (!updateDisplayIfNeeded(cm, update)) break; + updateHeightsInViewport(cm); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + setDocumentHeight(cm, barMeasure); + updateScrollbars(cm, barMeasure); + } + + update.signal(cm, "update", cm); + if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) { + update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); + cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo; + } + } + + function updateDisplaySimple(cm, viewport) { + var update = new DisplayUpdate(cm, viewport); + if (updateDisplayIfNeeded(cm, update)) { + updateHeightsInViewport(cm); + postUpdateDisplay(cm, update); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + setDocumentHeight(cm, barMeasure); + updateScrollbars(cm, barMeasure); + update.finish(); + } + } + + function setDocumentHeight(cm, measure) { + cm.display.sizer.style.minHeight = measure.docHeight + "px"; + var total = measure.docHeight + cm.display.barHeight; + cm.display.heightForcer.style.top = total + "px"; + cm.display.gutters.style.height = Math.max(total + scrollGap(cm), measure.clientHeight) + "px"; + } + + // Read the actual heights of the rendered lines, and update their + // stored heights to match. + function updateHeightsInViewport(cm) { + var display = cm.display; + var prevBottom = display.lineDiv.offsetTop; + for (var i = 0; i < display.view.length; i++) { + var cur = display.view[i], height; + if (cur.hidden) continue; + if (ie && ie_version < 8) { + var bot = cur.node.offsetTop + cur.node.offsetHeight; + height = bot - prevBottom; + prevBottom = bot; + } else { + var box = cur.node.getBoundingClientRect(); + height = box.bottom - box.top; + } + var diff = cur.line.height - height; + if (height < 2) height = textHeight(display); + if (diff > .001 || diff < -.001) { + updateLineHeight(cur.line, height); + updateWidgetHeight(cur.line); + if (cur.rest) for (var j = 0; j < cur.rest.length; j++) + updateWidgetHeight(cur.rest[j]); + } + } + } + + // Read and store the height of line widgets associated with the + // given line. + function updateWidgetHeight(line) { + if (line.widgets) for (var i = 0; i < line.widgets.length; ++i) + line.widgets[i].height = line.widgets[i].node.offsetHeight; + } + + // Do a bulk-read of the DOM positions and sizes needed to draw the + // view, so that we don't interleave reading and writing to the DOM. + function getDimensions(cm) { + var d = cm.display, left = {}, width = {}; + var gutterLeft = d.gutters.clientLeft; + for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { + left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft; + width[cm.options.gutters[i]] = n.clientWidth; + } + return {fixedPos: compensateForHScroll(d), + gutterTotalWidth: d.gutters.offsetWidth, + gutterLeft: left, + gutterWidth: width, + wrapperWidth: d.wrapper.clientWidth}; + } + + // Sync the actual display DOM structure with display.view, removing + // nodes for lines that are no longer in view, and creating the ones + // that are not there yet, and updating the ones that are out of + // date. + function patchDisplay(cm, updateNumbersFrom, dims) { + var display = cm.display, lineNumbers = cm.options.lineNumbers; + var container = display.lineDiv, cur = container.firstChild; + + function rm(node) { + var next = node.nextSibling; + // Works around a throw-scroll bug in OS X Webkit + if (webkit && mac && cm.display.currentWheelTarget == node) + node.style.display = "none"; + else + node.parentNode.removeChild(node); + return next; + } + + var view = display.view, lineN = display.viewFrom; + // Loop over the elements in the view, syncing cur (the DOM nodes + // in display.lineDiv) with the view as we go. + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (lineView.hidden) { + } else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet + var node = buildLineElement(cm, lineView, lineN, dims); + container.insertBefore(node, cur); + } else { // Already drawn + while (cur != lineView.node) cur = rm(cur); + var updateNumber = lineNumbers && updateNumbersFrom != null && + updateNumbersFrom <= lineN && lineView.lineNumber; + if (lineView.changes) { + if (indexOf(lineView.changes, "gutter") > -1) updateNumber = false; + updateLineForChanges(cm, lineView, lineN, dims); + } + if (updateNumber) { + removeChildren(lineView.lineNumber); + lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN))); + } + cur = lineView.node.nextSibling; + } + lineN += lineView.size; + } + while (cur) cur = rm(cur); + } + + // When an aspect of a line changes, a string is added to + // lineView.changes. This updates the relevant part of the line's + // DOM structure. + function updateLineForChanges(cm, lineView, lineN, dims) { + for (var j = 0; j < lineView.changes.length; j++) { + var type = lineView.changes[j]; + if (type == "text") updateLineText(cm, lineView); + else if (type == "gutter") updateLineGutter(cm, lineView, lineN, dims); + else if (type == "class") updateLineClasses(lineView); + else if (type == "widget") updateLineWidgets(cm, lineView, dims); + } + lineView.changes = null; + } + + // Lines with gutter elements, widgets or a background class need to + // be wrapped, and have the extra elements added to the wrapper div + function ensureLineWrapped(lineView) { + if (lineView.node == lineView.text) { + lineView.node = elt("div", null, null, "position: relative"); + if (lineView.text.parentNode) + lineView.text.parentNode.replaceChild(lineView.node, lineView.text); + lineView.node.appendChild(lineView.text); + if (ie && ie_version < 8) lineView.node.style.zIndex = 2; + } + return lineView.node; + } + + function updateLineBackground(lineView) { + var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass; + if (cls) cls += " CodeMirror-linebackground"; + if (lineView.background) { + if (cls) lineView.background.className = cls; + else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; } + } else if (cls) { + var wrap = ensureLineWrapped(lineView); + lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild); + } + } + + // Wrapper around buildLineContent which will reuse the structure + // in display.externalMeasured when possible. + function getLineContent(cm, lineView) { + var ext = cm.display.externalMeasured; + if (ext && ext.line == lineView.line) { + cm.display.externalMeasured = null; + lineView.measure = ext.measure; + return ext.built; + } + return buildLineContent(cm, lineView); + } + + // Redraw the line's text. Interacts with the background and text + // classes because the mode may output tokens that influence these + // classes. + function updateLineText(cm, lineView) { + var cls = lineView.text.className; + var built = getLineContent(cm, lineView); + if (lineView.text == lineView.node) lineView.node = built.pre; + lineView.text.parentNode.replaceChild(built.pre, lineView.text); + lineView.text = built.pre; + if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) { + lineView.bgClass = built.bgClass; + lineView.textClass = built.textClass; + updateLineClasses(lineView); + } else if (cls) { + lineView.text.className = cls; + } + } + + function updateLineClasses(lineView) { + updateLineBackground(lineView); + if (lineView.line.wrapClass) + ensureLineWrapped(lineView).className = lineView.line.wrapClass; + else if (lineView.node != lineView.text) + lineView.node.className = ""; + var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass; + lineView.text.className = textClass || ""; + } + + function updateLineGutter(cm, lineView, lineN, dims) { + if (lineView.gutter) { + lineView.node.removeChild(lineView.gutter); + lineView.gutter = null; + } + var markers = lineView.line.gutterMarkers; + if (cm.options.lineNumbers || markers) { + var wrap = ensureLineWrapped(lineView); + var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", "left: " + + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + + "px; width: " + dims.gutterTotalWidth + "px"); + cm.display.input.setUneditable(gutterWrap); + wrap.insertBefore(gutterWrap, lineView.text); + if (lineView.line.gutterClass) + gutterWrap.className += " " + lineView.line.gutterClass; + if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) + lineView.lineNumber = gutterWrap.appendChild( + elt("div", lineNumberFor(cm.options, lineN), + "CodeMirror-linenumber CodeMirror-gutter-elt", + "left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: " + + cm.display.lineNumInnerWidth + "px")); + if (markers) for (var k = 0; k < cm.options.gutters.length; ++k) { + var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id]; + if (found) + gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " + + dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px")); + } + } + } + + function updateLineWidgets(cm, lineView, dims) { + if (lineView.alignable) lineView.alignable = null; + for (var node = lineView.node.firstChild, next; node; node = next) { + var next = node.nextSibling; + if (node.className == "CodeMirror-linewidget") + lineView.node.removeChild(node); + } + insertLineWidgets(cm, lineView, dims); + } + + // Build a line's DOM representation from scratch + function buildLineElement(cm, lineView, lineN, dims) { + var built = getLineContent(cm, lineView); + lineView.text = lineView.node = built.pre; + if (built.bgClass) lineView.bgClass = built.bgClass; + if (built.textClass) lineView.textClass = built.textClass; + + updateLineClasses(lineView); + updateLineGutter(cm, lineView, lineN, dims); + insertLineWidgets(cm, lineView, dims); + return lineView.node; + } + + // A lineView may contain multiple logical lines (when merged by + // collapsed spans). The widgets for all of them need to be drawn. + function insertLineWidgets(cm, lineView, dims) { + insertLineWidgetsFor(cm, lineView.line, lineView, dims, true); + if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) + insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false); + } + + function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) { + if (!line.widgets) return; + var wrap = ensureLineWrapped(lineView); + for (var i = 0, ws = line.widgets; i < ws.length; ++i) { + var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget"); + if (!widget.handleMouseEvents) node.setAttribute("cm-ignore-events", "true"); + positionLineWidget(widget, node, lineView, dims); + cm.display.input.setUneditable(node); + if (allowAbove && widget.above) + wrap.insertBefore(node, lineView.gutter || lineView.text); + else + wrap.appendChild(node); + signalLater(widget, "redraw"); + } + } + + function positionLineWidget(widget, node, lineView, dims) { + if (widget.noHScroll) { + (lineView.alignable || (lineView.alignable = [])).push(node); + var width = dims.wrapperWidth; + node.style.left = dims.fixedPos + "px"; + if (!widget.coverGutter) { + width -= dims.gutterTotalWidth; + node.style.paddingLeft = dims.gutterTotalWidth + "px"; + } + node.style.width = width + "px"; + } + if (widget.coverGutter) { + node.style.zIndex = 5; + node.style.position = "relative"; + if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px"; + } + } + + // POSITION OBJECT + + // A Pos instance represents a position within the text. + var Pos = CodeMirror.Pos = function(line, ch) { + if (!(this instanceof Pos)) return new Pos(line, ch); + this.line = line; this.ch = ch; + }; + + // Compare two positions, return 0 if they are the same, a negative + // number when a is less, and a positive number otherwise. + var cmp = CodeMirror.cmpPos = function(a, b) { return a.line - b.line || a.ch - b.ch; }; + + function copyPos(x) {return Pos(x.line, x.ch);} + function maxPos(a, b) { return cmp(a, b) < 0 ? b : a; } + function minPos(a, b) { return cmp(a, b) < 0 ? a : b; } + + // INPUT HANDLING + + function ensureFocus(cm) { + if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm); } + } + + function isReadOnly(cm) { + return cm.options.readOnly || cm.doc.cantEdit; + } + + // This will be set to an array of strings when copying, so that, + // when pasting, we know what kind of selections the copied text + // was made out of. + var lastCopied = null; + + function applyTextInput(cm, inserted, deleted, sel) { + var doc = cm.doc; + cm.display.shift = false; + if (!sel) sel = doc.sel; + + var textLines = splitLines(inserted), multiPaste = null; + // When pasing N lines into N selections, insert one line per selection + if (cm.state.pasteIncoming && sel.ranges.length > 1) { + if (lastCopied && lastCopied.join("\n") == inserted) + multiPaste = sel.ranges.length % lastCopied.length == 0 && map(lastCopied, splitLines); + else if (textLines.length == sel.ranges.length) + multiPaste = map(textLines, function(l) { return [l]; }); + } + + // Normal behavior is to insert the new text into every selection + for (var i = sel.ranges.length - 1; i >= 0; i--) { + var range = sel.ranges[i]; + var from = range.from(), to = range.to(); + if (range.empty()) { + if (deleted && deleted > 0) // Handle deletion + from = Pos(from.line, from.ch - deleted); + else if (cm.state.overwrite && !cm.state.pasteIncoming) // Handle overwrite + to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)); + } + var updateInput = cm.curOp.updateInput; + var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i % multiPaste.length] : textLines, + origin: cm.state.pasteIncoming ? "paste" : cm.state.cutIncoming ? "cut" : "+input"}; + makeChange(cm.doc, changeEvent); + signalLater(cm, "inputRead", cm, changeEvent); + // When an 'electric' character is inserted, immediately trigger a reindent + if (inserted && !cm.state.pasteIncoming && cm.options.electricChars && + cm.options.smartIndent && range.head.ch < 100 && + (!i || sel.ranges[i - 1].head.line != range.head.line)) { + var mode = cm.getModeAt(range.head); + var end = changeEnd(changeEvent); + if (mode.electricChars) { + for (var j = 0; j < mode.electricChars.length; j++) + if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) { + indentLine(cm, end.line, "smart"); + break; + } + } else if (mode.electricInput) { + if (mode.electricInput.test(getLine(doc, end.line).text.slice(0, end.ch))) + indentLine(cm, end.line, "smart"); + } + } + } + ensureCursorVisible(cm); + cm.curOp.updateInput = updateInput; + cm.curOp.typing = true; + cm.state.pasteIncoming = cm.state.cutIncoming = false; + } + + function copyableRanges(cm) { + var text = [], ranges = []; + for (var i = 0; i < cm.doc.sel.ranges.length; i++) { + var line = cm.doc.sel.ranges[i].head.line; + var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)}; + ranges.push(lineRange); + text.push(cm.getRange(lineRange.anchor, lineRange.head)); + } + return {text: text, ranges: ranges}; + } + + function disableBrowserMagic(field) { + field.setAttribute("autocorrect", "off"); + field.setAttribute("autocapitalize", "off"); + field.setAttribute("spellcheck", "false"); + } + + // TEXTAREA INPUT STYLE + + function TextareaInput(cm) { + this.cm = cm; + // See input.poll and input.reset + this.prevInput = ""; + + // Flag that indicates whether we expect input to appear real soon + // now (after some event like 'keypress' or 'input') and are + // polling intensively. + this.pollingFast = false; + // Self-resetting timeout for the poller + this.polling = new Delayed(); + // Tracks when input.reset has punted to just putting a short + // string into the textarea instead of the full selection. + this.inaccurateSelection = false; + // Used to work around IE issue with selection being forgotten when focus moves away from textarea + this.hasSelection = false; + }; + + function hiddenTextarea() { + var te = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none"); + var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); + // The textarea is kept positioned near the cursor to prevent the + // fact that it'll be scrolled into view on input from scrolling + // our fake cursor out of view. On webkit, when wrap=off, paste is + // very slow. So make the area wide instead. + if (webkit) te.style.width = "1000px"; + else te.setAttribute("wrap", "off"); + // If border: 0; -- iOS fails to open keyboard (issue #1287) + if (ios) te.style.border = "1px solid black"; + disableBrowserMagic(te); + return div; + } + + TextareaInput.prototype = copyObj({ + init: function(display) { + var input = this, cm = this.cm; + + // Wraps and hides input textarea + var div = this.wrapper = hiddenTextarea(); + // The semihidden textarea that is focused when the editor is + // focused, and receives input. + var te = this.textarea = div.firstChild; + display.wrapper.insertBefore(div, display.wrapper.firstChild); + + // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore) + if (ios) te.style.width = "0px"; + + on(te, "input", function() { + if (ie && ie_version >= 9 && input.hasSelection) input.hasSelection = null; + input.poll(); + }); + + on(te, "paste", function() { + // Workaround for webkit bug https://bugs.webkit.org/show_bug.cgi?id=90206 + // Add a char to the end of textarea before paste occur so that + // selection doesn't span to the end of textarea. + if (webkit && !cm.state.fakedLastChar && !(new Date - cm.state.lastMiddleDown < 200)) { + var start = te.selectionStart, end = te.selectionEnd; + te.value += "$"; + // The selection end needs to be set before the start, otherwise there + // can be an intermediate non-empty selection between the two, which + // can override the middle-click paste buffer on linux and cause the + // wrong thing to get pasted. + te.selectionEnd = end; + te.selectionStart = start; + cm.state.fakedLastChar = true; + } + cm.state.pasteIncoming = true; + input.fastPoll(); + }); + + function prepareCopyCut(e) { + if (cm.somethingSelected()) { + lastCopied = cm.getSelections(); + if (input.inaccurateSelection) { + input.prevInput = ""; + input.inaccurateSelection = false; + te.value = lastCopied.join("\n"); + selectInput(te); + } + } else if (!cm.options.lineWiseCopyCut) { + return; + } else { + var ranges = copyableRanges(cm); + lastCopied = ranges.text; + if (e.type == "cut") { + cm.setSelections(ranges.ranges, null, sel_dontScroll); + } else { + input.prevInput = ""; + te.value = ranges.text.join("\n"); + selectInput(te); + } + } + if (e.type == "cut") cm.state.cutIncoming = true; + } + on(te, "cut", prepareCopyCut); + on(te, "copy", prepareCopyCut); + + on(display.scroller, "paste", function(e) { + if (eventInWidget(display, e)) return; + cm.state.pasteIncoming = true; + input.focus(); + }); + + // Prevent normal selection in the editor (we handle our own) + on(display.lineSpace, "selectstart", function(e) { + if (!eventInWidget(display, e)) e_preventDefault(e); + }); + }, + + prepareSelection: function() { + // Redraw the selection and/or cursor + var cm = this.cm, display = cm.display, doc = cm.doc; + var result = prepareSelection(cm); + + // Move the hidden textarea near the cursor to prevent scrolling artifacts + if (cm.options.moveInputWithCursor) { + var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); + var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); + result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, + headPos.top + lineOff.top - wrapOff.top)); + result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, + headPos.left + lineOff.left - wrapOff.left)); + } + + return result; + }, + + showSelection: function(drawn) { + var cm = this.cm, display = cm.display; + removeChildrenAndAdd(display.cursorDiv, drawn.cursors); + removeChildrenAndAdd(display.selectionDiv, drawn.selection); + if (drawn.teTop != null) { + this.wrapper.style.top = drawn.teTop + "px"; + this.wrapper.style.left = drawn.teLeft + "px"; + } + }, + + // Reset the input to correspond to the selection (or to be empty, + // when not typing and nothing is selected) + reset: function(typing) { + if (this.contextMenuPending) return; + var minimal, selected, cm = this.cm, doc = cm.doc; + if (cm.somethingSelected()) { + this.prevInput = ""; + var range = doc.sel.primary(); + minimal = hasCopyEvent && + (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000); + var content = minimal ? "-" : selected || cm.getSelection(); + this.textarea.value = content; + if (cm.state.focused) selectInput(this.textarea); + if (ie && ie_version >= 9) this.hasSelection = content; + } else if (!typing) { + this.prevInput = this.textarea.value = ""; + if (ie && ie_version >= 9) this.hasSelection = null; + } + this.inaccurateSelection = minimal; + }, + + getField: function() { return this.textarea; }, + + supportsTouch: function() { return false; }, + + focus: function() { + if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { + try { this.textarea.focus(); } + catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM + } + }, + + blur: function() { this.textarea.blur(); }, + + resetPosition: function() { + this.wrapper.style.top = this.wrapper.style.left = 0; + }, + + receivedFocus: function() { this.slowPoll(); }, + + // Poll for input changes, using the normal rate of polling. This + // runs as long as the editor is focused. + slowPoll: function() { + var input = this; + if (input.pollingFast) return; + input.polling.set(this.cm.options.pollInterval, function() { + input.poll(); + if (input.cm.state.focused) input.slowPoll(); + }); + }, + + // When an event has just come in that is likely to add or change + // something in the input textarea, we poll faster, to ensure that + // the change appears on the screen quickly. + fastPoll: function() { + var missed = false, input = this; + input.pollingFast = true; + function p() { + var changed = input.poll(); + if (!changed && !missed) {missed = true; input.polling.set(60, p);} + else {input.pollingFast = false; input.slowPoll();} + } + input.polling.set(20, p); + }, + + // Read input from the textarea, and update the document to match. + // When something is selected, it is present in the textarea, and + // selected (unless it is huge, in which case a placeholder is + // used). When nothing is selected, the cursor sits after previously + // seen text (can be empty), which is stored in prevInput (we must + // not reset the textarea when typing, because that breaks IME). + poll: function() { + var cm = this.cm, input = this.textarea, prevInput = this.prevInput; + // Since this is called a *lot*, try to bail out as cheaply as + // possible when it is clear that nothing happened. hasSelection + // will be the case when there is a lot of text in the textarea, + // in which case reading its value would be expensive. + if (!cm.state.focused || (hasSelection(input) && !prevInput) || + isReadOnly(cm) || cm.options.disableInput || cm.state.keySeq) + return false; + // See paste handler for more on the fakedLastChar kludge + if (cm.state.pasteIncoming && cm.state.fakedLastChar) { + input.value = input.value.substring(0, input.value.length - 1); + cm.state.fakedLastChar = false; + } + var text = input.value; + // If nothing changed, bail. + if (text == prevInput && !cm.somethingSelected()) return false; + // Work around nonsensical selection resetting in IE9/10, and + // inexplicable appearance of private area unicode characters on + // some key combos in Mac (#2689). + if (ie && ie_version >= 9 && this.hasSelection === text || + mac && /[\uf700-\uf7ff]/.test(text)) { + cm.display.input.reset(); + return false; + } + + if (cm.doc.sel == cm.display.selForContextMenu) { + var first = text.charCodeAt(0); + if (first == 0x200b && !prevInput) prevInput = "\u200b"; + if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo"); } + } + // Find the part of the input that is actually new + var same = 0, l = Math.min(prevInput.length, text.length); + while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same; + + var self = this; + runInOp(cm, function() { + applyTextInput(cm, text.slice(same), prevInput.length - same); + + // Don't leave long text in the textarea, since it makes further polling slow + if (text.length > 1000 || text.indexOf("\n") > -1) input.value = self.prevInput = ""; + else self.prevInput = text; + }); + return true; + }, + + ensurePolled: function() { + if (this.pollingFast && this.poll()) this.pollingFast = false; + }, + + onKeyPress: function() { + if (ie && ie_version >= 9) this.hasSelection = null; + this.fastPoll(); + }, + + onContextMenu: function(e) { + var input = this, cm = input.cm, display = cm.display, te = input.textarea; + var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; + if (!pos || presto) return; // Opera is difficult. + + // Reset the current text selection only if the click is done outside of the selection + // and 'resetSelectionOnContextMenu' option is true. + var reset = cm.options.resetSelectionOnContextMenu; + if (reset && cm.doc.sel.contains(pos) == -1) + operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll); + + var oldCSS = te.style.cssText; + input.wrapper.style.position = "absolute"; + te.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + + "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: " + + (ie ? "rgba(255, 255, 255, .05)" : "transparent") + + "; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; + if (webkit) var oldScrollY = window.scrollY; // Work around Chrome issue (#2712) + display.input.focus(); + if (webkit) window.scrollTo(null, oldScrollY); + display.input.reset(); + // Adds "Select all" to context menu in FF + if (!cm.somethingSelected()) te.value = input.prevInput = " "; + input.contextMenuPending = true; + display.selForContextMenu = cm.doc.sel; + clearTimeout(display.detectingSelectAll); + + // Select-all will be greyed out if there's nothing to select, so + // this adds a zero-width space so that we can later check whether + // it got selected. + function prepareSelectAllHack() { + if (te.selectionStart != null) { + var selected = cm.somethingSelected(); + var extval = "\u200b" + (selected ? te.value : ""); + te.value = "\u21da"; // Used to catch context-menu undo + te.value = extval; + input.prevInput = selected ? "" : "\u200b"; + te.selectionStart = 1; te.selectionEnd = extval.length; + // Re-set this, in case some other handler touched the + // selection in the meantime. + display.selForContextMenu = cm.doc.sel; + } + } + function rehide() { + input.contextMenuPending = false; + input.wrapper.style.position = "relative"; + te.style.cssText = oldCSS; + if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos); + + // Try to detect the user choosing select-all + if (te.selectionStart != null) { + if (!ie || (ie && ie_version < 9)) prepareSelectAllHack(); + var i = 0, poll = function() { + if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 && + te.selectionEnd > 0 && input.prevInput == "\u200b") + operation(cm, commands.selectAll)(cm); + else if (i++ < 10) display.detectingSelectAll = setTimeout(poll, 500); + else display.input.reset(); + }; + display.detectingSelectAll = setTimeout(poll, 200); + } + } + + if (ie && ie_version >= 9) prepareSelectAllHack(); + if (captureRightClick) { + e_stop(e); + var mouseup = function() { + off(window, "mouseup", mouseup); + setTimeout(rehide, 20); + }; + on(window, "mouseup", mouseup); + } else { + setTimeout(rehide, 50); + } + }, + + setUneditable: nothing, + + needsContentAttribute: false + }, TextareaInput.prototype); + + // CONTENTEDITABLE INPUT STYLE + + function ContentEditableInput(cm) { + this.cm = cm; + this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null; + this.polling = new Delayed(); + this.gracePeriod = false; + } + + ContentEditableInput.prototype = copyObj({ + init: function(display) { + var input = this, cm = input.cm; + var div = input.div = display.lineDiv; + div.contentEditable = "true"; + disableBrowserMagic(div); + + on(div, "paste", function(e) { + var pasted = e.clipboardData && e.clipboardData.getData("text/plain"); + if (pasted) { + e.preventDefault(); + cm.replaceSelection(pasted, null, "paste"); + } + }); + + on(div, "compositionstart", function(e) { + var data = e.data; + input.composing = {sel: cm.doc.sel, data: data, startData: data}; + if (!data) return; + var prim = cm.doc.sel.primary(); + var line = cm.getLine(prim.head.line); + var found = line.indexOf(data, Math.max(0, prim.head.ch - data.length)); + if (found > -1 && found <= prim.head.ch) + input.composing.sel = simpleSelection(Pos(prim.head.line, found), + Pos(prim.head.line, found + data.length)); + }); + on(div, "compositionupdate", function(e) { + input.composing.data = e.data; + }); + on(div, "compositionend", function(e) { + var ours = input.composing; + if (!ours) return; + if (e.data != ours.startData && !/\u200b/.test(e.data)) + ours.data = e.data; + // Need a small delay to prevent other code (input event, + // selection polling) from doing damage when fired right after + // compositionend. + setTimeout(function() { + if (!ours.handled) + input.applyComposition(ours); + if (input.composing == ours) + input.composing = null; + }, 50); + }); + + on(div, "touchstart", function() { + input.forceCompositionEnd(); + }); + + on(div, "input", function() { + if (input.composing) return; + if (!input.pollContent()) + runInOp(input.cm, function() {regChange(cm);}); + }); + + function onCopyCut(e) { + if (cm.somethingSelected()) { + lastCopied = cm.getSelections(); + if (e.type == "cut") cm.replaceSelection("", null, "cut"); + } else if (!cm.options.lineWiseCopyCut) { + return; + } else { + var ranges = copyableRanges(cm); + lastCopied = ranges.text; + if (e.type == "cut") { + cm.operation(function() { + cm.setSelections(ranges.ranges, 0, sel_dontScroll); + cm.replaceSelection("", null, "cut"); + }); + } + } + // iOS exposes the clipboard API, but seems to discard content inserted into it + if (e.clipboardData && !ios) { + e.preventDefault(); + e.clipboardData.clearData(); + e.clipboardData.setData("text/plain", lastCopied.join("\n")); + } else { + // Old-fashioned briefly-focus-a-textarea hack + var kludge = hiddenTextarea(), te = kludge.firstChild; + cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild); + te.value = lastCopied.join("\n"); + var hadFocus = document.activeElement; + selectInput(te); + setTimeout(function() { + cm.display.lineSpace.removeChild(kludge); + hadFocus.focus(); + }, 50); + } + } + on(div, "copy", onCopyCut); + on(div, "cut", onCopyCut); + }, + + prepareSelection: function() { + var result = prepareSelection(this.cm, false); + result.focus = this.cm.state.focused; + return result; + }, + + showSelection: function(info) { + if (!info || !this.cm.display.view.length) return; + if (info.focus) this.showPrimarySelection(); + this.showMultipleSelections(info); + }, + + showPrimarySelection: function() { + var sel = window.getSelection(), prim = this.cm.doc.sel.primary(); + var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset); + var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset); + if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad && + cmp(minPos(curAnchor, curFocus), prim.from()) == 0 && + cmp(maxPos(curAnchor, curFocus), prim.to()) == 0) + return; + + var start = posToDOM(this.cm, prim.from()); + var end = posToDOM(this.cm, prim.to()); + if (!start && !end) return; + + var view = this.cm.display.view; + var old = sel.rangeCount && sel.getRangeAt(0); + if (!start) { + start = {node: view[0].measure.map[2], offset: 0}; + } else if (!end) { // FIXME dangerously hacky + var measure = view[view.length - 1].measure; + var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map; + end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]}; + } + + try { var rng = range(start.node, start.offset, end.offset, end.node); } + catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible + if (rng) { + sel.removeAllRanges(); + sel.addRange(rng); + if (old && sel.anchorNode == null) sel.addRange(old); + else if (gecko) this.startGracePeriod(); + } + this.rememberSelection(); + }, + + startGracePeriod: function() { + var input = this; + clearTimeout(this.gracePeriod); + this.gracePeriod = setTimeout(function() { + input.gracePeriod = false; + if (input.selectionChanged()) + input.cm.operation(function() { input.cm.curOp.selectionChanged = true; }); + }, 20); + }, + + showMultipleSelections: function(info) { + removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors); + removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection); + }, + + rememberSelection: function() { + var sel = window.getSelection(); + this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset; + this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset; + }, + + selectionInEditor: function() { + var sel = window.getSelection(); + if (!sel.rangeCount) return false; + var node = sel.getRangeAt(0).commonAncestorContainer; + return contains(this.div, node); + }, + + focus: function() { + if (this.cm.options.readOnly != "nocursor") this.div.focus(); + }, + blur: function() { this.div.blur(); }, + getField: function() { return this.div; }, + + supportsTouch: function() { return true; }, + + receivedFocus: function() { + var input = this; + if (this.selectionInEditor()) + this.pollSelection(); + else + runInOp(this.cm, function() { input.cm.curOp.selectionChanged = true; }); + + function poll() { + if (input.cm.state.focused) { + input.pollSelection(); + input.polling.set(input.cm.options.pollInterval, poll); + } + } + this.polling.set(this.cm.options.pollInterval, poll); + }, + + selectionChanged: function() { + var sel = window.getSelection(); + return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || + sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset; + }, + + pollSelection: function() { + if (!this.composing && !this.gracePeriod && this.selectionChanged()) { + var sel = window.getSelection(), cm = this.cm; + this.rememberSelection(); + var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); + var head = domToPos(cm, sel.focusNode, sel.focusOffset); + if (anchor && head) runInOp(cm, function() { + setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll); + if (anchor.bad || head.bad) cm.curOp.selectionChanged = true; + }); + } + }, + + pollContent: function() { + var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary(); + var from = sel.from(), to = sel.to(); + if (from.line < display.viewFrom || to.line > display.viewTo - 1) return false; + + var fromIndex; + if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) { + var fromLine = lineNo(display.view[0].line); + var fromNode = display.view[0].node; + } else { + var fromLine = lineNo(display.view[fromIndex].line); + var fromNode = display.view[fromIndex - 1].node.nextSibling; + } + var toIndex = findViewIndex(cm, to.line); + if (toIndex == display.view.length - 1) { + var toLine = display.viewTo - 1; + var toNode = display.view[toIndex].node; + } else { + var toLine = lineNo(display.view[toIndex + 1].line) - 1; + var toNode = display.view[toIndex + 1].node.previousSibling; + } + + var newText = splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine)); + var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length)); + while (newText.length > 1 && oldText.length > 1) { + if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; } + else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; } + else break; + } + + var cutFront = 0, cutEnd = 0; + var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length); + while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront)) + ++cutFront; + var newBot = lst(newText), oldBot = lst(oldText); + var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0), + oldBot.length - (oldText.length == 1 ? cutFront : 0)); + while (cutEnd < maxCutEnd && + newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) + ++cutEnd; + + newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd); + newText[0] = newText[0].slice(cutFront); + + var chFrom = Pos(fromLine, cutFront); + var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0); + if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) { + replaceRange(cm.doc, newText, chFrom, chTo, "+input"); + return true; + } + }, + + ensurePolled: function() { + this.forceCompositionEnd(); + }, + reset: function() { + this.forceCompositionEnd(); + }, + forceCompositionEnd: function() { + if (!this.composing || this.composing.handled) return; + this.applyComposition(this.composing); + this.composing.handled = true; + this.div.blur(); + this.div.focus(); + }, + applyComposition: function(composing) { + if (composing.data && composing.data != composing.startData) + operation(this.cm, applyTextInput)(this.cm, composing.data, 0, composing.sel); + }, + + setUneditable: function(node) { + node.setAttribute("contenteditable", "false"); + }, + + onKeyPress: function(e) { + e.preventDefault(); + operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); + }, + + onContextMenu: nothing, + resetPosition: nothing, + + needsContentAttribute: true + }, ContentEditableInput.prototype); + + function posToDOM(cm, pos) { + var view = findViewForLine(cm, pos.line); + if (!view || view.hidden) return null; + var line = getLine(cm.doc, pos.line); + var info = mapFromLineView(view, line, pos.line); + + var order = getOrder(line), side = "left"; + if (order) { + var partPos = getBidiPartAt(order, pos.ch); + side = partPos % 2 ? "right" : "left"; + } + var result = nodeAndOffsetInLineMap(info.map, pos.ch, "left"); + result.offset = result.collapse == "right" ? result.end : result.start; + return result; + } + + function badPos(pos, bad) { if (bad) pos.bad = true; return pos; } + + function domToPos(cm, node, offset) { + var lineNode; + if (node == cm.display.lineDiv) { + lineNode = cm.display.lineDiv.childNodes[offset]; + if (!lineNode) return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true); + node = null; offset = 0; + } else { + for (lineNode = node;; lineNode = lineNode.parentNode) { + if (!lineNode || lineNode == cm.display.lineDiv) return null; + if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) break; + } + } + for (var i = 0; i < cm.display.view.length; i++) { + var lineView = cm.display.view[i]; + if (lineView.node == lineNode) + return locateNodeInLineView(lineView, node, offset); + } + } + + function locateNodeInLineView(lineView, node, offset) { + var wrapper = lineView.text.firstChild, bad = false; + if (!node || !contains(wrapper, node)) return badPos(Pos(lineNo(lineView.line), 0), true); + if (node == wrapper) { + bad = true; + node = wrapper.childNodes[offset]; + offset = 0; + if (!node) { + var line = lineView.rest ? lst(lineView.rest) : lineView.line; + return badPos(Pos(lineNo(line), line.text.length), bad); + } + } + + var textNode = node.nodeType == 3 ? node : null, topNode = node; + if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) { + textNode = node.firstChild; + if (offset) offset = textNode.nodeValue.length; + } + while (topNode.parentNode != wrapper) topNode = topNode.parentNode; + var measure = lineView.measure, maps = measure.maps; + + function find(textNode, topNode, offset) { + for (var i = -1; i < (maps ? maps.length : 0); i++) { + var map = i < 0 ? measure.map : maps[i]; + for (var j = 0; j < map.length; j += 3) { + var curNode = map[j + 2]; + if (curNode == textNode || curNode == topNode) { + var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]); + var ch = map[j] + offset; + if (offset < 0 || curNode != textNode) ch = map[j + (offset ? 1 : 0)]; + return Pos(line, ch); + } + } + } + } + var found = find(textNode, topNode, offset); + if (found) return badPos(found, bad); + + // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems + for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) { + found = find(after, after.firstChild, 0); + if (found) + return badPos(Pos(found.line, found.ch - dist), bad); + else + dist += after.textContent.length; + } + for (var before = topNode.previousSibling, dist = offset; before; before = before.previousSibling) { + found = find(before, before.firstChild, -1); + if (found) + return badPos(Pos(found.line, found.ch + dist), bad); + else + dist += after.textContent.length; + } + } + + function domTextBetween(cm, from, to, fromLine, toLine) { + var text = "", closing = false; + function recognizeMarker(id) { return function(marker) { return marker.id == id; }; } + function walk(node) { + if (node.nodeType == 1) { + var cmText = node.getAttribute("cm-text"); + if (cmText != null) { + if (cmText == "") cmText = node.textContent.replace(/\u200b/g, ""); + text += cmText; + return; + } + var markerID = node.getAttribute("cm-marker"), range; + if (markerID) { + var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID)); + if (found.length && (range = found[0].find())) + text += getBetween(cm.doc, range.from, range.to).join("\n"); + return; + } + if (node.getAttribute("contenteditable") == "false") return; + for (var i = 0; i < node.childNodes.length; i++) + walk(node.childNodes[i]); + if (/^(pre|div|p)$/i.test(node.nodeName)) + closing = true; + } else if (node.nodeType == 3) { + var val = node.nodeValue; + if (!val) return; + if (closing) { + text += "\n"; + closing = false; + } + text += val; + } + } + for (;;) { + walk(from); + if (from == to) break; + from = from.nextSibling; + } + return text; + } + + CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; + + // SELECTION / CURSOR + + // Selection objects are immutable. A new one is created every time + // the selection changes. A selection is one or more non-overlapping + // (and non-touching) ranges, sorted, and an integer that indicates + // which one is the primary selection (the one that's scrolled into + // view, that getCursor returns, etc). + function Selection(ranges, primIndex) { + this.ranges = ranges; + this.primIndex = primIndex; + } + + Selection.prototype = { + primary: function() { return this.ranges[this.primIndex]; }, + equals: function(other) { + if (other == this) return true; + if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) return false; + for (var i = 0; i < this.ranges.length; i++) { + var here = this.ranges[i], there = other.ranges[i]; + if (cmp(here.anchor, there.anchor) != 0 || cmp(here.head, there.head) != 0) return false; + } + return true; + }, + deepCopy: function() { + for (var out = [], i = 0; i < this.ranges.length; i++) + out[i] = new Range(copyPos(this.ranges[i].anchor), copyPos(this.ranges[i].head)); + return new Selection(out, this.primIndex); + }, + somethingSelected: function() { + for (var i = 0; i < this.ranges.length; i++) + if (!this.ranges[i].empty()) return true; + return false; + }, + contains: function(pos, end) { + if (!end) end = pos; + for (var i = 0; i < this.ranges.length; i++) { + var range = this.ranges[i]; + if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) + return i; + } + return -1; + } + }; + + function Range(anchor, head) { + this.anchor = anchor; this.head = head; + } + + Range.prototype = { + from: function() { return minPos(this.anchor, this.head); }, + to: function() { return maxPos(this.anchor, this.head); }, + empty: function() { + return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch; + } + }; + + // Take an unsorted, potentially overlapping set of ranges, and + // build a selection out of it. 'Consumes' ranges array (modifying + // it). + function normalizeSelection(ranges, primIndex) { + var prim = ranges[primIndex]; + ranges.sort(function(a, b) { return cmp(a.from(), b.from()); }); + primIndex = indexOf(ranges, prim); + for (var i = 1; i < ranges.length; i++) { + var cur = ranges[i], prev = ranges[i - 1]; + if (cmp(prev.to(), cur.from()) >= 0) { + var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to()); + var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head; + if (i <= primIndex) --primIndex; + ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to)); + } + } + return new Selection(ranges, primIndex); + } + + function simpleSelection(anchor, head) { + return new Selection([new Range(anchor, head || anchor)], 0); + } + + // Most of the external API clips given positions to make sure they + // actually exist within the document. + function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));} + function clipPos(doc, pos) { + if (pos.line < doc.first) return Pos(doc.first, 0); + var last = doc.first + doc.size - 1; + if (pos.line > last) return Pos(last, getLine(doc, last).text.length); + return clipToLen(pos, getLine(doc, pos.line).text.length); + } + function clipToLen(pos, linelen) { + var ch = pos.ch; + if (ch == null || ch > linelen) return Pos(pos.line, linelen); + else if (ch < 0) return Pos(pos.line, 0); + else return pos; + } + function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;} + function clipPosArray(doc, array) { + for (var out = [], i = 0; i < array.length; i++) out[i] = clipPos(doc, array[i]); + return out; + } + + // SELECTION UPDATES + + // The 'scroll' parameter given to many of these indicated whether + // the new cursor position should be scrolled into view after + // modifying the selection. + + // If shift is held or the extend flag is set, extends a range to + // include a given position (and optionally a second position). + // Otherwise, simply returns the range between the given positions. + // Used for cursor motion and such. + function extendRange(doc, range, head, other) { + if (doc.cm && doc.cm.display.shift || doc.extend) { + var anchor = range.anchor; + if (other) { + var posBefore = cmp(head, anchor) < 0; + if (posBefore != (cmp(other, anchor) < 0)) { + anchor = head; + head = other; + } else if (posBefore != (cmp(head, other) < 0)) { + head = other; + } + } + return new Range(anchor, head); + } else { + return new Range(other || head, head); + } + } + + // Extend the primary selection range, discard the rest. + function extendSelection(doc, head, other, options) { + setSelection(doc, new Selection([extendRange(doc, doc.sel.primary(), head, other)], 0), options); + } + + // Extend all selections (pos is an array of selections with length + // equal the number of selections) + function extendSelections(doc, heads, options) { + for (var out = [], i = 0; i < doc.sel.ranges.length; i++) + out[i] = extendRange(doc, doc.sel.ranges[i], heads[i], null); + var newSel = normalizeSelection(out, doc.sel.primIndex); + setSelection(doc, newSel, options); + } + + // Updates a single range in the selection. + function replaceOneSelection(doc, i, range, options) { + var ranges = doc.sel.ranges.slice(0); + ranges[i] = range; + setSelection(doc, normalizeSelection(ranges, doc.sel.primIndex), options); + } + + // Reset the selection to a single range. + function setSimpleSelection(doc, anchor, head, options) { + setSelection(doc, simpleSelection(anchor, head), options); + } + + // Give beforeSelectionChange handlers a change to influence a + // selection update. + function filterSelectionChange(doc, sel) { + var obj = { + ranges: sel.ranges, + update: function(ranges) { + this.ranges = []; + for (var i = 0; i < ranges.length; i++) + this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), + clipPos(doc, ranges[i].head)); + } + }; + signal(doc, "beforeSelectionChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj); + if (obj.ranges != sel.ranges) return normalizeSelection(obj.ranges, obj.ranges.length - 1); + else return sel; + } + + function setSelectionReplaceHistory(doc, sel, options) { + var done = doc.history.done, last = lst(done); + if (last && last.ranges) { + done[done.length - 1] = sel; + setSelectionNoUndo(doc, sel, options); + } else { + setSelection(doc, sel, options); + } + } + + // Set a new selection. + function setSelection(doc, sel, options) { + setSelectionNoUndo(doc, sel, options); + addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options); + } + + function setSelectionNoUndo(doc, sel, options) { + if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) + sel = filterSelectionChange(doc, sel); + + var bias = options && options.bias || + (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1); + setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)); + + if (!(options && options.scroll === false) && doc.cm) + ensureCursorVisible(doc.cm); + } + + function setSelectionInner(doc, sel) { + if (sel.equals(doc.sel)) return; + + doc.sel = sel; + + if (doc.cm) { + doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = true; + signalCursorActivity(doc.cm); + } + signalLater(doc, "cursorActivity", doc); + } + + // Verify that the selection does not partially select any atomic + // marked ranges. + function reCheckSelection(doc) { + setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false), sel_dontScroll); + } + + // Return a selection that does not partially select any atomic + // ranges. + function skipAtomicInSelection(doc, sel, bias, mayClear) { + var out; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + var newAnchor = skipAtomic(doc, range.anchor, bias, mayClear); + var newHead = skipAtomic(doc, range.head, bias, mayClear); + if (out || newAnchor != range.anchor || newHead != range.head) { + if (!out) out = sel.ranges.slice(0, i); + out[i] = new Range(newAnchor, newHead); + } + } + return out ? normalizeSelection(out, sel.primIndex) : sel; + } + + // Ensure a given position is not inside an atomic range. + function skipAtomic(doc, pos, bias, mayClear) { + var flipped = false, curPos = pos; + var dir = bias || 1; + doc.cantEdit = false; + search: for (;;) { + var line = getLine(doc, curPos.line); + if (line.markedSpans) { + for (var i = 0; i < line.markedSpans.length; ++i) { + var sp = line.markedSpans[i], m = sp.marker; + if ((sp.from == null || (m.inclusiveLeft ? sp.from <= curPos.ch : sp.from < curPos.ch)) && + (sp.to == null || (m.inclusiveRight ? sp.to >= curPos.ch : sp.to > curPos.ch))) { + if (mayClear) { + signal(m, "beforeCursorEnter"); + if (m.explicitlyCleared) { + if (!line.markedSpans) break; + else {--i; continue;} + } + } + if (!m.atomic) continue; + var newPos = m.find(dir < 0 ? -1 : 1); + if (cmp(newPos, curPos) == 0) { + newPos.ch += dir; + if (newPos.ch < 0) { + if (newPos.line > doc.first) newPos = clipPos(doc, Pos(newPos.line - 1)); + else newPos = null; + } else if (newPos.ch > line.text.length) { + if (newPos.line < doc.first + doc.size - 1) newPos = Pos(newPos.line + 1, 0); + else newPos = null; + } + if (!newPos) { + if (flipped) { + // Driven in a corner -- no valid cursor position found at all + // -- try again *with* clearing, if we didn't already + if (!mayClear) return skipAtomic(doc, pos, bias, true); + // Otherwise, turn off editing until further notice, and return the start of the doc + doc.cantEdit = true; + return Pos(doc.first, 0); + } + flipped = true; newPos = pos; dir = -dir; + } + } + curPos = newPos; + continue search; + } + } + } + return curPos; + } + } + + // SELECTION DRAWING + + function updateSelection(cm) { + cm.display.input.showSelection(cm.display.input.prepareSelection()); + } + + function prepareSelection(cm, primary) { + var doc = cm.doc, result = {}; + var curFragment = result.cursors = document.createDocumentFragment(); + var selFragment = result.selection = document.createDocumentFragment(); + + for (var i = 0; i < doc.sel.ranges.length; i++) { + if (primary === false && i == doc.sel.primIndex) continue; + var range = doc.sel.ranges[i]; + var collapsed = range.empty(); + if (collapsed || cm.options.showCursorWhenSelecting) + drawSelectionCursor(cm, range, curFragment); + if (!collapsed) + drawSelectionRange(cm, range, selFragment); + } + return result; + } + + // Draws a cursor for the given range + function drawSelectionCursor(cm, range, output) { + var pos = cursorCoords(cm, range.head, "div", null, null, !cm.options.singleCursorHeightPerLine); + + var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")); + cursor.style.left = pos.left + "px"; + cursor.style.top = pos.top + "px"; + cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; + + if (pos.other) { + // Secondary cursor, shown when on a 'jump' in bi-directional text + var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor")); + otherCursor.style.display = ""; + otherCursor.style.left = pos.other.left + "px"; + otherCursor.style.top = pos.other.top + "px"; + otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; + } + } + + // Draws the given range as a highlighted selection + function drawSelectionRange(cm, range, output) { + var display = cm.display, doc = cm.doc; + var fragment = document.createDocumentFragment(); + var padding = paddingH(cm.display), leftSide = padding.left; + var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right; + + function add(left, top, width, bottom) { + if (top < 0) top = 0; + top = Math.round(top); + bottom = Math.round(bottom); + fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left + + "px; top: " + top + "px; width: " + (width == null ? rightSide - left : width) + + "px; height: " + (bottom - top) + "px")); + } + + function drawForLine(line, fromArg, toArg) { + var lineObj = getLine(doc, line); + var lineLen = lineObj.text.length; + var start, end; + function coords(ch, bias) { + return charCoords(cm, Pos(line, ch), "div", lineObj, bias); + } + + iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) { + var leftPos = coords(from, "left"), rightPos, left, right; + if (from == to) { + rightPos = leftPos; + left = right = leftPos.left; + } else { + rightPos = coords(to - 1, "right"); + if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; } + left = leftPos.left; + right = rightPos.right; + } + if (fromArg == null && from == 0) left = leftSide; + if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part + add(left, leftPos.top, null, leftPos.bottom); + left = leftSide; + if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top); + } + if (toArg == null && to == lineLen) right = rightSide; + if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left) + start = leftPos; + if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right) + end = rightPos; + if (left < leftSide + 1) left = leftSide; + add(left, rightPos.top, right - left, rightPos.bottom); + }); + return {start: start, end: end}; + } + + var sFrom = range.from(), sTo = range.to(); + if (sFrom.line == sTo.line) { + drawForLine(sFrom.line, sFrom.ch, sTo.ch); + } else { + var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line); + var singleVLine = visualLine(fromLine) == visualLine(toLine); + var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end; + var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start; + if (singleVLine) { + if (leftEnd.top < rightStart.top - 2) { + add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); + add(leftSide, rightStart.top, rightStart.left, rightStart.bottom); + } else { + add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); + } + } + if (leftEnd.bottom < rightStart.top) + add(leftSide, leftEnd.bottom, null, rightStart.top); + } + + output.appendChild(fragment); + } + + // Cursor-blinking + function restartBlink(cm) { + if (!cm.state.focused) return; + var display = cm.display; + clearInterval(display.blinker); + var on = true; + display.cursorDiv.style.visibility = ""; + if (cm.options.cursorBlinkRate > 0) + display.blinker = setInterval(function() { + display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden"; + }, cm.options.cursorBlinkRate); + else if (cm.options.cursorBlinkRate < 0) + display.cursorDiv.style.visibility = "hidden"; + } + + // HIGHLIGHT WORKER + + function startWorker(cm, time) { + if (cm.doc.mode.startState && cm.doc.frontier < cm.display.viewTo) + cm.state.highlight.set(time, bind(highlightWorker, cm)); + } + + function highlightWorker(cm) { + var doc = cm.doc; + if (doc.frontier < doc.first) doc.frontier = doc.first; + if (doc.frontier >= cm.display.viewTo) return; + var end = +new Date + cm.options.workTime; + var state = copyState(doc.mode, getStateBefore(cm, doc.frontier)); + var changedLines = []; + + doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function(line) { + if (doc.frontier >= cm.display.viewFrom) { // Visible + var oldStyles = line.styles; + var highlighted = highlightLine(cm, line, state, true); + line.styles = highlighted.styles; + var oldCls = line.styleClasses, newCls = highlighted.classes; + if (newCls) line.styleClasses = newCls; + else if (oldCls) line.styleClasses = null; + var ischange = !oldStyles || oldStyles.length != line.styles.length || + oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass); + for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i]; + if (ischange) changedLines.push(doc.frontier); + line.stateAfter = copyState(doc.mode, state); + } else { + processLine(cm, line.text, state); + line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null; + } + ++doc.frontier; + if (+new Date > end) { + startWorker(cm, cm.options.workDelay); + return true; + } + }); + if (changedLines.length) runInOp(cm, function() { + for (var i = 0; i < changedLines.length; i++) + regLineChange(cm, changedLines[i], "text"); + }); + } + + // Finds the line to start with when starting a parse. Tries to + // find a line with a stateAfter, so that it can start with a + // valid state. If that fails, it returns the line with the + // smallest indentation, which tends to need the least context to + // parse correctly. + function findStartLine(cm, n, precise) { + var minindent, minline, doc = cm.doc; + var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100); + for (var search = n; search > lim; --search) { + if (search <= doc.first) return doc.first; + var line = getLine(doc, search - 1); + if (line.stateAfter && (!precise || search <= doc.frontier)) return search; + var indented = countColumn(line.text, null, cm.options.tabSize); + if (minline == null || minindent > indented) { + minline = search - 1; + minindent = indented; + } + } + return minline; + } + + function getStateBefore(cm, n, precise) { + var doc = cm.doc, display = cm.display; + if (!doc.mode.startState) return true; + var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter; + if (!state) state = startState(doc.mode); + else state = copyState(doc.mode, state); + doc.iter(pos, n, function(line) { + processLine(cm, line.text, state); + var save = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo; + line.stateAfter = save ? copyState(doc.mode, state) : null; + ++pos; + }); + if (precise) doc.frontier = pos; + return state; + } + + // POSITION MEASUREMENT + + function paddingTop(display) {return display.lineSpace.offsetTop;} + function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;} + function paddingH(display) { + if (display.cachedPaddingH) return display.cachedPaddingH; + var e = removeChildrenAndAdd(display.measure, elt("pre", "x")); + var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle; + var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)}; + if (!isNaN(data.left) && !isNaN(data.right)) display.cachedPaddingH = data; + return data; + } + + function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth; } + function displayWidth(cm) { + return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth; + } + function displayHeight(cm) { + return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight; + } + + // Ensure the lineView.wrapping.heights array is populated. This is + // an array of bottom offsets for the lines that make up a drawn + // line. When lineWrapping is on, there might be more than one + // height. + function ensureLineHeights(cm, lineView, rect) { + var wrapping = cm.options.lineWrapping; + var curWidth = wrapping && displayWidth(cm); + if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) { + var heights = lineView.measure.heights = []; + if (wrapping) { + lineView.measure.width = curWidth; + var rects = lineView.text.firstChild.getClientRects(); + for (var i = 0; i < rects.length - 1; i++) { + var cur = rects[i], next = rects[i + 1]; + if (Math.abs(cur.bottom - next.bottom) > 2) + heights.push((cur.bottom + next.top) / 2 - rect.top); + } + } + heights.push(rect.bottom - rect.top); + } + } + + // Find a line map (mapping character offsets to text nodes) and a + // measurement cache for the given line number. (A line view might + // contain multiple lines when collapsed ranges are present.) + function mapFromLineView(lineView, line, lineN) { + if (lineView.line == line) + return {map: lineView.measure.map, cache: lineView.measure.cache}; + for (var i = 0; i < lineView.rest.length; i++) + if (lineView.rest[i] == line) + return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]}; + for (var i = 0; i < lineView.rest.length; i++) + if (lineNo(lineView.rest[i]) > lineN) + return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i], before: true}; + } + + // Render a line into the hidden node display.externalMeasured. Used + // when measurement is needed for a line that's not in the viewport. + function updateExternalMeasurement(cm, line) { + line = visualLine(line); + var lineN = lineNo(line); + var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN); + view.lineN = lineN; + var built = view.built = buildLineContent(cm, view); + view.text = built.pre; + removeChildrenAndAdd(cm.display.lineMeasure, built.pre); + return view; + } + + // Get a {top, bottom, left, right} box (in line-local coordinates) + // for a given character. + function measureChar(cm, line, ch, bias) { + return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias); + } + + // Find a line view that corresponds to the given line number. + function findViewForLine(cm, lineN) { + if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo) + return cm.display.view[findViewIndex(cm, lineN)]; + var ext = cm.display.externalMeasured; + if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size) + return ext; + } + + // Measurement can be split in two steps, the set-up work that + // applies to the whole line, and the measurement of the actual + // character. Functions like coordsChar, that need to do a lot of + // measurements in a row, can thus ensure that the set-up work is + // only done once. + function prepareMeasureForLine(cm, line) { + var lineN = lineNo(line); + var view = findViewForLine(cm, lineN); + if (view && !view.text) + view = null; + else if (view && view.changes) + updateLineForChanges(cm, view, lineN, getDimensions(cm)); + if (!view) + view = updateExternalMeasurement(cm, line); + + var info = mapFromLineView(view, line, lineN); + return { + line: line, view: view, rect: null, + map: info.map, cache: info.cache, before: info.before, + hasHeights: false + }; + } + + // Given a prepared measurement object, measures the position of an + // actual character (or fetches it from the cache). + function measureCharPrepared(cm, prepared, ch, bias, varHeight) { + if (prepared.before) ch = -1; + var key = ch + (bias || ""), found; + if (prepared.cache.hasOwnProperty(key)) { + found = prepared.cache[key]; + } else { + if (!prepared.rect) + prepared.rect = prepared.view.text.getBoundingClientRect(); + if (!prepared.hasHeights) { + ensureLineHeights(cm, prepared.view, prepared.rect); + prepared.hasHeights = true; + } + found = measureCharInner(cm, prepared, ch, bias); + if (!found.bogus) prepared.cache[key] = found; + } + return {left: found.left, right: found.right, + top: varHeight ? found.rtop : found.top, + bottom: varHeight ? found.rbottom : found.bottom}; + } + + var nullRect = {left: 0, right: 0, top: 0, bottom: 0}; + + function nodeAndOffsetInLineMap(map, ch, bias) { + var node, start, end, collapse; + // First, search the line map for the text node corresponding to, + // or closest to, the target character. + for (var i = 0; i < map.length; i += 3) { + var mStart = map[i], mEnd = map[i + 1]; + if (ch < mStart) { + start = 0; end = 1; + collapse = "left"; + } else if (ch < mEnd) { + start = ch - mStart; + end = start + 1; + } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) { + end = mEnd - mStart; + start = end - 1; + if (ch >= mEnd) collapse = "right"; + } + if (start != null) { + node = map[i + 2]; + if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) + collapse = bias; + if (bias == "left" && start == 0) + while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) { + node = map[(i -= 3) + 2]; + collapse = "left"; + } + if (bias == "right" && start == mEnd - mStart) + while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) { + node = map[(i += 3) + 2]; + collapse = "right"; + } + break; + } + } + return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd}; + } + + function measureCharInner(cm, prepared, ch, bias) { + var place = nodeAndOffsetInLineMap(prepared.map, ch, bias); + var node = place.node, start = place.start, end = place.end, collapse = place.collapse; + + var rect; + if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. + for (var i = 0; i < 4; i++) { // Retry a maximum of 4 times when nonsense rectangles are returned + while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) --start; + while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) ++end; + if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) { + rect = node.parentNode.getBoundingClientRect(); + } else if (ie && cm.options.lineWrapping) { + var rects = range(node, start, end).getClientRects(); + if (rects.length) + rect = rects[bias == "right" ? rects.length - 1 : 0]; + else + rect = nullRect; + } else { + rect = range(node, start, end).getBoundingClientRect() || nullRect; + } + if (rect.left || rect.right || start == 0) break; + end = start; + start = start - 1; + collapse = "right"; + } + if (ie && ie_version < 11) rect = maybeUpdateRectForZooming(cm.display.measure, rect); + } else { // If it is a widget, simply get the box for the whole widget. + if (start > 0) collapse = bias = "right"; + var rects; + if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1) + rect = rects[bias == "right" ? rects.length - 1 : 0]; + else + rect = node.getBoundingClientRect(); + } + if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) { + var rSpan = node.parentNode.getClientRects()[0]; + if (rSpan) + rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom}; + else + rect = nullRect; + } + + var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top; + var mid = (rtop + rbot) / 2; + var heights = prepared.view.measure.heights; + for (var i = 0; i < heights.length - 1; i++) + if (mid < heights[i]) break; + var top = i ? heights[i - 1] : 0, bot = heights[i]; + var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left, + right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left, + top: top, bottom: bot}; + if (!rect.left && !rect.right) result.bogus = true; + if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; } + + return result; + } + + // Work around problem with bounding client rects on ranges being + // returned incorrectly when zoomed on IE10 and below. + function maybeUpdateRectForZooming(measure, rect) { + if (!window.screen || screen.logicalXDPI == null || + screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure)) + return rect; + var scaleX = screen.logicalXDPI / screen.deviceXDPI; + var scaleY = screen.logicalYDPI / screen.deviceYDPI; + return {left: rect.left * scaleX, right: rect.right * scaleX, + top: rect.top * scaleY, bottom: rect.bottom * scaleY}; + } + + function clearLineMeasurementCacheFor(lineView) { + if (lineView.measure) { + lineView.measure.cache = {}; + lineView.measure.heights = null; + if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) + lineView.measure.caches[i] = {}; + } + } + + function clearLineMeasurementCache(cm) { + cm.display.externalMeasure = null; + removeChildren(cm.display.lineMeasure); + for (var i = 0; i < cm.display.view.length; i++) + clearLineMeasurementCacheFor(cm.display.view[i]); + } + + function clearCaches(cm) { + clearLineMeasurementCache(cm); + cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null; + if (!cm.options.lineWrapping) cm.display.maxLineChanged = true; + cm.display.lineNumChars = null; + } + + function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; } + function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; } + + // Converts a {top, bottom, left, right} box from line-local + // coordinates into another coordinate system. Context may be one of + // "line", "div" (display.lineDiv), "local"/null (editor), "window", + // or "page". + function intoCoordSystem(cm, lineObj, rect, context) { + if (lineObj.widgets) for (var i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) { + var size = widgetHeight(lineObj.widgets[i]); + rect.top += size; rect.bottom += size; + } + if (context == "line") return rect; + if (!context) context = "local"; + var yOff = heightAtLine(lineObj); + if (context == "local") yOff += paddingTop(cm.display); + else yOff -= cm.display.viewOffset; + if (context == "page" || context == "window") { + var lOff = cm.display.lineSpace.getBoundingClientRect(); + yOff += lOff.top + (context == "window" ? 0 : pageScrollY()); + var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()); + rect.left += xOff; rect.right += xOff; + } + rect.top += yOff; rect.bottom += yOff; + return rect; + } + + // Coverts a box from "div" coords to another coordinate system. + // Context may be "window", "page", "div", or "local"/null. + function fromCoordSystem(cm, coords, context) { + if (context == "div") return coords; + var left = coords.left, top = coords.top; + // First move into "page" coordinate system + if (context == "page") { + left -= pageScrollX(); + top -= pageScrollY(); + } else if (context == "local" || !context) { + var localBox = cm.display.sizer.getBoundingClientRect(); + left += localBox.left; + top += localBox.top; + } + + var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect(); + return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top}; + } + + function charCoords(cm, pos, context, lineObj, bias) { + if (!lineObj) lineObj = getLine(cm.doc, pos.line); + return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context); + } + + // Returns a box for a given cursor position, which may have an + // 'other' property containing the position of the secondary cursor + // on a bidi boundary. + function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) { + lineObj = lineObj || getLine(cm.doc, pos.line); + if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj); + function get(ch, right) { + var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight); + if (right) m.left = m.right; else m.right = m.left; + return intoCoordSystem(cm, lineObj, m, context); + } + function getBidi(ch, partPos) { + var part = order[partPos], right = part.level % 2; + if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) { + part = order[--partPos]; + ch = bidiRight(part) - (part.level % 2 ? 0 : 1); + right = true; + } else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) { + part = order[++partPos]; + ch = bidiLeft(part) - part.level % 2; + right = false; + } + if (right && ch == part.to && ch > part.from) return get(ch - 1); + return get(ch, right); + } + var order = getOrder(lineObj), ch = pos.ch; + if (!order) return get(ch); + var partPos = getBidiPartAt(order, ch); + var val = getBidi(ch, partPos); + if (bidiOther != null) val.other = getBidi(ch, bidiOther); + return val; + } + + // Used to cheaply estimate the coordinates for a position. Used for + // intermediate scroll updates. + function estimateCoords(cm, pos) { + var left = 0, pos = clipPos(cm.doc, pos); + if (!cm.options.lineWrapping) left = charWidth(cm.display) * pos.ch; + var lineObj = getLine(cm.doc, pos.line); + var top = heightAtLine(lineObj) + paddingTop(cm.display); + return {left: left, right: left, top: top, bottom: top + lineObj.height}; + } + + // Positions returned by coordsChar contain some extra information. + // xRel is the relative x position of the input coordinates compared + // to the found position (so xRel > 0 means the coordinates are to + // the right of the character position, for example). When outside + // is true, that means the coordinates lie outside the line's + // vertical range. + function PosWithInfo(line, ch, outside, xRel) { + var pos = Pos(line, ch); + pos.xRel = xRel; + if (outside) pos.outside = true; + return pos; + } + + // Compute the character position closest to the given coordinates. + // Input must be lineSpace-local ("div" coordinate system). + function coordsChar(cm, x, y) { + var doc = cm.doc; + y += cm.display.viewOffset; + if (y < 0) return PosWithInfo(doc.first, 0, true, -1); + var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1; + if (lineN > last) + return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1); + if (x < 0) x = 0; + + var lineObj = getLine(doc, lineN); + for (;;) { + var found = coordsCharInner(cm, lineObj, lineN, x, y); + var merged = collapsedSpanAtEnd(lineObj); + var mergedPos = merged && merged.find(0, true); + if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0)) + lineN = lineNo(lineObj = mergedPos.to.line); + else + return found; + } + } + + function coordsCharInner(cm, lineObj, lineNo, x, y) { + var innerOff = y - heightAtLine(lineObj); + var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth; + var preparedMeasure = prepareMeasureForLine(cm, lineObj); + + function getX(ch) { + var sp = cursorCoords(cm, Pos(lineNo, ch), "line", lineObj, preparedMeasure); + wrongLine = true; + if (innerOff > sp.bottom) return sp.left - adjust; + else if (innerOff < sp.top) return sp.left + adjust; + else wrongLine = false; + return sp.left; + } + + var bidi = getOrder(lineObj), dist = lineObj.text.length; + var from = lineLeft(lineObj), to = lineRight(lineObj); + var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine; + + if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1); + // Do a binary search between these bounds. + for (;;) { + if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) { + var ch = x < fromX || x - fromX <= toX - x ? from : to; + var xDiff = x - (ch == from ? fromX : toX); + while (isExtendingChar(lineObj.text.charAt(ch))) ++ch; + var pos = PosWithInfo(lineNo, ch, ch == from ? fromOutside : toOutside, + xDiff < -1 ? -1 : xDiff > 1 ? 1 : 0); + return pos; + } + var step = Math.ceil(dist / 2), middle = from + step; + if (bidi) { + middle = from; + for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1); + } + var middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;} + else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;} + } + } + + var measureText; + // Compute the default text height. + function textHeight(display) { + if (display.cachedTextHeight != null) return display.cachedTextHeight; + if (measureText == null) { + measureText = elt("pre"); + // Measure a bunch of lines, for browsers that compute + // fractional heights. + for (var i = 0; i < 49; ++i) { + measureText.appendChild(document.createTextNode("x")); + measureText.appendChild(elt("br")); + } + measureText.appendChild(document.createTextNode("x")); + } + removeChildrenAndAdd(display.measure, measureText); + var height = measureText.offsetHeight / 50; + if (height > 3) display.cachedTextHeight = height; + removeChildren(display.measure); + return height || 1; + } + + // Compute the default character width. + function charWidth(display) { + if (display.cachedCharWidth != null) return display.cachedCharWidth; + var anchor = elt("span", "xxxxxxxxxx"); + var pre = elt("pre", [anchor]); + removeChildrenAndAdd(display.measure, pre); + var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10; + if (width > 2) display.cachedCharWidth = width; + return width || 10; + } + + // OPERATIONS + + // Operations are used to wrap a series of changes to the editor + // state in such a way that each change won't have to update the + // cursor and display (which would be awkward, slow, and + // error-prone). Instead, display updates are batched and then all + // combined and executed at once. + + var operationGroup = null; + + var nextOpId = 0; + // Start a new operation. + function startOperation(cm) { + cm.curOp = { + cm: cm, + viewChanged: false, // Flag that indicates that lines might need to be redrawn + startHeight: cm.doc.height, // Used to detect need to update scrollbar + forceUpdate: false, // Used to force a redraw + updateInput: null, // Whether to reset the input textarea + typing: false, // Whether this reset should be careful to leave existing text (for compositing) + changeObjs: null, // Accumulated changes, for firing change events + cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on + cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already + selectionChanged: false, // Whether the selection needs to be redrawn + updateMaxLine: false, // Set when the widest line needs to be determined anew + scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet + scrollToPos: null, // Used to scroll to a specific position + focus: false, + id: ++nextOpId // Unique ID + }; + if (operationGroup) { + operationGroup.ops.push(cm.curOp); + } else { + cm.curOp.ownsGroup = operationGroup = { + ops: [cm.curOp], + delayedCallbacks: [] + }; + } + } + + function fireCallbacksForOps(group) { + // Calls delayed callbacks and cursorActivity handlers until no + // new ones appear + var callbacks = group.delayedCallbacks, i = 0; + do { + for (; i < callbacks.length; i++) + callbacks[i](); + for (var j = 0; j < group.ops.length; j++) { + var op = group.ops[j]; + if (op.cursorActivityHandlers) + while (op.cursorActivityCalled < op.cursorActivityHandlers.length) + op.cursorActivityHandlers[op.cursorActivityCalled++](op.cm); + } + } while (i < callbacks.length); + } + + // Finish an operation, updating the display and signalling delayed events + function endOperation(cm) { + var op = cm.curOp, group = op.ownsGroup; + if (!group) return; + + try { fireCallbacksForOps(group); } + finally { + operationGroup = null; + for (var i = 0; i < group.ops.length; i++) + group.ops[i].cm.curOp = null; + endOperations(group); + } + } + + // The DOM updates done when an operation finishes are batched so + // that the minimum number of relayouts are required. + function endOperations(group) { + var ops = group.ops; + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_R1(ops[i]); + for (var i = 0; i < ops.length; i++) // Write DOM (maybe) + endOperation_W1(ops[i]); + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_R2(ops[i]); + for (var i = 0; i < ops.length; i++) // Write DOM (maybe) + endOperation_W2(ops[i]); + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_finish(ops[i]); + } + + function endOperation_R1(op) { + var cm = op.cm, display = cm.display; + maybeClipScrollbars(cm); + if (op.updateMaxLine) findMaxLine(cm); + + op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null || + op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom || + op.scrollToPos.to.line >= display.viewTo) || + display.maxLineChanged && cm.options.lineWrapping; + op.update = op.mustUpdate && + new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate); + } + + function endOperation_W1(op) { + op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update); + } + + function endOperation_R2(op) { + var cm = op.cm, display = cm.display; + if (op.updatedDisplay) updateHeightsInViewport(cm); + + op.barMeasure = measureForScrollbars(cm); + + // If the max line changed since it was last measured, measure it, + // and ensure the document's width matches it. + // updateDisplay_W2 will use these properties to do the actual resizing + if (display.maxLineChanged && !cm.options.lineWrapping) { + op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3; + cm.display.sizerWidth = op.adjustWidthTo; + op.barMeasure.scrollWidth = + Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth); + op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm)); + } + + if (op.updatedDisplay || op.selectionChanged) + op.preparedSelection = display.input.prepareSelection(); + } + + function endOperation_W2(op) { + var cm = op.cm; + + if (op.adjustWidthTo != null) { + cm.display.sizer.style.minWidth = op.adjustWidthTo + "px"; + if (op.maxScrollLeft < cm.doc.scrollLeft) + setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true); + cm.display.maxLineChanged = false; + } + + if (op.preparedSelection) + cm.display.input.showSelection(op.preparedSelection); + if (op.updatedDisplay) + setDocumentHeight(cm, op.barMeasure); + if (op.updatedDisplay || op.startHeight != cm.doc.height) + updateScrollbars(cm, op.barMeasure); + + if (op.selectionChanged) restartBlink(cm); + + if (cm.state.focused && op.updateInput) + cm.display.input.reset(op.typing); + if (op.focus && op.focus == activeElt()) ensureFocus(op.cm); + } + + function endOperation_finish(op) { + var cm = op.cm, display = cm.display, doc = cm.doc; + + if (op.updatedDisplay) postUpdateDisplay(cm, op.update); + + // Abort mouse wheel delta measurement, when scrolling explicitly + if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos)) + display.wheelStartX = display.wheelStartY = null; + + // Propagate the scroll position to the actual DOM scroller + if (op.scrollTop != null && (display.scroller.scrollTop != op.scrollTop || op.forceScroll)) { + doc.scrollTop = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop)); + display.scrollbars.setScrollTop(doc.scrollTop); + display.scroller.scrollTop = doc.scrollTop; + } + if (op.scrollLeft != null && (display.scroller.scrollLeft != op.scrollLeft || op.forceScroll)) { + doc.scrollLeft = Math.max(0, Math.min(display.scroller.scrollWidth - displayWidth(cm), op.scrollLeft)); + display.scrollbars.setScrollLeft(doc.scrollLeft); + display.scroller.scrollLeft = doc.scrollLeft; + alignHorizontally(cm); + } + // If we need to scroll a specific position into view, do so. + if (op.scrollToPos) { + var coords = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from), + clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin); + if (op.scrollToPos.isCursor && cm.state.focused) maybeScrollWindow(cm, coords); + } + + // Fire events for markers that are hidden/unidden by editing or + // undoing + var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; + if (hidden) for (var i = 0; i < hidden.length; ++i) + if (!hidden[i].lines.length) signal(hidden[i], "hide"); + if (unhidden) for (var i = 0; i < unhidden.length; ++i) + if (unhidden[i].lines.length) signal(unhidden[i], "unhide"); + + if (display.wrapper.offsetHeight) + doc.scrollTop = cm.display.scroller.scrollTop; + + // Fire change events, and delayed event handlers + if (op.changeObjs) + signal(cm, "changes", cm, op.changeObjs); + if (op.update) + op.update.finish(); + } + + // Run the given function in an operation + function runInOp(cm, f) { + if (cm.curOp) return f(); + startOperation(cm); + try { return f(); } + finally { endOperation(cm); } + } + // Wraps a function in an operation. Returns the wrapped function. + function operation(cm, f) { + return function() { + if (cm.curOp) return f.apply(cm, arguments); + startOperation(cm); + try { return f.apply(cm, arguments); } + finally { endOperation(cm); } + }; + } + // Used to add methods to editor and doc instances, wrapping them in + // operations. + function methodOp(f) { + return function() { + if (this.curOp) return f.apply(this, arguments); + startOperation(this); + try { return f.apply(this, arguments); } + finally { endOperation(this); } + }; + } + function docMethodOp(f) { + return function() { + var cm = this.cm; + if (!cm || cm.curOp) return f.apply(this, arguments); + startOperation(cm); + try { return f.apply(this, arguments); } + finally { endOperation(cm); } + }; + } + + // VIEW TRACKING + + // These objects are used to represent the visible (currently drawn) + // part of the document. A LineView may correspond to multiple + // logical lines, if those are connected by collapsed ranges. + function LineView(doc, line, lineN) { + // The starting line + this.line = line; + // Continuing lines, if any + this.rest = visualLineContinued(line); + // Number of logical lines in this visual line + this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1; + this.node = this.text = null; + this.hidden = lineIsHidden(doc, line); + } + + // Create a range of LineView objects for the given lines. + function buildViewArray(cm, from, to) { + var array = [], nextPos; + for (var pos = from; pos < to; pos = nextPos) { + var view = new LineView(cm.doc, getLine(cm.doc, pos), pos); + nextPos = pos + view.size; + array.push(view); + } + return array; + } + + // Updates the display.view data structure for a given change to the + // document. From and to are in pre-change coordinates. Lendiff is + // the amount of lines added or subtracted by the change. This is + // used for changes that span multiple lines, or change the way + // lines are divided into visual lines. regLineChange (below) + // registers single-line changes. + function regChange(cm, from, to, lendiff) { + if (from == null) from = cm.doc.first; + if (to == null) to = cm.doc.first + cm.doc.size; + if (!lendiff) lendiff = 0; + + var display = cm.display; + if (lendiff && to < display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers > from)) + display.updateLineNumbers = from; + + cm.curOp.viewChanged = true; + + if (from >= display.viewTo) { // Change after + if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo) + resetView(cm); + } else if (to <= display.viewFrom) { // Change before + if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) { + resetView(cm); + } else { + display.viewFrom += lendiff; + display.viewTo += lendiff; + } + } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap + resetView(cm); + } else if (from <= display.viewFrom) { // Top overlap + var cut = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cut) { + display.view = display.view.slice(cut.index); + display.viewFrom = cut.lineN; + display.viewTo += lendiff; + } else { + resetView(cm); + } + } else if (to >= display.viewTo) { // Bottom overlap + var cut = viewCuttingPoint(cm, from, from, -1); + if (cut) { + display.view = display.view.slice(0, cut.index); + display.viewTo = cut.lineN; + } else { + resetView(cm); + } + } else { // Gap in the middle + var cutTop = viewCuttingPoint(cm, from, from, -1); + var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cutTop && cutBot) { + display.view = display.view.slice(0, cutTop.index) + .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN)) + .concat(display.view.slice(cutBot.index)); + display.viewTo += lendiff; + } else { + resetView(cm); + } + } + + var ext = display.externalMeasured; + if (ext) { + if (to < ext.lineN) + ext.lineN += lendiff; + else if (from < ext.lineN + ext.size) + display.externalMeasured = null; + } + } + + // Register a change to a single line. Type must be one of "text", + // "gutter", "class", "widget" + function regLineChange(cm, line, type) { + cm.curOp.viewChanged = true; + var display = cm.display, ext = cm.display.externalMeasured; + if (ext && line >= ext.lineN && line < ext.lineN + ext.size) + display.externalMeasured = null; + + if (line < display.viewFrom || line >= display.viewTo) return; + var lineView = display.view[findViewIndex(cm, line)]; + if (lineView.node == null) return; + var arr = lineView.changes || (lineView.changes = []); + if (indexOf(arr, type) == -1) arr.push(type); + } + + // Clear the view. + function resetView(cm) { + cm.display.viewFrom = cm.display.viewTo = cm.doc.first; + cm.display.view = []; + cm.display.viewOffset = 0; + } + + // Find the view element corresponding to a given line. Return null + // when the line isn't visible. + function findViewIndex(cm, n) { + if (n >= cm.display.viewTo) return null; + n -= cm.display.viewFrom; + if (n < 0) return null; + var view = cm.display.view; + for (var i = 0; i < view.length; i++) { + n -= view[i].size; + if (n < 0) return i; + } + } + + function viewCuttingPoint(cm, oldN, newN, dir) { + var index = findViewIndex(cm, oldN), diff, view = cm.display.view; + if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size) + return {index: index, lineN: newN}; + for (var i = 0, n = cm.display.viewFrom; i < index; i++) + n += view[i].size; + if (n != oldN) { + if (dir > 0) { + if (index == view.length - 1) return null; + diff = (n + view[index].size) - oldN; + index++; + } else { + diff = n - oldN; + } + oldN += diff; newN += diff; + } + while (visualLineNo(cm.doc, newN) != newN) { + if (index == (dir < 0 ? 0 : view.length - 1)) return null; + newN += dir * view[index - (dir < 0 ? 1 : 0)].size; + index += dir; + } + return {index: index, lineN: newN}; + } + + // Force the view to cover a given range, adding empty view element + // or clipping off existing ones as needed. + function adjustView(cm, from, to) { + var display = cm.display, view = display.view; + if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) { + display.view = buildViewArray(cm, from, to); + display.viewFrom = from; + } else { + if (display.viewFrom > from) + display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view); + else if (display.viewFrom < from) + display.view = display.view.slice(findViewIndex(cm, from)); + display.viewFrom = from; + if (display.viewTo < to) + display.view = display.view.concat(buildViewArray(cm, display.viewTo, to)); + else if (display.viewTo > to) + display.view = display.view.slice(0, findViewIndex(cm, to)); + } + display.viewTo = to; + } + + // Count the number of lines in the view whose DOM representation is + // out of date (or nonexistent). + function countDirtyView(cm) { + var view = cm.display.view, dirty = 0; + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (!lineView.hidden && (!lineView.node || lineView.changes)) ++dirty; + } + return dirty; + } + + // EVENT HANDLERS + + // Attach the necessary event handlers when initializing the editor + function registerEventHandlers(cm) { + var d = cm.display; + on(d.scroller, "mousedown", operation(cm, onMouseDown)); + // Older IE's will not fire a second mousedown for a double click + if (ie && ie_version < 11) + on(d.scroller, "dblclick", operation(cm, function(e) { + if (signalDOMEvent(cm, e)) return; + var pos = posFromMouse(cm, e); + if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return; + e_preventDefault(e); + var word = cm.findWordAt(pos); + extendSelection(cm.doc, word.anchor, word.head); + })); + else + on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); }); + // Some browsers fire contextmenu *after* opening the menu, at + // which point we can't mess with it anymore. Context menu is + // handled in onMouseDown for these browsers. + if (!captureRightClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);}); + + // Used to suppress mouse event handling when a touch happens + var touchFinished, prevTouch = {end: 0}; + function finishTouch() { + if (d.activeTouch) { + touchFinished = setTimeout(function() {d.activeTouch = null;}, 1000); + prevTouch = d.activeTouch; + prevTouch.end = +new Date; + } + }; + function isMouseLikeTouchEvent(e) { + if (e.touches.length != 1) return false; + var touch = e.touches[0]; + return touch.radiusX <= 1 && touch.radiusY <= 1; + } + function farAway(touch, other) { + if (other.left == null) return true; + var dx = other.left - touch.left, dy = other.top - touch.top; + return dx * dx + dy * dy > 20 * 20; + } + on(d.scroller, "touchstart", function(e) { + if (!isMouseLikeTouchEvent(e)) { + clearTimeout(touchFinished); + var now = +new Date; + d.activeTouch = {start: now, moved: false, + prev: now - prevTouch.end <= 300 ? prevTouch : null}; + if (e.touches.length == 1) { + d.activeTouch.left = e.touches[0].pageX; + d.activeTouch.top = e.touches[0].pageY; + } + } + }); + on(d.scroller, "touchmove", function() { + if (d.activeTouch) d.activeTouch.moved = true; + }); + on(d.scroller, "touchend", function(e) { + var touch = d.activeTouch; + if (touch && !eventInWidget(d, e) && touch.left != null && + !touch.moved && new Date - touch.start < 300) { + var pos = cm.coordsChar(d.activeTouch, "page"), range; + if (!touch.prev || farAway(touch, touch.prev)) // Single tap + range = new Range(pos, pos); + else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap + range = cm.findWordAt(pos); + else // Triple tap + range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))); + cm.setSelection(range.anchor, range.head); + cm.focus(); + e_preventDefault(e); + } + finishTouch(); + }); + on(d.scroller, "touchcancel", finishTouch); + + // Sync scrolling between fake scrollbars and real scrollable + // area, ensure viewport is updated when scrolling. + on(d.scroller, "scroll", function() { + if (d.scroller.clientHeight) { + setScrollTop(cm, d.scroller.scrollTop); + setScrollLeft(cm, d.scroller.scrollLeft, true); + signal(cm, "scroll", cm); + } + }); + + // Listen to wheel events in order to try and update the viewport on time. + on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);}); + on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);}); + + // Prevent wrapper from ever scrolling + on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); + + d.dragFunctions = { + simple: function(e) {if (!signalDOMEvent(cm, e)) e_stop(e);}, + start: function(e){onDragStart(cm, e);}, + drop: operation(cm, onDrop) + }; + + var inp = d.input.getField(); + on(inp, "keyup", function(e) { onKeyUp.call(cm, e); }); + on(inp, "keydown", operation(cm, onKeyDown)); + on(inp, "keypress", operation(cm, onKeyPress)); + on(inp, "focus", bind(onFocus, cm)); + on(inp, "blur", bind(onBlur, cm)); + } + + function dragDropChanged(cm, value, old) { + var wasOn = old && old != CodeMirror.Init; + if (!value != !wasOn) { + var funcs = cm.display.dragFunctions; + var toggle = value ? on : off; + toggle(cm.display.scroller, "dragstart", funcs.start); + toggle(cm.display.scroller, "dragenter", funcs.simple); + toggle(cm.display.scroller, "dragover", funcs.simple); + toggle(cm.display.scroller, "drop", funcs.drop); + } + } + + // Called when the window resizes + function onResize(cm) { + var d = cm.display; + if (d.lastWrapHeight == d.wrapper.clientHeight && d.lastWrapWidth == d.wrapper.clientWidth) + return; + // Might be a text scaling operation, clear size caches. + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + d.scrollbarsClipped = false; + cm.setSize(); + } + + // MOUSE EVENTS + + // Return true when the given mouse event happened in a widget + function eventInWidget(display, e) { + for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { + if (!n || (n.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") || + (n.parentNode == display.sizer && n != display.mover)) + return true; + } + } + + // Given a mouse event, find the corresponding position. If liberal + // is false, it checks whether a gutter or scrollbar was clicked, + // and returns null if it was. forRect is used by rectangular + // selections, and tries to estimate a character position even for + // coordinates beyond the right of the text. + function posFromMouse(cm, e, liberal, forRect) { + var display = cm.display; + if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") return null; + + var x, y, space = display.lineSpace.getBoundingClientRect(); + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX - space.left; y = e.clientY - space.top; } + catch (e) { return null; } + var coords = coordsChar(cm, x, y), line; + if (forRect && coords.xRel == 1 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) { + var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length; + coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff)); + } + return coords; + } + + // A mouse down can be a single click, double click, triple click, + // start of selection drag, start of text drag, new cursor + // (ctrl-click), rectangle drag (alt-drag), or xwin + // middle-click-paste. Or it might be a click on something we should + // not interfere with, such as a scrollbar or widget. + function onMouseDown(e) { + var cm = this, display = cm.display; + if (display.activeTouch && display.input.supportsTouch() || signalDOMEvent(cm, e)) return; + display.shift = e.shiftKey; + + if (eventInWidget(display, e)) { + if (!webkit) { + // Briefly turn off draggability, to allow widgets to do + // normal dragging things. + display.scroller.draggable = false; + setTimeout(function(){display.scroller.draggable = true;}, 100); + } + return; + } + if (clickInGutter(cm, e)) return; + var start = posFromMouse(cm, e); + window.focus(); + + switch (e_button(e)) { + case 1: + if (start) + leftButtonDown(cm, e, start); + else if (e_target(e) == display.scroller) + e_preventDefault(e); + break; + case 2: + if (webkit) cm.state.lastMiddleDown = +new Date; + if (start) extendSelection(cm.doc, start); + setTimeout(function() {display.input.focus();}, 20); + e_preventDefault(e); + break; + case 3: + if (captureRightClick) onContextMenu(cm, e); + else delayBlurEvent(cm); + break; + } + } + + var lastClick, lastDoubleClick; + function leftButtonDown(cm, e, start) { + if (ie) setTimeout(bind(ensureFocus, cm), 0); + else cm.curOp.focus = activeElt(); + + var now = +new Date, type; + if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) { + type = "triple"; + } else if (lastClick && lastClick.time > now - 400 && cmp(lastClick.pos, start) == 0) { + type = "double"; + lastDoubleClick = {time: now, pos: start}; + } else { + type = "single"; + lastClick = {time: now, pos: start}; + } + + var sel = cm.doc.sel, modifier = mac ? e.metaKey : e.ctrlKey, contained; + if (cm.options.dragDrop && dragAndDrop && !isReadOnly(cm) && + type == "single" && (contained = sel.contains(start)) > -1 && + !sel.ranges[contained].empty()) + leftButtonStartDrag(cm, e, start, modifier); + else + leftButtonSelect(cm, e, start, type, modifier); + } + + // Start a text drag. When it ends, see if any dragging actually + // happen, and treat as a click if it didn't. + function leftButtonStartDrag(cm, e, start, modifier) { + var display = cm.display; + var dragEnd = operation(cm, function(e2) { + if (webkit) display.scroller.draggable = false; + cm.state.draggingText = false; + off(document, "mouseup", dragEnd); + off(display.scroller, "drop", dragEnd); + if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { + e_preventDefault(e2); + if (!modifier) + extendSelection(cm.doc, start); + // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081) + if (webkit || ie && ie_version == 9) + setTimeout(function() {document.body.focus(); display.input.focus();}, 20); + else + display.input.focus(); + } + }); + // Let the drag handler handle this. + if (webkit) display.scroller.draggable = true; + cm.state.draggingText = dragEnd; + // IE's approach to draggable + if (display.scroller.dragDrop) display.scroller.dragDrop(); + on(document, "mouseup", dragEnd); + on(display.scroller, "drop", dragEnd); + } + + // Normal selection, as opposed to text dragging. + function leftButtonSelect(cm, e, start, type, addNew) { + var display = cm.display, doc = cm.doc; + e_preventDefault(e); + + var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges; + if (addNew && !e.shiftKey) { + ourIndex = doc.sel.contains(start); + if (ourIndex > -1) + ourRange = ranges[ourIndex]; + else + ourRange = new Range(start, start); + } else { + ourRange = doc.sel.primary(); + ourIndex = doc.sel.primIndex; + } + + if (e.altKey) { + type = "rect"; + if (!addNew) ourRange = new Range(start, start); + start = posFromMouse(cm, e, true, true); + ourIndex = -1; + } else if (type == "double") { + var word = cm.findWordAt(start); + if (cm.display.shift || doc.extend) + ourRange = extendRange(doc, ourRange, word.anchor, word.head); + else + ourRange = word; + } else if (type == "triple") { + var line = new Range(Pos(start.line, 0), clipPos(doc, Pos(start.line + 1, 0))); + if (cm.display.shift || doc.extend) + ourRange = extendRange(doc, ourRange, line.anchor, line.head); + else + ourRange = line; + } else { + ourRange = extendRange(doc, ourRange, start); + } + + if (!addNew) { + ourIndex = 0; + setSelection(doc, new Selection([ourRange], 0), sel_mouse); + startSel = doc.sel; + } else if (ourIndex == -1) { + ourIndex = ranges.length; + setSelection(doc, normalizeSelection(ranges.concat([ourRange]), ourIndex), + {scroll: false, origin: "*mouse"}); + } else if (ranges.length > 1 && ranges[ourIndex].empty() && type == "single" && !e.shiftKey) { + setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0)); + startSel = doc.sel; + } else { + replaceOneSelection(doc, ourIndex, ourRange, sel_mouse); + } + + var lastPos = start; + function extendTo(pos) { + if (cmp(lastPos, pos) == 0) return; + lastPos = pos; + + if (type == "rect") { + var ranges = [], tabSize = cm.options.tabSize; + var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize); + var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize); + var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol); + for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line)); + line <= end; line++) { + var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize); + if (left == right) + ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))); + else if (text.length > leftPos) + ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))); + } + if (!ranges.length) ranges.push(new Range(start, start)); + setSelection(doc, normalizeSelection(startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), + {origin: "*mouse", scroll: false}); + cm.scrollIntoView(pos); + } else { + var oldRange = ourRange; + var anchor = oldRange.anchor, head = pos; + if (type != "single") { + if (type == "double") + var range = cm.findWordAt(pos); + else + var range = new Range(Pos(pos.line, 0), clipPos(doc, Pos(pos.line + 1, 0))); + if (cmp(range.anchor, anchor) > 0) { + head = range.head; + anchor = minPos(oldRange.from(), range.anchor); + } else { + head = range.anchor; + anchor = maxPos(oldRange.to(), range.head); + } + } + var ranges = startSel.ranges.slice(0); + ranges[ourIndex] = new Range(clipPos(doc, anchor), head); + setSelection(doc, normalizeSelection(ranges, ourIndex), sel_mouse); + } + } + + var editorSize = display.wrapper.getBoundingClientRect(); + // Used to ensure timeout re-tries don't fire when another extend + // happened in the meantime (clearTimeout isn't reliable -- at + // least on Chrome, the timeouts still happen even when cleared, + // if the clear happens after their scheduled firing time). + var counter = 0; + + function extend(e) { + var curCount = ++counter; + var cur = posFromMouse(cm, e, true, type == "rect"); + if (!cur) return; + if (cmp(cur, lastPos) != 0) { + cm.curOp.focus = activeElt(); + extendTo(cur); + var visible = visibleLines(display, doc); + if (cur.line >= visible.to || cur.line < visible.from) + setTimeout(operation(cm, function(){if (counter == curCount) extend(e);}), 150); + } else { + var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0; + if (outside) setTimeout(operation(cm, function() { + if (counter != curCount) return; + display.scroller.scrollTop += outside; + extend(e); + }), 50); + } + } + + function done(e) { + counter = Infinity; + e_preventDefault(e); + display.input.focus(); + off(document, "mousemove", move); + off(document, "mouseup", up); + doc.history.lastSelOrigin = null; + } + + var move = operation(cm, function(e) { + if (!e_button(e)) done(e); + else extend(e); + }); + var up = operation(cm, done); + on(document, "mousemove", move); + on(document, "mouseup", up); + } + + // Determines whether an event happened in the gutter, and fires the + // handlers for the corresponding event. + function gutterEvent(cm, e, type, prevent, signalfn) { + try { var mX = e.clientX, mY = e.clientY; } + catch(e) { return false; } + if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false; + if (prevent) e_preventDefault(e); + + var display = cm.display; + var lineBox = display.lineDiv.getBoundingClientRect(); + + if (mY > lineBox.bottom || !hasHandler(cm, type)) return e_defaultPrevented(e); + mY -= lineBox.top - display.viewOffset; + + for (var i = 0; i < cm.options.gutters.length; ++i) { + var g = display.gutters.childNodes[i]; + if (g && g.getBoundingClientRect().right >= mX) { + var line = lineAtHeight(cm.doc, mY); + var gutter = cm.options.gutters[i]; + signalfn(cm, type, cm, line, gutter, e); + return e_defaultPrevented(e); + } + } + } + + function clickInGutter(cm, e) { + return gutterEvent(cm, e, "gutterClick", true, signalLater); + } + + // Kludge to work around strange IE behavior where it'll sometimes + // re-fire a series of drag-related events right after the drop (#1551) + var lastDrop = 0; + + function onDrop(e) { + var cm = this; + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) + return; + e_preventDefault(e); + if (ie) lastDrop = +new Date; + var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; + if (!pos || isReadOnly(cm)) return; + // Might be a file drop, in which case we simply extract the text + // and insert it. + if (files && files.length && window.FileReader && window.File) { + var n = files.length, text = Array(n), read = 0; + var loadFile = function(file, i) { + var reader = new FileReader; + reader.onload = operation(cm, function() { + text[i] = reader.result; + if (++read == n) { + pos = clipPos(cm.doc, pos); + var change = {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"}; + makeChange(cm.doc, change); + setSelectionReplaceHistory(cm.doc, simpleSelection(pos, changeEnd(change))); + } + }); + reader.readAsText(file); + }; + for (var i = 0; i < n; ++i) loadFile(files[i], i); + } else { // Normal drop + // Don't do a replace if the drop happened inside of the selected text. + if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) { + cm.state.draggingText(e); + // Ensure the editor is re-focused + setTimeout(function() {cm.display.input.focus();}, 20); + return; + } + try { + var text = e.dataTransfer.getData("Text"); + if (text) { + if (cm.state.draggingText && !(mac ? e.altKey : e.ctrlKey)) + var selected = cm.listSelections(); + setSelectionNoUndo(cm.doc, simpleSelection(pos, pos)); + if (selected) for (var i = 0; i < selected.length; ++i) + replaceRange(cm.doc, "", selected[i].anchor, selected[i].head, "drag"); + cm.replaceSelection(text, "around", "paste"); + cm.display.input.focus(); + } + } + catch(e){} + } + } + + function onDragStart(cm, e) { + if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; } + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return; + + e.dataTransfer.setData("Text", cm.getSelection()); + + // Use dummy image instead of default browsers image. + // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. + if (e.dataTransfer.setDragImage && !safari) { + var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); + img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + if (presto) { + img.width = img.height = 1; + cm.display.wrapper.appendChild(img); + // Force a relayout, or Opera won't use our image for some obscure reason + img._top = img.offsetTop; + } + e.dataTransfer.setDragImage(img, 0, 0); + if (presto) img.parentNode.removeChild(img); + } + } + + // SCROLL EVENTS + + // Sync the scrollable area and scrollbars, ensure the viewport + // covers the visible area. + function setScrollTop(cm, val) { + if (Math.abs(cm.doc.scrollTop - val) < 2) return; + cm.doc.scrollTop = val; + if (!gecko) updateDisplaySimple(cm, {top: val}); + if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val; + cm.display.scrollbars.setScrollTop(val); + if (gecko) updateDisplaySimple(cm); + startWorker(cm, 100); + } + // Sync scroller and scrollbar, ensure the gutter elements are + // aligned. + function setScrollLeft(cm, val, isScroller) { + if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return; + val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth); + cm.doc.scrollLeft = val; + alignHorizontally(cm); + if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val; + cm.display.scrollbars.setScrollLeft(val); + } + + // Since the delta values reported on mouse wheel events are + // unstandardized between browsers and even browser versions, and + // generally horribly unpredictable, this code starts by measuring + // the scroll effect that the first few mouse wheel events have, + // and, from that, detects the way it can convert deltas to pixel + // offsets afterwards. + // + // The reason we want to know the amount a wheel event will scroll + // is that it gives us a chance to update the display before the + // actual scrolling happens, reducing flickering. + + var wheelSamples = 0, wheelPixelsPerUnit = null; + // Fill in a browser-detected starting value on browsers where we + // know one. These don't have to be accurate -- the result of them + // being wrong would just be a slight flicker on the first wheel + // scroll (if it is large enough). + if (ie) wheelPixelsPerUnit = -.53; + else if (gecko) wheelPixelsPerUnit = 15; + else if (chrome) wheelPixelsPerUnit = -.7; + else if (safari) wheelPixelsPerUnit = -1/3; + + var wheelEventDelta = function(e) { + var dx = e.wheelDeltaX, dy = e.wheelDeltaY; + if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail; + if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail; + else if (dy == null) dy = e.wheelDelta; + return {x: dx, y: dy}; + }; + CodeMirror.wheelEventPixels = function(e) { + var delta = wheelEventDelta(e); + delta.x *= wheelPixelsPerUnit; + delta.y *= wheelPixelsPerUnit; + return delta; + }; + + function onScrollWheel(cm, e) { + var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y; + + var display = cm.display, scroll = display.scroller; + // Quit if there's nothing to scroll here + if (!(dx && scroll.scrollWidth > scroll.clientWidth || + dy && scroll.scrollHeight > scroll.clientHeight)) return; + + // Webkit browsers on OS X abort momentum scrolls when the target + // of the scroll event is removed from the scrollable element. + // This hack (see related code in patchDisplay) makes sure the + // element is kept around. + if (dy && mac && webkit) { + outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { + for (var i = 0; i < view.length; i++) { + if (view[i].node == cur) { + cm.display.currentWheelTarget = cur; + break outer; + } + } + } + } + + // On some browsers, horizontal scrolling will cause redraws to + // happen before the gutter has been realigned, causing it to + // wriggle around in a most unseemly way. When we have an + // estimated pixels/delta value, we just handle horizontal + // scrolling entirely here. It'll be slightly off from native, but + // better than glitching out. + if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { + if (dy) + setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight))); + setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth))); + e_preventDefault(e); + display.wheelStartX = null; // Abort measurement, if in progress + return; + } + + // 'Project' the visible viewport to cover the area that is being + // scrolled into view (if we know enough to estimate it). + if (dy && wheelPixelsPerUnit != null) { + var pixels = dy * wheelPixelsPerUnit; + var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; + if (pixels < 0) top = Math.max(0, top + pixels - 50); + else bot = Math.min(cm.doc.height, bot + pixels + 50); + updateDisplaySimple(cm, {top: top, bottom: bot}); + } + + if (wheelSamples < 20) { + if (display.wheelStartX == null) { + display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop; + display.wheelDX = dx; display.wheelDY = dy; + setTimeout(function() { + if (display.wheelStartX == null) return; + var movedX = scroll.scrollLeft - display.wheelStartX; + var movedY = scroll.scrollTop - display.wheelStartY; + var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || + (movedX && display.wheelDX && movedX / display.wheelDX); + display.wheelStartX = display.wheelStartY = null; + if (!sample) return; + wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1); + ++wheelSamples; + }, 200); + } else { + display.wheelDX += dx; display.wheelDY += dy; + } + } + } + + // KEY EVENTS + + // Run a handler that was bound to a key. + function doHandleBinding(cm, bound, dropShift) { + if (typeof bound == "string") { + bound = commands[bound]; + if (!bound) return false; + } + // Ensure previous input has been read, so that the handler sees a + // consistent view of the document + cm.display.input.ensurePolled(); + var prevShift = cm.display.shift, done = false; + try { + if (isReadOnly(cm)) cm.state.suppressEdits = true; + if (dropShift) cm.display.shift = false; + done = bound(cm) != Pass; + } finally { + cm.display.shift = prevShift; + cm.state.suppressEdits = false; + } + return done; + } + + function lookupKeyForEditor(cm, name, handle) { + for (var i = 0; i < cm.state.keyMaps.length; i++) { + var result = lookupKey(name, cm.state.keyMaps[i], handle, cm); + if (result) return result; + } + return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm)) + || lookupKey(name, cm.options.keyMap, handle, cm); + } + + var stopSeq = new Delayed; + function dispatchKey(cm, name, e, handle) { + var seq = cm.state.keySeq; + if (seq) { + if (isModifierKey(name)) return "handled"; + stopSeq.set(50, function() { + if (cm.state.keySeq == seq) { + cm.state.keySeq = null; + cm.display.input.reset(); + } + }); + name = seq + " " + name; + } + var result = lookupKeyForEditor(cm, name, handle); + + if (result == "multi") + cm.state.keySeq = name; + if (result == "handled") + signalLater(cm, "keyHandled", cm, name, e); + + if (result == "handled" || result == "multi") { + e_preventDefault(e); + restartBlink(cm); + } + + if (seq && !result && /\'$/.test(name)) { + e_preventDefault(e); + return true; + } + return !!result; + } + + // Handle a key from the keydown event. + function handleKeyBinding(cm, e) { + var name = keyName(e, true); + if (!name) return false; + + if (e.shiftKey && !cm.state.keySeq) { + // First try to resolve full name (including 'Shift-'). Failing + // that, see if there is a cursor-motion command (starting with + // 'go') bound to the keyname without 'Shift-'. + return dispatchKey(cm, "Shift-" + name, e, function(b) {return doHandleBinding(cm, b, true);}) + || dispatchKey(cm, name, e, function(b) { + if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion) + return doHandleBinding(cm, b); + }); + } else { + return dispatchKey(cm, name, e, function(b) { return doHandleBinding(cm, b); }); + } + } + + // Handle a key from the keypress event + function handleCharBinding(cm, e, ch) { + return dispatchKey(cm, "'" + ch + "'", e, + function(b) { return doHandleBinding(cm, b, true); }); + } + + var lastStoppedKey = null; + function onKeyDown(e) { + var cm = this; + cm.curOp.focus = activeElt(); + if (signalDOMEvent(cm, e)) return; + // IE does strange things with escape. + if (ie && ie_version < 11 && e.keyCode == 27) e.returnValue = false; + var code = e.keyCode; + cm.display.shift = code == 16 || e.shiftKey; + var handled = handleKeyBinding(cm, e); + if (presto) { + lastStoppedKey = handled ? code : null; + // Opera has no cut event... we try to at least catch the key combo + if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) + cm.replaceSelection("", null, "cut"); + } + + // Turn mouse into crosshair when Alt is held on Mac. + if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className)) + showCrossHair(cm); + } + + function showCrossHair(cm) { + var lineDiv = cm.display.lineDiv; + addClass(lineDiv, "CodeMirror-crosshair"); + + function up(e) { + if (e.keyCode == 18 || !e.altKey) { + rmClass(lineDiv, "CodeMirror-crosshair"); + off(document, "keyup", up); + off(document, "mouseover", up); + } + } + on(document, "keyup", up); + on(document, "mouseover", up); + } + + function onKeyUp(e) { + if (e.keyCode == 16) this.doc.sel.shift = false; + signalDOMEvent(this, e); + } + + function onKeyPress(e) { + var cm = this; + if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) return; + var keyCode = e.keyCode, charCode = e.charCode; + if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} + if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) return; + var ch = String.fromCharCode(charCode == null ? keyCode : charCode); + if (handleCharBinding(cm, e, ch)) return; + cm.display.input.onKeyPress(e); + } + + // FOCUS/BLUR EVENTS + + function delayBlurEvent(cm) { + cm.state.delayingBlurEvent = true; + setTimeout(function() { + if (cm.state.delayingBlurEvent) { + cm.state.delayingBlurEvent = false; + onBlur(cm); + } + }, 100); + } + + function onFocus(cm) { + if (cm.state.delayingBlurEvent) cm.state.delayingBlurEvent = false; + + if (cm.options.readOnly == "nocursor") return; + if (!cm.state.focused) { + signal(cm, "focus", cm); + cm.state.focused = true; + addClass(cm.display.wrapper, "CodeMirror-focused"); + // This test prevents this from firing when a context + // menu is closed (since the input reset would kill the + // select-all detection hack) + if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) { + cm.display.input.reset(); + if (webkit) setTimeout(function() { cm.display.input.reset(true); }, 20); // Issue #1730 + } + cm.display.input.receivedFocus(); + } + restartBlink(cm); + } + function onBlur(cm) { + if (cm.state.delayingBlurEvent) return; + + if (cm.state.focused) { + signal(cm, "blur", cm); + cm.state.focused = false; + rmClass(cm.display.wrapper, "CodeMirror-focused"); + } + clearInterval(cm.display.blinker); + setTimeout(function() {if (!cm.state.focused) cm.display.shift = false;}, 150); + } + + // CONTEXT MENU HANDLING + + // To make the context menu work, we need to briefly unhide the + // textarea (making it as unobtrusive as possible) to let the + // right-click take effect on it. + function onContextMenu(cm, e) { + if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) return; + cm.display.input.onContextMenu(e); + } + + function contextMenuInGutter(cm, e) { + if (!hasHandler(cm, "gutterContextMenu")) return false; + return gutterEvent(cm, e, "gutterContextMenu", false, signal); + } + + // UPDATING + + // Compute the position of the end of a change (its 'to' property + // refers to the pre-change end). + var changeEnd = CodeMirror.changeEnd = function(change) { + if (!change.text) return change.to; + return Pos(change.from.line + change.text.length - 1, + lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)); + }; + + // Adjust a position to refer to the post-change position of the + // same text, or the end of the change if the change covers it. + function adjustForChange(pos, change) { + if (cmp(pos, change.from) < 0) return pos; + if (cmp(pos, change.to) <= 0) return changeEnd(change); + + var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; + if (pos.line == change.to.line) ch += changeEnd(change).ch - change.to.ch; + return Pos(line, ch); + } + + function computeSelAfterChange(doc, change) { + var out = []; + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + out.push(new Range(adjustForChange(range.anchor, change), + adjustForChange(range.head, change))); + } + return normalizeSelection(out, doc.sel.primIndex); + } + + function offsetPos(pos, old, nw) { + if (pos.line == old.line) + return Pos(nw.line, pos.ch - old.ch + nw.ch); + else + return Pos(nw.line + (pos.line - old.line), pos.ch); + } + + // Used by replaceSelections to allow moving the selection to the + // start or around the replaced test. Hint may be "start" or "around". + function computeReplacedSel(doc, changes, hint) { + var out = []; + var oldPrev = Pos(doc.first, 0), newPrev = oldPrev; + for (var i = 0; i < changes.length; i++) { + var change = changes[i]; + var from = offsetPos(change.from, oldPrev, newPrev); + var to = offsetPos(changeEnd(change), oldPrev, newPrev); + oldPrev = change.to; + newPrev = to; + if (hint == "around") { + var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0; + out[i] = new Range(inv ? to : from, inv ? from : to); + } else { + out[i] = new Range(from, from); + } + } + return new Selection(out, doc.sel.primIndex); + } + + // Allow "beforeChange" event handlers to influence a change + function filterChange(doc, change, update) { + var obj = { + canceled: false, + from: change.from, + to: change.to, + text: change.text, + origin: change.origin, + cancel: function() { this.canceled = true; } + }; + if (update) obj.update = function(from, to, text, origin) { + if (from) this.from = clipPos(doc, from); + if (to) this.to = clipPos(doc, to); + if (text) this.text = text; + if (origin !== undefined) this.origin = origin; + }; + signal(doc, "beforeChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj); + + if (obj.canceled) return null; + return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin}; + } + + // Apply a change to a document, and add it to the document's + // history, and propagating it to all linked documents. + function makeChange(doc, change, ignoreReadOnly) { + if (doc.cm) { + if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly); + if (doc.cm.state.suppressEdits) return; + } + + if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { + change = filterChange(doc, change, true); + if (!change) return; + } + + // Possibly split or suppress the update based on the presence + // of read-only spans in its range. + var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to); + if (split) { + for (var i = split.length - 1; i >= 0; --i) + makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text}); + } else { + makeChangeInner(doc, change); + } + } + + function makeChangeInner(doc, change) { + if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return; + var selAfter = computeSelAfterChange(doc, change); + addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); + + makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)); + var rebased = []; + + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)); + }); + } + + // Revert a change stored in a document's history. + function makeChangeFromHistory(doc, type, allowSelectionOnly) { + if (doc.cm && doc.cm.state.suppressEdits) return; + + var hist = doc.history, event, selAfter = doc.sel; + var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done; + + // Verify that there is a useable event (so that ctrl-z won't + // needlessly clear selection events) + for (var i = 0; i < source.length; i++) { + event = source[i]; + if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges) + break; + } + if (i == source.length) return; + hist.lastOrigin = hist.lastSelOrigin = null; + + for (;;) { + event = source.pop(); + if (event.ranges) { + pushSelectionToHistory(event, dest); + if (allowSelectionOnly && !event.equals(doc.sel)) { + setSelection(doc, event, {clearRedo: false}); + return; + } + selAfter = event; + } + else break; + } + + // Build up a reverse change object to add to the opposite history + // stack (redo when undoing, and vice versa). + var antiChanges = []; + pushSelectionToHistory(selAfter, dest); + dest.push({changes: antiChanges, generation: hist.generation}); + hist.generation = event.generation || ++hist.maxGeneration; + + var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange"); + + for (var i = event.changes.length - 1; i >= 0; --i) { + var change = event.changes[i]; + change.origin = type; + if (filter && !filterChange(doc, change, false)) { + source.length = 0; + return; + } + + antiChanges.push(historyChangeFromChange(doc, change)); + + var after = i ? computeSelAfterChange(doc, change) : lst(source); + makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); + if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)}); + var rebased = []; + + // Propagate to the linked documents + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)); + }); + } + } + + // Sub-views need their line numbers shifted when text is added + // above or below them in the parent document. + function shiftDoc(doc, distance) { + if (distance == 0) return; + doc.first += distance; + doc.sel = new Selection(map(doc.sel.ranges, function(range) { + return new Range(Pos(range.anchor.line + distance, range.anchor.ch), + Pos(range.head.line + distance, range.head.ch)); + }), doc.sel.primIndex); + if (doc.cm) { + regChange(doc.cm, doc.first, doc.first - distance, distance); + for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++) + regLineChange(doc.cm, l, "gutter"); + } + } + + // More lower-level change function, handling only a single document + // (not linked ones). + function makeChangeSingleDoc(doc, change, selAfter, spans) { + if (doc.cm && !doc.cm.curOp) + return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans); + + if (change.to.line < doc.first) { + shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)); + return; + } + if (change.from.line > doc.lastLine()) return; + + // Clip the change to the size of this doc + if (change.from.line < doc.first) { + var shift = change.text.length - 1 - (doc.first - change.from.line); + shiftDoc(doc, shift); + change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), + text: [lst(change.text)], origin: change.origin}; + } + var last = doc.lastLine(); + if (change.to.line > last) { + change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), + text: [change.text[0]], origin: change.origin}; + } + + change.removed = getBetween(doc, change.from, change.to); + + if (!selAfter) selAfter = computeSelAfterChange(doc, change); + if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans); + else updateDoc(doc, change, spans); + setSelectionNoUndo(doc, selAfter, sel_dontScroll); + } + + // Handle the interaction of a change to a document with the editor + // that this document is part of. + function makeChangeSingleDocInEditor(cm, change, spans) { + var doc = cm.doc, display = cm.display, from = change.from, to = change.to; + + var recomputeMaxLength = false, checkWidthStart = from.line; + if (!cm.options.lineWrapping) { + checkWidthStart = lineNo(visualLine(getLine(doc, from.line))); + doc.iter(checkWidthStart, to.line + 1, function(line) { + if (line == display.maxLine) { + recomputeMaxLength = true; + return true; + } + }); + } + + if (doc.sel.contains(change.from, change.to) > -1) + signalCursorActivity(cm); + + updateDoc(doc, change, spans, estimateHeight(cm)); + + if (!cm.options.lineWrapping) { + doc.iter(checkWidthStart, from.line + change.text.length, function(line) { + var len = lineLength(line); + if (len > display.maxLineLength) { + display.maxLine = line; + display.maxLineLength = len; + display.maxLineChanged = true; + recomputeMaxLength = false; + } + }); + if (recomputeMaxLength) cm.curOp.updateMaxLine = true; + } + + // Adjust frontier, schedule worker + doc.frontier = Math.min(doc.frontier, from.line); + startWorker(cm, 400); + + var lendiff = change.text.length - (to.line - from.line) - 1; + // Remember that these lines changed, for updating the display + if (change.full) + regChange(cm); + else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change)) + regLineChange(cm, from.line, "text"); + else + regChange(cm, from.line, to.line + 1, lendiff); + + var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change"); + if (changeHandler || changesHandler) { + var obj = { + from: from, to: to, + text: change.text, + removed: change.removed, + origin: change.origin + }; + if (changeHandler) signalLater(cm, "change", cm, obj); + if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj); + } + cm.display.selForContextMenu = null; + } + + function replaceRange(doc, code, from, to, origin) { + if (!to) to = from; + if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp; } + if (typeof code == "string") code = splitLines(code); + makeChange(doc, {from: from, to: to, text: code, origin: origin}); + } + + // SCROLLING THINGS INTO VIEW + + // If an editor sits on the top or bottom of the window, partially + // scrolled out of view, this ensures that the cursor is visible. + function maybeScrollWindow(cm, coords) { + if (signalDOMEvent(cm, "scrollCursorIntoView")) return; + + var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null; + if (coords.top + box.top < 0) doScroll = true; + else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false; + if (doScroll != null && !phantom) { + var scrollNode = elt("div", "\u200b", null, "position: absolute; top: " + + (coords.top - display.viewOffset - paddingTop(cm.display)) + "px; height: " + + (coords.bottom - coords.top + scrollGap(cm) + display.barHeight) + "px; left: " + + coords.left + "px; width: 2px;"); + cm.display.lineSpace.appendChild(scrollNode); + scrollNode.scrollIntoView(doScroll); + cm.display.lineSpace.removeChild(scrollNode); + } + } + + // Scroll a given position into view (immediately), verifying that + // it actually became visible (as line heights are accurately + // measured, the position of something may 'drift' during drawing). + function scrollPosIntoView(cm, pos, end, margin) { + if (margin == null) margin = 0; + for (var limit = 0; limit < 5; limit++) { + var changed = false, coords = cursorCoords(cm, pos); + var endCoords = !end || end == pos ? coords : cursorCoords(cm, end); + var scrollPos = calculateScrollPos(cm, Math.min(coords.left, endCoords.left), + Math.min(coords.top, endCoords.top) - margin, + Math.max(coords.left, endCoords.left), + Math.max(coords.bottom, endCoords.bottom) + margin); + var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft; + if (scrollPos.scrollTop != null) { + setScrollTop(cm, scrollPos.scrollTop); + if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true; + } + if (scrollPos.scrollLeft != null) { + setScrollLeft(cm, scrollPos.scrollLeft); + if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true; + } + if (!changed) break; + } + return coords; + } + + // Scroll a given set of coordinates into view (immediately). + function scrollIntoView(cm, x1, y1, x2, y2) { + var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2); + if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop); + if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft); + } + + // Calculate a new scroll position needed to scroll the given + // rectangle into view. Returns an object with scrollTop and + // scrollLeft properties. When these are undefined, the + // vertical/horizontal position does not need to be adjusted. + function calculateScrollPos(cm, x1, y1, x2, y2) { + var display = cm.display, snapMargin = textHeight(cm.display); + if (y1 < 0) y1 = 0; + var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop; + var screen = displayHeight(cm), result = {}; + if (y2 - y1 > screen) y2 = y1 + screen; + var docBottom = cm.doc.height + paddingVert(display); + var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin; + if (y1 < screentop) { + result.scrollTop = atTop ? 0 : y1; + } else if (y2 > screentop + screen) { + var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen); + if (newTop != screentop) result.scrollTop = newTop; + } + + var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft; + var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0); + var tooWide = x2 - x1 > screenw; + if (tooWide) x2 = x1 + screenw; + if (x1 < 10) + result.scrollLeft = 0; + else if (x1 < screenleft) + result.scrollLeft = Math.max(0, x1 - (tooWide ? 0 : 10)); + else if (x2 > screenw + screenleft - 3) + result.scrollLeft = x2 + (tooWide ? 0 : 10) - screenw; + return result; + } + + // Store a relative adjustment to the scroll position in the current + // operation (to be applied when the operation finishes). + function addToScrollPos(cm, left, top) { + if (left != null || top != null) resolveScrollToPos(cm); + if (left != null) + cm.curOp.scrollLeft = (cm.curOp.scrollLeft == null ? cm.doc.scrollLeft : cm.curOp.scrollLeft) + left; + if (top != null) + cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top; + } + + // Make sure that at the end of the operation the current cursor is + // shown. + function ensureCursorVisible(cm) { + resolveScrollToPos(cm); + var cur = cm.getCursor(), from = cur, to = cur; + if (!cm.options.lineWrapping) { + from = cur.ch ? Pos(cur.line, cur.ch - 1) : cur; + to = Pos(cur.line, cur.ch + 1); + } + cm.curOp.scrollToPos = {from: from, to: to, margin: cm.options.cursorScrollMargin, isCursor: true}; + } + + // When an operation has its scrollToPos property set, and another + // scroll action is applied before the end of the operation, this + // 'simulates' scrolling that position into view in a cheap way, so + // that the effect of intermediate scroll commands is not ignored. + function resolveScrollToPos(cm) { + var range = cm.curOp.scrollToPos; + if (range) { + cm.curOp.scrollToPos = null; + var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to); + var sPos = calculateScrollPos(cm, Math.min(from.left, to.left), + Math.min(from.top, to.top) - range.margin, + Math.max(from.right, to.right), + Math.max(from.bottom, to.bottom) + range.margin); + cm.scrollTo(sPos.scrollLeft, sPos.scrollTop); + } + } + + // API UTILITIES + + // Indent the given line. The how parameter can be "smart", + // "add"/null, "subtract", or "prev". When aggressive is false + // (typically set to true for forced single-line indents), empty + // lines are not indented, and places where the mode returns Pass + // are left alone. + function indentLine(cm, n, how, aggressive) { + var doc = cm.doc, state; + if (how == null) how = "add"; + if (how == "smart") { + // Fall back to "prev" when the mode doesn't have an indentation + // method. + if (!doc.mode.indent) how = "prev"; + else state = getStateBefore(cm, n); + } + + var tabSize = cm.options.tabSize; + var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); + if (line.stateAfter) line.stateAfter = null; + var curSpaceString = line.text.match(/^\s*/)[0], indentation; + if (!aggressive && !/\S/.test(line.text)) { + indentation = 0; + how = "not"; + } else if (how == "smart") { + indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text); + if (indentation == Pass || indentation > 150) { + if (!aggressive) return; + how = "prev"; + } + } + if (how == "prev") { + if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize); + else indentation = 0; + } else if (how == "add") { + indentation = curSpace + cm.options.indentUnit; + } else if (how == "subtract") { + indentation = curSpace - cm.options.indentUnit; + } else if (typeof how == "number") { + indentation = curSpace + how; + } + indentation = Math.max(0, indentation); + + var indentString = "", pos = 0; + if (cm.options.indentWithTabs) + for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} + if (pos < indentation) indentString += spaceStr(indentation - pos); + + if (indentString != curSpaceString) { + replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); + } else { + // Ensure that, if the cursor was in the whitespace at the start + // of the line, it is moved to the end of that space. + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + if (range.head.line == n && range.head.ch < curSpaceString.length) { + var pos = Pos(n, curSpaceString.length); + replaceOneSelection(doc, i, new Range(pos, pos)); + break; + } + } + } + line.stateAfter = null; + } + + // Utility for applying a change to a line by handle or number, + // returning the number and optionally registering the line as + // changed. + function changeLine(doc, handle, changeType, op) { + var no = handle, line = handle; + if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle)); + else no = lineNo(handle); + if (no == null) return null; + if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType); + return line; + } + + // Helper for deleting text near the selection(s), used to implement + // backspace, delete, and similar functionality. + function deleteNearSelection(cm, compute) { + var ranges = cm.doc.sel.ranges, kill = []; + // Build up a set of ranges to kill first, merging overlapping + // ranges. + for (var i = 0; i < ranges.length; i++) { + var toKill = compute(ranges[i]); + while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) { + var replaced = kill.pop(); + if (cmp(replaced.from, toKill.from) < 0) { + toKill.from = replaced.from; + break; + } + } + kill.push(toKill); + } + // Next, remove those actual ranges. + runInOp(cm, function() { + for (var i = kill.length - 1; i >= 0; i--) + replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete"); + ensureCursorVisible(cm); + }); + } + + // Used for horizontal relative motion. Dir is -1 or 1 (left or + // right), unit can be "char", "column" (like char, but doesn't + // cross line boundaries), "word" (across next word), or "group" (to + // the start of next group of word or non-word-non-whitespace + // chars). The visually param controls whether, in right-to-left + // text, direction 1 means to move towards the next index in the + // string, or towards the character to the right of the current + // position. The resulting position will have a hitSide=true + // property if it reached the end of the document. + function findPosH(doc, pos, dir, unit, visually) { + var line = pos.line, ch = pos.ch, origDir = dir; + var lineObj = getLine(doc, line); + var possible = true; + function findNextLine() { + var l = line + dir; + if (l < doc.first || l >= doc.first + doc.size) return (possible = false); + line = l; + return lineObj = getLine(doc, l); + } + function moveOnce(boundToLine) { + var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true); + if (next == null) { + if (!boundToLine && findNextLine()) { + if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj); + else ch = dir < 0 ? lineObj.text.length : 0; + } else return (possible = false); + } else ch = next; + return true; + } + + if (unit == "char") moveOnce(); + else if (unit == "column") moveOnce(true); + else if (unit == "word" || unit == "group") { + var sawType = null, group = unit == "group"; + var helper = doc.cm && doc.cm.getHelper(pos, "wordChars"); + for (var first = true;; first = false) { + if (dir < 0 && !moveOnce(!first)) break; + var cur = lineObj.text.charAt(ch) || "\n"; + var type = isWordChar(cur, helper) ? "w" + : group && cur == "\n" ? "n" + : !group || /\s/.test(cur) ? null + : "p"; + if (group && !first && !type) type = "s"; + if (sawType && sawType != type) { + if (dir < 0) {dir = 1; moveOnce();} + break; + } + + if (type) sawType = type; + if (dir > 0 && !moveOnce(!first)) break; + } + } + var result = skipAtomic(doc, Pos(line, ch), origDir, true); + if (!possible) result.hitSide = true; + return result; + } + + // For relative vertical movement. Dir may be -1 or 1. Unit can be + // "page" or "line". The resulting position will have a hitSide=true + // property if it reached the end of the document. + function findPosV(cm, pos, dir, unit) { + var doc = cm.doc, x = pos.left, y; + if (unit == "page") { + var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight); + y = pos.top + dir * (pageSize - (dir < 0 ? 1.5 : .5) * textHeight(cm.display)); + } else if (unit == "line") { + y = dir > 0 ? pos.bottom + 3 : pos.top - 3; + } + for (;;) { + var target = coordsChar(cm, x, y); + if (!target.outside) break; + if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; } + y += dir * 5; + } + return target; + } + + // EDITOR METHODS + + // The publicly visible API. Note that methodOp(f) means + // 'wrap f in an operation, performed on its `this` parameter'. + + // This is not the complete set of editor methods. Most of the + // methods defined on the Doc type are also injected into + // CodeMirror.prototype, for backwards compatibility and + // convenience. + + CodeMirror.prototype = { + constructor: CodeMirror, + focus: function(){window.focus(); this.display.input.focus();}, + + setOption: function(option, value) { + var options = this.options, old = options[option]; + if (options[option] == value && option != "mode") return; + options[option] = value; + if (optionHandlers.hasOwnProperty(option)) + operation(this, optionHandlers[option])(this, value, old); + }, + + getOption: function(option) {return this.options[option];}, + getDoc: function() {return this.doc;}, + + addKeyMap: function(map, bottom) { + this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map)); + }, + removeKeyMap: function(map) { + var maps = this.state.keyMaps; + for (var i = 0; i < maps.length; ++i) + if (maps[i] == map || maps[i].name == map) { + maps.splice(i, 1); + return true; + } + }, + + addOverlay: methodOp(function(spec, options) { + var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec); + if (mode.startState) throw new Error("Overlays may not be stateful."); + this.state.overlays.push({mode: mode, modeSpec: spec, opaque: options && options.opaque}); + this.state.modeGen++; + regChange(this); + }), + removeOverlay: methodOp(function(spec) { + var overlays = this.state.overlays; + for (var i = 0; i < overlays.length; ++i) { + var cur = overlays[i].modeSpec; + if (cur == spec || typeof spec == "string" && cur.name == spec) { + overlays.splice(i, 1); + this.state.modeGen++; + regChange(this); + return; + } + } + }), + + indentLine: methodOp(function(n, dir, aggressive) { + if (typeof dir != "string" && typeof dir != "number") { + if (dir == null) dir = this.options.smartIndent ? "smart" : "prev"; + else dir = dir ? "add" : "subtract"; + } + if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive); + }), + indentSelection: methodOp(function(how) { + var ranges = this.doc.sel.ranges, end = -1; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + if (!range.empty()) { + var from = range.from(), to = range.to(); + var start = Math.max(end, from.line); + end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1; + for (var j = start; j < end; ++j) + indentLine(this, j, how); + var newRanges = this.doc.sel.ranges; + if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0) + replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll); + } else if (range.head.line > end) { + indentLine(this, range.head.line, how, true); + end = range.head.line; + if (i == this.doc.sel.primIndex) ensureCursorVisible(this); + } + } + }), + + // Fetch the parser token for a given character. Useful for hacks + // that want to inspect the mode state (say, for completion). + getTokenAt: function(pos, precise) { + return takeToken(this, pos, precise); + }, + + getLineTokens: function(line, precise) { + return takeToken(this, Pos(line), precise, true); + }, + + getTokenTypeAt: function(pos) { + pos = clipPos(this.doc, pos); + var styles = getLineStyles(this, getLine(this.doc, pos.line)); + var before = 0, after = (styles.length - 1) / 2, ch = pos.ch; + var type; + if (ch == 0) type = styles[2]; + else for (;;) { + var mid = (before + after) >> 1; + if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid; + else if (styles[mid * 2 + 1] < ch) before = mid + 1; + else { type = styles[mid * 2 + 2]; break; } + } + var cut = type ? type.indexOf("cm-overlay ") : -1; + return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1); + }, + + getModeAt: function(pos) { + var mode = this.doc.mode; + if (!mode.innerMode) return mode; + return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode; + }, + + getHelper: function(pos, type) { + return this.getHelpers(pos, type)[0]; + }, + + getHelpers: function(pos, type) { + var found = []; + if (!helpers.hasOwnProperty(type)) return found; + var help = helpers[type], mode = this.getModeAt(pos); + if (typeof mode[type] == "string") { + if (help[mode[type]]) found.push(help[mode[type]]); + } else if (mode[type]) { + for (var i = 0; i < mode[type].length; i++) { + var val = help[mode[type][i]]; + if (val) found.push(val); + } + } else if (mode.helperType && help[mode.helperType]) { + found.push(help[mode.helperType]); + } else if (help[mode.name]) { + found.push(help[mode.name]); + } + for (var i = 0; i < help._global.length; i++) { + var cur = help._global[i]; + if (cur.pred(mode, this) && indexOf(found, cur.val) == -1) + found.push(cur.val); + } + return found; + }, + + getStateAfter: function(line, precise) { + var doc = this.doc; + line = clipLine(doc, line == null ? doc.first + doc.size - 1: line); + return getStateBefore(this, line + 1, precise); + }, + + cursorCoords: function(start, mode) { + var pos, range = this.doc.sel.primary(); + if (start == null) pos = range.head; + else if (typeof start == "object") pos = clipPos(this.doc, start); + else pos = start ? range.from() : range.to(); + return cursorCoords(this, pos, mode || "page"); + }, + + charCoords: function(pos, mode) { + return charCoords(this, clipPos(this.doc, pos), mode || "page"); + }, + + coordsChar: function(coords, mode) { + coords = fromCoordSystem(this, coords, mode || "page"); + return coordsChar(this, coords.left, coords.top); + }, + + lineAtHeight: function(height, mode) { + height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top; + return lineAtHeight(this.doc, height + this.display.viewOffset); + }, + heightAtLine: function(line, mode) { + var end = false, lineObj; + if (typeof line == "number") { + var last = this.doc.first + this.doc.size - 1; + if (line < this.doc.first) line = this.doc.first; + else if (line > last) { line = last; end = true; } + lineObj = getLine(this.doc, line); + } else { + lineObj = line; + } + return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page").top + + (end ? this.doc.height - heightAtLine(lineObj) : 0); + }, + + defaultTextHeight: function() { return textHeight(this.display); }, + defaultCharWidth: function() { return charWidth(this.display); }, + + setGutterMarker: methodOp(function(line, gutterID, value) { + return changeLine(this.doc, line, "gutter", function(line) { + var markers = line.gutterMarkers || (line.gutterMarkers = {}); + markers[gutterID] = value; + if (!value && isEmpty(markers)) line.gutterMarkers = null; + return true; + }); + }), + + clearGutter: methodOp(function(gutterID) { + var cm = this, doc = cm.doc, i = doc.first; + doc.iter(function(line) { + if (line.gutterMarkers && line.gutterMarkers[gutterID]) { + line.gutterMarkers[gutterID] = null; + regLineChange(cm, i, "gutter"); + if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null; + } + ++i; + }); + }), + + lineInfo: function(line) { + if (typeof line == "number") { + if (!isLine(this.doc, line)) return null; + var n = line; + line = getLine(this.doc, line); + if (!line) return null; + } else { + var n = lineNo(line); + if (n == null) return null; + } + return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, + textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, + widgets: line.widgets}; + }, + + getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo};}, + + addWidget: function(pos, node, scroll, vert, horiz) { + var display = this.display; + pos = cursorCoords(this, clipPos(this.doc, pos)); + var top = pos.bottom, left = pos.left; + node.style.position = "absolute"; + node.setAttribute("cm-ignore-events", "true"); + this.display.input.setUneditable(node); + display.sizer.appendChild(node); + if (vert == "over") { + top = pos.top; + } else if (vert == "above" || vert == "near") { + var vspace = Math.max(display.wrapper.clientHeight, this.doc.height), + hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth); + // Default to positioning above (if specified and possible); otherwise default to positioning below + if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) + top = pos.top - node.offsetHeight; + else if (pos.bottom + node.offsetHeight <= vspace) + top = pos.bottom; + if (left + node.offsetWidth > hspace) + left = hspace - node.offsetWidth; + } + node.style.top = top + "px"; + node.style.left = node.style.right = ""; + if (horiz == "right") { + left = display.sizer.clientWidth - node.offsetWidth; + node.style.right = "0px"; + } else { + if (horiz == "left") left = 0; + else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2; + node.style.left = left + "px"; + } + if (scroll) + scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight); + }, + + triggerOnKeyDown: methodOp(onKeyDown), + triggerOnKeyPress: methodOp(onKeyPress), + triggerOnKeyUp: onKeyUp, + + execCommand: function(cmd) { + if (commands.hasOwnProperty(cmd)) + return commands[cmd](this); + }, + + findPosH: function(from, amount, unit, visually) { + var dir = 1; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + cur = findPosH(this.doc, cur, dir, unit, visually); + if (cur.hitSide) break; + } + return cur; + }, + + moveH: methodOp(function(dir, unit) { + var cm = this; + cm.extendSelectionsBy(function(range) { + if (cm.display.shift || cm.doc.extend || range.empty()) + return findPosH(cm.doc, range.head, dir, unit, cm.options.rtlMoveVisually); + else + return dir < 0 ? range.from() : range.to(); + }, sel_move); + }), + + deleteH: methodOp(function(dir, unit) { + var sel = this.doc.sel, doc = this.doc; + if (sel.somethingSelected()) + doc.replaceSelection("", null, "+delete"); + else + deleteNearSelection(this, function(range) { + var other = findPosH(doc, range.head, dir, unit, false); + return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other}; + }); + }), + + findPosV: function(from, amount, unit, goalColumn) { + var dir = 1, x = goalColumn; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + var coords = cursorCoords(this, cur, "div"); + if (x == null) x = coords.left; + else coords.left = x; + cur = findPosV(this, coords, dir, unit); + if (cur.hitSide) break; + } + return cur; + }, + + moveV: methodOp(function(dir, unit) { + var cm = this, doc = this.doc, goals = []; + var collapse = !cm.display.shift && !doc.extend && doc.sel.somethingSelected(); + doc.extendSelectionsBy(function(range) { + if (collapse) + return dir < 0 ? range.from() : range.to(); + var headPos = cursorCoords(cm, range.head, "div"); + if (range.goalColumn != null) headPos.left = range.goalColumn; + goals.push(headPos.left); + var pos = findPosV(cm, headPos, dir, unit); + if (unit == "page" && range == doc.sel.primary()) + addToScrollPos(cm, null, charCoords(cm, pos, "div").top - headPos.top); + return pos; + }, sel_move); + if (goals.length) for (var i = 0; i < doc.sel.ranges.length; i++) + doc.sel.ranges[i].goalColumn = goals[i]; + }), + + // Find the word at the given position (as returned by coordsChar). + findWordAt: function(pos) { + var doc = this.doc, line = getLine(doc, pos.line).text; + var start = pos.ch, end = pos.ch; + if (line) { + var helper = this.getHelper(pos, "wordChars"); + if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end; + var startChar = line.charAt(start); + var check = isWordChar(startChar, helper) + ? function(ch) { return isWordChar(ch, helper); } + : /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);} + : function(ch) {return !/\s/.test(ch) && !isWordChar(ch);}; + while (start > 0 && check(line.charAt(start - 1))) --start; + while (end < line.length && check(line.charAt(end))) ++end; + } + return new Range(Pos(pos.line, start), Pos(pos.line, end)); + }, + + toggleOverwrite: function(value) { + if (value != null && value == this.state.overwrite) return; + if (this.state.overwrite = !this.state.overwrite) + addClass(this.display.cursorDiv, "CodeMirror-overwrite"); + else + rmClass(this.display.cursorDiv, "CodeMirror-overwrite"); + + signal(this, "overwriteToggle", this, this.state.overwrite); + }, + hasFocus: function() { return this.display.input.getField() == activeElt(); }, + + scrollTo: methodOp(function(x, y) { + if (x != null || y != null) resolveScrollToPos(this); + if (x != null) this.curOp.scrollLeft = x; + if (y != null) this.curOp.scrollTop = y; + }), + getScrollInfo: function() { + var scroller = this.display.scroller; + return {left: scroller.scrollLeft, top: scroller.scrollTop, + height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight, + width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth, + clientHeight: displayHeight(this), clientWidth: displayWidth(this)}; + }, + + scrollIntoView: methodOp(function(range, margin) { + if (range == null) { + range = {from: this.doc.sel.primary().head, to: null}; + if (margin == null) margin = this.options.cursorScrollMargin; + } else if (typeof range == "number") { + range = {from: Pos(range, 0), to: null}; + } else if (range.from == null) { + range = {from: range, to: null}; + } + if (!range.to) range.to = range.from; + range.margin = margin || 0; + + if (range.from.line != null) { + resolveScrollToPos(this); + this.curOp.scrollToPos = range; + } else { + var sPos = calculateScrollPos(this, Math.min(range.from.left, range.to.left), + Math.min(range.from.top, range.to.top) - range.margin, + Math.max(range.from.right, range.to.right), + Math.max(range.from.bottom, range.to.bottom) + range.margin); + this.scrollTo(sPos.scrollLeft, sPos.scrollTop); + } + }), + + setSize: methodOp(function(width, height) { + var cm = this; + function interpret(val) { + return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; + } + if (width != null) cm.display.wrapper.style.width = interpret(width); + if (height != null) cm.display.wrapper.style.height = interpret(height); + if (cm.options.lineWrapping) clearLineMeasurementCache(this); + var lineNo = cm.display.viewFrom; + cm.doc.iter(lineNo, cm.display.viewTo, function(line) { + if (line.widgets) for (var i = 0; i < line.widgets.length; i++) + if (line.widgets[i].noHScroll) { regLineChange(cm, lineNo, "widget"); break; } + ++lineNo; + }); + cm.curOp.forceUpdate = true; + signal(cm, "refresh", this); + }), + + operation: function(f){return runInOp(this, f);}, + + refresh: methodOp(function() { + var oldHeight = this.display.cachedTextHeight; + regChange(this); + this.curOp.forceUpdate = true; + clearCaches(this); + this.scrollTo(this.doc.scrollLeft, this.doc.scrollTop); + updateGutterSpace(this); + if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5) + estimateLineHeights(this); + signal(this, "refresh", this); + }), + + swapDoc: methodOp(function(doc) { + var old = this.doc; + old.cm = null; + attachDoc(this, doc); + clearCaches(this); + this.display.input.reset(); + this.scrollTo(doc.scrollLeft, doc.scrollTop); + this.curOp.forceScroll = true; + signalLater(this, "swapDoc", this, old); + return old; + }), + + getInputField: function(){return this.display.input.getField();}, + getWrapperElement: function(){return this.display.wrapper;}, + getScrollerElement: function(){return this.display.scroller;}, + getGutterElement: function(){return this.display.gutters;} + }; + eventMixin(CodeMirror); + + // OPTION DEFAULTS + + // The default configuration options. + var defaults = CodeMirror.defaults = {}; + // Functions to run when options are changed. + var optionHandlers = CodeMirror.optionHandlers = {}; + + function option(name, deflt, handle, notOnInit) { + CodeMirror.defaults[name] = deflt; + if (handle) optionHandlers[name] = + notOnInit ? function(cm, val, old) {if (old != Init) handle(cm, val, old);} : handle; + } + + // Passed to option handlers when there is no old value. + var Init = CodeMirror.Init = {toString: function(){return "CodeMirror.Init";}}; + + // These two are, on init, called from the constructor because they + // have to be initialized before the editor can start at all. + option("value", "", function(cm, val) { + cm.setValue(val); + }, true); + option("mode", null, function(cm, val) { + cm.doc.modeOption = val; + loadMode(cm); + }, true); + + option("indentUnit", 2, loadMode, true); + option("indentWithTabs", false); + option("smartIndent", true); + option("tabSize", 4, function(cm) { + resetModeState(cm); + clearCaches(cm); + regChange(cm); + }, true); + option("specialChars", /[\t\u0000-\u0019\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g, function(cm, val, old) { + cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); + if (old != CodeMirror.Init) cm.refresh(); + }); + option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function(cm) {cm.refresh();}, true); + option("electricChars", true); + option("inputStyle", mobile ? "contenteditable" : "textarea", function() { + throw new Error("inputStyle can not (yet) be changed in a running editor"); // FIXME + }, true); + option("rtlMoveVisually", !windows); + option("wholeLineUpdateBefore", true); + + option("theme", "default", function(cm) { + themeChanged(cm); + guttersChanged(cm); + }, true); + option("keyMap", "default", function(cm, val, old) { + var next = getKeyMap(val); + var prev = old != CodeMirror.Init && getKeyMap(old); + if (prev && prev.detach) prev.detach(cm, next); + if (next.attach) next.attach(cm, prev || null); + }); + option("extraKeys", null); + + option("lineWrapping", false, wrappingChanged, true); + option("gutters", [], function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("fixedGutter", true, function(cm, val) { + cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; + cm.refresh(); + }, true); + option("coverGutterNextToScrollbar", false, function(cm) {updateScrollbars(cm);}, true); + option("scrollbarStyle", "native", function(cm) { + initScrollbars(cm); + updateScrollbars(cm); + cm.display.scrollbars.setScrollTop(cm.doc.scrollTop); + cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft); + }, true); + option("lineNumbers", false, function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("firstLineNumber", 1, guttersChanged, true); + option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true); + option("showCursorWhenSelecting", false, updateSelection, true); + + option("resetSelectionOnContextMenu", true); + option("lineWiseCopyCut", true); + + option("readOnly", false, function(cm, val) { + if (val == "nocursor") { + onBlur(cm); + cm.display.input.blur(); + cm.display.disabled = true; + } else { + cm.display.disabled = false; + if (!val) cm.display.input.reset(); + } + }); + option("disableInput", false, function(cm, val) {if (!val) cm.display.input.reset();}, true); + option("dragDrop", true, dragDropChanged); + + option("cursorBlinkRate", 530); + option("cursorScrollMargin", 0); + option("cursorHeight", 1, updateSelection, true); + option("singleCursorHeightPerLine", true, updateSelection, true); + option("workTime", 100); + option("workDelay", 100); + option("flattenSpans", true, resetModeState, true); + option("addModeClass", false, resetModeState, true); + option("pollInterval", 100); + option("undoDepth", 200, function(cm, val){cm.doc.history.undoDepth = val;}); + option("historyEventDelay", 1250); + option("viewportMargin", 10, function(cm){cm.refresh();}, true); + option("maxHighlightLength", 10000, resetModeState, true); + option("moveInputWithCursor", true, function(cm, val) { + if (!val) cm.display.input.resetPosition(); + }); + + option("tabindex", null, function(cm, val) { + cm.display.input.getField().tabIndex = val || ""; + }); + option("autofocus", null); + + // MODE DEFINITION AND QUERYING + + // Known modes, by name and by MIME + var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {}; + + // Extra arguments are stored as the mode's dependencies, which is + // used by (legacy) mechanisms like loadmode.js to automatically + // load a mode. (Preferred mechanism is the require/define calls.) + CodeMirror.defineMode = function(name, mode) { + if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name; + if (arguments.length > 2) + mode.dependencies = Array.prototype.slice.call(arguments, 2); + modes[name] = mode; + }; + + CodeMirror.defineMIME = function(mime, spec) { + mimeModes[mime] = spec; + }; + + // Given a MIME type, a {name, ...options} config object, or a name + // string, return a mode config object. + CodeMirror.resolveMode = function(spec) { + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { + spec = mimeModes[spec]; + } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { + var found = mimeModes[spec.name]; + if (typeof found == "string") found = {name: found}; + spec = createObj(found, spec); + spec.name = found.name; + } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { + return CodeMirror.resolveMode("application/xml"); + } + if (typeof spec == "string") return {name: spec}; + else return spec || {name: "null"}; + }; + + // Given a mode spec (anything that resolveMode accepts), find and + // initialize an actual mode object. + CodeMirror.getMode = function(options, spec) { + var spec = CodeMirror.resolveMode(spec); + var mfactory = modes[spec.name]; + if (!mfactory) return CodeMirror.getMode(options, "text/plain"); + var modeObj = mfactory(options, spec); + if (modeExtensions.hasOwnProperty(spec.name)) { + var exts = modeExtensions[spec.name]; + for (var prop in exts) { + if (!exts.hasOwnProperty(prop)) continue; + if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop]; + modeObj[prop] = exts[prop]; + } + } + modeObj.name = spec.name; + if (spec.helperType) modeObj.helperType = spec.helperType; + if (spec.modeProps) for (var prop in spec.modeProps) + modeObj[prop] = spec.modeProps[prop]; + + return modeObj; + }; + + // Minimal default mode. + CodeMirror.defineMode("null", function() { + return {token: function(stream) {stream.skipToEnd();}}; + }); + CodeMirror.defineMIME("text/plain", "null"); + + // This can be used to attach properties to mode objects from + // outside the actual mode definition. + var modeExtensions = CodeMirror.modeExtensions = {}; + CodeMirror.extendMode = function(mode, properties) { + var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); + copyObj(properties, exts); + }; + + // EXTENSIONS + + CodeMirror.defineExtension = function(name, func) { + CodeMirror.prototype[name] = func; + }; + CodeMirror.defineDocExtension = function(name, func) { + Doc.prototype[name] = func; + }; + CodeMirror.defineOption = option; + + var initHooks = []; + CodeMirror.defineInitHook = function(f) {initHooks.push(f);}; + + var helpers = CodeMirror.helpers = {}; + CodeMirror.registerHelper = function(type, name, value) { + if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []}; + helpers[type][name] = value; + }; + CodeMirror.registerGlobalHelper = function(type, name, predicate, value) { + CodeMirror.registerHelper(type, name, value); + helpers[type]._global.push({pred: predicate, val: value}); + }; + + // MODE STATE HANDLING + + // Utility functions for working with state. Exported because nested + // modes need to do this for their inner modes. + + var copyState = CodeMirror.copyState = function(mode, state) { + if (state === true) return state; + if (mode.copyState) return mode.copyState(state); + var nstate = {}; + for (var n in state) { + var val = state[n]; + if (val instanceof Array) val = val.concat([]); + nstate[n] = val; + } + return nstate; + }; + + var startState = CodeMirror.startState = function(mode, a1, a2) { + return mode.startState ? mode.startState(a1, a2) : true; + }; + + // Given a mode and a state (for that mode), find the inner mode and + // state at the position that the state refers to. + CodeMirror.innerMode = function(mode, state) { + while (mode.innerMode) { + var info = mode.innerMode(state); + if (!info || info.mode == mode) break; + state = info.state; + mode = info.mode; + } + return info || {mode: mode, state: state}; + }; + + // STANDARD COMMANDS + + // Commands are parameter-less actions that can be performed on an + // editor, mostly used for keybindings. + var commands = CodeMirror.commands = { + selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll);}, + singleSelection: function(cm) { + cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll); + }, + killLine: function(cm) { + deleteNearSelection(cm, function(range) { + if (range.empty()) { + var len = getLine(cm.doc, range.head.line).text.length; + if (range.head.ch == len && range.head.line < cm.lastLine()) + return {from: range.head, to: Pos(range.head.line + 1, 0)}; + else + return {from: range.head, to: Pos(range.head.line, len)}; + } else { + return {from: range.from(), to: range.to()}; + } + }); + }, + deleteLine: function(cm) { + deleteNearSelection(cm, function(range) { + return {from: Pos(range.from().line, 0), + to: clipPos(cm.doc, Pos(range.to().line + 1, 0))}; + }); + }, + delLineLeft: function(cm) { + deleteNearSelection(cm, function(range) { + return {from: Pos(range.from().line, 0), to: range.from()}; + }); + }, + delWrappedLineLeft: function(cm) { + deleteNearSelection(cm, function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var leftPos = cm.coordsChar({left: 0, top: top}, "div"); + return {from: leftPos, to: range.from()}; + }); + }, + delWrappedLineRight: function(cm) { + deleteNearSelection(cm, function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + return {from: range.from(), to: rightPos }; + }); + }, + undo: function(cm) {cm.undo();}, + redo: function(cm) {cm.redo();}, + undoSelection: function(cm) {cm.undoSelection();}, + redoSelection: function(cm) {cm.redoSelection();}, + goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));}, + goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));}, + goLineStart: function(cm) { + cm.extendSelectionsBy(function(range) { return lineStart(cm, range.head.line); }, + {origin: "+move", bias: 1}); + }, + goLineStartSmart: function(cm) { + cm.extendSelectionsBy(function(range) { + return lineStartSmart(cm, range.head); + }, {origin: "+move", bias: 1}); + }, + goLineEnd: function(cm) { + cm.extendSelectionsBy(function(range) { return lineEnd(cm, range.head.line); }, + {origin: "+move", bias: -1}); + }, + goLineRight: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + }, sel_move); + }, + goLineLeft: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + return cm.coordsChar({left: 0, top: top}, "div"); + }, sel_move); + }, + goLineLeftSmart: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var pos = cm.coordsChar({left: 0, top: top}, "div"); + if (pos.ch < cm.getLine(pos.line).search(/\S/)) return lineStartSmart(cm, range.head); + return pos; + }, sel_move); + }, + goLineUp: function(cm) {cm.moveV(-1, "line");}, + goLineDown: function(cm) {cm.moveV(1, "line");}, + goPageUp: function(cm) {cm.moveV(-1, "page");}, + goPageDown: function(cm) {cm.moveV(1, "page");}, + goCharLeft: function(cm) {cm.moveH(-1, "char");}, + goCharRight: function(cm) {cm.moveH(1, "char");}, + goColumnLeft: function(cm) {cm.moveH(-1, "column");}, + goColumnRight: function(cm) {cm.moveH(1, "column");}, + goWordLeft: function(cm) {cm.moveH(-1, "word");}, + goGroupRight: function(cm) {cm.moveH(1, "group");}, + goGroupLeft: function(cm) {cm.moveH(-1, "group");}, + goWordRight: function(cm) {cm.moveH(1, "word");}, + delCharBefore: function(cm) {cm.deleteH(-1, "char");}, + delCharAfter: function(cm) {cm.deleteH(1, "char");}, + delWordBefore: function(cm) {cm.deleteH(-1, "word");}, + delWordAfter: function(cm) {cm.deleteH(1, "word");}, + delGroupBefore: function(cm) {cm.deleteH(-1, "group");}, + delGroupAfter: function(cm) {cm.deleteH(1, "group");}, + indentAuto: function(cm) {cm.indentSelection("smart");}, + indentMore: function(cm) {cm.indentSelection("add");}, + indentLess: function(cm) {cm.indentSelection("subtract");}, + insertTab: function(cm) {cm.replaceSelection("\t");}, + insertSoftTab: function(cm) { + var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize; + for (var i = 0; i < ranges.length; i++) { + var pos = ranges[i].from(); + var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize); + spaces.push(new Array(tabSize - col % tabSize + 1).join(" ")); + } + cm.replaceSelections(spaces); + }, + defaultTab: function(cm) { + if (cm.somethingSelected()) cm.indentSelection("add"); + else cm.execCommand("insertTab"); + }, + transposeChars: function(cm) { + runInOp(cm, function() { + var ranges = cm.listSelections(), newSel = []; + for (var i = 0; i < ranges.length; i++) { + var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text; + if (line) { + if (cur.ch == line.length) cur = new Pos(cur.line, cur.ch - 1); + if (cur.ch > 0) { + cur = new Pos(cur.line, cur.ch + 1); + cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2), + Pos(cur.line, cur.ch - 2), cur, "+transpose"); + } else if (cur.line > cm.doc.first) { + var prev = getLine(cm.doc, cur.line - 1).text; + if (prev) + cm.replaceRange(line.charAt(0) + "\n" + prev.charAt(prev.length - 1), + Pos(cur.line - 1, prev.length - 1), Pos(cur.line, 1), "+transpose"); + } + } + newSel.push(new Range(cur, cur)); + } + cm.setSelections(newSel); + }); + }, + newlineAndIndent: function(cm) { + runInOp(cm, function() { + var len = cm.listSelections().length; + for (var i = 0; i < len; i++) { + var range = cm.listSelections()[i]; + cm.replaceRange("\n", range.anchor, range.head, "+input"); + cm.indentLine(range.from().line + 1, null, true); + ensureCursorVisible(cm); + } + }); + }, + toggleOverwrite: function(cm) {cm.toggleOverwrite();} + }; + + + // STANDARD KEYMAPS + + var keyMap = CodeMirror.keyMap = {}; + + keyMap.basic = { + "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", + "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", + "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", + "Tab": "defaultTab", "Shift-Tab": "indentAuto", + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite", + "Esc": "singleSelection" + }; + // Note that the save and find-related commands aren't defined by + // default. User code or addons can define them. Unknown commands + // are simply ignored. + keyMap.pcDefault = { + "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", + "Ctrl-Home": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown", + "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", + "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", + "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", + "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", + "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection", + fallthrough: "basic" + }; + // Very basic readline/emacs-style bindings, which are standard on Mac. + keyMap.emacsy = { + "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", + "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", + "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars" + }; + keyMap.macDefault = { + "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", + "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft", + "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore", + "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", + "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", + "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight", + "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd", + fallthrough: ["basic", "emacsy"] + }; + keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + + // KEYMAP DISPATCH + + function normalizeKeyName(name) { + var parts = name.split(/-(?!$)/), name = parts[parts.length - 1]; + var alt, ctrl, shift, cmd; + for (var i = 0; i < parts.length - 1; i++) { + var mod = parts[i]; + if (/^(cmd|meta|m)$/i.test(mod)) cmd = true; + else if (/^a(lt)?$/i.test(mod)) alt = true; + else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true; + else if (/^s(hift)$/i.test(mod)) shift = true; + else throw new Error("Unrecognized modifier name: " + mod); + } + if (alt) name = "Alt-" + name; + if (ctrl) name = "Ctrl-" + name; + if (cmd) name = "Cmd-" + name; + if (shift) name = "Shift-" + name; + return name; + } + + // This is a kludge to keep keymaps mostly working as raw objects + // (backwards compatibility) while at the same time support features + // like normalization and multi-stroke key bindings. It compiles a + // new normalized keymap, and then updates the old object to reflect + // this. + CodeMirror.normalizeKeyMap = function(keymap) { + var copy = {}; + for (var keyname in keymap) if (keymap.hasOwnProperty(keyname)) { + var value = keymap[keyname]; + if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) continue; + if (value == "...") { delete keymap[keyname]; continue; } + + var keys = map(keyname.split(" "), normalizeKeyName); + for (var i = 0; i < keys.length; i++) { + var val, name; + if (i == keys.length - 1) { + name = keyname; + val = value; + } else { + name = keys.slice(0, i + 1).join(" "); + val = "..."; + } + var prev = copy[name]; + if (!prev) copy[name] = val; + else if (prev != val) throw new Error("Inconsistent bindings for " + name); + } + delete keymap[keyname]; + } + for (var prop in copy) keymap[prop] = copy[prop]; + return keymap; + }; + + var lookupKey = CodeMirror.lookupKey = function(key, map, handle, context) { + map = getKeyMap(map); + var found = map.call ? map.call(key, context) : map[key]; + if (found === false) return "nothing"; + if (found === "...") return "multi"; + if (found != null && handle(found)) return "handled"; + + if (map.fallthrough) { + if (Object.prototype.toString.call(map.fallthrough) != "[object Array]") + return lookupKey(key, map.fallthrough, handle, context); + for (var i = 0; i < map.fallthrough.length; i++) { + var result = lookupKey(key, map.fallthrough[i], handle, context); + if (result) return result; + } + } + }; + + // Modifier key presses don't count as 'real' key presses for the + // purpose of keymap fallthrough. + var isModifierKey = CodeMirror.isModifierKey = function(value) { + var name = typeof value == "string" ? value : keyNames[value.keyCode]; + return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod"; + }; + + // Look up the name of a key as indicated by an event object. + var keyName = CodeMirror.keyName = function(event, noShift) { + if (presto && event.keyCode == 34 && event["char"]) return false; + var base = keyNames[event.keyCode], name = base; + if (name == null || event.altGraphKey) return false; + if (event.altKey && base != "Alt") name = "Alt-" + name; + if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") name = "Ctrl-" + name; + if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Cmd") name = "Cmd-" + name; + if (!noShift && event.shiftKey && base != "Shift") name = "Shift-" + name; + return name; + }; + + function getKeyMap(val) { + return typeof val == "string" ? keyMap[val] : val; + } + + // FROMTEXTAREA + + CodeMirror.fromTextArea = function(textarea, options) { + options = options ? copyObj(options) : {}; + options.value = textarea.value; + if (!options.tabindex && textarea.tabIndex) + options.tabindex = textarea.tabIndex; + if (!options.placeholder && textarea.placeholder) + options.placeholder = textarea.placeholder; + // Set autofocus to true if this textarea is focused, or if it has + // autofocus and no other element is focused. + if (options.autofocus == null) { + var hasFocus = activeElt(); + options.autofocus = hasFocus == textarea || + textarea.getAttribute("autofocus") != null && hasFocus == document.body; + } + + function save() {textarea.value = cm.getValue();} + if (textarea.form) { + on(textarea.form, "submit", save); + // Deplorable hack to make the submit method do the right thing. + if (!options.leaveSubmitMethodAlone) { + var form = textarea.form, realSubmit = form.submit; + try { + var wrappedSubmit = form.submit = function() { + save(); + form.submit = realSubmit; + form.submit(); + form.submit = wrappedSubmit; + }; + } catch(e) {} + } + } + + options.finishInit = function(cm) { + cm.save = save; + cm.getTextArea = function() { return textarea; }; + cm.toTextArea = function() { + cm.toTextArea = isNaN; // Prevent this from being ran twice + save(); + textarea.parentNode.removeChild(cm.getWrapperElement()); + textarea.style.display = ""; + if (textarea.form) { + off(textarea.form, "submit", save); + if (typeof textarea.form.submit == "function") + textarea.form.submit = realSubmit; + } + }; + }; + + textarea.style.display = "none"; + var cm = CodeMirror(function(node) { + textarea.parentNode.insertBefore(node, textarea.nextSibling); + }, options); + return cm; + }; + + // STRING STREAM + + // Fed to the mode parsers, provides helper functions to make + // parsers more succinct. + + var StringStream = CodeMirror.StringStream = function(string, tabSize) { + this.pos = this.start = 0; + this.string = string; + this.tabSize = tabSize || 8; + this.lastColumnPos = this.lastColumnValue = 0; + this.lineStart = 0; + }; + + StringStream.prototype = { + eol: function() {return this.pos >= this.string.length;}, + sol: function() {return this.pos == this.lineStart;}, + peek: function() {return this.string.charAt(this.pos) || undefined;}, + next: function() { + if (this.pos < this.string.length) + return this.string.charAt(this.pos++); + }, + eat: function(match) { + var ch = this.string.charAt(this.pos); + if (typeof match == "string") var ok = ch == match; + else var ok = ch && (match.test ? match.test(ch) : match(ch)); + if (ok) {++this.pos; return ch;} + }, + eatWhile: function(match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start; + }, + eatSpace: function() { + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos; + return this.pos > start; + }, + skipToEnd: function() {this.pos = this.string.length;}, + skipTo: function(ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true;} + }, + backUp: function(n) {this.pos -= n;}, + column: function() { + if (this.lastColumnPos < this.start) { + this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); + this.lastColumnPos = this.start; + } + return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); + }, + indentation: function() { + return countColumn(this.string, null, this.tabSize) - + (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); + }, + match: function(pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;}; + var substr = this.string.substr(this.pos, pattern.length); + if (cased(substr) == cased(pattern)) { + if (consume !== false) this.pos += pattern.length; + return true; + } + } else { + var match = this.string.slice(this.pos).match(pattern); + if (match && match.index > 0) return null; + if (match && consume !== false) this.pos += match[0].length; + return match; + } + }, + current: function(){return this.string.slice(this.start, this.pos);}, + hideFirstChars: function(n, inner) { + this.lineStart += n; + try { return inner(); } + finally { this.lineStart -= n; } + } + }; + + // TEXTMARKERS + + // Created with markText and setBookmark methods. A TextMarker is a + // handle that can be used to clear or find a marked position in the + // document. Line objects hold arrays (markedSpans) containing + // {from, to, marker} object pointing to such marker objects, and + // indicating that such a marker is present on that line. Multiple + // lines may point to the same marker when it spans across lines. + // The spans will have null for their from/to properties when the + // marker continues beyond the start/end of the line. Markers have + // links back to the lines they currently touch. + + var nextMarkerId = 0; + + var TextMarker = CodeMirror.TextMarker = function(doc, type) { + this.lines = []; + this.type = type; + this.doc = doc; + this.id = ++nextMarkerId; + }; + eventMixin(TextMarker); + + // Clear the marker. + TextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + var cm = this.doc.cm, withOp = cm && !cm.curOp; + if (withOp) startOperation(cm); + if (hasHandler(this, "clear")) { + var found = this.find(); + if (found) signalLater(this, "clear", found.from, found.to); + } + var min = null, max = null; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (cm && !this.collapsed) regLineChange(cm, lineNo(line), "text"); + else if (cm) { + if (span.to != null) max = lineNo(line); + if (span.from != null) min = lineNo(line); + } + line.markedSpans = removeMarkedSpan(line.markedSpans, span); + if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm) + updateLineHeight(line, textHeight(cm.display)); + } + if (cm && this.collapsed && !cm.options.lineWrapping) for (var i = 0; i < this.lines.length; ++i) { + var visual = visualLine(this.lines[i]), len = lineLength(visual); + if (len > cm.display.maxLineLength) { + cm.display.maxLine = visual; + cm.display.maxLineLength = len; + cm.display.maxLineChanged = true; + } + } + + if (min != null && cm && this.collapsed) regChange(cm, min, max + 1); + this.lines.length = 0; + this.explicitlyCleared = true; + if (this.atomic && this.doc.cantEdit) { + this.doc.cantEdit = false; + if (cm) reCheckSelection(cm.doc); + } + if (cm) signalLater(cm, "markerCleared", cm, this); + if (withOp) endOperation(cm); + if (this.parent) this.parent.clear(); + }; + + // Find the position of the marker in the document. Returns a {from, + // to} object by default. Side can be passed to get a specific side + // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the + // Pos objects returned contain a line object, rather than a line + // number (used to prevent looking up the same line twice). + TextMarker.prototype.find = function(side, lineObj) { + if (side == null && this.type == "bookmark") side = 1; + var from, to; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (span.from != null) { + from = Pos(lineObj ? line : lineNo(line), span.from); + if (side == -1) return from; + } + if (span.to != null) { + to = Pos(lineObj ? line : lineNo(line), span.to); + if (side == 1) return to; + } + } + return from && {from: from, to: to}; + }; + + // Signals that the marker's widget changed, and surrounding layout + // should be recomputed. + TextMarker.prototype.changed = function() { + var pos = this.find(-1, true), widget = this, cm = this.doc.cm; + if (!pos || !cm) return; + runInOp(cm, function() { + var line = pos.line, lineN = lineNo(pos.line); + var view = findViewForLine(cm, lineN); + if (view) { + clearLineMeasurementCacheFor(view); + cm.curOp.selectionChanged = cm.curOp.forceUpdate = true; + } + cm.curOp.updateMaxLine = true; + if (!lineIsHidden(widget.doc, line) && widget.height != null) { + var oldHeight = widget.height; + widget.height = null; + var dHeight = widgetHeight(widget) - oldHeight; + if (dHeight) + updateLineHeight(line, line.height + dHeight); + } + }); + }; + + TextMarker.prototype.attachLine = function(line) { + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) + (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this); + } + this.lines.push(line); + }; + TextMarker.prototype.detachLine = function(line) { + this.lines.splice(indexOf(this.lines, line), 1); + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + (op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this); + } + }; + + // Collapsed markers have unique ids, in order to be able to order + // them, which is needed for uniquely determining an outer marker + // when they overlap (they may nest, but not partially overlap). + var nextMarkerId = 0; + + // Create a marker, wire it up to the right lines, and + function markText(doc, from, to, options, type) { + // Shared markers (across linked documents) are handled separately + // (markTextShared will call out to this again, once per + // document). + if (options && options.shared) return markTextShared(doc, from, to, options, type); + // Ensure we are in an operation. + if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type); + + var marker = new TextMarker(doc, type), diff = cmp(from, to); + if (options) copyObj(options, marker, false); + // Don't connect empty markers unless clearWhenEmpty is false + if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false) + return marker; + if (marker.replacedWith) { + // Showing up as a widget implies collapsed (widget replaces text) + marker.collapsed = true; + marker.widgetNode = elt("span", [marker.replacedWith], "CodeMirror-widget"); + if (!options.handleMouseEvents) marker.widgetNode.setAttribute("cm-ignore-events", "true"); + if (options.insertLeft) marker.widgetNode.insertLeft = true; + } + if (marker.collapsed) { + if (conflictingCollapsedRange(doc, from.line, from, to, marker) || + from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker)) + throw new Error("Inserting collapsed marker partially overlapping an existing one"); + sawCollapsedSpans = true; + } + + if (marker.addToHistory) + addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN); + + var curLine = from.line, cm = doc.cm, updateMaxLine; + doc.iter(curLine, to.line + 1, function(line) { + if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine) + updateMaxLine = true; + if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0); + addMarkedSpan(line, new MarkedSpan(marker, + curLine == from.line ? from.ch : null, + curLine == to.line ? to.ch : null)); + ++curLine; + }); + // lineIsHidden depends on the presence of the spans, so needs a second pass + if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) { + if (lineIsHidden(doc, line)) updateLineHeight(line, 0); + }); + + if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); }); + + if (marker.readOnly) { + sawReadOnlySpans = true; + if (doc.history.done.length || doc.history.undone.length) + doc.clearHistory(); + } + if (marker.collapsed) { + marker.id = ++nextMarkerId; + marker.atomic = true; + } + if (cm) { + // Sync editor state + if (updateMaxLine) cm.curOp.updateMaxLine = true; + if (marker.collapsed) + regChange(cm, from.line, to.line + 1); + else if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.css) + for (var i = from.line; i <= to.line; i++) regLineChange(cm, i, "text"); + if (marker.atomic) reCheckSelection(cm.doc); + signalLater(cm, "markerAdded", cm, marker); + } + return marker; + } + + // SHARED TEXTMARKERS + + // A shared marker spans multiple linked documents. It is + // implemented as a meta-marker-object controlling multiple normal + // markers. + var SharedTextMarker = CodeMirror.SharedTextMarker = function(markers, primary) { + this.markers = markers; + this.primary = primary; + for (var i = 0; i < markers.length; ++i) + markers[i].parent = this; + }; + eventMixin(SharedTextMarker); + + SharedTextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + this.explicitlyCleared = true; + for (var i = 0; i < this.markers.length; ++i) + this.markers[i].clear(); + signalLater(this, "clear"); + }; + SharedTextMarker.prototype.find = function(side, lineObj) { + return this.primary.find(side, lineObj); + }; + + function markTextShared(doc, from, to, options, type) { + options = copyObj(options); + options.shared = false; + var markers = [markText(doc, from, to, options, type)], primary = markers[0]; + var widget = options.widgetNode; + linkedDocs(doc, function(doc) { + if (widget) options.widgetNode = widget.cloneNode(true); + markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)); + for (var i = 0; i < doc.linked.length; ++i) + if (doc.linked[i].isParent) return; + primary = lst(markers); + }); + return new SharedTextMarker(markers, primary); + } + + function findSharedMarkers(doc) { + return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), + function(m) { return m.parent; }); + } + + function copySharedMarkers(doc, markers) { + for (var i = 0; i < markers.length; i++) { + var marker = markers[i], pos = marker.find(); + var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to); + if (cmp(mFrom, mTo)) { + var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type); + marker.markers.push(subMark); + subMark.parent = marker; + } + } + } + + function detachSharedMarkers(markers) { + for (var i = 0; i < markers.length; i++) { + var marker = markers[i], linked = [marker.primary.doc];; + linkedDocs(marker.primary.doc, function(d) { linked.push(d); }); + for (var j = 0; j < marker.markers.length; j++) { + var subMarker = marker.markers[j]; + if (indexOf(linked, subMarker.doc) == -1) { + subMarker.parent = null; + marker.markers.splice(j--, 1); + } + } + } + } + + // TEXTMARKER SPANS + + function MarkedSpan(marker, from, to) { + this.marker = marker; + this.from = from; this.to = to; + } + + // Search an array of spans for a span matching the given marker. + function getMarkedSpanFor(spans, marker) { + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.marker == marker) return span; + } + } + // Remove a span from an array, returning undefined if no spans are + // left (we don't store arrays for lines without spans). + function removeMarkedSpan(spans, span) { + for (var r, i = 0; i < spans.length; ++i) + if (spans[i] != span) (r || (r = [])).push(spans[i]); + return r; + } + // Add a span to a line. + function addMarkedSpan(line, span) { + line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; + span.marker.attachLine(line); + } + + // Used for the algorithm that adjusts markers for a change in the + // document. These functions cut an array of spans at a given + // character position, returning an array of remaining chunks (or + // undefined if nothing remains). + function markedSpansBefore(old, startCh, isInsert) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); + if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh); + (nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)); + } + } + return nw; + } + function markedSpansAfter(old, endCh, isInsert) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); + if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh); + (nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, + span.to == null ? null : span.to - endCh)); + } + } + return nw; + } + + // Given a change object, compute the new set of marker spans that + // cover the line in which the change took place. Removes spans + // entirely within the change, reconnects spans belonging to the + // same marker that appear on both sides of the change, and cuts off + // spans partially within the change. Returns an array of span + // arrays with one element for each line in (after) the change. + function stretchSpansOverChange(doc, change) { + if (change.full) return null; + var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans; + var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans; + if (!oldFirst && !oldLast) return null; + + var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0; + // Get the spans that 'stick out' on both sides + var first = markedSpansBefore(oldFirst, startCh, isInsert); + var last = markedSpansAfter(oldLast, endCh, isInsert); + + // Next, merge those two ends + var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0); + if (first) { + // Fix up .to properties of first + for (var i = 0; i < first.length; ++i) { + var span = first[i]; + if (span.to == null) { + var found = getMarkedSpanFor(last, span.marker); + if (!found) span.to = startCh; + else if (sameLine) span.to = found.to == null ? null : found.to + offset; + } + } + } + if (last) { + // Fix up .from in last (or move them into first in case of sameLine) + for (var i = 0; i < last.length; ++i) { + var span = last[i]; + if (span.to != null) span.to += offset; + if (span.from == null) { + var found = getMarkedSpanFor(first, span.marker); + if (!found) { + span.from = offset; + if (sameLine) (first || (first = [])).push(span); + } + } else { + span.from += offset; + if (sameLine) (first || (first = [])).push(span); + } + } + } + // Make sure we didn't create any zero-length spans + if (first) first = clearEmptySpans(first); + if (last && last != first) last = clearEmptySpans(last); + + var newMarkers = [first]; + if (!sameLine) { + // Fill gap with whole-line-spans + var gap = change.text.length - 2, gapMarkers; + if (gap > 0 && first) + for (var i = 0; i < first.length; ++i) + if (first[i].to == null) + (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i].marker, null, null)); + for (var i = 0; i < gap; ++i) + newMarkers.push(gapMarkers); + newMarkers.push(last); + } + return newMarkers; + } + + // Remove spans that are empty and don't have a clearWhenEmpty + // option of false. + function clearEmptySpans(spans) { + for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false) + spans.splice(i--, 1); + } + if (!spans.length) return null; + return spans; + } + + // Used for un/re-doing changes from the history. Combines the + // result of computing the existing spans with the set of spans that + // existed in the history (so that deleting around a span and then + // undoing brings back the span). + function mergeOldSpans(doc, change) { + var old = getOldSpans(doc, change); + var stretched = stretchSpansOverChange(doc, change); + if (!old) return stretched; + if (!stretched) return old; + + for (var i = 0; i < old.length; ++i) { + var oldCur = old[i], stretchCur = stretched[i]; + if (oldCur && stretchCur) { + spans: for (var j = 0; j < stretchCur.length; ++j) { + var span = stretchCur[j]; + for (var k = 0; k < oldCur.length; ++k) + if (oldCur[k].marker == span.marker) continue spans; + oldCur.push(span); + } + } else if (stretchCur) { + old[i] = stretchCur; + } + } + return old; + } + + // Used to 'clip' out readOnly ranges when making a change. + function removeReadOnlyRanges(doc, from, to) { + var markers = null; + doc.iter(from.line, to.line + 1, function(line) { + if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) { + var mark = line.markedSpans[i].marker; + if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) + (markers || (markers = [])).push(mark); + } + }); + if (!markers) return null; + var parts = [{from: from, to: to}]; + for (var i = 0; i < markers.length; ++i) { + var mk = markers[i], m = mk.find(0); + for (var j = 0; j < parts.length; ++j) { + var p = parts[j]; + if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) continue; + var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to); + if (dfrom < 0 || !mk.inclusiveLeft && !dfrom) + newParts.push({from: p.from, to: m.from}); + if (dto > 0 || !mk.inclusiveRight && !dto) + newParts.push({from: m.to, to: p.to}); + parts.splice.apply(parts, newParts); + j += newParts.length - 1; + } + } + return parts; + } + + // Connect or disconnect spans from a line. + function detachMarkedSpans(line) { + var spans = line.markedSpans; + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.detachLine(line); + line.markedSpans = null; + } + function attachMarkedSpans(line, spans) { + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.attachLine(line); + line.markedSpans = spans; + } + + // Helpers used when computing which overlapping collapsed span + // counts as the larger one. + function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0; } + function extraRight(marker) { return marker.inclusiveRight ? 1 : 0; } + + // Returns a number indicating which of two overlapping collapsed + // spans is larger (and thus includes the other). Falls back to + // comparing ids when the spans cover exactly the same range. + function compareCollapsedMarkers(a, b) { + var lenDiff = a.lines.length - b.lines.length; + if (lenDiff != 0) return lenDiff; + var aPos = a.find(), bPos = b.find(); + var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b); + if (fromCmp) return -fromCmp; + var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b); + if (toCmp) return toCmp; + return b.id - a.id; + } + + // Find out whether a line ends or starts in a collapsed span. If + // so, return the marker for that span. + function collapsedSpanAtSide(line, start) { + var sps = sawCollapsedSpans && line.markedSpans, found; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (sp.marker.collapsed && (start ? sp.from : sp.to) == null && + (!found || compareCollapsedMarkers(found, sp.marker) < 0)) + found = sp.marker; + } + return found; + } + function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true); } + function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false); } + + // Test whether there exists a collapsed span that partially + // overlaps (covers the start or end, but not both) of a new span. + // Such overlap is not allowed. + function conflictingCollapsedRange(doc, lineNo, from, to, marker) { + var line = getLine(doc, lineNo); + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) for (var i = 0; i < sps.length; ++i) { + var sp = sps[i]; + if (!sp.marker.collapsed) continue; + var found = sp.marker.find(0); + var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker); + var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker); + if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue; + if (fromCmp <= 0 && (cmp(found.to, from) > 0 || (sp.marker.inclusiveRight && marker.inclusiveLeft)) || + fromCmp >= 0 && (cmp(found.from, to) < 0 || (sp.marker.inclusiveLeft && marker.inclusiveRight))) + return true; + } + } + + // A visual line is a line as drawn on the screen. Folding, for + // example, can cause multiple logical lines to appear on the same + // visual line. This finds the start of the visual line that the + // given line is part of (usually that is the line itself). + function visualLine(line) { + var merged; + while (merged = collapsedSpanAtStart(line)) + line = merged.find(-1, true).line; + return line; + } + + // Returns an array of logical lines that continue the visual line + // started by the argument, or undefined if there are no such lines. + function visualLineContinued(line) { + var merged, lines; + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line; + (lines || (lines = [])).push(line); + } + return lines; + } + + // Get the line number of the start of the visual line that the + // given line number is part of. + function visualLineNo(doc, lineN) { + var line = getLine(doc, lineN), vis = visualLine(line); + if (line == vis) return lineN; + return lineNo(vis); + } + // Get the line number of the start of the next visual line after + // the given line. + function visualLineEndNo(doc, lineN) { + if (lineN > doc.lastLine()) return lineN; + var line = getLine(doc, lineN), merged; + if (!lineIsHidden(doc, line)) return lineN; + while (merged = collapsedSpanAtEnd(line)) + line = merged.find(1, true).line; + return lineNo(line) + 1; + } + + // Compute whether a line is hidden. Lines count as hidden when they + // are part of a visual line that starts with another line, or when + // they are entirely covered by collapsed, non-widget span. + function lineIsHidden(doc, line) { + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (!sp.marker.collapsed) continue; + if (sp.from == null) return true; + if (sp.marker.widgetNode) continue; + if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) + return true; + } + } + function lineIsHiddenInner(doc, line, span) { + if (span.to == null) { + var end = span.marker.find(1, true); + return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker)); + } + if (span.marker.inclusiveRight && span.to == line.text.length) + return true; + for (var sp, i = 0; i < line.markedSpans.length; ++i) { + sp = line.markedSpans[i]; + if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to && + (sp.to == null || sp.to != span.from) && + (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && + lineIsHiddenInner(doc, line, sp)) return true; + } + } + + // LINE WIDGETS + + // Line widgets are block elements displayed above or below a line. + + var LineWidget = CodeMirror.LineWidget = function(doc, node, options) { + if (options) for (var opt in options) if (options.hasOwnProperty(opt)) + this[opt] = options[opt]; + this.doc = doc; + this.node = node; + }; + eventMixin(LineWidget); + + function adjustScrollWhenAboveVisible(cm, line, diff) { + if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) + addToScrollPos(cm, null, diff); + } + + LineWidget.prototype.clear = function() { + var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line); + if (no == null || !ws) return; + for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1); + if (!ws.length) line.widgets = null; + var height = widgetHeight(this); + updateLineHeight(line, Math.max(0, line.height - height)); + if (cm) runInOp(cm, function() { + adjustScrollWhenAboveVisible(cm, line, -height); + regLineChange(cm, no, "widget"); + }); + }; + LineWidget.prototype.changed = function() { + var oldH = this.height, cm = this.doc.cm, line = this.line; + this.height = null; + var diff = widgetHeight(this) - oldH; + if (!diff) return; + updateLineHeight(line, line.height + diff); + if (cm) runInOp(cm, function() { + cm.curOp.forceUpdate = true; + adjustScrollWhenAboveVisible(cm, line, diff); + }); + }; + + function widgetHeight(widget) { + if (widget.height != null) return widget.height; + var cm = widget.doc.cm; + if (!cm) return 0; + if (!contains(document.body, widget.node)) { + var parentStyle = "position: relative;"; + if (widget.coverGutter) + parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;"; + if (widget.noHScroll) + parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;"; + removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle)); + } + return widget.height = widget.node.offsetHeight; + } + + function addLineWidget(doc, handle, node, options) { + var widget = new LineWidget(doc, node, options); + var cm = doc.cm; + if (cm && widget.noHScroll) cm.display.alignWidgets = true; + changeLine(doc, handle, "widget", function(line) { + var widgets = line.widgets || (line.widgets = []); + if (widget.insertAt == null) widgets.push(widget); + else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget); + widget.line = line; + if (cm && !lineIsHidden(doc, line)) { + var aboveVisible = heightAtLine(line) < doc.scrollTop; + updateLineHeight(line, line.height + widgetHeight(widget)); + if (aboveVisible) addToScrollPos(cm, null, widget.height); + cm.curOp.forceUpdate = true; + } + return true; + }); + return widget; + } + + // LINE DATA STRUCTURE + + // Line objects. These hold state related to a line, including + // highlighting info (the styles array). + var Line = CodeMirror.Line = function(text, markedSpans, estimateHeight) { + this.text = text; + attachMarkedSpans(this, markedSpans); + this.height = estimateHeight ? estimateHeight(this) : 1; + }; + eventMixin(Line); + Line.prototype.lineNo = function() { return lineNo(this); }; + + // Change the content (text, markers) of a line. Automatically + // invalidates cached information and tries to re-estimate the + // line's height. + function updateLine(line, text, markedSpans, estimateHeight) { + line.text = text; + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + if (line.order != null) line.order = null; + detachMarkedSpans(line); + attachMarkedSpans(line, markedSpans); + var estHeight = estimateHeight ? estimateHeight(line) : 1; + if (estHeight != line.height) updateLineHeight(line, estHeight); + } + + // Detach a line from the document tree and its markers. + function cleanUpLine(line) { + line.parent = null; + detachMarkedSpans(line); + } + + function extractLineClasses(type, output) { + if (type) for (;;) { + var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/); + if (!lineClass) break; + type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length); + var prop = lineClass[1] ? "bgClass" : "textClass"; + if (output[prop] == null) + output[prop] = lineClass[2]; + else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop])) + output[prop] += " " + lineClass[2]; + } + return type; + } + + function callBlankLine(mode, state) { + if (mode.blankLine) return mode.blankLine(state); + if (!mode.innerMode) return; + var inner = CodeMirror.innerMode(mode, state); + if (inner.mode.blankLine) return inner.mode.blankLine(inner.state); + } + + function readToken(mode, stream, state, inner) { + for (var i = 0; i < 10; i++) { + if (inner) inner[0] = CodeMirror.innerMode(mode, state).mode; + var style = mode.token(stream, state); + if (stream.pos > stream.start) return style; + } + throw new Error("Mode " + mode.name + " failed to advance stream."); + } + + // Utility for getTokenAt and getLineTokens + function takeToken(cm, pos, precise, asArray) { + function getObj(copy) { + return {start: stream.start, end: stream.pos, + string: stream.current(), + type: style || null, + state: copy ? copyState(doc.mode, state) : state}; + } + + var doc = cm.doc, mode = doc.mode, style; + pos = clipPos(doc, pos); + var line = getLine(doc, pos.line), state = getStateBefore(cm, pos.line, precise); + var stream = new StringStream(line.text, cm.options.tabSize), tokens; + if (asArray) tokens = []; + while ((asArray || stream.pos < pos.ch) && !stream.eol()) { + stream.start = stream.pos; + style = readToken(mode, stream, state); + if (asArray) tokens.push(getObj(true)); + } + return asArray ? tokens : getObj(); + } + + // Run the given mode's parser over a line, calling f for each token. + function runMode(cm, text, mode, state, f, lineClasses, forceToEnd) { + var flattenSpans = mode.flattenSpans; + if (flattenSpans == null) flattenSpans = cm.options.flattenSpans; + var curStart = 0, curStyle = null; + var stream = new StringStream(text, cm.options.tabSize), style; + var inner = cm.options.addModeClass && [null]; + if (text == "") extractLineClasses(callBlankLine(mode, state), lineClasses); + while (!stream.eol()) { + if (stream.pos > cm.options.maxHighlightLength) { + flattenSpans = false; + if (forceToEnd) processLine(cm, text, state, stream.pos); + stream.pos = text.length; + style = null; + } else { + style = extractLineClasses(readToken(mode, stream, state, inner), lineClasses); + } + if (inner) { + var mName = inner[0].name; + if (mName) style = "m-" + (style ? mName + " " + style : mName); + } + if (!flattenSpans || curStyle != style) { + while (curStart < stream.start) { + curStart = Math.min(stream.start, curStart + 50000); + f(curStart, curStyle); + } + curStyle = style; + } + stream.start = stream.pos; + } + while (curStart < stream.pos) { + // Webkit seems to refuse to render text nodes longer than 57444 characters + var pos = Math.min(stream.pos, curStart + 50000); + f(pos, curStyle); + curStart = pos; + } + } + + // Compute a style array (an array starting with a mode generation + // -- for invalidation -- followed by pairs of end positions and + // style strings), which is used to highlight the tokens on the + // line. + function highlightLine(cm, line, state, forceToEnd) { + // A styles array always starts with a number identifying the + // mode/overlays that it is based on (for easy invalidation). + var st = [cm.state.modeGen], lineClasses = {}; + // Compute the base array of styles + runMode(cm, line.text, cm.doc.mode, state, function(end, style) { + st.push(end, style); + }, lineClasses, forceToEnd); + + // Run overlays, adjust style array. + for (var o = 0; o < cm.state.overlays.length; ++o) { + var overlay = cm.state.overlays[o], i = 1, at = 0; + runMode(cm, line.text, overlay.mode, true, function(end, style) { + var start = i; + // Ensure there's a token end at the current position, and that i points at it + while (at < end) { + var i_end = st[i]; + if (i_end > end) + st.splice(i, 1, end, st[i+1], i_end); + i += 2; + at = Math.min(end, i_end); + } + if (!style) return; + if (overlay.opaque) { + st.splice(start, i - start, end, "cm-overlay " + style); + i = start + 2; + } else { + for (; start < i; start += 2) { + var cur = st[start+1]; + st[start+1] = (cur ? cur + " " : "") + "cm-overlay " + style; + } + } + }, lineClasses); + } + + return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null}; + } + + function getLineStyles(cm, line, updateFrontier) { + if (!line.styles || line.styles[0] != cm.state.modeGen) { + var result = highlightLine(cm, line, line.stateAfter = getStateBefore(cm, lineNo(line))); + line.styles = result.styles; + if (result.classes) line.styleClasses = result.classes; + else if (line.styleClasses) line.styleClasses = null; + if (updateFrontier === cm.doc.frontier) cm.doc.frontier++; + } + return line.styles; + } + + // Lightweight form of highlight -- proceed over this line and + // update state, but don't save a style array. Used for lines that + // aren't currently visible. + function processLine(cm, text, state, startAt) { + var mode = cm.doc.mode; + var stream = new StringStream(text, cm.options.tabSize); + stream.start = stream.pos = startAt || 0; + if (text == "") callBlankLine(mode, state); + while (!stream.eol() && stream.pos <= cm.options.maxHighlightLength) { + readToken(mode, stream, state); + stream.start = stream.pos; + } + } + + // Convert a style as returned by a mode (either null, or a string + // containing one or more styles) to a CSS style. This is cached, + // and also looks for line-wide styles. + var styleToClassCache = {}, styleToClassCacheWithMode = {}; + function interpretTokenStyle(style, options) { + if (!style || /^\s*$/.test(style)) return null; + var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache; + return cache[style] || + (cache[style] = style.replace(/\S+/g, "cm-$&")); + } + + // Render the DOM representation of the text of a line. Also builds + // up a 'line map', which points at the DOM nodes that represent + // specific stretches of text, and is used by the measuring code. + // The returned object contains the DOM node, this map, and + // information about line-wide styles that were set by the mode. + function buildLineContent(cm, lineView) { + // The padding-right forces the element to have a 'border', which + // is needed on Webkit to be able to get line-level bounding + // rectangles for it (in measureChar). + var content = elt("span", null, null, webkit ? "padding-right: .1px" : null); + var builder = {pre: elt("pre", [content]), content: content, + col: 0, pos: 0, cm: cm, + splitSpaces: (ie || webkit) && cm.getOption("lineWrapping")}; + lineView.measure = {}; + + // Iterate over the logical lines that make up this visual line. + for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) { + var line = i ? lineView.rest[i - 1] : lineView.line, order; + builder.pos = 0; + builder.addToken = buildToken; + // Optionally wire in some hacks into the token-rendering + // algorithm, to deal with browser quirks. + if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line))) + builder.addToken = buildTokenBadBidi(builder.addToken, order); + builder.map = []; + var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line); + insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate)); + if (line.styleClasses) { + if (line.styleClasses.bgClass) + builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || ""); + if (line.styleClasses.textClass) + builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || ""); + } + + // Ensure at least a single node is present, for measuring. + if (builder.map.length == 0) + builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure))); + + // Store the map and a cache object for the current logical line + if (i == 0) { + lineView.measure.map = builder.map; + lineView.measure.cache = {}; + } else { + (lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map); + (lineView.measure.caches || (lineView.measure.caches = [])).push({}); + } + } + + // See issue #2901 + if (webkit && /\bcm-tab\b/.test(builder.content.lastChild.className)) + builder.content.className = "cm-tab-wrap-hack"; + + signal(cm, "renderLine", cm, lineView.line, builder.pre); + if (builder.pre.className) + builder.textClass = joinClasses(builder.pre.className, builder.textClass || ""); + + return builder; + } + + function defaultSpecialCharPlaceholder(ch) { + var token = elt("span", "\u2022", "cm-invalidchar"); + token.title = "\\u" + ch.charCodeAt(0).toString(16); + token.setAttribute("aria-label", token.title); + return token; + } + + // Build up the DOM representation for a single token, and add it to + // the line map. Takes care to render special characters separately. + function buildToken(builder, text, style, startStyle, endStyle, title, css) { + if (!text) return; + var displayText = builder.splitSpaces ? text.replace(/ {3,}/g, splitSpaces) : text; + var special = builder.cm.state.specialChars, mustWrap = false; + if (!special.test(text)) { + builder.col += text.length; + var content = document.createTextNode(displayText); + builder.map.push(builder.pos, builder.pos + text.length, content); + if (ie && ie_version < 9) mustWrap = true; + builder.pos += text.length; + } else { + var content = document.createDocumentFragment(), pos = 0; + while (true) { + special.lastIndex = pos; + var m = special.exec(text); + var skipped = m ? m.index - pos : text.length - pos; + if (skipped) { + var txt = document.createTextNode(displayText.slice(pos, pos + skipped)); + if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); + else content.appendChild(txt); + builder.map.push(builder.pos, builder.pos + skipped, txt); + builder.col += skipped; + builder.pos += skipped; + } + if (!m) break; + pos += skipped + 1; + if (m[0] == "\t") { + var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; + var txt = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); + txt.setAttribute("role", "presentation"); + txt.setAttribute("cm-text", "\t"); + builder.col += tabWidth; + } else { + var txt = builder.cm.options.specialCharPlaceholder(m[0]); + txt.setAttribute("cm-text", m[0]); + if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); + else content.appendChild(txt); + builder.col += 1; + } + builder.map.push(builder.pos, builder.pos + 1, txt); + builder.pos++; + } + } + if (style || startStyle || endStyle || mustWrap || css) { + var fullStyle = style || ""; + if (startStyle) fullStyle += startStyle; + if (endStyle) fullStyle += endStyle; + var token = elt("span", [content], fullStyle, css); + if (title) token.title = title; + return builder.content.appendChild(token); + } + builder.content.appendChild(content); + } + + function splitSpaces(old) { + var out = " "; + for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0"; + out += " "; + return out; + } + + // Work around nonsense dimensions being reported for stretches of + // right-to-left text. + function buildTokenBadBidi(inner, order) { + return function(builder, text, style, startStyle, endStyle, title, css) { + style = style ? style + " cm-force-border" : "cm-force-border"; + var start = builder.pos, end = start + text.length; + for (;;) { + // Find the part that overlaps with the start of this text + for (var i = 0; i < order.length; i++) { + var part = order[i]; + if (part.to > start && part.from <= start) break; + } + if (part.to >= end) return inner(builder, text, style, startStyle, endStyle, title, css); + inner(builder, text.slice(0, part.to - start), style, startStyle, null, title, css); + startStyle = null; + text = text.slice(part.to - start); + start = part.to; + } + }; + } + + function buildCollapsedSpan(builder, size, marker, ignoreWidget) { + var widget = !ignoreWidget && marker.widgetNode; + if (widget) builder.map.push(builder.pos, builder.pos + size, widget); + if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) { + if (!widget) + widget = builder.content.appendChild(document.createElement("span")); + widget.setAttribute("cm-marker", marker.id); + } + if (widget) { + builder.cm.display.input.setUneditable(widget); + builder.content.appendChild(widget); + } + builder.pos += size; + } + + // Outputs a number of spans to make up a line, taking highlighting + // and marked text into account. + function insertLineContent(line, builder, styles) { + var spans = line.markedSpans, allText = line.text, at = 0; + if (!spans) { + for (var i = 1; i < styles.length; i+=2) + builder.addToken(builder, allText.slice(at, at = styles[i]), interpretTokenStyle(styles[i+1], builder.cm.options)); + return; + } + + var len = allText.length, pos = 0, i = 1, text = "", style, css; + var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed; + for (;;) { + if (nextChange == pos) { // Update current marker set + spanStyle = spanEndStyle = spanStartStyle = title = css = ""; + collapsed = null; nextChange = Infinity; + var foundBookmarks = []; + for (var j = 0; j < spans.length; ++j) { + var sp = spans[j], m = sp.marker; + if (sp.from <= pos && (sp.to == null || sp.to > pos)) { + if (sp.to != null && nextChange > sp.to) { nextChange = sp.to; spanEndStyle = ""; } + if (m.className) spanStyle += " " + m.className; + if (m.css) css = m.css; + if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle; + if (m.endStyle && sp.to == nextChange) spanEndStyle += " " + m.endStyle; + if (m.title && !title) title = m.title; + if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0)) + collapsed = sp; + } else if (sp.from > pos && nextChange > sp.from) { + nextChange = sp.from; + } + if (m.type == "bookmark" && sp.from == pos && m.widgetNode) foundBookmarks.push(m); + } + if (collapsed && (collapsed.from || 0) == pos) { + buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos, + collapsed.marker, collapsed.from == null); + if (collapsed.to == null) return; + } + if (!collapsed && foundBookmarks.length) for (var j = 0; j < foundBookmarks.length; ++j) + buildCollapsedSpan(builder, 0, foundBookmarks[j]); + } + if (pos >= len) break; + + var upto = Math.min(len, nextChange); + while (true) { + if (text) { + var end = pos + text.length; + if (!collapsed) { + var tokenText = end > upto ? text.slice(0, upto - pos) : text; + builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle, + spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title, css); + } + if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;} + pos = end; + spanStartStyle = ""; + } + text = allText.slice(at, at = styles[i++]); + style = interpretTokenStyle(styles[i++], builder.cm.options); + } + } + } + + // DOCUMENT DATA STRUCTURE + + // By default, updates that start and end at the beginning of a line + // are treated specially, in order to make the association of line + // widgets and marker elements with the text behave more intuitive. + function isWholeLineUpdate(doc, change) { + return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" && + (!doc.cm || doc.cm.options.wholeLineUpdateBefore); + } + + // Perform a change on the document data structure. + function updateDoc(doc, change, markedSpans, estimateHeight) { + function spansFor(n) {return markedSpans ? markedSpans[n] : null;} + function update(line, text, spans) { + updateLine(line, text, spans, estimateHeight); + signalLater(line, "change", line, change); + } + function linesFor(start, end) { + for (var i = start, result = []; i < end; ++i) + result.push(new Line(text[i], spansFor(i), estimateHeight)); + return result; + } + + var from = change.from, to = change.to, text = change.text; + var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line); + var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; + + // Adjust the line structure + if (change.full) { + doc.insert(0, linesFor(0, text.length)); + doc.remove(text.length, doc.size - text.length); + } else if (isWholeLineUpdate(doc, change)) { + // This is a whole-line replace. Treated specially to make + // sure line objects move the way they are supposed to. + var added = linesFor(0, text.length - 1); + update(lastLine, lastLine.text, lastSpans); + if (nlines) doc.remove(from.line, nlines); + if (added.length) doc.insert(from.line, added); + } else if (firstLine == lastLine) { + if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans); + } else { + var added = linesFor(1, text.length - 1); + added.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight)); + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + doc.insert(from.line + 1, added); + } + } else if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)); + doc.remove(from.line + 1, nlines); + } else { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans); + var added = linesFor(1, text.length - 1); + if (nlines > 1) doc.remove(from.line + 1, nlines - 1); + doc.insert(from.line + 1, added); + } + + signalLater(doc, "change", doc, change); + } + + // The document is represented as a BTree consisting of leaves, with + // chunk of lines in them, and branches, with up to ten leaves or + // other branch nodes below them. The top node is always a branch + // node, and is the document object itself (meaning it has + // additional methods and properties). + // + // All nodes have parent links. The tree is used both to go from + // line numbers to line objects, and to go from objects to numbers. + // It also indexes by height, and is used to convert between height + // and line object, and to find the total height of the document. + // + // See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html + + function LeafChunk(lines) { + this.lines = lines; + this.parent = null; + for (var i = 0, height = 0; i < lines.length; ++i) { + lines[i].parent = this; + height += lines[i].height; + } + this.height = height; + } + + LeafChunk.prototype = { + chunkSize: function() { return this.lines.length; }, + // Remove the n lines at offset 'at'. + removeInner: function(at, n) { + for (var i = at, e = at + n; i < e; ++i) { + var line = this.lines[i]; + this.height -= line.height; + cleanUpLine(line); + signalLater(line, "delete"); + } + this.lines.splice(at, n); + }, + // Helper used to collapse a small branch into a single leaf. + collapse: function(lines) { + lines.push.apply(lines, this.lines); + }, + // Insert the given array of lines at offset 'at', count them as + // having the given height. + insertInner: function(at, lines, height) { + this.height += height; + this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); + for (var i = 0; i < lines.length; ++i) lines[i].parent = this; + }, + // Used to iterate over a part of the tree. + iterN: function(at, n, op) { + for (var e = at + n; at < e; ++at) + if (op(this.lines[at])) return true; + } + }; + + function BranchChunk(children) { + this.children = children; + var size = 0, height = 0; + for (var i = 0; i < children.length; ++i) { + var ch = children[i]; + size += ch.chunkSize(); height += ch.height; + ch.parent = this; + } + this.size = size; + this.height = height; + this.parent = null; + } + + BranchChunk.prototype = { + chunkSize: function() { return this.size; }, + removeInner: function(at, n) { + this.size -= n; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var rm = Math.min(n, sz - at), oldHeight = child.height; + child.removeInner(at, rm); + this.height -= oldHeight - child.height; + if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } + if ((n -= rm) == 0) break; + at = 0; + } else at -= sz; + } + // If the result is smaller than 25 lines, ensure that it is a + // single leaf node. + if (this.size - n < 25 && + (this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) { + var lines = []; + this.collapse(lines); + this.children = [new LeafChunk(lines)]; + this.children[0].parent = this; + } + }, + collapse: function(lines) { + for (var i = 0; i < this.children.length; ++i) this.children[i].collapse(lines); + }, + insertInner: function(at, lines, height) { + this.size += lines.length; + this.height += height; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at <= sz) { + child.insertInner(at, lines, height); + if (child.lines && child.lines.length > 50) { + while (child.lines.length > 50) { + var spilled = child.lines.splice(child.lines.length - 25, 25); + var newleaf = new LeafChunk(spilled); + child.height -= newleaf.height; + this.children.splice(i + 1, 0, newleaf); + newleaf.parent = this; + } + this.maybeSpill(); + } + break; + } + at -= sz; + } + }, + // When a node has grown, check whether it should be split. + maybeSpill: function() { + if (this.children.length <= 10) return; + var me = this; + do { + var spilled = me.children.splice(me.children.length - 5, 5); + var sibling = new BranchChunk(spilled); + if (!me.parent) { // Become the parent node + var copy = new BranchChunk(me.children); + copy.parent = me; + me.children = [copy, sibling]; + me = copy; + } else { + me.size -= sibling.size; + me.height -= sibling.height; + var myIndex = indexOf(me.parent.children, me); + me.parent.children.splice(myIndex + 1, 0, sibling); + } + sibling.parent = me.parent; + } while (me.children.length > 10); + me.parent.maybeSpill(); + }, + iterN: function(at, n, op) { + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var used = Math.min(n, sz - at); + if (child.iterN(at, used, op)) return true; + if ((n -= used) == 0) break; + at = 0; + } else at -= sz; + } + } + }; + + var nextDocId = 0; + var Doc = CodeMirror.Doc = function(text, mode, firstLine) { + if (!(this instanceof Doc)) return new Doc(text, mode, firstLine); + if (firstLine == null) firstLine = 0; + + BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); + this.first = firstLine; + this.scrollTop = this.scrollLeft = 0; + this.cantEdit = false; + this.cleanGeneration = 1; + this.frontier = firstLine; + var start = Pos(firstLine, 0); + this.sel = simpleSelection(start); + this.history = new History(null); + this.id = ++nextDocId; + this.modeOption = mode; + + if (typeof text == "string") text = splitLines(text); + updateDoc(this, {from: start, to: start, text: text}); + setSelection(this, simpleSelection(start), sel_dontScroll); + }; + + Doc.prototype = createObj(BranchChunk.prototype, { + constructor: Doc, + // Iterate over the document. Supports two forms -- with only one + // argument, it calls that for each line in the document. With + // three, it iterates over the range given by the first two (with + // the second being non-inclusive). + iter: function(from, to, op) { + if (op) this.iterN(from - this.first, to - from, op); + else this.iterN(this.first, this.first + this.size, from); + }, + + // Non-public interface for adding and removing lines. + insert: function(at, lines) { + var height = 0; + for (var i = 0; i < lines.length; ++i) height += lines[i].height; + this.insertInner(at - this.first, lines, height); + }, + remove: function(at, n) { this.removeInner(at - this.first, n); }, + + // From here, the methods are part of the public interface. Most + // are also available from CodeMirror (editor) instances. + + getValue: function(lineSep) { + var lines = getLines(this, this.first, this.first + this.size); + if (lineSep === false) return lines; + return lines.join(lineSep || "\n"); + }, + setValue: docMethodOp(function(code) { + var top = Pos(this.first, 0), last = this.first + this.size - 1; + makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), + text: splitLines(code), origin: "setValue", full: true}, true); + setSelection(this, simpleSelection(top)); + }), + replaceRange: function(code, from, to, origin) { + from = clipPos(this, from); + to = to ? clipPos(this, to) : from; + replaceRange(this, code, from, to, origin); + }, + getRange: function(from, to, lineSep) { + var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); + if (lineSep === false) return lines; + return lines.join(lineSep || "\n"); + }, + + getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;}, + + getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line);}, + getLineNumber: function(line) {return lineNo(line);}, + + getLineHandleVisualStart: function(line) { + if (typeof line == "number") line = getLine(this, line); + return visualLine(line); + }, + + lineCount: function() {return this.size;}, + firstLine: function() {return this.first;}, + lastLine: function() {return this.first + this.size - 1;}, + + clipPos: function(pos) {return clipPos(this, pos);}, + + getCursor: function(start) { + var range = this.sel.primary(), pos; + if (start == null || start == "head") pos = range.head; + else if (start == "anchor") pos = range.anchor; + else if (start == "end" || start == "to" || start === false) pos = range.to(); + else pos = range.from(); + return pos; + }, + listSelections: function() { return this.sel.ranges; }, + somethingSelected: function() {return this.sel.somethingSelected();}, + + setCursor: docMethodOp(function(line, ch, options) { + setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options); + }), + setSelection: docMethodOp(function(anchor, head, options) { + setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options); + }), + extendSelection: docMethodOp(function(head, other, options) { + extendSelection(this, clipPos(this, head), other && clipPos(this, other), options); + }), + extendSelections: docMethodOp(function(heads, options) { + extendSelections(this, clipPosArray(this, heads, options)); + }), + extendSelectionsBy: docMethodOp(function(f, options) { + extendSelections(this, map(this.sel.ranges, f), options); + }), + setSelections: docMethodOp(function(ranges, primary, options) { + if (!ranges.length) return; + for (var i = 0, out = []; i < ranges.length; i++) + out[i] = new Range(clipPos(this, ranges[i].anchor), + clipPos(this, ranges[i].head)); + if (primary == null) primary = Math.min(ranges.length - 1, this.sel.primIndex); + setSelection(this, normalizeSelection(out, primary), options); + }), + addSelection: docMethodOp(function(anchor, head, options) { + var ranges = this.sel.ranges.slice(0); + ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor))); + setSelection(this, normalizeSelection(ranges, ranges.length - 1), options); + }), + + getSelection: function(lineSep) { + var ranges = this.sel.ranges, lines; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + lines = lines ? lines.concat(sel) : sel; + } + if (lineSep === false) return lines; + else return lines.join(lineSep || "\n"); + }, + getSelections: function(lineSep) { + var parts = [], ranges = this.sel.ranges; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + if (lineSep !== false) sel = sel.join(lineSep || "\n"); + parts[i] = sel; + } + return parts; + }, + replaceSelection: function(code, collapse, origin) { + var dup = []; + for (var i = 0; i < this.sel.ranges.length; i++) + dup[i] = code; + this.replaceSelections(dup, collapse, origin || "+input"); + }, + replaceSelections: docMethodOp(function(code, collapse, origin) { + var changes = [], sel = this.sel; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + changes[i] = {from: range.from(), to: range.to(), text: splitLines(code[i]), origin: origin}; + } + var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse); + for (var i = changes.length - 1; i >= 0; i--) + makeChange(this, changes[i]); + if (newSel) setSelectionReplaceHistory(this, newSel); + else if (this.cm) ensureCursorVisible(this.cm); + }), + undo: docMethodOp(function() {makeChangeFromHistory(this, "undo");}), + redo: docMethodOp(function() {makeChangeFromHistory(this, "redo");}), + undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true);}), + redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true);}), + + setExtending: function(val) {this.extend = val;}, + getExtending: function() {return this.extend;}, + + historySize: function() { + var hist = this.history, done = 0, undone = 0; + for (var i = 0; i < hist.done.length; i++) if (!hist.done[i].ranges) ++done; + for (var i = 0; i < hist.undone.length; i++) if (!hist.undone[i].ranges) ++undone; + return {undo: done, redo: undone}; + }, + clearHistory: function() {this.history = new History(this.history.maxGeneration);}, + + markClean: function() { + this.cleanGeneration = this.changeGeneration(true); + }, + changeGeneration: function(forceSplit) { + if (forceSplit) + this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null; + return this.history.generation; + }, + isClean: function (gen) { + return this.history.generation == (gen || this.cleanGeneration); + }, + + getHistory: function() { + return {done: copyHistoryArray(this.history.done), + undone: copyHistoryArray(this.history.undone)}; + }, + setHistory: function(histData) { + var hist = this.history = new History(this.history.maxGeneration); + hist.done = copyHistoryArray(histData.done.slice(0), null, true); + hist.undone = copyHistoryArray(histData.undone.slice(0), null, true); + }, + + addLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + if (!line[prop]) line[prop] = cls; + else if (classTest(cls).test(line[prop])) return false; + else line[prop] += " " + cls; + return true; + }); + }), + removeLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + var cur = line[prop]; + if (!cur) return false; + else if (cls == null) line[prop] = null; + else { + var found = cur.match(classTest(cls)); + if (!found) return false; + var end = found.index + found[0].length; + line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null; + } + return true; + }); + }), + + addLineWidget: docMethodOp(function(handle, node, options) { + return addLineWidget(this, handle, node, options); + }), + removeLineWidget: function(widget) { widget.clear(); }, + + markText: function(from, to, options) { + return markText(this, clipPos(this, from), clipPos(this, to), options, "range"); + }, + setBookmark: function(pos, options) { + var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), + insertLeft: options && options.insertLeft, + clearWhenEmpty: false, shared: options && options.shared, + handleMouseEvents: options && options.handleMouseEvents}; + pos = clipPos(this, pos); + return markText(this, pos, pos, realOpts, "bookmark"); + }, + findMarksAt: function(pos) { + pos = clipPos(this, pos); + var markers = [], spans = getLine(this, pos.line).markedSpans; + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if ((span.from == null || span.from <= pos.ch) && + (span.to == null || span.to >= pos.ch)) + markers.push(span.marker.parent || span.marker); + } + return markers; + }, + findMarks: function(from, to, filter) { + from = clipPos(this, from); to = clipPos(this, to); + var found = [], lineNo = from.line; + this.iter(from.line, to.line + 1, function(line) { + var spans = line.markedSpans; + if (spans) for (var i = 0; i < spans.length; i++) { + var span = spans[i]; + if (!(lineNo == from.line && from.ch > span.to || + span.from == null && lineNo != from.line|| + lineNo == to.line && span.from > to.ch) && + (!filter || filter(span.marker))) + found.push(span.marker.parent || span.marker); + } + ++lineNo; + }); + return found; + }, + getAllMarks: function() { + var markers = []; + this.iter(function(line) { + var sps = line.markedSpans; + if (sps) for (var i = 0; i < sps.length; ++i) + if (sps[i].from != null) markers.push(sps[i].marker); + }); + return markers; + }, + + posFromIndex: function(off) { + var ch, lineNo = this.first; + this.iter(function(line) { + var sz = line.text.length + 1; + if (sz > off) { ch = off; return true; } + off -= sz; + ++lineNo; + }); + return clipPos(this, Pos(lineNo, ch)); + }, + indexFromPos: function (coords) { + coords = clipPos(this, coords); + var index = coords.ch; + if (coords.line < this.first || coords.ch < 0) return 0; + this.iter(this.first, coords.line, function (line) { + index += line.text.length + 1; + }); + return index; + }, + + copy: function(copyHistory) { + var doc = new Doc(getLines(this, this.first, this.first + this.size), this.modeOption, this.first); + doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; + doc.sel = this.sel; + doc.extend = false; + if (copyHistory) { + doc.history.undoDepth = this.history.undoDepth; + doc.setHistory(this.getHistory()); + } + return doc; + }, + + linkedDoc: function(options) { + if (!options) options = {}; + var from = this.first, to = this.first + this.size; + if (options.from != null && options.from > from) from = options.from; + if (options.to != null && options.to < to) to = options.to; + var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from); + if (options.sharedHist) copy.history = this.history; + (this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); + copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; + copySharedMarkers(copy, findSharedMarkers(this)); + return copy; + }, + unlinkDoc: function(other) { + if (other instanceof CodeMirror) other = other.doc; + if (this.linked) for (var i = 0; i < this.linked.length; ++i) { + var link = this.linked[i]; + if (link.doc != other) continue; + this.linked.splice(i, 1); + other.unlinkDoc(this); + detachSharedMarkers(findSharedMarkers(this)); + break; + } + // If the histories were shared, split them again + if (other.history == this.history) { + var splitIds = [other.id]; + linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true); + other.history = new History(null); + other.history.done = copyHistoryArray(this.history.done, splitIds); + other.history.undone = copyHistoryArray(this.history.undone, splitIds); + } + }, + iterLinkedDocs: function(f) {linkedDocs(this, f);}, + + getMode: function() {return this.mode;}, + getEditor: function() {return this.cm;} + }); + + // Public alias. + Doc.prototype.eachLine = Doc.prototype.iter; + + // Set up methods on CodeMirror's prototype to redirect to the editor's document. + var dontDelegate = "iter insert remove copy getEditor".split(" "); + for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) + CodeMirror.prototype[prop] = (function(method) { + return function() {return method.apply(this.doc, arguments);}; + })(Doc.prototype[prop]); + + eventMixin(Doc); + + // Call f for all linked documents. + function linkedDocs(doc, f, sharedHistOnly) { + function propagate(doc, skip, sharedHist) { + if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) { + var rel = doc.linked[i]; + if (rel.doc == skip) continue; + var shared = sharedHist && rel.sharedHist; + if (sharedHistOnly && !shared) continue; + f(rel.doc, shared); + propagate(rel.doc, doc, shared); + } + } + propagate(doc, null, true); + } + + // Attach a document to an editor. + function attachDoc(cm, doc) { + if (doc.cm) throw new Error("This document is already in use."); + cm.doc = doc; + doc.cm = cm; + estimateLineHeights(cm); + loadMode(cm); + if (!cm.options.lineWrapping) findMaxLine(cm); + cm.options.mode = doc.modeOption; + regChange(cm); + } + + // LINE UTILITIES + + // Find the line object corresponding to the given line number. + function getLine(doc, n) { + n -= doc.first; + if (n < 0 || n >= doc.size) throw new Error("There is no line " + (n + doc.first) + " in the document."); + for (var chunk = doc; !chunk.lines;) { + for (var i = 0;; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; break; } + n -= sz; + } + } + return chunk.lines[n]; + } + + // Get the part of a document between two positions, as an array of + // strings. + function getBetween(doc, start, end) { + var out = [], n = start.line; + doc.iter(start.line, end.line + 1, function(line) { + var text = line.text; + if (n == end.line) text = text.slice(0, end.ch); + if (n == start.line) text = text.slice(start.ch); + out.push(text); + ++n; + }); + return out; + } + // Get the lines between from and to, as array of strings. + function getLines(doc, from, to) { + var out = []; + doc.iter(from, to, function(line) { out.push(line.text); }); + return out; + } + + // Update the height of a line, propagating the height change + // upwards to parent nodes. + function updateLineHeight(line, height) { + var diff = height - line.height; + if (diff) for (var n = line; n; n = n.parent) n.height += diff; + } + + // Given a line object, find its line number by walking up through + // its parent links. + function lineNo(line) { + if (line.parent == null) return null; + var cur = line.parent, no = indexOf(cur.lines, line); + for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { + for (var i = 0;; ++i) { + if (chunk.children[i] == cur) break; + no += chunk.children[i].chunkSize(); + } + } + return no + cur.first; + } + + // Find the line at the given vertical position, using the height + // information in the document tree. + function lineAtHeight(chunk, h) { + var n = chunk.first; + outer: do { + for (var i = 0; i < chunk.children.length; ++i) { + var child = chunk.children[i], ch = child.height; + if (h < ch) { chunk = child; continue outer; } + h -= ch; + n += child.chunkSize(); + } + return n; + } while (!chunk.lines); + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i], lh = line.height; + if (h < lh) break; + h -= lh; + } + return n + i; + } + + + // Find the height above the given line. + function heightAtLine(lineObj) { + lineObj = visualLine(lineObj); + + var h = 0, chunk = lineObj.parent; + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i]; + if (line == lineObj) break; + else h += line.height; + } + for (var p = chunk.parent; p; chunk = p, p = chunk.parent) { + for (var i = 0; i < p.children.length; ++i) { + var cur = p.children[i]; + if (cur == chunk) break; + else h += cur.height; + } + } + return h; + } + + // Get the bidi ordering for the given line (and cache it). Returns + // false for lines that are fully left-to-right, and an array of + // BidiSpan objects otherwise. + function getOrder(line) { + var order = line.order; + if (order == null) order = line.order = bidiOrdering(line.text); + return order; + } + + // HISTORY + + function History(startGen) { + // Arrays of change events and selections. Doing something adds an + // event to done and clears undo. Undoing moves events from done + // to undone, redoing moves them in the other direction. + this.done = []; this.undone = []; + this.undoDepth = Infinity; + // Used to track when changes can be merged into a single undo + // event + this.lastModTime = this.lastSelTime = 0; + this.lastOp = this.lastSelOp = null; + this.lastOrigin = this.lastSelOrigin = null; + // Used by the isClean() method + this.generation = this.maxGeneration = startGen || 1; + } + + // Create a history change event from an updateDoc-style change + // object. + function historyChangeFromChange(doc, change) { + var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; + attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); + linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true); + return histChange; + } + + // Pop all selection events off the end of a history array. Stop at + // a change event. + function clearSelectionEvents(array) { + while (array.length) { + var last = lst(array); + if (last.ranges) array.pop(); + else break; + } + } + + // Find the top change event in the history. Pop off selection + // events that are in the way. + function lastChangeEvent(hist, force) { + if (force) { + clearSelectionEvents(hist.done); + return lst(hist.done); + } else if (hist.done.length && !lst(hist.done).ranges) { + return lst(hist.done); + } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) { + hist.done.pop(); + return lst(hist.done); + } + } + + // Register a change in the history. Merges changes that are within + // a single operation, ore are close together with an origin that + // allows merging (starting with "+") into a single event. + function addChangeToHistory(doc, change, selAfter, opId) { + var hist = doc.history; + hist.undone.length = 0; + var time = +new Date, cur; + + if ((hist.lastOp == opId || + hist.lastOrigin == change.origin && change.origin && + ((change.origin.charAt(0) == "+" && doc.cm && hist.lastModTime > time - doc.cm.options.historyEventDelay) || + change.origin.charAt(0) == "*")) && + (cur = lastChangeEvent(hist, hist.lastOp == opId))) { + // Merge this change into the last event + var last = lst(cur.changes); + if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) { + // Optimized case for simple insertion -- don't want to add + // new changesets for every character typed + last.to = changeEnd(change); + } else { + // Add new sub-event + cur.changes.push(historyChangeFromChange(doc, change)); + } + } else { + // Can not be merged, start a new event. + var before = lst(hist.done); + if (!before || !before.ranges) + pushSelectionToHistory(doc.sel, hist.done); + cur = {changes: [historyChangeFromChange(doc, change)], + generation: hist.generation}; + hist.done.push(cur); + while (hist.done.length > hist.undoDepth) { + hist.done.shift(); + if (!hist.done[0].ranges) hist.done.shift(); + } + } + hist.done.push(selAfter); + hist.generation = ++hist.maxGeneration; + hist.lastModTime = hist.lastSelTime = time; + hist.lastOp = hist.lastSelOp = opId; + hist.lastOrigin = hist.lastSelOrigin = change.origin; + + if (!last) signal(doc, "historyAdded"); + } + + function selectionEventCanBeMerged(doc, origin, prev, sel) { + var ch = origin.charAt(0); + return ch == "*" || + ch == "+" && + prev.ranges.length == sel.ranges.length && + prev.somethingSelected() == sel.somethingSelected() && + new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500); + } + + // Called whenever the selection changes, sets the new selection as + // the pending selection in the history, and pushes the old pending + // selection into the 'done' array when it was significantly + // different (in number of selected ranges, emptiness, or time). + function addSelectionToHistory(doc, sel, opId, options) { + var hist = doc.history, origin = options && options.origin; + + // A new event is started when the previous origin does not match + // the current, or the origins don't allow matching. Origins + // starting with * are always merged, those starting with + are + // merged when similar and close together in time. + if (opId == hist.lastSelOp || + (origin && hist.lastSelOrigin == origin && + (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin || + selectionEventCanBeMerged(doc, origin, lst(hist.done), sel)))) + hist.done[hist.done.length - 1] = sel; + else + pushSelectionToHistory(sel, hist.done); + + hist.lastSelTime = +new Date; + hist.lastSelOrigin = origin; + hist.lastSelOp = opId; + if (options && options.clearRedo !== false) + clearSelectionEvents(hist.undone); + } + + function pushSelectionToHistory(sel, dest) { + var top = lst(dest); + if (!(top && top.ranges && top.equals(sel))) + dest.push(sel); + } + + // Used to store marked span information in the history. + function attachLocalSpans(doc, change, from, to) { + var existing = change["spans_" + doc.id], n = 0; + doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function(line) { + if (line.markedSpans) + (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans; + ++n; + }); + } + + // When un/re-doing restores text containing marked spans, those + // that have been explicitly cleared should not be restored. + function removeClearedSpans(spans) { + if (!spans) return null; + for (var i = 0, out; i < spans.length; ++i) { + if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); } + else if (out) out.push(spans[i]); + } + return !out ? spans : out.length ? out : null; + } + + // Retrieve and filter the old marked spans stored in a change event. + function getOldSpans(doc, change) { + var found = change["spans_" + doc.id]; + if (!found) return null; + for (var i = 0, nw = []; i < change.text.length; ++i) + nw.push(removeClearedSpans(found[i])); + return nw; + } + + // Used both to provide a JSON-safe object in .getHistory, and, when + // detaching a document, to split the history in two + function copyHistoryArray(events, newGroup, instantiateSel) { + for (var i = 0, copy = []; i < events.length; ++i) { + var event = events[i]; + if (event.ranges) { + copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event); + continue; + } + var changes = event.changes, newChanges = []; + copy.push({changes: newChanges}); + for (var j = 0; j < changes.length; ++j) { + var change = changes[j], m; + newChanges.push({from: change.from, to: change.to, text: change.text}); + if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) { + if (indexOf(newGroup, Number(m[1])) > -1) { + lst(newChanges)[prop] = change[prop]; + delete change[prop]; + } + } + } + } + return copy; + } + + // Rebasing/resetting history to deal with externally-sourced changes + + function rebaseHistSelSingle(pos, from, to, diff) { + if (to < pos.line) { + pos.line += diff; + } else if (from < pos.line) { + pos.line = from; + pos.ch = 0; + } + } + + // Tries to rebase an array of history events given a change in the + // document. If the change touches the same lines as the event, the + // event, and everything 'behind' it, is discarded. If the change is + // before the event, the event's positions are updated. Uses a + // copy-on-write scheme for the positions, to avoid having to + // reallocate them all on every rebase, but also avoid problems with + // shared position objects being unsafely updated. + function rebaseHistArray(array, from, to, diff) { + for (var i = 0; i < array.length; ++i) { + var sub = array[i], ok = true; + if (sub.ranges) { + if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true; } + for (var j = 0; j < sub.ranges.length; j++) { + rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff); + rebaseHistSelSingle(sub.ranges[j].head, from, to, diff); + } + continue; + } + for (var j = 0; j < sub.changes.length; ++j) { + var cur = sub.changes[j]; + if (to < cur.from.line) { + cur.from = Pos(cur.from.line + diff, cur.from.ch); + cur.to = Pos(cur.to.line + diff, cur.to.ch); + } else if (from <= cur.to.line) { + ok = false; + break; + } + } + if (!ok) { + array.splice(0, i + 1); + i = 0; + } + } + } + + function rebaseHist(hist, change) { + var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1; + rebaseHistArray(hist.done, from, to, diff); + rebaseHistArray(hist.undone, from, to, diff); + } + + // EVENT UTILITIES + + // Due to the fact that we still support jurassic IE versions, some + // compatibility wrappers are needed. + + var e_preventDefault = CodeMirror.e_preventDefault = function(e) { + if (e.preventDefault) e.preventDefault(); + else e.returnValue = false; + }; + var e_stopPropagation = CodeMirror.e_stopPropagation = function(e) { + if (e.stopPropagation) e.stopPropagation(); + else e.cancelBubble = true; + }; + function e_defaultPrevented(e) { + return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false; + } + var e_stop = CodeMirror.e_stop = function(e) {e_preventDefault(e); e_stopPropagation(e);}; + + function e_target(e) {return e.target || e.srcElement;} + function e_button(e) { + var b = e.which; + if (b == null) { + if (e.button & 1) b = 1; + else if (e.button & 2) b = 3; + else if (e.button & 4) b = 2; + } + if (mac && e.ctrlKey && b == 1) b = 3; + return b; + } + + // EVENT HANDLING + + // Lightweight event framework. on/off also work on DOM nodes, + // registering native DOM handlers. + + var on = CodeMirror.on = function(emitter, type, f) { + if (emitter.addEventListener) + emitter.addEventListener(type, f, false); + else if (emitter.attachEvent) + emitter.attachEvent("on" + type, f); + else { + var map = emitter._handlers || (emitter._handlers = {}); + var arr = map[type] || (map[type] = []); + arr.push(f); + } + }; + + var off = CodeMirror.off = function(emitter, type, f) { + if (emitter.removeEventListener) + emitter.removeEventListener(type, f, false); + else if (emitter.detachEvent) + emitter.detachEvent("on" + type, f); + else { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + for (var i = 0; i < arr.length; ++i) + if (arr[i] == f) { arr.splice(i, 1); break; } + } + }; + + var signal = CodeMirror.signal = function(emitter, type /*, values...*/) { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + var args = Array.prototype.slice.call(arguments, 2); + for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args); + }; + + var orphanDelayedCallbacks = null; + + // Often, we want to signal events at a point where we are in the + // middle of some work, but don't want the handler to start calling + // other methods on the editor, which might be in an inconsistent + // state or simply not expect any other events to happen. + // signalLater looks whether there are any handlers, and schedules + // them to be executed when the last operation ends, or, if no + // operation is active, when a timeout fires. + function signalLater(emitter, type /*, values...*/) { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + var args = Array.prototype.slice.call(arguments, 2), list; + if (operationGroup) { + list = operationGroup.delayedCallbacks; + } else if (orphanDelayedCallbacks) { + list = orphanDelayedCallbacks; + } else { + list = orphanDelayedCallbacks = []; + setTimeout(fireOrphanDelayed, 0); + } + function bnd(f) {return function(){f.apply(null, args);};}; + for (var i = 0; i < arr.length; ++i) + list.push(bnd(arr[i])); + } + + function fireOrphanDelayed() { + var delayed = orphanDelayedCallbacks; + orphanDelayedCallbacks = null; + for (var i = 0; i < delayed.length; ++i) delayed[i](); + } + + // The DOM events that CodeMirror handles can be overridden by + // registering a (non-DOM) handler on the editor for the event name, + // and preventDefault-ing the event in that handler. + function signalDOMEvent(cm, e, override) { + if (typeof e == "string") + e = {type: e, preventDefault: function() { this.defaultPrevented = true; }}; + signal(cm, override || e.type, cm, e); + return e_defaultPrevented(e) || e.codemirrorIgnore; + } + + function signalCursorActivity(cm) { + var arr = cm._handlers && cm._handlers.cursorActivity; + if (!arr) return; + var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []); + for (var i = 0; i < arr.length; ++i) if (indexOf(set, arr[i]) == -1) + set.push(arr[i]); + } + + function hasHandler(emitter, type) { + var arr = emitter._handlers && emitter._handlers[type]; + return arr && arr.length > 0; + } + + // Add on and off methods to a constructor's prototype, to make + // registering events on such objects more convenient. + function eventMixin(ctor) { + ctor.prototype.on = function(type, f) {on(this, type, f);}; + ctor.prototype.off = function(type, f) {off(this, type, f);}; + } + + // MISC UTILITIES + + // Number of pixels added to scroller and sizer to hide scrollbar + var scrollerGap = 30; + + // Returned or thrown by various protocols to signal 'I'm not + // handling this'. + var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}}; + + // Reused option objects for setSelection & friends + var sel_dontScroll = {scroll: false}, sel_mouse = {origin: "*mouse"}, sel_move = {origin: "+move"}; + + function Delayed() {this.id = null;} + Delayed.prototype.set = function(ms, f) { + clearTimeout(this.id); + this.id = setTimeout(f, ms); + }; + + // Counts the column offset in a string, taking tabs into account. + // Used mostly to find indentation. + var countColumn = CodeMirror.countColumn = function(string, end, tabSize, startIndex, startValue) { + if (end == null) { + end = string.search(/[^\s\u00a0]/); + if (end == -1) end = string.length; + } + for (var i = startIndex || 0, n = startValue || 0;;) { + var nextTab = string.indexOf("\t", i); + if (nextTab < 0 || nextTab >= end) + return n + (end - i); + n += nextTab - i; + n += tabSize - (n % tabSize); + i = nextTab + 1; + } + }; + + // The inverse of countColumn -- find the offset that corresponds to + // a particular column. + function findColumn(string, goal, tabSize) { + for (var pos = 0, col = 0;;) { + var nextTab = string.indexOf("\t", pos); + if (nextTab == -1) nextTab = string.length; + var skipped = nextTab - pos; + if (nextTab == string.length || col + skipped >= goal) + return pos + Math.min(skipped, goal - col); + col += nextTab - pos; + col += tabSize - (col % tabSize); + pos = nextTab + 1; + if (col >= goal) return pos; + } + } + + var spaceStrs = [""]; + function spaceStr(n) { + while (spaceStrs.length <= n) + spaceStrs.push(lst(spaceStrs) + " "); + return spaceStrs[n]; + } + + function lst(arr) { return arr[arr.length-1]; } + + var selectInput = function(node) { node.select(); }; + if (ios) // Mobile Safari apparently has a bug where select() is broken. + selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length; }; + else if (ie) // Suppress mysterious IE10 errors + selectInput = function(node) { try { node.select(); } catch(_e) {} }; + + function indexOf(array, elt) { + for (var i = 0; i < array.length; ++i) + if (array[i] == elt) return i; + return -1; + } + function map(array, f) { + var out = []; + for (var i = 0; i < array.length; i++) out[i] = f(array[i], i); + return out; + } + + function nothing() {} + + function createObj(base, props) { + var inst; + if (Object.create) { + inst = Object.create(base); + } else { + nothing.prototype = base; + inst = new nothing(); + } + if (props) copyObj(props, inst); + return inst; + }; + + function copyObj(obj, target, overwrite) { + if (!target) target = {}; + for (var prop in obj) + if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop))) + target[prop] = obj[prop]; + return target; + } + + function bind(f) { + var args = Array.prototype.slice.call(arguments, 1); + return function(){return f.apply(null, args);}; + } + + var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; + var isWordCharBasic = CodeMirror.isWordChar = function(ch) { + return /\w/.test(ch) || ch > "\x80" && + (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)); + }; + function isWordChar(ch, helper) { + if (!helper) return isWordCharBasic(ch); + if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) return true; + return helper.test(ch); + } + + function isEmpty(obj) { + for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false; + return true; + } + + // Extending unicode characters. A series of a non-extending char + + // any number of extending chars is treated as a single unit as far + // as editing and measuring is concerned. This is not fully correct, + // since some scripts/fonts/browsers also treat other configurations + // of code points as a group. + var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/; + function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch); } + + // DOM UTILITIES + + function elt(tag, content, className, style) { + var e = document.createElement(tag); + if (className) e.className = className; + if (style) e.style.cssText = style; + if (typeof content == "string") e.appendChild(document.createTextNode(content)); + else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]); + return e; + } + + var range; + if (document.createRange) range = function(node, start, end, endNode) { + var r = document.createRange(); + r.setEnd(endNode || node, end); + r.setStart(node, start); + return r; + }; + else range = function(node, start, end) { + var r = document.body.createTextRange(); + try { r.moveToElementText(node.parentNode); } + catch(e) { return r; } + r.collapse(true); + r.moveEnd("character", end); + r.moveStart("character", start); + return r; + }; + + function removeChildren(e) { + for (var count = e.childNodes.length; count > 0; --count) + e.removeChild(e.firstChild); + return e; + } + + function removeChildrenAndAdd(parent, e) { + return removeChildren(parent).appendChild(e); + } + + var contains = CodeMirror.contains = function(parent, child) { + if (child.nodeType == 3) // Android browser always returns false when child is a textnode + child = child.parentNode; + if (parent.contains) + return parent.contains(child); + do { + if (child.nodeType == 11) child = child.host; + if (child == parent) return true; + } while (child = child.parentNode); + }; + + function activeElt() { return document.activeElement; } + // Older versions of IE throws unspecified error when touching + // document.activeElement in some cases (during loading, in iframe) + if (ie && ie_version < 11) activeElt = function() { + try { return document.activeElement; } + catch(e) { return document.body; } + }; + + function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*"); } + var rmClass = CodeMirror.rmClass = function(node, cls) { + var current = node.className; + var match = classTest(cls).exec(current); + if (match) { + var after = current.slice(match.index + match[0].length); + node.className = current.slice(0, match.index) + (after ? match[1] + after : ""); + } + }; + var addClass = CodeMirror.addClass = function(node, cls) { + var current = node.className; + if (!classTest(cls).test(current)) node.className += (current ? " " : "") + cls; + }; + function joinClasses(a, b) { + var as = a.split(" "); + for (var i = 0; i < as.length; i++) + if (as[i] && !classTest(as[i]).test(b)) b += " " + as[i]; + return b; + } + + // WINDOW-WIDE EVENTS + + // These must be handled carefully, because naively registering a + // handler for each editor will cause the editors to never be + // garbage collected. + + function forEachCodeMirror(f) { + if (!document.body.getElementsByClassName) return; + var byClass = document.body.getElementsByClassName("CodeMirror"); + for (var i = 0; i < byClass.length; i++) { + var cm = byClass[i].CodeMirror; + if (cm) f(cm); + } + } + + var globalsRegistered = false; + function ensureGlobalHandlers() { + if (globalsRegistered) return; + registerGlobalHandlers(); + globalsRegistered = true; + } + function registerGlobalHandlers() { + // When the window resizes, we need to refresh active editors. + var resizeTimer; + on(window, "resize", function() { + if (resizeTimer == null) resizeTimer = setTimeout(function() { + resizeTimer = null; + forEachCodeMirror(onResize); + }, 100); + }); + // When the window loses focus, we want to show the editor as blurred + on(window, "blur", function() { + forEachCodeMirror(onBlur); + }); + } + + // FEATURE DETECTION + + // Detect drag-and-drop + var dragAndDrop = function() { + // There is *some* kind of drag-and-drop support in IE6-8, but I + // couldn't get it to work yet. + if (ie && ie_version < 9) return false; + var div = elt('div'); + return "draggable" in div || "dragDrop" in div; + }(); + + var zwspSupported; + function zeroWidthElement(measure) { + if (zwspSupported == null) { + var test = elt("span", "\u200b"); + removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])); + if (measure.firstChild.offsetHeight != 0) + zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8); + } + var node = zwspSupported ? elt("span", "\u200b") : + elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); + node.setAttribute("cm-text", ""); + return node; + } + + // Feature-detect IE's crummy client rect reporting for bidi text + var badBidiRects; + function hasBadBidiRects(measure) { + if (badBidiRects != null) return badBidiRects; + var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA")); + var r0 = range(txt, 0, 1).getBoundingClientRect(); + if (!r0 || r0.left == r0.right) return false; // Safari returns null in some cases (#2780) + var r1 = range(txt, 1, 2).getBoundingClientRect(); + return badBidiRects = (r1.right - r0.right < 3); + } + + // See if "".split is the broken IE version, if so, provide an + // alternative way to split lines. + var splitLines = CodeMirror.splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { + var pos = 0, result = [], l = string.length; + while (pos <= l) { + var nl = string.indexOf("\n", pos); + if (nl == -1) nl = string.length; + var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl); + var rt = line.indexOf("\r"); + if (rt != -1) { + result.push(line.slice(0, rt)); + pos += rt + 1; + } else { + result.push(line); + pos = nl + 1; + } + } + return result; + } : function(string){return string.split(/\r\n?|\n/);}; + + var hasSelection = window.getSelection ? function(te) { + try { return te.selectionStart != te.selectionEnd; } + catch(e) { return false; } + } : function(te) { + try {var range = te.ownerDocument.selection.createRange();} + catch(e) {} + if (!range || range.parentElement() != te) return false; + return range.compareEndPoints("StartToEnd", range) != 0; + }; + + var hasCopyEvent = (function() { + var e = elt("div"); + if ("oncopy" in e) return true; + e.setAttribute("oncopy", "return;"); + return typeof e.oncopy == "function"; + })(); + + var badZoomedRects = null; + function hasBadZoomedRects(measure) { + if (badZoomedRects != null) return badZoomedRects; + var node = removeChildrenAndAdd(measure, elt("span", "x")); + var normal = node.getBoundingClientRect(); + var fromRange = range(node, 0, 1).getBoundingClientRect(); + return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1; + } + + // KEY NAMES + + var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", + 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", + 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", + 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", 107: "=", 109: "-", 127: "Delete", + 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", + 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", + 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"}; + CodeMirror.keyNames = keyNames; + (function() { + // Number keys + for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i); + // Alphabetic keys + for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i); + // Function keys + for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i; + })(); + + // BIDI HELPERS + + function iterateBidiSections(order, from, to, f) { + if (!order) return f(from, to, "ltr"); + var found = false; + for (var i = 0; i < order.length; ++i) { + var part = order[i]; + if (part.from < to && part.to > from || from == to && part.to == from) { + f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr"); + found = true; + } + } + if (!found) f(from, to, "ltr"); + } + + function bidiLeft(part) { return part.level % 2 ? part.to : part.from; } + function bidiRight(part) { return part.level % 2 ? part.from : part.to; } + + function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; } + function lineRight(line) { + var order = getOrder(line); + if (!order) return line.text.length; + return bidiRight(lst(order)); + } + + function lineStart(cm, lineN) { + var line = getLine(cm.doc, lineN); + var visual = visualLine(line); + if (visual != line) lineN = lineNo(visual); + var order = getOrder(visual); + var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual); + return Pos(lineN, ch); + } + function lineEnd(cm, lineN) { + var merged, line = getLine(cm.doc, lineN); + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line; + lineN = null; + } + var order = getOrder(line); + var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line); + return Pos(lineN == null ? lineNo(line) : lineN, ch); + } + function lineStartSmart(cm, pos) { + var start = lineStart(cm, pos.line); + var line = getLine(cm.doc, start.line); + var order = getOrder(line); + if (!order || order[0].level == 0) { + var firstNonWS = Math.max(0, line.text.search(/\S/)); + var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch; + return Pos(start.line, inWS ? 0 : firstNonWS); + } + return start; + } + + function compareBidiLevel(order, a, b) { + var linedir = order[0].level; + if (a == linedir) return true; + if (b == linedir) return false; + return a < b; + } + var bidiOther; + function getBidiPartAt(order, pos) { + bidiOther = null; + for (var i = 0, found; i < order.length; ++i) { + var cur = order[i]; + if (cur.from < pos && cur.to > pos) return i; + if ((cur.from == pos || cur.to == pos)) { + if (found == null) { + found = i; + } else if (compareBidiLevel(order, cur.level, order[found].level)) { + if (cur.from != cur.to) bidiOther = found; + return i; + } else { + if (cur.from != cur.to) bidiOther = i; + return found; + } + } + } + return found; + } + + function moveInLine(line, pos, dir, byUnit) { + if (!byUnit) return pos + dir; + do pos += dir; + while (pos > 0 && isExtendingChar(line.text.charAt(pos))); + return pos; + } + + // This is needed in order to move 'visually' through bi-directional + // text -- i.e., pressing left should make the cursor go left, even + // when in RTL text. The tricky part is the 'jumps', where RTL and + // LTR text touch each other. This often requires the cursor offset + // to move more than one unit, in order to visually move one unit. + function moveVisually(line, start, dir, byUnit) { + var bidi = getOrder(line); + if (!bidi) return moveLogically(line, start, dir, byUnit); + var pos = getBidiPartAt(bidi, start), part = bidi[pos]; + var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit); + + for (;;) { + if (target > part.from && target < part.to) return target; + if (target == part.from || target == part.to) { + if (getBidiPartAt(bidi, target) == pos) return target; + part = bidi[pos += dir]; + return (dir > 0) == part.level % 2 ? part.to : part.from; + } else { + part = bidi[pos += dir]; + if (!part) return null; + if ((dir > 0) == part.level % 2) + target = moveInLine(line, part.to, -1, byUnit); + else + target = moveInLine(line, part.from, 1, byUnit); + } + } + } + + function moveLogically(line, start, dir, byUnit) { + var target = start + dir; + if (byUnit) while (target > 0 && isExtendingChar(line.text.charAt(target))) target += dir; + return target < 0 || target > line.text.length ? null : target; + } + + // Bidirectional ordering algorithm + // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm + // that this (partially) implements. + + // One-char codes used for character types: + // L (L): Left-to-Right + // R (R): Right-to-Left + // r (AL): Right-to-Left Arabic + // 1 (EN): European Number + // + (ES): European Number Separator + // % (ET): European Number Terminator + // n (AN): Arabic Number + // , (CS): Common Number Separator + // m (NSM): Non-Spacing Mark + // b (BN): Boundary Neutral + // s (B): Paragraph Separator + // t (S): Segment Separator + // w (WS): Whitespace + // N (ON): Other Neutrals + + // Returns null if characters are ordered as they appear + // (left-to-right), or an array of sections ({from, to, level} + // objects) in the order in which they occur visually. + var bidiOrdering = (function() { + // Character types for codepoints 0 to 0xff + var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"; + // Character types for codepoints 0x600 to 0x6ff + var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm"; + function charType(code) { + if (code <= 0xf7) return lowTypes.charAt(code); + else if (0x590 <= code && code <= 0x5f4) return "R"; + else if (0x600 <= code && code <= 0x6ed) return arabicTypes.charAt(code - 0x600); + else if (0x6ee <= code && code <= 0x8ac) return "r"; + else if (0x2000 <= code && code <= 0x200b) return "w"; + else if (code == 0x200c) return "b"; + else return "L"; + } + + var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/; + var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/; + // Browsers seem to always treat the boundaries of block elements as being L. + var outerType = "L"; + + function BidiSpan(level, from, to) { + this.level = level; + this.from = from; this.to = to; + } + + return function(str) { + if (!bidiRE.test(str)) return false; + var len = str.length, types = []; + for (var i = 0, type; i < len; ++i) + types.push(type = charType(str.charCodeAt(i))); + + // W1. Examine each non-spacing mark (NSM) in the level run, and + // change the type of the NSM to the type of the previous + // character. If the NSM is at the start of the level run, it will + // get the type of sor. + for (var i = 0, prev = outerType; i < len; ++i) { + var type = types[i]; + if (type == "m") types[i] = prev; + else prev = type; + } + + // W2. Search backwards from each instance of a European number + // until the first strong type (R, L, AL, or sor) is found. If an + // AL is found, change the type of the European number to Arabic + // number. + // W3. Change all ALs to R. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (type == "1" && cur == "r") types[i] = "n"; + else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; } + } + + // W4. A single European separator between two European numbers + // changes to a European number. A single common separator between + // two numbers of the same type changes to that type. + for (var i = 1, prev = types[0]; i < len - 1; ++i) { + var type = types[i]; + if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1"; + else if (type == "," && prev == types[i+1] && + (prev == "1" || prev == "n")) types[i] = prev; + prev = type; + } + + // W5. A sequence of European terminators adjacent to European + // numbers changes to all European numbers. + // W6. Otherwise, separators and terminators change to Other + // Neutral. + for (var i = 0; i < len; ++i) { + var type = types[i]; + if (type == ",") types[i] = "N"; + else if (type == "%") { + for (var end = i + 1; end < len && types[end] == "%"; ++end) {} + var replace = (i && types[i-1] == "!") || (end < len && types[end] == "1") ? "1" : "N"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // W7. Search backwards from each instance of a European number + // until the first strong type (R, L, or sor) is found. If an L is + // found, then change the type of the European number to L. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (cur == "L" && type == "1") types[i] = "L"; + else if (isStrong.test(type)) cur = type; + } + + // N1. A sequence of neutrals takes the direction of the + // surrounding strong text if the text on both sides has the same + // direction. European and Arabic numbers act as if they were R in + // terms of their influence on neutrals. Start-of-level-run (sor) + // and end-of-level-run (eor) are used at level run boundaries. + // N2. Any remaining neutrals take the embedding direction. + for (var i = 0; i < len; ++i) { + if (isNeutral.test(types[i])) { + for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {} + var before = (i ? types[i-1] : outerType) == "L"; + var after = (end < len ? types[end] : outerType) == "L"; + var replace = before || after ? "L" : "R"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // Here we depart from the documented algorithm, in order to avoid + // building up an actual levels array. Since there are only three + // levels (0, 1, 2) in an implementation that doesn't take + // explicit embedding into account, we can build up the order on + // the fly, without following the level-based algorithm. + var order = [], m; + for (var i = 0; i < len;) { + if (countsAsLeft.test(types[i])) { + var start = i; + for (++i; i < len && countsAsLeft.test(types[i]); ++i) {} + order.push(new BidiSpan(0, start, i)); + } else { + var pos = i, at = order.length; + for (++i; i < len && types[i] != "L"; ++i) {} + for (var j = pos; j < i;) { + if (countsAsNum.test(types[j])) { + if (pos < j) order.splice(at, 0, new BidiSpan(1, pos, j)); + var nstart = j; + for (++j; j < i && countsAsNum.test(types[j]); ++j) {} + order.splice(at, 0, new BidiSpan(2, nstart, j)); + pos = j; + } else ++j; + } + if (pos < i) order.splice(at, 0, new BidiSpan(1, pos, i)); + } + } + if (order[0].level == 1 && (m = str.match(/^\s+/))) { + order[0].from = m[0].length; + order.unshift(new BidiSpan(0, 0, m[0].length)); + } + if (lst(order).level == 1 && (m = str.match(/\s+$/))) { + lst(order).to -= m[0].length; + order.push(new BidiSpan(0, len - m[0].length, len)); + } + if (order[0].level != lst(order).level) + order.push(new BidiSpan(order[0].level, len, len)); + + return order; + }; + })(); + + // THE END + + CodeMirror.version = "5.1.1"; + + return CodeMirror; +}); \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/public/js/policy-edit.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/public/js/policy-edit.js new file mode 100644 index 000000000..d42dd0109 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/public/js/policy-edit.js @@ -0,0 +1,729 @@ +/* + * 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. + */ + +var validateStep = {}; +var skipStep = {}; +var stepForwardFrom = {}; +var stepBackFrom = {}; +var policy = {}; +var configuredOperations = []; + +/** + * Method to update the visibility (i.e. disabled or enabled view) + * of grouped input according to the values + * that they currently possess. + * @param domElement HTML grouped-input element with class name "grouped-input" + */ +var updateGroupedInputVisibility = function (domElement) { + if ($(".parent-input:first", domElement).is(":checked")) { + if ($(".grouped-child-input:first", domElement).hasClass("disabled")) { + $(".grouped-child-input:first", domElement).removeClass("disabled"); + } + $(".child-input", domElement).each(function () { + $(this).prop('disabled', false); + }); + } else { + if (!$(".grouped-child-input:first", domElement).hasClass("disabled")) { + $(".grouped-child-input:first", domElement).addClass("disabled"); + } + $(".child-input", domElement).each(function () { + $(this).prop('disabled', true); + }); + } +}; + +skipStep["policy-platform"] = function (policyPayloadObj) { + policy["name"] = policyPayloadObj["policyName"]; + policy["platform"] = policyPayloadObj["profile"]["deviceType"]["name"]; + policy["platformId"] = policyPayloadObj["profile"]["deviceType"]["id"]; + var userRoleInput = $("#user-roles-input"); + var ownershipInput = $("#ownership-input"); + var userInput = $("#users-input"); + var actionInput = $("#action-input"); + var policyNameInput = $("#policy-name-input"); + var policyDescriptionInput = $("#policy-description-input"); + userRoleInput.val(policyPayloadObj.roles).trigger("change"); + userInput.val(policyPayloadObj.users).trigger("change"); + ownershipInput.val(policyPayloadObj.ownershipType); + actionInput.val(policyPayloadObj.compliance); + policyNameInput.val(policyPayloadObj["policyName"]); + policyDescriptionInput.val(policyPayloadObj["description"]); + // updating next-page wizard title with selected platform + $("#policy-profile-page-wizard-title").text("EDIT " + policy["platform"] + " POLICY - " + policy["name"]); + + var profileFeaturesList = policyPayloadObj["profile"]["profileFeaturesList"]; + if (profileFeaturesList.length > 0){ + var content = profileFeaturesList[0]["content"]; + var policyDefinitionObj = JSON.parse(content); + window.queryEditor.setValue(policyDefinitionObj["policyDefinition"]); + } +}; + +/** + * Checks if provided number is valid against a range. + * + * @param numberInput Number Input + * @param min Minimum Limit + * @param max Maximum Limit + * @returns {boolean} Returns true if input is within the specified range + */ +var inputIsValidAgainstRange = function (numberInput, min, max) { + return (numberInput == min || (numberInput > min && numberInput < max) || numberInput == max); +}; + +/** + * Checks if provided input is valid against RegEx input. + * + * @param regExp Regular expression + * @param input Input string to check + * @returns {boolean} Returns true if input matches RegEx + */ +var inputIsValidAgainstRegExp = function (regExp, input) { + return regExp.test(input); +}; + +validateStep["policy-profile"] = function () { + return true; +} + +stepForwardFrom["policy-profile"] = function () { + policy["profile"] = operationModule.generateProfile(policy["platform"], configuredOperations); + // updating next-page wizard title with selected platform + $("#policy-criteria-page-wizard-title").text("EDIT " + policy["platform"] + " POLICY - " + policy["name"]); +}; + +stepForwardFrom["policy-criteria"] = function () { + $("input[type='radio'].select-users-radio").each(function () { + if ($(this).is(':radio')) { + if ($(this).is(":checked")) { + if ($(this).attr("id") == "users-radio-btn") { + policy["selectedUsers"] = $("#users-input").val(); + } else if ($(this).attr("id") == "user-roles-radio-btn") { + policy["selectedUserRoles"] = $("#user-roles-input").val(); + } + } + } + }); + policy["selectedNonCompliantAction"] = $("#action-input").find(":selected").data("action"); + policy["selectedOwnership"] = $("#ownership-input").val(); + // updating next-page wizard title with selected platform + $("#policy-naming-page-wizard-title").text("EDIT " + policy["platform"] + " POLICY - " + policy["name"]); +}; + +/** + * Checks if provided input is valid against provided length range. + * + * @param input Alphanumeric or non-alphanumeric input + * @param minLength Minimum Required Length + * @param maxLength Maximum Required Length + * @returns {boolean} Returns true if input matches the provided minimum length and maximum length + */ +var inputIsValidAgainstLength = function (input, minLength, maxLength) { + var length = input.length; + return (length == minLength || (length > minLength && length < maxLength) || length == maxLength); +}; + +validateStep["policy-naming"] = function () { + var validationStatus = {}; + + // taking values of inputs to be validated + var policyName = $("input#policy-name-input").val(); + // starting validation process and updating validationStatus + if (!policyName) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = "Policy name is empty. You cannot proceed."; + } else if (!inputIsValidAgainstLength(policyName, 1, 30)) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = + "Policy name exceeds maximum allowed length. Please check."; + } else { + validationStatus["error"] = false; + } + // ending validation process + + // start taking specific actions upon validation + var wizardIsToBeContinued; + if (validationStatus["error"]) { + wizardIsToBeContinued = false; + var mainErrorMsgWrapper = "#policy-naming-main-error-msg"; + var mainErrorMsg = mainErrorMsgWrapper + " span"; + $(mainErrorMsg).text(validationStatus["mainErrorMsg"]); + $(mainErrorMsgWrapper).removeClass("hidden"); + } else { + wizardIsToBeContinued = true; + } + + return wizardIsToBeContinued; +}; + +validateStep["policy-naming-publish"] = function () { + var validationStatus = {}; + + // taking values of inputs to be validated + var policyName = $("input#policy-name-input").val(); + // starting validation process and updating validationStatus + if (!policyName) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = "Policy name is empty. You cannot proceed."; + } else if (!inputIsValidAgainstLength(policyName, 1, 30)) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = + "Policy name exceeds maximum allowed length. Please check."; + } else { + validationStatus["error"] = false; + } + // ending validation process + + // start taking specific actions upon validation + var wizardIsToBeContinued; + if (validationStatus["error"]) { + wizardIsToBeContinued = false; + var mainErrorMsgWrapper = "#policy-naming-main-error-msg"; + var mainErrorMsg = mainErrorMsgWrapper + " span"; + $(mainErrorMsg).text(validationStatus["mainErrorMsg"]); + $(mainErrorMsgWrapper).removeClass("hidden"); + } else { + wizardIsToBeContinued = true; + } + + return wizardIsToBeContinued; +}; + +stepForwardFrom["policy-naming-publish"] = function () { + policy["policyName"] = $("#policy-name-input").val(); + policy["description"] = $("#policy-description-input").val(); + //All data is collected. Policy can now be updated. + updatePolicy(policy, "publish"); +}; +stepForwardFrom["policy-naming"] = function () { + policy["policyName"] = $("#policy-name-input").val(); + policy["description"] = $("#policy-description-input").val(); + //All data is collected. Policy can now be updated. + updatePolicy(policy, "save"); +}; + +var updatePolicy = function (policy, state) { + var profilePayloads = [{ + "featureCode": "CONFIG", + "deviceTypeId": policy["platformId"], + "content": {"policyDefinition": window.queryEditor.getValue()} + }]; + + var payload = { + "policyName": policy["policyName"], + "description": policy["description"], + "compliance": policy["selectedNonCompliantAction"], + "ownershipType": "ANY", + "profile": { + "profileName": policy["policyName"], + "deviceType": { + "id": policy["platformId"], + "name": policy["platform"] + }, + "profileFeaturesList": profilePayloads + } + }; + + if (policy["selectedUsers"]) { + payload["users"] = policy["selectedUsers"]; + } else if (policy["selectedUserRoles"]) { + payload["roles"] = policy["selectedUserRoles"]; + } else { + payload["users"] = []; + payload["roles"] = []; + } + + var serviceURL = "/devicemgt_admin/policies/" + getParameterByName("id"); + invokerUtil.put( + serviceURL, + payload, + // on success + function () { + if (state == "save"){ + var policyList = []; + policyList.push(getParameterByName("id")); + serviceURL = "/devicemgt_admin/policies/inactivate"; + invokerUtil.put( + serviceURL, + policyList, + // on success + function () { + $(".add-policy").addClass("hidden"); + $(".policy-message").removeClass("hidden"); + }, + // on error + function () { + + } + ); + }else if(state == "publish"){ + var policyList = []; + policyList.push(getParameterByName("id")); + serviceURL = "/devicemgt_admin/policies/activate"; + invokerUtil.put( + serviceURL, + policyList, + // on success + function () { + $(".add-policy").addClass("hidden"); + $(".policy-naming").addClass("hidden"); + $(".policy-message").removeClass("hidden"); + publishToDevice(); + }, + // on error + function () { + + } + ); + } + }, + // on error + function () { + + } + ); +}; + + +function publishToDevice() { + var payload = { + "policyName": policy["policyName"], + "description": policy["description"], + "compliance": policy["selectedNonCompliantAction"], + "ownershipType": "ANY", + "profile": { + "profileName": policy["policyName"], + "deviceType": { + "id": policy["platformId"], + "name": policy["platform"] + }, + "policyDefinition": window.queryEditor.getValue(), + "policyDescription": policy["description"] + } + }; + + var successCallback = function (data, status) { + console.log("Data: " + data + "\nStatus: " + status); + }; + + var data = { + url: "/devicemgt/api/policies/add", + type: "POST", + contentType: "application/json", + accept: "application/json", + success: successCallback, + data: JSON.stringify(payload) + }; + + $.ajax(data).fail(function (jqXHR) { + console.log("Error: " + jqXHR); + }); + +} + +// Start of HTML embedded invoke methods +var showAdvanceOperation = function (operation, button) { + $(button).addClass('selected'); + $(button).siblings().removeClass('selected'); + var hiddenOperation = ".wr-hidden-operations-content > div"; + $(hiddenOperation + '[data-operation="' + operation + '"]').show(); + $(hiddenOperation + '[data-operation="' + operation + '"]').siblings().hide(); +}; + +// Start of functions related to grid-input-view + +/** + * Method to set count id to cloned elements. + * @param {object} addFormContainer + */ +var setId = function (addFormContainer) { + $(addFormContainer).find("[data-add-form-clone]").each(function (i) { + $(this).attr("id", $(this).attr("data-add-form-clone").slice(1) + "-" + (i + 1)); + if ($(this).find(".index").length > 0) { + $(this).find(".index").html(i + 1); + } + }); +}; + +/** + * Method to set count id to cloned elements. + * @param {object} addFormContainer + */ +var showHideHelpText = function (addFormContainer) { + var helpText = "[data-help-text=add-form]"; + if ($(addFormContainer).find("[data-add-form-clone]").length > 0) { + $(addFormContainer).find(helpText).hide(); + } else { + $(addFormContainer).find(helpText).show(); + } +}; + +// End of functions related to grid-input-view + +/** + * This method will return query parameter value given its name. + * @param name Query parameter name + * @returns {string} Query parameter value + */ +var getParameterByName = function (name) { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), + results = regex.exec(location.search); + return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); +}; + +function formatRepo (user) { + if (user.loading) { + return user.text + } + if (!user.username){ + return; + } + var markup = '
      ' + + '
      ' + + '
      ' + + '
      ' + user.username + '
      '; + if (user.firstname) { + markup += '
      ' + user.firstname + '
      '; + } + if (user.emailAddress) { + markup += '
      ' + user.emailAddress + '
      '; + } + markup += '
      '; + return markup; +} + +function formatRepoSelection (user) { + return user.username || user.text;; +} + +$(document).ready(function () { + + window.queryEditor = CodeMirror.fromTextArea(document.getElementById('policy-definition-input'), { + mode: MIME_TYPE_SIDDHI_QL, + indentWithTabs: true, + smartIndent: true, + lineNumbers: true, + matchBrackets: true, + autofocus: true, + extraKeys: { + "Shift-2": function (cm) { + insertStr(cm, cm.getCursor(), '@'); + CodeMirror.showHint(cm, getAnnotationHints); + }, + "Ctrl-Space": "autocomplete" + } + }); + + // Adding initial state of wizard-steps. + $("#policy-profile-wizard-steps").html($(".wr-steps").html()); + + $("select.select2[multiple=multiple]").select2({ + "tags": true + }); + + $("#users-input").select2({ + multiple:true, + tags: true, + ajax: { + url: window.location.origin + "/devicemgt/api/invoker/execute/", + method: "POST", + dataType: 'json', + delay: 250, + id: function (user) { + return user.username; + }, + data: function (params) { + var postData = {}; + postData.actionMethod = "GET"; + postData.actionUrl = "/devicemgt_admin/users?q=ad"; + postData.actionPayload = JSON.stringify({ + q: params.term, // search term + page: params.page + }); + + return JSON.stringify(postData); + }, + processResults: function (data, page) { + var newData = []; + $.each(data.responseContent, function (index, value) { + value.id = value.username; + newData.push(value); + }); + return { + results: newData + }; + }, + cache: true + }, + escapeMarkup: function (markup) { return markup; }, // let our custom formatter work + minimumInputLength: 1, + templateResult: formatRepo, // omitted for brevity, see the source of this page + templateSelection: formatRepoSelection // omitted for brevity, see the source of this page + }); + + var policyPayloadObj; + invokerUtil.get( + "/devicemgt_admin/policies/" + getParameterByName("id"), + // on success + function (data) { + data = JSON.parse(data); + policyPayloadObj = data["responseContent"]; + skipStep["policy-platform"](policyPayloadObj); + }, + // on error + function () { + // should be redirected to an error page + } + ); + + $("#users-select-field").hide(); + $("#user-roles-select-field").show(); + + $("input[type='radio'].select-users-radio").change(function () { + if ($("#users-radio-btn").is(":checked")) { + $("#user-roles-select-field").hide(); + $("#users-select-field").show(); + } + if ($("#user-roles-radio-btn").is(":checked")) { + $("#users-select-field").hide(); + $("#user-roles-select-field").show(); + } + }); + + // Support for special input type "ANY" on user(s) & user-role(s) selection + $("#user-roles-input").select2({ + "tags": true + }).on("select2:select", function (e) { + if (e.params.data.id == "ANY") { + $(this).val("ANY").trigger("change"); + } else { + $("option[value=ANY]", this).prop("selected", false).parent().trigger("change"); + } + }); + + // Maintains an array of configured features of the profile + var advanceOperations = ".wr-advance-operations"; + $(advanceOperations).on("click", ".wr-input-control.switch", function (event) { + var operationCode = $(this).parents(".operation-data").data("operation-code"); + var operation = $(this).parents(".operation-data").data("operation"); + var operationDataWrapper = $(this).data("target"); + // prevents event bubbling by figuring out what element it's being called from. + if (event.target.tagName == "INPUT") { + var featureConfiguredIcon; + if ($("input[type='checkbox']", this).is(":checked")) { + configuredOperations.push(operationCode); + // when a feature is enabled, if "zero-configured-features" msg is available, hide that. + var zeroConfiguredOperationsErrorMsg = "#policy-profile-main-error-msg"; + if (!$(zeroConfiguredOperationsErrorMsg).hasClass("hidden")) { + $(zeroConfiguredOperationsErrorMsg).addClass("hidden"); + } + // add configured-state-icon to the feature + featureConfiguredIcon = "#" + operation + "-configured"; + if ($(featureConfiguredIcon).hasClass("hidden")) { + $(featureConfiguredIcon).removeClass("hidden"); + } + } else { + //splicing the array if operation is present. + var index = $.inArray(operationCode, configuredOperations); + if (index != -1) { + configuredOperations.splice(index, 1); + } + // when a feature is disabled, clearing all its current configured, error or success states + var subErrorMsgWrapper = "#" + operation + "-feature-error-msg"; + var subErrorIcon = "#" + operation + "-error"; + var subOkIcon = "#" + operation + "-ok"; + featureConfiguredIcon = "#" + operation + "-configured"; + + if (!$(subErrorMsgWrapper).hasClass("hidden")) { + $(subErrorMsgWrapper).addClass("hidden"); + } + if (!$(subErrorIcon).hasClass("hidden")) { + $(subErrorIcon).addClass("hidden"); + } + if (!$(subOkIcon).hasClass("hidden")) { + $(subOkIcon).addClass("hidden"); + } + if (!$(featureConfiguredIcon).hasClass("hidden")) { + $(featureConfiguredIcon).addClass("hidden"); + } + // reinitializing input fields into the defaults + $(operationDataWrapper + " input").each( + function () { + if ($(this).is("input:text")) { + $(this).val($(this).data("default")); + } else if ($(this).is("input:password")) { + $(this).val(""); + } else if ($(this).is("input:checkbox")) { + $(this).prop("checked", $(this).data("default")); + // if this checkbox is the parent input of a grouped-input + if ($(this).hasClass("parent-input")) { + var groupedInput = $(this).parent().parent().parent(); + updateGroupedInputVisibility(groupedInput); + } + } + } + ); + // reinitializing select fields into the defaults + $(operationDataWrapper + " select").each( + function () { + var defaultOption = $(this).data("default"); + $("option:eq(" + defaultOption + ")", this).prop("selected", "selected"); + } + ); + // collapsing expanded-panes (upon the selection of html-select-options) if any + $(operationDataWrapper + " .expanded").each( + function () { + if ($(this).hasClass("expanded")) { + $(this).removeClass("expanded"); + } + $(this).slideUp(); + } + ); + // removing all entries of grid-input elements if exist + $(operationDataWrapper + " .grouped-array-input").each( + function () { + var gridInputs = $(this).find("[data-add-form-clone]"); + if (gridInputs.length > 0) { + gridInputs.remove(); + } + var helpTexts = $(this).find("[data-help-text=add-form]"); + if (helpTexts.length > 0) { + helpTexts.show(); + } + } + ); + } + } + }); + + // adding support for cloning multiple profiles per feature with cloneable class definitions + $(advanceOperations).on("click", ".multi-view.add.enabled", function () { + // get a copy of .cloneable and create new .cloned div element + var cloned = "

      " + $(".cloneable", $(this).parent().parent()).html() + "
      "; + // append newly created .cloned div element to panel-body + $(this).parent().parent().append(cloned); + // enable remove action of newly cloned div element + $(".cloned", $(this).parent().parent()).each( + function () { + if ($(".multi-view.remove", this).hasClass("disabled")) { + $(".multi-view.remove", this).removeClass("disabled"); + } + if (!$(".multi-view.remove", this).hasClass("enabled")) { + $(".multi-view.remove", this).addClass("enabled"); + } + } + ); + }); + + $(advanceOperations).on("click", ".multi-view.remove.enabled", function () { + $(this).parent().remove(); + }); + + // enabling or disabling grouped-input based on the status of a parent check-box + $(advanceOperations).on("click", ".grouped-input", function () { + updateGroupedInputVisibility(this); + }); + + // add form entry click function for grid inputs + $(advanceOperations).on("click", "[data-click-event=add-form]", function () { + var addFormContainer = $("[data-add-form-container=" + $(this).attr("href") + "]"); + var clonedForm = $("[data-add-form=" + $(this).attr("href") + "]").clone(). + find("[data-add-form-element=clone]").attr("data-add-form-clone", $(this).attr("href")); + + // adding class .child-input to capture text-input-array-values + $("input, select", clonedForm).addClass("child-input"); + + $(addFormContainer).append(clonedForm); + setId(addFormContainer); + showHideHelpText(addFormContainer); + }); + + // remove form entry click function for grid inputs + $(advanceOperations).on("click", "[data-click-event=remove-form]", function () { + var addFormContainer = $("[data-add-form-container=" + $(this).attr("href") + "]"); + + $(this).closest("[data-add-form-element=clone]").remove(); + setId(addFormContainer); + showHideHelpText(addFormContainer); + }); + + $(".wizard-stepper").click(function () { + // button clicked here can be either a continue button or a back button. + var currentStep = $(this).data("current"); + var validationIsRequired = $(this).data("validate"); + var wizardIsToBeContinued; + + if (validationIsRequired) { + wizardIsToBeContinued = validateStep[currentStep](); + } else { + wizardIsToBeContinued = true; + } + + if (wizardIsToBeContinued) { + // When moving back and forth, following code segment will + // remove if there are any visible error-messages. + var errorMsgWrappers = ".alert.alert-danger"; + $(errorMsgWrappers).each( + function () { + if (!$(this).hasClass("hidden")) { + $(this).addClass("hidden"); + } + } + ); + + var nextStep = $(this).data("next"); + var isBackBtn = $(this).data("is-back-btn"); + + // if current button is a continuation... + if (!isBackBtn) { + // initiate stepForwardFrom[*] functions to gather form data. + if (stepForwardFrom[currentStep]) { + stepForwardFrom[currentStep](this); + } + } else { + // initiate stepBackFrom[*] functions to rollback. + if (stepBackFrom[currentStep]) { + stepBackFrom[currentStep](); + } + } + + // following step occurs only at the last stage of the wizard. + if (!nextStep) { + window.location.href = $(this).data("direct"); + } + + // updating next wizard step as current. + $(".itm-wiz").each(function () { + var step = $(this).data("step"); + if (step == nextStep) { + $(this).addClass("itm-wiz-current"); + } else { + $(this).removeClass("itm-wiz-current"); + } + }); + + // adding next update of wizard-steps. + $("#" + nextStep + "-wizard-steps").html($(".wr-steps").html()); + + // hiding current section of the wizard and showing next section. + $("." + currentStep).addClass("hidden"); + $("." + nextStep).removeClass("hidden"); + } + }); +}); \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/public/js/sql.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/public/js/sql.js new file mode 100644 index 000000000..9f92fbc93 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.edit/public/js/sql.js @@ -0,0 +1,310 @@ +/* + * 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. + */ + +/*Annotations, Annotation Names and relevant tokens*/ +var ANNOTATION_IMPORT = "Import"; +var ANNOTATION_EXPORT = "Export"; + +var ANNOTATION_TOKEN_AT = "@"; +var ANNOTATION_TOKEN_OPENING_BRACKET = "("; +var ANNOTATION_TOKEN_CLOSING_BRACKET = ")"; + +var REGEX_LINE_STARTING_WITH_PLAN = /^@Plan.*/g; +var REGEX_LINE_STARTING_WITH_SINGLE_LINE_COMMENT = /^--.*/g; +var REGEX_LINE_STARTING_WITH_MULTI_LINE_COMMENT = /^\/\*.*\*\//g; +var REGEX_LINE_STARTING_WITH_IMPORT_STATEMENT = /^@Import.*/g; + +var SIDDHI_STATEMENT_DELIMETER = ";"; +var SIDDHI_LINE_BREAK = "\n"; +var SIDDHI_LINE_BREAK_CHARACTER = '\n'; +var SIDDHI_SINGLE_QUOTE = "'"; +var SIDDHI_SPACE_LITERAL = " "; + +var SIDDHI_LITERAL_DEFINE_STREAM = "define stream"; + +var MIME_TYPE_SIDDHI_QL = "text/siddhi-ql"; + + +CodeMirror.defineMode("sql", function (config, parserConfig) { + "use strict"; + + var client = parserConfig.client || {}, + atoms = parserConfig.atoms || {"false":true, "true":true, "null":true}, + builtin = parserConfig.builtin || {}, + keywords = parserConfig.keywords || {}, + operatorChars = parserConfig.operatorChars || /^[*+\-%<>!=&|~^]/, + support = parserConfig.support || {}, + hooks = parserConfig.hooks || {}, + dateSQL = parserConfig.dateSQL || {"date":true, "time":true, "timestamp":true}; + + function tokenBase(stream, state) { + var ch = stream.next(); + + // call hooks from the mime type + if (hooks[ch]) { + var result = hooks[ch](stream, state); + if (result !== false) return result; + } + + if (ch.charCodeAt(0) > 47 && ch.charCodeAt(0) < 58) { + // numbers + // ref: http://dev.mysql.com/doc/refman/5.5/en/number-literals.html + stream.match(/^[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?/); + support.decimallessFloat == true && stream.eat('.'); + return "number"; + } else if (ch == "'" || (ch == '"' && support.doubleQuote)) { + // strings + // ref: http://dev.mysql.com/doc/refman/5.5/en/string-literals.html + state.tokenize = tokenLiteral(ch); + return state.tokenize(stream, state); + } else if (/^[\(\),\;\[\]]/.test(ch)) { + // no highlightning + return null; + } else if ((ch == "-" && stream.eat("-") && (!support.commentSpaceRequired || stream.eat(" ")))) { + // 1-line comments + // ref: https://kb.askmonty.org/en/comment-syntax/ + stream.skipToEnd(); + return "comment"; + } else if (ch == "/" && stream.eat("*")) { + // multi-line comments + // ref: https://kb.askmonty.org/en/comment-syntax/ + state.tokenize = tokenComment; + return state.tokenize(stream, state); + } else if (ch == ".") { + // .1 for 0.1 + if (support.zerolessFloat == true && stream.match(/^(?:\d+(?:e[+-]?\d+)?)/i)) { + return "number"; + } + } else { + stream.eatWhile(/^[_\-\w\d]/); /* Character '-' will also be eaten, to prevent the highlight happening in keywords being embedded in non-keyword strings. For example, 'all' in 'all-nonkeyword' */ + var word = stream.current().toLowerCase(); // Added toLowerCase() to highlight keywords in a case insensitive manner. + // dates (standard SQL syntax) + // ref: http://dev.mysql.com/doc/refman/5.5/en/date-and-time-literals.html + if (dateSQL.hasOwnProperty(word) && (stream.match(/^( )+'[^']*'/) || stream.match(/^( )+"[^"]*"/))) + return "number"; + if (atoms.hasOwnProperty(word)) return "atom"; + if (builtin.hasOwnProperty(word)) return "builtin"; + if (keywords.hasOwnProperty(word)) return "keyword"; + if (client.hasOwnProperty(word)) return "string-2"; + return null; + } + } + + // 'string', with char specified in quote escaped by '\' + function tokenLiteral(quote) { + return function (stream, state) { + var escaped = false, ch; + while ((ch = stream.next()) != null) { + if (ch == quote && !escaped) { + state.tokenize = tokenBase; + break; + } + escaped = !escaped && ch == "\\"; + } + return "string"; + }; + } + + function tokenComment(stream, state) { + while (true) { + if (stream.skipTo("*")) { + stream.next(); + if (stream.eat("/")) { + state.tokenize = tokenBase; + break; + } + } else { + stream.skipToEnd(); + break; + } + } + return "comment"; + } + + function pushContext(stream, state, type) { + state.context = { + prev:state.context, + indent:stream.indentation(), + col:stream.column(), + type:type + }; + } + + function popContext(state) { + state.indent = state.context.indent; + state.context = state.context.prev; + } + + return { + startState:function () { + return {tokenize:tokenBase, context:null}; + }, + + token:function (stream, state) { + if (stream.sol()) { + if (state.context && state.context.align == null) + state.context.align = false; + } + if (stream.eatSpace()) return null; + + var style = state.tokenize(stream, state); + if (style == "comment") return style; + + if (state.context && state.context.align == null) + state.context.align = true; + + var tok = stream.current(); + if (tok == "(") + pushContext(stream, state, ")"); + else if (tok == "[") + pushContext(stream, state, "]"); + else if (state.context && state.context.type == tok) + popContext(state); + return style; + }, + + indent:function (state, textAfter) { + var cx = state.context; + if (!cx) return CodeMirror.Pass; + var closing = textAfter.charAt(0) == cx.type; + if (cx.align) return cx.col + (closing ? 0 : 1); + else return cx.indent + (closing ? 0 : config.indentUnit); + }, + + blockCommentStart: "/*", + blockCommentEnd: "*/", + lineComment: "--" + }; +}); + +(function () { + "use strict"; + + // `identifier` + function hookIdentifier(stream) { + // MySQL/MariaDB identifiers + // ref: http://dev.mysql.com/doc/refman/5.6/en/identifier-qualifiers.html + var ch; + while ((ch = stream.next()) != null) { + if (ch == "`" && !stream.eat("`")) return "variable-2"; + } + stream.backUp(stream.current().length - 1); + return stream.eatWhile(/\w/) ? "variable-2" : null; + } + + // variable token + function hookVar(stream) { + // variables + // @@prefix.varName @varName + // varName can be quoted with ` or ' or " + // ref: http://dev.mysql.com/doc/refman/5.5/en/user-variables.html + if (stream.eat("@")) { + stream.match(/^session\./); + stream.match(/^local\./); + stream.match(/^global\./); + } + + if (stream.eat("'")) { + stream.match(/^.*'/); + return "variable-2"; + } else if (stream.eat('"')) { + stream.match(/^.*"/); + return "variable-2"; + } else if (stream.eat("`")) { + stream.match(/^.*`/); + return "variable-2"; + } else if (stream.match(/^[0-9a-zA-Z$\.\_]+/)) { + return "variable-2"; + } + return null; + } + + ; + + // short client keyword token + function hookClient(stream) { + // \N means NULL + // ref: http://dev.mysql.com/doc/refman/5.5/en/null-values.html + if (stream.eat("N")) { + return "atom"; + } + // \g, etc + // ref: http://dev.mysql.com/doc/refman/5.5/en/mysql-commands.html + return stream.match(/^[a-zA-Z.#!?]/) ? "variable-2" : null; + } + + // these keywords are used by all SQL dialects (however, a mode can still overwrite it) + var sqlKeywordsWithoutSymbols = "all and as begin by contains define delete end events " + + "every first for from full group having inner insert into join last " + + "left not of on or outer output partition raw return right select snapshot stream table "; + var sqlKeywords = ", : ? # ( ) " + sqlKeywordsWithoutSymbols; + var builtIn = "bool double float int long object string "; + var atoms = "false true null "; + var dateSQL = "days hours milliseconds minutes months seconds "; + var allSqlSuggestions = sqlKeywordsWithoutSymbols + builtIn + atoms + dateSQL; + + // turn a space-separated list into an array + function set(str) { + var obj = {}, words = str.split(" "); + for (var i = 0; i < words.length; ++i) obj[words[i]] = true; + return obj; + } + + // A generic SQL Mode. It's not a standard, it just try to support what is generally supported + CodeMirror.defineMIME(MIME_TYPE_SIDDHI_QL, { + name:"sql", + keywords:set(sqlKeywords), + builtin:set(builtIn), + atoms:set(atoms), + operatorChars:/^[*+%<>!=/]/, + dateSQL:set(dateSQL), + support:set("doubleQuote "), + allSqlSuggestions:set(allSqlSuggestions) + }); +}()); + +/* + How Properties of Mime Types are used by SQL Mode + ================================================= + + keywords: + A list of keywords you want to be highlighted. + functions: + A list of function names you want to be highlighted. + builtin: + A list of builtin types you want to be highlighted (if you want types to be of class "builtin" instead of "keyword"). + operatorChars: + All characters that must be handled as operators. + client: + Commands parsed and executed by the client (not the server). + support: + A list of supported syntaxes which are not common, but are supported by more than 1 DBMS. + * ODBCdotTable: .tableName + * zerolessFloat: .1 + * doubleQuote + * nCharCast: N'string' + * charsetCast: _utf8'string' + * commentHash: use # char for comments + * commentSlashSlash: use // for comments + * commentSpaceRequired: require a space after -- for comments + atoms: + Keywords that must be highlighted as atoms,. Some DBMS's support more atoms than others: + UNKNOWN, INFINITY, UNDERFLOW, NaN... + dateSQL: + Used for date/time SQL standard syntax, because not all DBMS's support same temporal types. + */ \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/public/css/codemirror.css b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/public/css/codemirror.css new file mode 100644 index 000000000..e749a5211 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/public/css/codemirror.css @@ -0,0 +1,342 @@ +/* + * 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. + */ + +/* BASICS */ + +.CodeMirror { + /* Set height, width, borders, and global font properties here */ + font-family: monospace; + height: 300px; + color: black; +} + +/* PADDING */ + +.CodeMirror-lines { + padding: 4px 0; /* Vertical padding around content */ +} +.CodeMirror pre { + padding: 0 4px; /* Horizontal padding of content */ +} + +.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + background-color: white; /* The little square between H and V scrollbars */ +} + +/* GUTTER */ + +.CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; +} +.CodeMirror-linenumbers {} +.CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; + white-space: nowrap; + left: -30px; +} + +.CodeMirror-guttermarker { color: black; } +.CodeMirror-guttermarker-subtle { color: #999; } + +/* CURSOR */ + +.CodeMirror div.CodeMirror-cursor { + border-left: 1px solid black; +} +/* Shown when moving in bi-directional text */ +.CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; +} +.CodeMirror.cm-fat-cursor div.CodeMirror-cursor { + width: auto; + border: 0; + background: #7e7; +} +.CodeMirror.cm-fat-cursor div.CodeMirror-cursors { + z-index: 1; +} + +.cm-animate-fat-cursor { + width: auto; + border: 0; + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; +} +@-moz-keyframes blink { + 0% { background: #7e7; } + 50% { background: none; } + 100% { background: #7e7; } +} +@-webkit-keyframes blink { + 0% { background: #7e7; } + 50% { background: none; } + 100% { background: #7e7; } +} +@keyframes blink { + 0% { background: #7e7; } + 50% { background: none; } + 100% { background: #7e7; } +} + +/* Can style cursor different in overwrite (non-insert) mode */ +div.CodeMirror-overwrite div.CodeMirror-cursor {} + +.cm-tab { display: inline-block; text-decoration: inherit; } + +.CodeMirror-ruler { + border-left: 1px solid #ccc; + position: absolute; +} + +/* DEFAULT THEME */ + +.cm-s-default .cm-keyword {color: #708;} +.cm-s-default .cm-atom {color: #219;} +.cm-s-default .cm-number {color: #164;} +.cm-s-default .cm-def {color: #00f;} +.cm-s-default .cm-variable, +.cm-s-default .cm-punctuation, +.cm-s-default .cm-property, +.cm-s-default .cm-operator {} +.cm-s-default .cm-variable-2 {color: #05a;} +.cm-s-default .cm-variable-3 {color: #085;} +.cm-s-default .cm-comment {color: #a50;} +.cm-s-default .cm-string {color: #a11;} +.cm-s-default .cm-string-2 {color: #f50;} +.cm-s-default .cm-meta {color: #555;} +.cm-s-default .cm-qualifier {color: #555;} +.cm-s-default .cm-builtin {color: #30a;} +.cm-s-default .cm-bracket {color: #997;} +.cm-s-default .cm-tag {color: #170;} +.cm-s-default .cm-attribute {color: #00c;} +.cm-s-default .cm-header {color: blue;} +.cm-s-default .cm-quote {color: #090;} +.cm-s-default .cm-hr {color: #999;} +.cm-s-default .cm-link {color: #00c;} + +.cm-negative {color: #d44;} +.cm-positive {color: #292;} +.cm-header, .cm-strong {font-weight: bold;} +.cm-em {font-style: italic;} +.cm-link {text-decoration: underline;} +.cm-strikethrough {text-decoration: line-through;} + +.cm-s-default .cm-error {color: #f00;} +.cm-invalidchar {color: #f00;} + +/* Default styles for common addons */ + +div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} +.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } +.CodeMirror-activeline-background {background: #e8f2ff;} + +/* STOP */ + +/* The rest of this file contains styles related to the mechanics of + the editor. You probably shouldn't touch them. */ + +.CodeMirror { + position: relative; + overflow: hidden; + background: white; +} + +.CodeMirror-scroll { + overflow: scroll !important; /* Things will break if this is overridden */ + /* 30px is the magic margin used to hide the element's real scrollbars */ + /* See overflow: hidden in .CodeMirror */ + margin-bottom: -30px; margin-right: -30px; + padding-bottom: 30px; + height: 100%; + outline: none; /* Prevent dragging from highlighting the element */ + position: relative; +} +.CodeMirror-sizer { + position: relative; + border-right: 30px solid transparent; +} + +/* The fake, visible scrollbars. Used to force redraw during scrolling + before actuall scrolling happens, thus preventing shaking and + flickering artifacts. */ +.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + position: absolute; + z-index: 6; + display: none; +} +.CodeMirror-vscrollbar { + right: 0; top: 0; + overflow-x: hidden; + overflow-y: scroll; +} +.CodeMirror-hscrollbar { + bottom: 0; left: 0; + overflow-y: hidden; + overflow-x: scroll; +} +.CodeMirror-scrollbar-filler { + right: 0; bottom: 0; +} +.CodeMirror-gutter-filler { + left: 0; bottom: 0; +} + +.CodeMirror-gutters { + position: absolute; left: 0; top: 0; + z-index: 3; +} +.CodeMirror-gutter { + white-space: normal; + height: 100%; + display: inline-block; + /* Hack to make IE7 behave */ + *zoom:1; + *display:inline; +} +.CodeMirror-gutter-wrapper { + position: absolute; + z-index: 4; + height: 100%; +} +.CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4; + left: -30px; +} +.CodeMirror-gutter-wrapper { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.CodeMirror-lines { + cursor: text; + min-height: 1px; /* prevents collapsing before first draw */ +} +.CodeMirror pre { + /* Reset some styles that the rest of the page might have set */ + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; + -webkit-tap-highlight-color: transparent; +} +.CodeMirror-wrap pre { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} + +.CodeMirror-linebackground { + position: absolute; + left: 0; right: 0; top: 0; bottom: 0; + z-index: 0; +} + +.CodeMirror-linewidget { + position: relative; + z-index: 2; + overflow: auto; +} + +.CodeMirror-widget {} + +.CodeMirror-code { + outline: none; +} + +/* Force content-box sizing for the elements where we expect it */ +.CodeMirror-scroll, +.CodeMirror-sizer, +.CodeMirror-gutter, +.CodeMirror-gutters, +.CodeMirror-linenumber { + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +.CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; +} +.CodeMirror-measure pre { position: static; } + +.CodeMirror div.CodeMirror-cursor { + position: absolute; + border-right: none; + width: 0; +} + +div.CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 3; +} +.CodeMirror-focused div.CodeMirror-cursors { + visibility: visible; +} + +.CodeMirror-selected { background: #d9d9d9; } +.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } +.CodeMirror-crosshair { cursor: crosshair; } +.CodeMirror ::selection { background: #d7d4f0; } +.CodeMirror ::-moz-selection { background: #d7d4f0; } + +.cm-searching { + background: #ffa; + background: rgba(255, 255, 0, .4); +} + +/* IE7 hack to prevent it from returning funny offsetTops on the spans */ +.CodeMirror span { *vertical-align: text-bottom; } + +/* Used to force a border model for a node */ +.cm-force-border { padding-right: .1px; } + +@media print { + /* Hide the cursor when printing */ + .CodeMirror div.CodeMirror-cursors { + visibility: hidden; + } +} + +/* See issue #2901 */ +.cm-tab-wrap-hack:after { content: ''; } + +/* Help users use markselection to safely style text background */ +span.CodeMirror-selectedtext { background: none; } \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/public/js/codemirror.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/public/js/codemirror.js new file mode 100644 index 000000000..20f3f95ed --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/public/js/codemirror.js @@ -0,0 +1,8720 @@ +/* + * 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. + */ + +// This is CodeMirror (http://codemirror.net), a code editor +// implemented in JavaScript on top of the browser's DOM. +// +// You can find some technical background for some of the code below +// at http://marijnhaverbeke.nl/blog/#cm-internals . + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + module.exports = mod(); + else if (typeof define == "function" && define.amd) // AMD + return define([], mod); + else // Plain browser env + this.CodeMirror = mod(); +})(function() { + "use strict"; + + // BROWSER SNIFFING + + // Kludges for bugs and behavior differences that can't be feature + // detected are enabled based on userAgent etc sniffing. + + var gecko = /gecko\/\d/i.test(navigator.userAgent); + var ie_upto10 = /MSIE \d/.test(navigator.userAgent); + var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent); + var ie = ie_upto10 || ie_11up; + var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]); + var webkit = /WebKit\//.test(navigator.userAgent); + var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent); + var chrome = /Chrome\//.test(navigator.userAgent); + var presto = /Opera\//.test(navigator.userAgent); + var safari = /Apple Computer/.test(navigator.vendor); + var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent); + var phantom = /PhantomJS/.test(navigator.userAgent); + + var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent); + // This is woefully incomplete. Suggestions for alternative methods welcome. + var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent); + var mac = ios || /Mac/.test(navigator.platform); + var windows = /win/i.test(navigator.platform); + + var presto_version = presto && navigator.userAgent.match(/Version\/(\d*\.\d*)/); + if (presto_version) presto_version = Number(presto_version[1]); + if (presto_version && presto_version >= 15) { presto = false; webkit = true; } + // Some browsers use the wrong event properties to signal cmd/ctrl on OS X + var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11)); + var captureRightClick = gecko || (ie && ie_version >= 9); + + // Optimize some code when these features are not used. + var sawReadOnlySpans = false, sawCollapsedSpans = false; + + // EDITOR CONSTRUCTOR + + // A CodeMirror instance represents an editor. This is the object + // that user code is usually dealing with. + + function CodeMirror(place, options) { + if (!(this instanceof CodeMirror)) return new CodeMirror(place, options); + + this.options = options = options ? copyObj(options) : {}; + // Determine effective options based on given values and defaults. + copyObj(defaults, options, false); + setGuttersForLineNumbers(options); + + var doc = options.value; + if (typeof doc == "string") doc = new Doc(doc, options.mode); + this.doc = doc; + + var input = new CodeMirror.inputStyles[options.inputStyle](this); + var display = this.display = new Display(place, doc, input); + display.wrapper.CodeMirror = this; + updateGutters(this); + themeChanged(this); + if (options.lineWrapping) + this.display.wrapper.className += " CodeMirror-wrap"; + if (options.autofocus && !mobile) display.input.focus(); + initScrollbars(this); + + this.state = { + keyMaps: [], // stores maps added by addKeyMap + overlays: [], // highlighting overlays, as added by addOverlay + modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info + overwrite: false, + delayingBlurEvent: false, + focused: false, + suppressEdits: false, // used to disable editing during key handlers when in readOnly mode + pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in input.poll + draggingText: false, + highlight: new Delayed(), // stores highlight worker timeout + keySeq: null, // Unfinished key sequence + specialChars: null + }; + + var cm = this; + + // Override magic textarea content restore that IE sometimes does + // on our hidden textarea on reload + if (ie && ie_version < 11) setTimeout(function() { cm.display.input.reset(true); }, 20); + + registerEventHandlers(this); + ensureGlobalHandlers(); + + startOperation(this); + this.curOp.forceUpdate = true; + attachDoc(this, doc); + + if ((options.autofocus && !mobile) || cm.hasFocus()) + setTimeout(bind(onFocus, this), 20); + else + onBlur(this); + + for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt)) + optionHandlers[opt](this, options[opt], Init); + maybeUpdateLineNumberWidth(this); + if (options.finishInit) options.finishInit(this); + for (var i = 0; i < initHooks.length; ++i) initHooks[i](this); + endOperation(this); + // Suppress optimizelegibility in Webkit, since it breaks text + // measuring on line wrapping boundaries. + if (webkit && options.lineWrapping && + getComputedStyle(display.lineDiv).textRendering == "optimizelegibility") + display.lineDiv.style.textRendering = "auto"; + } + + // DISPLAY CONSTRUCTOR + + // The display handles the DOM integration, both for input reading + // and content drawing. It holds references to DOM nodes and + // display-related state. + + function Display(place, doc, input) { + var d = this; + this.input = input; + + // Covers bottom-right square when both scrollbars are present. + d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); + d.scrollbarFiller.setAttribute("cm-not-content", "true"); + // Covers bottom of gutter when coverGutterNextToScrollbar is on + // and h scrollbar is present. + d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); + d.gutterFiller.setAttribute("cm-not-content", "true"); + // Will contain the actual code, positioned to cover the viewport. + d.lineDiv = elt("div", null, "CodeMirror-code"); + // Elements are added to these to represent selection and cursors. + d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); + d.cursorDiv = elt("div", null, "CodeMirror-cursors"); + // A visibility: hidden element used to find the size of things. + d.measure = elt("div", null, "CodeMirror-measure"); + // When lines outside of the viewport are measured, they are drawn in this. + d.lineMeasure = elt("div", null, "CodeMirror-measure"); + // Wraps everything that needs to exist inside the vertically-padded coordinate system + d.lineSpace = elt("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], + null, "position: relative; outline: none"); + // Moved around its parent to cover visible view. + d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative"); + // Set to the height of the document, allowing scrolling. + d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); + d.sizerWidth = null; + // Behavior of elts with overflow: auto and padding is + // inconsistent across browsers. This is used to ensure the + // scrollable area is big enough. + d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;"); + // Will contain the gutters, if any. + d.gutters = elt("div", null, "CodeMirror-gutters"); + d.lineGutter = null; + // Actual scrollable element. + d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); + d.scroller.setAttribute("tabIndex", "-1"); + // The element in which the editor lives. + d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); + + // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) + if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } + if (!webkit && !(gecko && mobile)) d.scroller.draggable = true; + + if (place) { + if (place.appendChild) place.appendChild(d.wrapper); + else place(d.wrapper); + } + + // Current rendered range (may be bigger than the view window). + d.viewFrom = d.viewTo = doc.first; + d.reportedViewFrom = d.reportedViewTo = doc.first; + // Information about the rendered lines. + d.view = []; + d.renderedView = null; + // Holds info about a single rendered line when it was rendered + // for measurement, while not in view. + d.externalMeasured = null; + // Empty space (in pixels) above the view + d.viewOffset = 0; + d.lastWrapHeight = d.lastWrapWidth = 0; + d.updateLineNumbers = null; + + d.nativeBarWidth = d.barHeight = d.barWidth = 0; + d.scrollbarsClipped = false; + + // Used to only resize the line number gutter when necessary (when + // the amount of lines crosses a boundary that makes its width change) + d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; + // Set to true when a non-horizontal-scrolling line widget is + // added. As an optimization, line widget aligning is skipped when + // this is false. + d.alignWidgets = false; + + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + + // Tracks the maximum line length so that the horizontal scrollbar + // can be kept static when scrolling. + d.maxLine = null; + d.maxLineLength = 0; + d.maxLineChanged = false; + + // Used for measuring wheel scrolling granularity + d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; + + // True when shift is held down. + d.shift = false; + + // Used to track whether anything happened since the context menu + // was opened. + d.selForContextMenu = null; + + d.activeTouch = null; + + input.init(d); + } + + // STATE UPDATES + + // Used to get the editor into a consistent state again when options change. + + function loadMode(cm) { + cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption); + resetModeState(cm); + } + + function resetModeState(cm) { + cm.doc.iter(function(line) { + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + }); + cm.doc.frontier = cm.doc.first; + startWorker(cm, 100); + cm.state.modeGen++; + if (cm.curOp) regChange(cm); + } + + function wrappingChanged(cm) { + if (cm.options.lineWrapping) { + addClass(cm.display.wrapper, "CodeMirror-wrap"); + cm.display.sizer.style.minWidth = ""; + cm.display.sizerWidth = null; + } else { + rmClass(cm.display.wrapper, "CodeMirror-wrap"); + findMaxLine(cm); + } + estimateLineHeights(cm); + regChange(cm); + clearCaches(cm); + setTimeout(function(){updateScrollbars(cm);}, 100); + } + + // Returns a function that estimates the height of a line, to use as + // first approximation until the line becomes visible (and is thus + // properly measurable). + function estimateHeight(cm) { + var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; + var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); + return function(line) { + if (lineIsHidden(cm.doc, line)) return 0; + + var widgetsHeight = 0; + if (line.widgets) for (var i = 0; i < line.widgets.length; i++) { + if (line.widgets[i].height) widgetsHeight += line.widgets[i].height; + } + + if (wrapping) + return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th; + else + return widgetsHeight + th; + }; + } + + function estimateLineHeights(cm) { + var doc = cm.doc, est = estimateHeight(cm); + doc.iter(function(line) { + var estHeight = est(line); + if (estHeight != line.height) updateLineHeight(line, estHeight); + }); + } + + function themeChanged(cm) { + cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + + cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-"); + clearCaches(cm); + } + + function guttersChanged(cm) { + updateGutters(cm); + regChange(cm); + setTimeout(function(){alignHorizontally(cm);}, 20); + } + + // Rebuild the gutter elements, ensure the margin to the left of the + // code matches their width. + function updateGutters(cm) { + var gutters = cm.display.gutters, specs = cm.options.gutters; + removeChildren(gutters); + for (var i = 0; i < specs.length; ++i) { + var gutterClass = specs[i]; + var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass)); + if (gutterClass == "CodeMirror-linenumbers") { + cm.display.lineGutter = gElt; + gElt.style.width = (cm.display.lineNumWidth || 1) + "px"; + } + } + gutters.style.display = i ? "" : "none"; + updateGutterSpace(cm); + } + + function updateGutterSpace(cm) { + var width = cm.display.gutters.offsetWidth; + cm.display.sizer.style.marginLeft = width + "px"; + } + + // Compute the character length of a line, taking into account + // collapsed ranges (see markText) that might hide parts, and join + // other lines onto it. + function lineLength(line) { + if (line.height == 0) return 0; + var len = line.text.length, merged, cur = line; + while (merged = collapsedSpanAtStart(cur)) { + var found = merged.find(0, true); + cur = found.from.line; + len += found.from.ch - found.to.ch; + } + cur = line; + while (merged = collapsedSpanAtEnd(cur)) { + var found = merged.find(0, true); + len -= cur.text.length - found.from.ch; + cur = found.to.line; + len += cur.text.length - found.to.ch; + } + return len; + } + + // Find the longest line in the document. + function findMaxLine(cm) { + var d = cm.display, doc = cm.doc; + d.maxLine = getLine(doc, doc.first); + d.maxLineLength = lineLength(d.maxLine); + d.maxLineChanged = true; + doc.iter(function(line) { + var len = lineLength(line); + if (len > d.maxLineLength) { + d.maxLineLength = len; + d.maxLine = line; + } + }); + } + + // Make sure the gutters options contains the element + // "CodeMirror-linenumbers" when the lineNumbers option is true. + function setGuttersForLineNumbers(options) { + var found = indexOf(options.gutters, "CodeMirror-linenumbers"); + if (found == -1 && options.lineNumbers) { + options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]); + } else if (found > -1 && !options.lineNumbers) { + options.gutters = options.gutters.slice(0); + options.gutters.splice(found, 1); + } + } + + // SCROLLBARS + + // Prepare DOM reads needed to update the scrollbars. Done in one + // shot to minimize update/measure roundtrips. + function measureForScrollbars(cm) { + var d = cm.display, gutterW = d.gutters.offsetWidth; + var docH = Math.round(cm.doc.height + paddingVert(cm.display)); + return { + clientHeight: d.scroller.clientHeight, + viewHeight: d.wrapper.clientHeight, + scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth, + viewWidth: d.wrapper.clientWidth, + barLeft: cm.options.fixedGutter ? gutterW : 0, + docHeight: docH, + scrollHeight: docH + scrollGap(cm) + d.barHeight, + nativeBarWidth: d.nativeBarWidth, + gutterWidth: gutterW + }; + } + + function NativeScrollbars(place, scroll, cm) { + this.cm = cm; + var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); + var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); + place(vert); place(horiz); + + on(vert, "scroll", function() { + if (vert.clientHeight) scroll(vert.scrollTop, "vertical"); + }); + on(horiz, "scroll", function() { + if (horiz.clientWidth) scroll(horiz.scrollLeft, "horizontal"); + }); + + this.checkedOverlay = false; + // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). + if (ie && ie_version < 8) this.horiz.style.minHeight = this.vert.style.minWidth = "18px"; + } + + NativeScrollbars.prototype = copyObj({ + update: function(measure) { + var needsH = measure.scrollWidth > measure.clientWidth + 1; + var needsV = measure.scrollHeight > measure.clientHeight + 1; + var sWidth = measure.nativeBarWidth; + + if (needsV) { + this.vert.style.display = "block"; + this.vert.style.bottom = needsH ? sWidth + "px" : "0"; + var totalHeight = measure.viewHeight - (needsH ? sWidth : 0); + // A bug in IE8 can cause this value to be negative, so guard it. + this.vert.firstChild.style.height = + Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px"; + } else { + this.vert.style.display = ""; + this.vert.firstChild.style.height = "0"; + } + + if (needsH) { + this.horiz.style.display = "block"; + this.horiz.style.right = needsV ? sWidth + "px" : "0"; + this.horiz.style.left = measure.barLeft + "px"; + var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0); + this.horiz.firstChild.style.width = + (measure.scrollWidth - measure.clientWidth + totalWidth) + "px"; + } else { + this.horiz.style.display = ""; + this.horiz.firstChild.style.width = "0"; + } + + if (!this.checkedOverlay && measure.clientHeight > 0) { + if (sWidth == 0) this.overlayHack(); + this.checkedOverlay = true; + } + + return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0}; + }, + setScrollLeft: function(pos) { + if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos; + }, + setScrollTop: function(pos) { + if (this.vert.scrollTop != pos) this.vert.scrollTop = pos; + }, + overlayHack: function() { + var w = mac && !mac_geMountainLion ? "12px" : "18px"; + this.horiz.style.minHeight = this.vert.style.minWidth = w; + var self = this; + var barMouseDown = function(e) { + if (e_target(e) != self.vert && e_target(e) != self.horiz) + operation(self.cm, onMouseDown)(e); + }; + on(this.vert, "mousedown", barMouseDown); + on(this.horiz, "mousedown", barMouseDown); + }, + clear: function() { + var parent = this.horiz.parentNode; + parent.removeChild(this.horiz); + parent.removeChild(this.vert); + } + }, NativeScrollbars.prototype); + + function NullScrollbars() {} + + NullScrollbars.prototype = copyObj({ + update: function() { return {bottom: 0, right: 0}; }, + setScrollLeft: function() {}, + setScrollTop: function() {}, + clear: function() {} + }, NullScrollbars.prototype); + + CodeMirror.scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars}; + + function initScrollbars(cm) { + if (cm.display.scrollbars) { + cm.display.scrollbars.clear(); + if (cm.display.scrollbars.addClass) + rmClass(cm.display.wrapper, cm.display.scrollbars.addClass); + } + + cm.display.scrollbars = new CodeMirror.scrollbarModel[cm.options.scrollbarStyle](function(node) { + cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller); + // Prevent clicks in the scrollbars from killing focus + on(node, "mousedown", function() { + if (cm.state.focused) setTimeout(function() { cm.display.input.focus(); }, 0); + }); + node.setAttribute("cm-not-content", "true"); + }, function(pos, axis) { + if (axis == "horizontal") setScrollLeft(cm, pos); + else setScrollTop(cm, pos); + }, cm); + if (cm.display.scrollbars.addClass) + addClass(cm.display.wrapper, cm.display.scrollbars.addClass); + } + + function updateScrollbars(cm, measure) { + if (!measure) measure = measureForScrollbars(cm); + var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight; + updateScrollbarsInner(cm, measure); + for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) { + if (startWidth != cm.display.barWidth && cm.options.lineWrapping) + updateHeightsInViewport(cm); + updateScrollbarsInner(cm, measureForScrollbars(cm)); + startWidth = cm.display.barWidth; startHeight = cm.display.barHeight; + } + } + + // Re-synchronize the fake scrollbars with the actual size of the + // content. + function updateScrollbarsInner(cm, measure) { + var d = cm.display; + var sizes = d.scrollbars.update(measure); + + d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px"; + d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px"; + + if (sizes.right && sizes.bottom) { + d.scrollbarFiller.style.display = "block"; + d.scrollbarFiller.style.height = sizes.bottom + "px"; + d.scrollbarFiller.style.width = sizes.right + "px"; + } else d.scrollbarFiller.style.display = ""; + if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { + d.gutterFiller.style.display = "block"; + d.gutterFiller.style.height = sizes.bottom + "px"; + d.gutterFiller.style.width = measure.gutterWidth + "px"; + } else d.gutterFiller.style.display = ""; + } + + // Compute the lines that are visible in a given viewport (defaults + // the the current scroll position). viewport may contain top, + // height, and ensure (see op.scrollToPos) properties. + function visibleLines(display, doc, viewport) { + var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop; + top = Math.floor(top - paddingTop(display)); + var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight; + + var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom); + // Ensure is a {from: {line, ch}, to: {line, ch}} object, and + // forces those lines into the viewport (if possible). + if (viewport && viewport.ensure) { + var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line; + if (ensureFrom < from) { + from = ensureFrom; + to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight); + } else if (Math.min(ensureTo, doc.lastLine()) >= to) { + from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight); + to = ensureTo; + } + } + return {from: from, to: Math.max(to, from + 1)}; + } + + // LINE NUMBERS + + // Re-align line numbers and gutter marks to compensate for + // horizontal scrolling. + function alignHorizontally(cm) { + var display = cm.display, view = display.view; + if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return; + var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; + var gutterW = display.gutters.offsetWidth, left = comp + "px"; + for (var i = 0; i < view.length; i++) if (!view[i].hidden) { + if (cm.options.fixedGutter && view[i].gutter) + view[i].gutter.style.left = left; + var align = view[i].alignable; + if (align) for (var j = 0; j < align.length; j++) + align[j].style.left = left; + } + if (cm.options.fixedGutter) + display.gutters.style.left = (comp + gutterW) + "px"; + } + + // Used to ensure that the line number gutter is still the right + // size for the current document size. Returns true when an update + // is needed. + function maybeUpdateLineNumberWidth(cm) { + if (!cm.options.lineNumbers) return false; + var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; + if (last.length != display.lineNumChars) { + var test = display.measure.appendChild(elt("div", [elt("div", last)], + "CodeMirror-linenumber CodeMirror-gutter-elt")); + var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; + display.lineGutter.style.width = ""; + display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1; + display.lineNumWidth = display.lineNumInnerWidth + padding; + display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; + display.lineGutter.style.width = display.lineNumWidth + "px"; + updateGutterSpace(cm); + return true; + } + return false; + } + + function lineNumberFor(options, i) { + return String(options.lineNumberFormatter(i + options.firstLineNumber)); + } + + // Computes display.scroller.scrollLeft + display.gutters.offsetWidth, + // but using getBoundingClientRect to get a sub-pixel-accurate + // result. + function compensateForHScroll(display) { + return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left; + } + + // DISPLAY DRAWING + + function DisplayUpdate(cm, viewport, force) { + var display = cm.display; + + this.viewport = viewport; + // Store some values that we'll need later (but don't want to force a relayout for) + this.visible = visibleLines(display, cm.doc, viewport); + this.editorIsHidden = !display.wrapper.offsetWidth; + this.wrapperHeight = display.wrapper.clientHeight; + this.wrapperWidth = display.wrapper.clientWidth; + this.oldDisplayWidth = displayWidth(cm); + this.force = force; + this.dims = getDimensions(cm); + this.events = []; + } + + DisplayUpdate.prototype.signal = function(emitter, type) { + if (hasHandler(emitter, type)) + this.events.push(arguments); + }; + DisplayUpdate.prototype.finish = function() { + for (var i = 0; i < this.events.length; i++) + signal.apply(null, this.events[i]); + }; + + function maybeClipScrollbars(cm) { + var display = cm.display; + if (!display.scrollbarsClipped && display.scroller.offsetWidth) { + display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth; + display.heightForcer.style.height = scrollGap(cm) + "px"; + display.sizer.style.marginBottom = -display.nativeBarWidth + "px"; + display.sizer.style.borderRightWidth = scrollGap(cm) + "px"; + display.scrollbarsClipped = true; + } + } + + // Does the actual updating of the line display. Bails out + // (returning false) when there is nothing to be done and forced is + // false. + function updateDisplayIfNeeded(cm, update) { + var display = cm.display, doc = cm.doc; + + if (update.editorIsHidden) { + resetView(cm); + return false; + } + + // Bail out if the visible area is already rendered and nothing changed. + if (!update.force && + update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) && + display.renderedView == display.view && countDirtyView(cm) == 0) + return false; + + if (maybeUpdateLineNumberWidth(cm)) { + resetView(cm); + update.dims = getDimensions(cm); + } + + // Compute a suitable new viewport (from & to) + var end = doc.first + doc.size; + var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first); + var to = Math.min(end, update.visible.to + cm.options.viewportMargin); + if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom); + if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo); + if (sawCollapsedSpans) { + from = visualLineNo(cm.doc, from); + to = visualLineEndNo(cm.doc, to); + } + + var different = from != display.viewFrom || to != display.viewTo || + display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth; + adjustView(cm, from, to); + + display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)); + // Position the mover div to align with the current scroll position + cm.display.mover.style.top = display.viewOffset + "px"; + + var toUpdate = countDirtyView(cm); + if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo)) + return false; + + // For big changes, we hide the enclosing element during the + // update, since that speeds up the operations on most browsers. + var focused = activeElt(); + if (toUpdate > 4) display.lineDiv.style.display = "none"; + patchDisplay(cm, display.updateLineNumbers, update.dims); + if (toUpdate > 4) display.lineDiv.style.display = ""; + display.renderedView = display.view; + // There might have been a widget with a focused element that got + // hidden or updated, if so re-focus it. + if (focused && activeElt() != focused && focused.offsetHeight) focused.focus(); + + // Prevent selection and cursors from interfering with the scroll + // width and height. + removeChildren(display.cursorDiv); + removeChildren(display.selectionDiv); + display.gutters.style.height = 0; + + if (different) { + display.lastWrapHeight = update.wrapperHeight; + display.lastWrapWidth = update.wrapperWidth; + startWorker(cm, 400); + } + + display.updateLineNumbers = null; + + return true; + } + + function postUpdateDisplay(cm, update) { + var force = update.force, viewport = update.viewport; + for (var first = true;; first = false) { + if (first && cm.options.lineWrapping && update.oldDisplayWidth != displayWidth(cm)) { + force = true; + } else { + force = false; + // Clip forced viewport to actual scrollable area. + if (viewport && viewport.top != null) + viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)}; + // Updated line heights might result in the drawn area not + // actually covering the viewport. Keep looping until it does. + update.visible = visibleLines(cm.display, cm.doc, viewport); + if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo) + break; + } + if (!updateDisplayIfNeeded(cm, update)) break; + updateHeightsInViewport(cm); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + setDocumentHeight(cm, barMeasure); + updateScrollbars(cm, barMeasure); + } + + update.signal(cm, "update", cm); + if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) { + update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); + cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo; + } + } + + function updateDisplaySimple(cm, viewport) { + var update = new DisplayUpdate(cm, viewport); + if (updateDisplayIfNeeded(cm, update)) { + updateHeightsInViewport(cm); + postUpdateDisplay(cm, update); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + setDocumentHeight(cm, barMeasure); + updateScrollbars(cm, barMeasure); + update.finish(); + } + } + + function setDocumentHeight(cm, measure) { + cm.display.sizer.style.minHeight = measure.docHeight + "px"; + var total = measure.docHeight + cm.display.barHeight; + cm.display.heightForcer.style.top = total + "px"; + cm.display.gutters.style.height = Math.max(total + scrollGap(cm), measure.clientHeight) + "px"; + } + + // Read the actual heights of the rendered lines, and update their + // stored heights to match. + function updateHeightsInViewport(cm) { + var display = cm.display; + var prevBottom = display.lineDiv.offsetTop; + for (var i = 0; i < display.view.length; i++) { + var cur = display.view[i], height; + if (cur.hidden) continue; + if (ie && ie_version < 8) { + var bot = cur.node.offsetTop + cur.node.offsetHeight; + height = bot - prevBottom; + prevBottom = bot; + } else { + var box = cur.node.getBoundingClientRect(); + height = box.bottom - box.top; + } + var diff = cur.line.height - height; + if (height < 2) height = textHeight(display); + if (diff > .001 || diff < -.001) { + updateLineHeight(cur.line, height); + updateWidgetHeight(cur.line); + if (cur.rest) for (var j = 0; j < cur.rest.length; j++) + updateWidgetHeight(cur.rest[j]); + } + } + } + + // Read and store the height of line widgets associated with the + // given line. + function updateWidgetHeight(line) { + if (line.widgets) for (var i = 0; i < line.widgets.length; ++i) + line.widgets[i].height = line.widgets[i].node.offsetHeight; + } + + // Do a bulk-read of the DOM positions and sizes needed to draw the + // view, so that we don't interleave reading and writing to the DOM. + function getDimensions(cm) { + var d = cm.display, left = {}, width = {}; + var gutterLeft = d.gutters.clientLeft; + for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { + left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft; + width[cm.options.gutters[i]] = n.clientWidth; + } + return {fixedPos: compensateForHScroll(d), + gutterTotalWidth: d.gutters.offsetWidth, + gutterLeft: left, + gutterWidth: width, + wrapperWidth: d.wrapper.clientWidth}; + } + + // Sync the actual display DOM structure with display.view, removing + // nodes for lines that are no longer in view, and creating the ones + // that are not there yet, and updating the ones that are out of + // date. + function patchDisplay(cm, updateNumbersFrom, dims) { + var display = cm.display, lineNumbers = cm.options.lineNumbers; + var container = display.lineDiv, cur = container.firstChild; + + function rm(node) { + var next = node.nextSibling; + // Works around a throw-scroll bug in OS X Webkit + if (webkit && mac && cm.display.currentWheelTarget == node) + node.style.display = "none"; + else + node.parentNode.removeChild(node); + return next; + } + + var view = display.view, lineN = display.viewFrom; + // Loop over the elements in the view, syncing cur (the DOM nodes + // in display.lineDiv) with the view as we go. + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (lineView.hidden) { + } else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet + var node = buildLineElement(cm, lineView, lineN, dims); + container.insertBefore(node, cur); + } else { // Already drawn + while (cur != lineView.node) cur = rm(cur); + var updateNumber = lineNumbers && updateNumbersFrom != null && + updateNumbersFrom <= lineN && lineView.lineNumber; + if (lineView.changes) { + if (indexOf(lineView.changes, "gutter") > -1) updateNumber = false; + updateLineForChanges(cm, lineView, lineN, dims); + } + if (updateNumber) { + removeChildren(lineView.lineNumber); + lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN))); + } + cur = lineView.node.nextSibling; + } + lineN += lineView.size; + } + while (cur) cur = rm(cur); + } + + // When an aspect of a line changes, a string is added to + // lineView.changes. This updates the relevant part of the line's + // DOM structure. + function updateLineForChanges(cm, lineView, lineN, dims) { + for (var j = 0; j < lineView.changes.length; j++) { + var type = lineView.changes[j]; + if (type == "text") updateLineText(cm, lineView); + else if (type == "gutter") updateLineGutter(cm, lineView, lineN, dims); + else if (type == "class") updateLineClasses(lineView); + else if (type == "widget") updateLineWidgets(cm, lineView, dims); + } + lineView.changes = null; + } + + // Lines with gutter elements, widgets or a background class need to + // be wrapped, and have the extra elements added to the wrapper div + function ensureLineWrapped(lineView) { + if (lineView.node == lineView.text) { + lineView.node = elt("div", null, null, "position: relative"); + if (lineView.text.parentNode) + lineView.text.parentNode.replaceChild(lineView.node, lineView.text); + lineView.node.appendChild(lineView.text); + if (ie && ie_version < 8) lineView.node.style.zIndex = 2; + } + return lineView.node; + } + + function updateLineBackground(lineView) { + var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass; + if (cls) cls += " CodeMirror-linebackground"; + if (lineView.background) { + if (cls) lineView.background.className = cls; + else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; } + } else if (cls) { + var wrap = ensureLineWrapped(lineView); + lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild); + } + } + + // Wrapper around buildLineContent which will reuse the structure + // in display.externalMeasured when possible. + function getLineContent(cm, lineView) { + var ext = cm.display.externalMeasured; + if (ext && ext.line == lineView.line) { + cm.display.externalMeasured = null; + lineView.measure = ext.measure; + return ext.built; + } + return buildLineContent(cm, lineView); + } + + // Redraw the line's text. Interacts with the background and text + // classes because the mode may output tokens that influence these + // classes. + function updateLineText(cm, lineView) { + var cls = lineView.text.className; + var built = getLineContent(cm, lineView); + if (lineView.text == lineView.node) lineView.node = built.pre; + lineView.text.parentNode.replaceChild(built.pre, lineView.text); + lineView.text = built.pre; + if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) { + lineView.bgClass = built.bgClass; + lineView.textClass = built.textClass; + updateLineClasses(lineView); + } else if (cls) { + lineView.text.className = cls; + } + } + + function updateLineClasses(lineView) { + updateLineBackground(lineView); + if (lineView.line.wrapClass) + ensureLineWrapped(lineView).className = lineView.line.wrapClass; + else if (lineView.node != lineView.text) + lineView.node.className = ""; + var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass; + lineView.text.className = textClass || ""; + } + + function updateLineGutter(cm, lineView, lineN, dims) { + if (lineView.gutter) { + lineView.node.removeChild(lineView.gutter); + lineView.gutter = null; + } + var markers = lineView.line.gutterMarkers; + if (cm.options.lineNumbers || markers) { + var wrap = ensureLineWrapped(lineView); + var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", "left: " + + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + + "px; width: " + dims.gutterTotalWidth + "px"); + cm.display.input.setUneditable(gutterWrap); + wrap.insertBefore(gutterWrap, lineView.text); + if (lineView.line.gutterClass) + gutterWrap.className += " " + lineView.line.gutterClass; + if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) + lineView.lineNumber = gutterWrap.appendChild( + elt("div", lineNumberFor(cm.options, lineN), + "CodeMirror-linenumber CodeMirror-gutter-elt", + "left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: " + + cm.display.lineNumInnerWidth + "px")); + if (markers) for (var k = 0; k < cm.options.gutters.length; ++k) { + var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id]; + if (found) + gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " + + dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px")); + } + } + } + + function updateLineWidgets(cm, lineView, dims) { + if (lineView.alignable) lineView.alignable = null; + for (var node = lineView.node.firstChild, next; node; node = next) { + var next = node.nextSibling; + if (node.className == "CodeMirror-linewidget") + lineView.node.removeChild(node); + } + insertLineWidgets(cm, lineView, dims); + } + + // Build a line's DOM representation from scratch + function buildLineElement(cm, lineView, lineN, dims) { + var built = getLineContent(cm, lineView); + lineView.text = lineView.node = built.pre; + if (built.bgClass) lineView.bgClass = built.bgClass; + if (built.textClass) lineView.textClass = built.textClass; + + updateLineClasses(lineView); + updateLineGutter(cm, lineView, lineN, dims); + insertLineWidgets(cm, lineView, dims); + return lineView.node; + } + + // A lineView may contain multiple logical lines (when merged by + // collapsed spans). The widgets for all of them need to be drawn. + function insertLineWidgets(cm, lineView, dims) { + insertLineWidgetsFor(cm, lineView.line, lineView, dims, true); + if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) + insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false); + } + + function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) { + if (!line.widgets) return; + var wrap = ensureLineWrapped(lineView); + for (var i = 0, ws = line.widgets; i < ws.length; ++i) { + var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget"); + if (!widget.handleMouseEvents) node.setAttribute("cm-ignore-events", "true"); + positionLineWidget(widget, node, lineView, dims); + cm.display.input.setUneditable(node); + if (allowAbove && widget.above) + wrap.insertBefore(node, lineView.gutter || lineView.text); + else + wrap.appendChild(node); + signalLater(widget, "redraw"); + } + } + + function positionLineWidget(widget, node, lineView, dims) { + if (widget.noHScroll) { + (lineView.alignable || (lineView.alignable = [])).push(node); + var width = dims.wrapperWidth; + node.style.left = dims.fixedPos + "px"; + if (!widget.coverGutter) { + width -= dims.gutterTotalWidth; + node.style.paddingLeft = dims.gutterTotalWidth + "px"; + } + node.style.width = width + "px"; + } + if (widget.coverGutter) { + node.style.zIndex = 5; + node.style.position = "relative"; + if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px"; + } + } + + // POSITION OBJECT + + // A Pos instance represents a position within the text. + var Pos = CodeMirror.Pos = function(line, ch) { + if (!(this instanceof Pos)) return new Pos(line, ch); + this.line = line; this.ch = ch; + }; + + // Compare two positions, return 0 if they are the same, a negative + // number when a is less, and a positive number otherwise. + var cmp = CodeMirror.cmpPos = function(a, b) { return a.line - b.line || a.ch - b.ch; }; + + function copyPos(x) {return Pos(x.line, x.ch);} + function maxPos(a, b) { return cmp(a, b) < 0 ? b : a; } + function minPos(a, b) { return cmp(a, b) < 0 ? a : b; } + + // INPUT HANDLING + + function ensureFocus(cm) { + if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm); } + } + + function isReadOnly(cm) { + return cm.options.readOnly || cm.doc.cantEdit; + } + + // This will be set to an array of strings when copying, so that, + // when pasting, we know what kind of selections the copied text + // was made out of. + var lastCopied = null; + + function applyTextInput(cm, inserted, deleted, sel) { + var doc = cm.doc; + cm.display.shift = false; + if (!sel) sel = doc.sel; + + var textLines = splitLines(inserted), multiPaste = null; + // When pasing N lines into N selections, insert one line per selection + if (cm.state.pasteIncoming && sel.ranges.length > 1) { + if (lastCopied && lastCopied.join("\n") == inserted) + multiPaste = sel.ranges.length % lastCopied.length == 0 && map(lastCopied, splitLines); + else if (textLines.length == sel.ranges.length) + multiPaste = map(textLines, function(l) { return [l]; }); + } + + // Normal behavior is to insert the new text into every selection + for (var i = sel.ranges.length - 1; i >= 0; i--) { + var range = sel.ranges[i]; + var from = range.from(), to = range.to(); + if (range.empty()) { + if (deleted && deleted > 0) // Handle deletion + from = Pos(from.line, from.ch - deleted); + else if (cm.state.overwrite && !cm.state.pasteIncoming) // Handle overwrite + to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)); + } + var updateInput = cm.curOp.updateInput; + var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i % multiPaste.length] : textLines, + origin: cm.state.pasteIncoming ? "paste" : cm.state.cutIncoming ? "cut" : "+input"}; + makeChange(cm.doc, changeEvent); + signalLater(cm, "inputRead", cm, changeEvent); + // When an 'electric' character is inserted, immediately trigger a reindent + if (inserted && !cm.state.pasteIncoming && cm.options.electricChars && + cm.options.smartIndent && range.head.ch < 100 && + (!i || sel.ranges[i - 1].head.line != range.head.line)) { + var mode = cm.getModeAt(range.head); + var end = changeEnd(changeEvent); + if (mode.electricChars) { + for (var j = 0; j < mode.electricChars.length; j++) + if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) { + indentLine(cm, end.line, "smart"); + break; + } + } else if (mode.electricInput) { + if (mode.electricInput.test(getLine(doc, end.line).text.slice(0, end.ch))) + indentLine(cm, end.line, "smart"); + } + } + } + ensureCursorVisible(cm); + cm.curOp.updateInput = updateInput; + cm.curOp.typing = true; + cm.state.pasteIncoming = cm.state.cutIncoming = false; + } + + function copyableRanges(cm) { + var text = [], ranges = []; + for (var i = 0; i < cm.doc.sel.ranges.length; i++) { + var line = cm.doc.sel.ranges[i].head.line; + var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)}; + ranges.push(lineRange); + text.push(cm.getRange(lineRange.anchor, lineRange.head)); + } + return {text: text, ranges: ranges}; + } + + function disableBrowserMagic(field) { + field.setAttribute("autocorrect", "off"); + field.setAttribute("autocapitalize", "off"); + field.setAttribute("spellcheck", "false"); + } + + // TEXTAREA INPUT STYLE + + function TextareaInput(cm) { + this.cm = cm; + // See input.poll and input.reset + this.prevInput = ""; + + // Flag that indicates whether we expect input to appear real soon + // now (after some event like 'keypress' or 'input') and are + // polling intensively. + this.pollingFast = false; + // Self-resetting timeout for the poller + this.polling = new Delayed(); + // Tracks when input.reset has punted to just putting a short + // string into the textarea instead of the full selection. + this.inaccurateSelection = false; + // Used to work around IE issue with selection being forgotten when focus moves away from textarea + this.hasSelection = false; + }; + + function hiddenTextarea() { + var te = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none"); + var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); + // The textarea is kept positioned near the cursor to prevent the + // fact that it'll be scrolled into view on input from scrolling + // our fake cursor out of view. On webkit, when wrap=off, paste is + // very slow. So make the area wide instead. + if (webkit) te.style.width = "1000px"; + else te.setAttribute("wrap", "off"); + // If border: 0; -- iOS fails to open keyboard (issue #1287) + if (ios) te.style.border = "1px solid black"; + disableBrowserMagic(te); + return div; + } + + TextareaInput.prototype = copyObj({ + init: function(display) { + var input = this, cm = this.cm; + + // Wraps and hides input textarea + var div = this.wrapper = hiddenTextarea(); + // The semihidden textarea that is focused when the editor is + // focused, and receives input. + var te = this.textarea = div.firstChild; + display.wrapper.insertBefore(div, display.wrapper.firstChild); + + // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore) + if (ios) te.style.width = "0px"; + + on(te, "input", function() { + if (ie && ie_version >= 9 && input.hasSelection) input.hasSelection = null; + input.poll(); + }); + + on(te, "paste", function() { + // Workaround for webkit bug https://bugs.webkit.org/show_bug.cgi?id=90206 + // Add a char to the end of textarea before paste occur so that + // selection doesn't span to the end of textarea. + if (webkit && !cm.state.fakedLastChar && !(new Date - cm.state.lastMiddleDown < 200)) { + var start = te.selectionStart, end = te.selectionEnd; + te.value += "$"; + // The selection end needs to be set before the start, otherwise there + // can be an intermediate non-empty selection between the two, which + // can override the middle-click paste buffer on linux and cause the + // wrong thing to get pasted. + te.selectionEnd = end; + te.selectionStart = start; + cm.state.fakedLastChar = true; + } + cm.state.pasteIncoming = true; + input.fastPoll(); + }); + + function prepareCopyCut(e) { + if (cm.somethingSelected()) { + lastCopied = cm.getSelections(); + if (input.inaccurateSelection) { + input.prevInput = ""; + input.inaccurateSelection = false; + te.value = lastCopied.join("\n"); + selectInput(te); + } + } else if (!cm.options.lineWiseCopyCut) { + return; + } else { + var ranges = copyableRanges(cm); + lastCopied = ranges.text; + if (e.type == "cut") { + cm.setSelections(ranges.ranges, null, sel_dontScroll); + } else { + input.prevInput = ""; + te.value = ranges.text.join("\n"); + selectInput(te); + } + } + if (e.type == "cut") cm.state.cutIncoming = true; + } + on(te, "cut", prepareCopyCut); + on(te, "copy", prepareCopyCut); + + on(display.scroller, "paste", function(e) { + if (eventInWidget(display, e)) return; + cm.state.pasteIncoming = true; + input.focus(); + }); + + // Prevent normal selection in the editor (we handle our own) + on(display.lineSpace, "selectstart", function(e) { + if (!eventInWidget(display, e)) e_preventDefault(e); + }); + }, + + prepareSelection: function() { + // Redraw the selection and/or cursor + var cm = this.cm, display = cm.display, doc = cm.doc; + var result = prepareSelection(cm); + + // Move the hidden textarea near the cursor to prevent scrolling artifacts + if (cm.options.moveInputWithCursor) { + var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); + var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); + result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, + headPos.top + lineOff.top - wrapOff.top)); + result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, + headPos.left + lineOff.left - wrapOff.left)); + } + + return result; + }, + + showSelection: function(drawn) { + var cm = this.cm, display = cm.display; + removeChildrenAndAdd(display.cursorDiv, drawn.cursors); + removeChildrenAndAdd(display.selectionDiv, drawn.selection); + if (drawn.teTop != null) { + this.wrapper.style.top = drawn.teTop + "px"; + this.wrapper.style.left = drawn.teLeft + "px"; + } + }, + + // Reset the input to correspond to the selection (or to be empty, + // when not typing and nothing is selected) + reset: function(typing) { + if (this.contextMenuPending) return; + var minimal, selected, cm = this.cm, doc = cm.doc; + if (cm.somethingSelected()) { + this.prevInput = ""; + var range = doc.sel.primary(); + minimal = hasCopyEvent && + (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000); + var content = minimal ? "-" : selected || cm.getSelection(); + this.textarea.value = content; + if (cm.state.focused) selectInput(this.textarea); + if (ie && ie_version >= 9) this.hasSelection = content; + } else if (!typing) { + this.prevInput = this.textarea.value = ""; + if (ie && ie_version >= 9) this.hasSelection = null; + } + this.inaccurateSelection = minimal; + }, + + getField: function() { return this.textarea; }, + + supportsTouch: function() { return false; }, + + focus: function() { + if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { + try { this.textarea.focus(); } + catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM + } + }, + + blur: function() { this.textarea.blur(); }, + + resetPosition: function() { + this.wrapper.style.top = this.wrapper.style.left = 0; + }, + + receivedFocus: function() { this.slowPoll(); }, + + // Poll for input changes, using the normal rate of polling. This + // runs as long as the editor is focused. + slowPoll: function() { + var input = this; + if (input.pollingFast) return; + input.polling.set(this.cm.options.pollInterval, function() { + input.poll(); + if (input.cm.state.focused) input.slowPoll(); + }); + }, + + // When an event has just come in that is likely to add or change + // something in the input textarea, we poll faster, to ensure that + // the change appears on the screen quickly. + fastPoll: function() { + var missed = false, input = this; + input.pollingFast = true; + function p() { + var changed = input.poll(); + if (!changed && !missed) {missed = true; input.polling.set(60, p);} + else {input.pollingFast = false; input.slowPoll();} + } + input.polling.set(20, p); + }, + + // Read input from the textarea, and update the document to match. + // When something is selected, it is present in the textarea, and + // selected (unless it is huge, in which case a placeholder is + // used). When nothing is selected, the cursor sits after previously + // seen text (can be empty), which is stored in prevInput (we must + // not reset the textarea when typing, because that breaks IME). + poll: function() { + var cm = this.cm, input = this.textarea, prevInput = this.prevInput; + // Since this is called a *lot*, try to bail out as cheaply as + // possible when it is clear that nothing happened. hasSelection + // will be the case when there is a lot of text in the textarea, + // in which case reading its value would be expensive. + if (!cm.state.focused || (hasSelection(input) && !prevInput) || + isReadOnly(cm) || cm.options.disableInput || cm.state.keySeq) + return false; + // See paste handler for more on the fakedLastChar kludge + if (cm.state.pasteIncoming && cm.state.fakedLastChar) { + input.value = input.value.substring(0, input.value.length - 1); + cm.state.fakedLastChar = false; + } + var text = input.value; + // If nothing changed, bail. + if (text == prevInput && !cm.somethingSelected()) return false; + // Work around nonsensical selection resetting in IE9/10, and + // inexplicable appearance of private area unicode characters on + // some key combos in Mac (#2689). + if (ie && ie_version >= 9 && this.hasSelection === text || + mac && /[\uf700-\uf7ff]/.test(text)) { + cm.display.input.reset(); + return false; + } + + if (cm.doc.sel == cm.display.selForContextMenu) { + var first = text.charCodeAt(0); + if (first == 0x200b && !prevInput) prevInput = "\u200b"; + if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo"); } + } + // Find the part of the input that is actually new + var same = 0, l = Math.min(prevInput.length, text.length); + while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same; + + var self = this; + runInOp(cm, function() { + applyTextInput(cm, text.slice(same), prevInput.length - same); + + // Don't leave long text in the textarea, since it makes further polling slow + if (text.length > 1000 || text.indexOf("\n") > -1) input.value = self.prevInput = ""; + else self.prevInput = text; + }); + return true; + }, + + ensurePolled: function() { + if (this.pollingFast && this.poll()) this.pollingFast = false; + }, + + onKeyPress: function() { + if (ie && ie_version >= 9) this.hasSelection = null; + this.fastPoll(); + }, + + onContextMenu: function(e) { + var input = this, cm = input.cm, display = cm.display, te = input.textarea; + var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; + if (!pos || presto) return; // Opera is difficult. + + // Reset the current text selection only if the click is done outside of the selection + // and 'resetSelectionOnContextMenu' option is true. + var reset = cm.options.resetSelectionOnContextMenu; + if (reset && cm.doc.sel.contains(pos) == -1) + operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll); + + var oldCSS = te.style.cssText; + input.wrapper.style.position = "absolute"; + te.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + + "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: " + + (ie ? "rgba(255, 255, 255, .05)" : "transparent") + + "; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; + if (webkit) var oldScrollY = window.scrollY; // Work around Chrome issue (#2712) + display.input.focus(); + if (webkit) window.scrollTo(null, oldScrollY); + display.input.reset(); + // Adds "Select all" to context menu in FF + if (!cm.somethingSelected()) te.value = input.prevInput = " "; + input.contextMenuPending = true; + display.selForContextMenu = cm.doc.sel; + clearTimeout(display.detectingSelectAll); + + // Select-all will be greyed out if there's nothing to select, so + // this adds a zero-width space so that we can later check whether + // it got selected. + function prepareSelectAllHack() { + if (te.selectionStart != null) { + var selected = cm.somethingSelected(); + var extval = "\u200b" + (selected ? te.value : ""); + te.value = "\u21da"; // Used to catch context-menu undo + te.value = extval; + input.prevInput = selected ? "" : "\u200b"; + te.selectionStart = 1; te.selectionEnd = extval.length; + // Re-set this, in case some other handler touched the + // selection in the meantime. + display.selForContextMenu = cm.doc.sel; + } + } + function rehide() { + input.contextMenuPending = false; + input.wrapper.style.position = "relative"; + te.style.cssText = oldCSS; + if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos); + + // Try to detect the user choosing select-all + if (te.selectionStart != null) { + if (!ie || (ie && ie_version < 9)) prepareSelectAllHack(); + var i = 0, poll = function() { + if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 && + te.selectionEnd > 0 && input.prevInput == "\u200b") + operation(cm, commands.selectAll)(cm); + else if (i++ < 10) display.detectingSelectAll = setTimeout(poll, 500); + else display.input.reset(); + }; + display.detectingSelectAll = setTimeout(poll, 200); + } + } + + if (ie && ie_version >= 9) prepareSelectAllHack(); + if (captureRightClick) { + e_stop(e); + var mouseup = function() { + off(window, "mouseup", mouseup); + setTimeout(rehide, 20); + }; + on(window, "mouseup", mouseup); + } else { + setTimeout(rehide, 50); + } + }, + + setUneditable: nothing, + + needsContentAttribute: false + }, TextareaInput.prototype); + + // CONTENTEDITABLE INPUT STYLE + + function ContentEditableInput(cm) { + this.cm = cm; + this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null; + this.polling = new Delayed(); + this.gracePeriod = false; + } + + ContentEditableInput.prototype = copyObj({ + init: function(display) { + var input = this, cm = input.cm; + var div = input.div = display.lineDiv; + div.contentEditable = "true"; + disableBrowserMagic(div); + + on(div, "paste", function(e) { + var pasted = e.clipboardData && e.clipboardData.getData("text/plain"); + if (pasted) { + e.preventDefault(); + cm.replaceSelection(pasted, null, "paste"); + } + }); + + on(div, "compositionstart", function(e) { + var data = e.data; + input.composing = {sel: cm.doc.sel, data: data, startData: data}; + if (!data) return; + var prim = cm.doc.sel.primary(); + var line = cm.getLine(prim.head.line); + var found = line.indexOf(data, Math.max(0, prim.head.ch - data.length)); + if (found > -1 && found <= prim.head.ch) + input.composing.sel = simpleSelection(Pos(prim.head.line, found), + Pos(prim.head.line, found + data.length)); + }); + on(div, "compositionupdate", function(e) { + input.composing.data = e.data; + }); + on(div, "compositionend", function(e) { + var ours = input.composing; + if (!ours) return; + if (e.data != ours.startData && !/\u200b/.test(e.data)) + ours.data = e.data; + // Need a small delay to prevent other code (input event, + // selection polling) from doing damage when fired right after + // compositionend. + setTimeout(function() { + if (!ours.handled) + input.applyComposition(ours); + if (input.composing == ours) + input.composing = null; + }, 50); + }); + + on(div, "touchstart", function() { + input.forceCompositionEnd(); + }); + + on(div, "input", function() { + if (input.composing) return; + if (!input.pollContent()) + runInOp(input.cm, function() {regChange(cm);}); + }); + + function onCopyCut(e) { + if (cm.somethingSelected()) { + lastCopied = cm.getSelections(); + if (e.type == "cut") cm.replaceSelection("", null, "cut"); + } else if (!cm.options.lineWiseCopyCut) { + return; + } else { + var ranges = copyableRanges(cm); + lastCopied = ranges.text; + if (e.type == "cut") { + cm.operation(function() { + cm.setSelections(ranges.ranges, 0, sel_dontScroll); + cm.replaceSelection("", null, "cut"); + }); + } + } + // iOS exposes the clipboard API, but seems to discard content inserted into it + if (e.clipboardData && !ios) { + e.preventDefault(); + e.clipboardData.clearData(); + e.clipboardData.setData("text/plain", lastCopied.join("\n")); + } else { + // Old-fashioned briefly-focus-a-textarea hack + var kludge = hiddenTextarea(), te = kludge.firstChild; + cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild); + te.value = lastCopied.join("\n"); + var hadFocus = document.activeElement; + selectInput(te); + setTimeout(function() { + cm.display.lineSpace.removeChild(kludge); + hadFocus.focus(); + }, 50); + } + } + on(div, "copy", onCopyCut); + on(div, "cut", onCopyCut); + }, + + prepareSelection: function() { + var result = prepareSelection(this.cm, false); + result.focus = this.cm.state.focused; + return result; + }, + + showSelection: function(info) { + if (!info || !this.cm.display.view.length) return; + if (info.focus) this.showPrimarySelection(); + this.showMultipleSelections(info); + }, + + showPrimarySelection: function() { + var sel = window.getSelection(), prim = this.cm.doc.sel.primary(); + var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset); + var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset); + if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad && + cmp(minPos(curAnchor, curFocus), prim.from()) == 0 && + cmp(maxPos(curAnchor, curFocus), prim.to()) == 0) + return; + + var start = posToDOM(this.cm, prim.from()); + var end = posToDOM(this.cm, prim.to()); + if (!start && !end) return; + + var view = this.cm.display.view; + var old = sel.rangeCount && sel.getRangeAt(0); + if (!start) { + start = {node: view[0].measure.map[2], offset: 0}; + } else if (!end) { // FIXME dangerously hacky + var measure = view[view.length - 1].measure; + var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map; + end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]}; + } + + try { var rng = range(start.node, start.offset, end.offset, end.node); } + catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible + if (rng) { + sel.removeAllRanges(); + sel.addRange(rng); + if (old && sel.anchorNode == null) sel.addRange(old); + else if (gecko) this.startGracePeriod(); + } + this.rememberSelection(); + }, + + startGracePeriod: function() { + var input = this; + clearTimeout(this.gracePeriod); + this.gracePeriod = setTimeout(function() { + input.gracePeriod = false; + if (input.selectionChanged()) + input.cm.operation(function() { input.cm.curOp.selectionChanged = true; }); + }, 20); + }, + + showMultipleSelections: function(info) { + removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors); + removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection); + }, + + rememberSelection: function() { + var sel = window.getSelection(); + this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset; + this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset; + }, + + selectionInEditor: function() { + var sel = window.getSelection(); + if (!sel.rangeCount) return false; + var node = sel.getRangeAt(0).commonAncestorContainer; + return contains(this.div, node); + }, + + focus: function() { + if (this.cm.options.readOnly != "nocursor") this.div.focus(); + }, + blur: function() { this.div.blur(); }, + getField: function() { return this.div; }, + + supportsTouch: function() { return true; }, + + receivedFocus: function() { + var input = this; + if (this.selectionInEditor()) + this.pollSelection(); + else + runInOp(this.cm, function() { input.cm.curOp.selectionChanged = true; }); + + function poll() { + if (input.cm.state.focused) { + input.pollSelection(); + input.polling.set(input.cm.options.pollInterval, poll); + } + } + this.polling.set(this.cm.options.pollInterval, poll); + }, + + selectionChanged: function() { + var sel = window.getSelection(); + return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || + sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset; + }, + + pollSelection: function() { + if (!this.composing && !this.gracePeriod && this.selectionChanged()) { + var sel = window.getSelection(), cm = this.cm; + this.rememberSelection(); + var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); + var head = domToPos(cm, sel.focusNode, sel.focusOffset); + if (anchor && head) runInOp(cm, function() { + setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll); + if (anchor.bad || head.bad) cm.curOp.selectionChanged = true; + }); + } + }, + + pollContent: function() { + var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary(); + var from = sel.from(), to = sel.to(); + if (from.line < display.viewFrom || to.line > display.viewTo - 1) return false; + + var fromIndex; + if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) { + var fromLine = lineNo(display.view[0].line); + var fromNode = display.view[0].node; + } else { + var fromLine = lineNo(display.view[fromIndex].line); + var fromNode = display.view[fromIndex - 1].node.nextSibling; + } + var toIndex = findViewIndex(cm, to.line); + if (toIndex == display.view.length - 1) { + var toLine = display.viewTo - 1; + var toNode = display.view[toIndex].node; + } else { + var toLine = lineNo(display.view[toIndex + 1].line) - 1; + var toNode = display.view[toIndex + 1].node.previousSibling; + } + + var newText = splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine)); + var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length)); + while (newText.length > 1 && oldText.length > 1) { + if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; } + else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; } + else break; + } + + var cutFront = 0, cutEnd = 0; + var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length); + while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront)) + ++cutFront; + var newBot = lst(newText), oldBot = lst(oldText); + var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0), + oldBot.length - (oldText.length == 1 ? cutFront : 0)); + while (cutEnd < maxCutEnd && + newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) + ++cutEnd; + + newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd); + newText[0] = newText[0].slice(cutFront); + + var chFrom = Pos(fromLine, cutFront); + var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0); + if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) { + replaceRange(cm.doc, newText, chFrom, chTo, "+input"); + return true; + } + }, + + ensurePolled: function() { + this.forceCompositionEnd(); + }, + reset: function() { + this.forceCompositionEnd(); + }, + forceCompositionEnd: function() { + if (!this.composing || this.composing.handled) return; + this.applyComposition(this.composing); + this.composing.handled = true; + this.div.blur(); + this.div.focus(); + }, + applyComposition: function(composing) { + if (composing.data && composing.data != composing.startData) + operation(this.cm, applyTextInput)(this.cm, composing.data, 0, composing.sel); + }, + + setUneditable: function(node) { + node.setAttribute("contenteditable", "false"); + }, + + onKeyPress: function(e) { + e.preventDefault(); + operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); + }, + + onContextMenu: nothing, + resetPosition: nothing, + + needsContentAttribute: true + }, ContentEditableInput.prototype); + + function posToDOM(cm, pos) { + var view = findViewForLine(cm, pos.line); + if (!view || view.hidden) return null; + var line = getLine(cm.doc, pos.line); + var info = mapFromLineView(view, line, pos.line); + + var order = getOrder(line), side = "left"; + if (order) { + var partPos = getBidiPartAt(order, pos.ch); + side = partPos % 2 ? "right" : "left"; + } + var result = nodeAndOffsetInLineMap(info.map, pos.ch, "left"); + result.offset = result.collapse == "right" ? result.end : result.start; + return result; + } + + function badPos(pos, bad) { if (bad) pos.bad = true; return pos; } + + function domToPos(cm, node, offset) { + var lineNode; + if (node == cm.display.lineDiv) { + lineNode = cm.display.lineDiv.childNodes[offset]; + if (!lineNode) return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true); + node = null; offset = 0; + } else { + for (lineNode = node;; lineNode = lineNode.parentNode) { + if (!lineNode || lineNode == cm.display.lineDiv) return null; + if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) break; + } + } + for (var i = 0; i < cm.display.view.length; i++) { + var lineView = cm.display.view[i]; + if (lineView.node == lineNode) + return locateNodeInLineView(lineView, node, offset); + } + } + + function locateNodeInLineView(lineView, node, offset) { + var wrapper = lineView.text.firstChild, bad = false; + if (!node || !contains(wrapper, node)) return badPos(Pos(lineNo(lineView.line), 0), true); + if (node == wrapper) { + bad = true; + node = wrapper.childNodes[offset]; + offset = 0; + if (!node) { + var line = lineView.rest ? lst(lineView.rest) : lineView.line; + return badPos(Pos(lineNo(line), line.text.length), bad); + } + } + + var textNode = node.nodeType == 3 ? node : null, topNode = node; + if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) { + textNode = node.firstChild; + if (offset) offset = textNode.nodeValue.length; + } + while (topNode.parentNode != wrapper) topNode = topNode.parentNode; + var measure = lineView.measure, maps = measure.maps; + + function find(textNode, topNode, offset) { + for (var i = -1; i < (maps ? maps.length : 0); i++) { + var map = i < 0 ? measure.map : maps[i]; + for (var j = 0; j < map.length; j += 3) { + var curNode = map[j + 2]; + if (curNode == textNode || curNode == topNode) { + var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]); + var ch = map[j] + offset; + if (offset < 0 || curNode != textNode) ch = map[j + (offset ? 1 : 0)]; + return Pos(line, ch); + } + } + } + } + var found = find(textNode, topNode, offset); + if (found) return badPos(found, bad); + + // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems + for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) { + found = find(after, after.firstChild, 0); + if (found) + return badPos(Pos(found.line, found.ch - dist), bad); + else + dist += after.textContent.length; + } + for (var before = topNode.previousSibling, dist = offset; before; before = before.previousSibling) { + found = find(before, before.firstChild, -1); + if (found) + return badPos(Pos(found.line, found.ch + dist), bad); + else + dist += after.textContent.length; + } + } + + function domTextBetween(cm, from, to, fromLine, toLine) { + var text = "", closing = false; + function recognizeMarker(id) { return function(marker) { return marker.id == id; }; } + function walk(node) { + if (node.nodeType == 1) { + var cmText = node.getAttribute("cm-text"); + if (cmText != null) { + if (cmText == "") cmText = node.textContent.replace(/\u200b/g, ""); + text += cmText; + return; + } + var markerID = node.getAttribute("cm-marker"), range; + if (markerID) { + var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID)); + if (found.length && (range = found[0].find())) + text += getBetween(cm.doc, range.from, range.to).join("\n"); + return; + } + if (node.getAttribute("contenteditable") == "false") return; + for (var i = 0; i < node.childNodes.length; i++) + walk(node.childNodes[i]); + if (/^(pre|div|p)$/i.test(node.nodeName)) + closing = true; + } else if (node.nodeType == 3) { + var val = node.nodeValue; + if (!val) return; + if (closing) { + text += "\n"; + closing = false; + } + text += val; + } + } + for (;;) { + walk(from); + if (from == to) break; + from = from.nextSibling; + } + return text; + } + + CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; + + // SELECTION / CURSOR + + // Selection objects are immutable. A new one is created every time + // the selection changes. A selection is one or more non-overlapping + // (and non-touching) ranges, sorted, and an integer that indicates + // which one is the primary selection (the one that's scrolled into + // view, that getCursor returns, etc). + function Selection(ranges, primIndex) { + this.ranges = ranges; + this.primIndex = primIndex; + } + + Selection.prototype = { + primary: function() { return this.ranges[this.primIndex]; }, + equals: function(other) { + if (other == this) return true; + if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) return false; + for (var i = 0; i < this.ranges.length; i++) { + var here = this.ranges[i], there = other.ranges[i]; + if (cmp(here.anchor, there.anchor) != 0 || cmp(here.head, there.head) != 0) return false; + } + return true; + }, + deepCopy: function() { + for (var out = [], i = 0; i < this.ranges.length; i++) + out[i] = new Range(copyPos(this.ranges[i].anchor), copyPos(this.ranges[i].head)); + return new Selection(out, this.primIndex); + }, + somethingSelected: function() { + for (var i = 0; i < this.ranges.length; i++) + if (!this.ranges[i].empty()) return true; + return false; + }, + contains: function(pos, end) { + if (!end) end = pos; + for (var i = 0; i < this.ranges.length; i++) { + var range = this.ranges[i]; + if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) + return i; + } + return -1; + } + }; + + function Range(anchor, head) { + this.anchor = anchor; this.head = head; + } + + Range.prototype = { + from: function() { return minPos(this.anchor, this.head); }, + to: function() { return maxPos(this.anchor, this.head); }, + empty: function() { + return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch; + } + }; + + // Take an unsorted, potentially overlapping set of ranges, and + // build a selection out of it. 'Consumes' ranges array (modifying + // it). + function normalizeSelection(ranges, primIndex) { + var prim = ranges[primIndex]; + ranges.sort(function(a, b) { return cmp(a.from(), b.from()); }); + primIndex = indexOf(ranges, prim); + for (var i = 1; i < ranges.length; i++) { + var cur = ranges[i], prev = ranges[i - 1]; + if (cmp(prev.to(), cur.from()) >= 0) { + var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to()); + var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head; + if (i <= primIndex) --primIndex; + ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to)); + } + } + return new Selection(ranges, primIndex); + } + + function simpleSelection(anchor, head) { + return new Selection([new Range(anchor, head || anchor)], 0); + } + + // Most of the external API clips given positions to make sure they + // actually exist within the document. + function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));} + function clipPos(doc, pos) { + if (pos.line < doc.first) return Pos(doc.first, 0); + var last = doc.first + doc.size - 1; + if (pos.line > last) return Pos(last, getLine(doc, last).text.length); + return clipToLen(pos, getLine(doc, pos.line).text.length); + } + function clipToLen(pos, linelen) { + var ch = pos.ch; + if (ch == null || ch > linelen) return Pos(pos.line, linelen); + else if (ch < 0) return Pos(pos.line, 0); + else return pos; + } + function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;} + function clipPosArray(doc, array) { + for (var out = [], i = 0; i < array.length; i++) out[i] = clipPos(doc, array[i]); + return out; + } + + // SELECTION UPDATES + + // The 'scroll' parameter given to many of these indicated whether + // the new cursor position should be scrolled into view after + // modifying the selection. + + // If shift is held or the extend flag is set, extends a range to + // include a given position (and optionally a second position). + // Otherwise, simply returns the range between the given positions. + // Used for cursor motion and such. + function extendRange(doc, range, head, other) { + if (doc.cm && doc.cm.display.shift || doc.extend) { + var anchor = range.anchor; + if (other) { + var posBefore = cmp(head, anchor) < 0; + if (posBefore != (cmp(other, anchor) < 0)) { + anchor = head; + head = other; + } else if (posBefore != (cmp(head, other) < 0)) { + head = other; + } + } + return new Range(anchor, head); + } else { + return new Range(other || head, head); + } + } + + // Extend the primary selection range, discard the rest. + function extendSelection(doc, head, other, options) { + setSelection(doc, new Selection([extendRange(doc, doc.sel.primary(), head, other)], 0), options); + } + + // Extend all selections (pos is an array of selections with length + // equal the number of selections) + function extendSelections(doc, heads, options) { + for (var out = [], i = 0; i < doc.sel.ranges.length; i++) + out[i] = extendRange(doc, doc.sel.ranges[i], heads[i], null); + var newSel = normalizeSelection(out, doc.sel.primIndex); + setSelection(doc, newSel, options); + } + + // Updates a single range in the selection. + function replaceOneSelection(doc, i, range, options) { + var ranges = doc.sel.ranges.slice(0); + ranges[i] = range; + setSelection(doc, normalizeSelection(ranges, doc.sel.primIndex), options); + } + + // Reset the selection to a single range. + function setSimpleSelection(doc, anchor, head, options) { + setSelection(doc, simpleSelection(anchor, head), options); + } + + // Give beforeSelectionChange handlers a change to influence a + // selection update. + function filterSelectionChange(doc, sel) { + var obj = { + ranges: sel.ranges, + update: function(ranges) { + this.ranges = []; + for (var i = 0; i < ranges.length; i++) + this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), + clipPos(doc, ranges[i].head)); + } + }; + signal(doc, "beforeSelectionChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj); + if (obj.ranges != sel.ranges) return normalizeSelection(obj.ranges, obj.ranges.length - 1); + else return sel; + } + + function setSelectionReplaceHistory(doc, sel, options) { + var done = doc.history.done, last = lst(done); + if (last && last.ranges) { + done[done.length - 1] = sel; + setSelectionNoUndo(doc, sel, options); + } else { + setSelection(doc, sel, options); + } + } + + // Set a new selection. + function setSelection(doc, sel, options) { + setSelectionNoUndo(doc, sel, options); + addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options); + } + + function setSelectionNoUndo(doc, sel, options) { + if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) + sel = filterSelectionChange(doc, sel); + + var bias = options && options.bias || + (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1); + setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)); + + if (!(options && options.scroll === false) && doc.cm) + ensureCursorVisible(doc.cm); + } + + function setSelectionInner(doc, sel) { + if (sel.equals(doc.sel)) return; + + doc.sel = sel; + + if (doc.cm) { + doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = true; + signalCursorActivity(doc.cm); + } + signalLater(doc, "cursorActivity", doc); + } + + // Verify that the selection does not partially select any atomic + // marked ranges. + function reCheckSelection(doc) { + setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false), sel_dontScroll); + } + + // Return a selection that does not partially select any atomic + // ranges. + function skipAtomicInSelection(doc, sel, bias, mayClear) { + var out; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + var newAnchor = skipAtomic(doc, range.anchor, bias, mayClear); + var newHead = skipAtomic(doc, range.head, bias, mayClear); + if (out || newAnchor != range.anchor || newHead != range.head) { + if (!out) out = sel.ranges.slice(0, i); + out[i] = new Range(newAnchor, newHead); + } + } + return out ? normalizeSelection(out, sel.primIndex) : sel; + } + + // Ensure a given position is not inside an atomic range. + function skipAtomic(doc, pos, bias, mayClear) { + var flipped = false, curPos = pos; + var dir = bias || 1; + doc.cantEdit = false; + search: for (;;) { + var line = getLine(doc, curPos.line); + if (line.markedSpans) { + for (var i = 0; i < line.markedSpans.length; ++i) { + var sp = line.markedSpans[i], m = sp.marker; + if ((sp.from == null || (m.inclusiveLeft ? sp.from <= curPos.ch : sp.from < curPos.ch)) && + (sp.to == null || (m.inclusiveRight ? sp.to >= curPos.ch : sp.to > curPos.ch))) { + if (mayClear) { + signal(m, "beforeCursorEnter"); + if (m.explicitlyCleared) { + if (!line.markedSpans) break; + else {--i; continue;} + } + } + if (!m.atomic) continue; + var newPos = m.find(dir < 0 ? -1 : 1); + if (cmp(newPos, curPos) == 0) { + newPos.ch += dir; + if (newPos.ch < 0) { + if (newPos.line > doc.first) newPos = clipPos(doc, Pos(newPos.line - 1)); + else newPos = null; + } else if (newPos.ch > line.text.length) { + if (newPos.line < doc.first + doc.size - 1) newPos = Pos(newPos.line + 1, 0); + else newPos = null; + } + if (!newPos) { + if (flipped) { + // Driven in a corner -- no valid cursor position found at all + // -- try again *with* clearing, if we didn't already + if (!mayClear) return skipAtomic(doc, pos, bias, true); + // Otherwise, turn off editing until further notice, and return the start of the doc + doc.cantEdit = true; + return Pos(doc.first, 0); + } + flipped = true; newPos = pos; dir = -dir; + } + } + curPos = newPos; + continue search; + } + } + } + return curPos; + } + } + + // SELECTION DRAWING + + function updateSelection(cm) { + cm.display.input.showSelection(cm.display.input.prepareSelection()); + } + + function prepareSelection(cm, primary) { + var doc = cm.doc, result = {}; + var curFragment = result.cursors = document.createDocumentFragment(); + var selFragment = result.selection = document.createDocumentFragment(); + + for (var i = 0; i < doc.sel.ranges.length; i++) { + if (primary === false && i == doc.sel.primIndex) continue; + var range = doc.sel.ranges[i]; + var collapsed = range.empty(); + if (collapsed || cm.options.showCursorWhenSelecting) + drawSelectionCursor(cm, range, curFragment); + if (!collapsed) + drawSelectionRange(cm, range, selFragment); + } + return result; + } + + // Draws a cursor for the given range + function drawSelectionCursor(cm, range, output) { + var pos = cursorCoords(cm, range.head, "div", null, null, !cm.options.singleCursorHeightPerLine); + + var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")); + cursor.style.left = pos.left + "px"; + cursor.style.top = pos.top + "px"; + cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; + + if (pos.other) { + // Secondary cursor, shown when on a 'jump' in bi-directional text + var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor")); + otherCursor.style.display = ""; + otherCursor.style.left = pos.other.left + "px"; + otherCursor.style.top = pos.other.top + "px"; + otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; + } + } + + // Draws the given range as a highlighted selection + function drawSelectionRange(cm, range, output) { + var display = cm.display, doc = cm.doc; + var fragment = document.createDocumentFragment(); + var padding = paddingH(cm.display), leftSide = padding.left; + var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right; + + function add(left, top, width, bottom) { + if (top < 0) top = 0; + top = Math.round(top); + bottom = Math.round(bottom); + fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left + + "px; top: " + top + "px; width: " + (width == null ? rightSide - left : width) + + "px; height: " + (bottom - top) + "px")); + } + + function drawForLine(line, fromArg, toArg) { + var lineObj = getLine(doc, line); + var lineLen = lineObj.text.length; + var start, end; + function coords(ch, bias) { + return charCoords(cm, Pos(line, ch), "div", lineObj, bias); + } + + iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) { + var leftPos = coords(from, "left"), rightPos, left, right; + if (from == to) { + rightPos = leftPos; + left = right = leftPos.left; + } else { + rightPos = coords(to - 1, "right"); + if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; } + left = leftPos.left; + right = rightPos.right; + } + if (fromArg == null && from == 0) left = leftSide; + if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part + add(left, leftPos.top, null, leftPos.bottom); + left = leftSide; + if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top); + } + if (toArg == null && to == lineLen) right = rightSide; + if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left) + start = leftPos; + if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right) + end = rightPos; + if (left < leftSide + 1) left = leftSide; + add(left, rightPos.top, right - left, rightPos.bottom); + }); + return {start: start, end: end}; + } + + var sFrom = range.from(), sTo = range.to(); + if (sFrom.line == sTo.line) { + drawForLine(sFrom.line, sFrom.ch, sTo.ch); + } else { + var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line); + var singleVLine = visualLine(fromLine) == visualLine(toLine); + var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end; + var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start; + if (singleVLine) { + if (leftEnd.top < rightStart.top - 2) { + add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); + add(leftSide, rightStart.top, rightStart.left, rightStart.bottom); + } else { + add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); + } + } + if (leftEnd.bottom < rightStart.top) + add(leftSide, leftEnd.bottom, null, rightStart.top); + } + + output.appendChild(fragment); + } + + // Cursor-blinking + function restartBlink(cm) { + if (!cm.state.focused) return; + var display = cm.display; + clearInterval(display.blinker); + var on = true; + display.cursorDiv.style.visibility = ""; + if (cm.options.cursorBlinkRate > 0) + display.blinker = setInterval(function() { + display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden"; + }, cm.options.cursorBlinkRate); + else if (cm.options.cursorBlinkRate < 0) + display.cursorDiv.style.visibility = "hidden"; + } + + // HIGHLIGHT WORKER + + function startWorker(cm, time) { + if (cm.doc.mode.startState && cm.doc.frontier < cm.display.viewTo) + cm.state.highlight.set(time, bind(highlightWorker, cm)); + } + + function highlightWorker(cm) { + var doc = cm.doc; + if (doc.frontier < doc.first) doc.frontier = doc.first; + if (doc.frontier >= cm.display.viewTo) return; + var end = +new Date + cm.options.workTime; + var state = copyState(doc.mode, getStateBefore(cm, doc.frontier)); + var changedLines = []; + + doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function(line) { + if (doc.frontier >= cm.display.viewFrom) { // Visible + var oldStyles = line.styles; + var highlighted = highlightLine(cm, line, state, true); + line.styles = highlighted.styles; + var oldCls = line.styleClasses, newCls = highlighted.classes; + if (newCls) line.styleClasses = newCls; + else if (oldCls) line.styleClasses = null; + var ischange = !oldStyles || oldStyles.length != line.styles.length || + oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass); + for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i]; + if (ischange) changedLines.push(doc.frontier); + line.stateAfter = copyState(doc.mode, state); + } else { + processLine(cm, line.text, state); + line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null; + } + ++doc.frontier; + if (+new Date > end) { + startWorker(cm, cm.options.workDelay); + return true; + } + }); + if (changedLines.length) runInOp(cm, function() { + for (var i = 0; i < changedLines.length; i++) + regLineChange(cm, changedLines[i], "text"); + }); + } + + // Finds the line to start with when starting a parse. Tries to + // find a line with a stateAfter, so that it can start with a + // valid state. If that fails, it returns the line with the + // smallest indentation, which tends to need the least context to + // parse correctly. + function findStartLine(cm, n, precise) { + var minindent, minline, doc = cm.doc; + var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100); + for (var search = n; search > lim; --search) { + if (search <= doc.first) return doc.first; + var line = getLine(doc, search - 1); + if (line.stateAfter && (!precise || search <= doc.frontier)) return search; + var indented = countColumn(line.text, null, cm.options.tabSize); + if (minline == null || minindent > indented) { + minline = search - 1; + minindent = indented; + } + } + return minline; + } + + function getStateBefore(cm, n, precise) { + var doc = cm.doc, display = cm.display; + if (!doc.mode.startState) return true; + var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter; + if (!state) state = startState(doc.mode); + else state = copyState(doc.mode, state); + doc.iter(pos, n, function(line) { + processLine(cm, line.text, state); + var save = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo; + line.stateAfter = save ? copyState(doc.mode, state) : null; + ++pos; + }); + if (precise) doc.frontier = pos; + return state; + } + + // POSITION MEASUREMENT + + function paddingTop(display) {return display.lineSpace.offsetTop;} + function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;} + function paddingH(display) { + if (display.cachedPaddingH) return display.cachedPaddingH; + var e = removeChildrenAndAdd(display.measure, elt("pre", "x")); + var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle; + var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)}; + if (!isNaN(data.left) && !isNaN(data.right)) display.cachedPaddingH = data; + return data; + } + + function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth; } + function displayWidth(cm) { + return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth; + } + function displayHeight(cm) { + return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight; + } + + // Ensure the lineView.wrapping.heights array is populated. This is + // an array of bottom offsets for the lines that make up a drawn + // line. When lineWrapping is on, there might be more than one + // height. + function ensureLineHeights(cm, lineView, rect) { + var wrapping = cm.options.lineWrapping; + var curWidth = wrapping && displayWidth(cm); + if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) { + var heights = lineView.measure.heights = []; + if (wrapping) { + lineView.measure.width = curWidth; + var rects = lineView.text.firstChild.getClientRects(); + for (var i = 0; i < rects.length - 1; i++) { + var cur = rects[i], next = rects[i + 1]; + if (Math.abs(cur.bottom - next.bottom) > 2) + heights.push((cur.bottom + next.top) / 2 - rect.top); + } + } + heights.push(rect.bottom - rect.top); + } + } + + // Find a line map (mapping character offsets to text nodes) and a + // measurement cache for the given line number. (A line view might + // contain multiple lines when collapsed ranges are present.) + function mapFromLineView(lineView, line, lineN) { + if (lineView.line == line) + return {map: lineView.measure.map, cache: lineView.measure.cache}; + for (var i = 0; i < lineView.rest.length; i++) + if (lineView.rest[i] == line) + return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]}; + for (var i = 0; i < lineView.rest.length; i++) + if (lineNo(lineView.rest[i]) > lineN) + return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i], before: true}; + } + + // Render a line into the hidden node display.externalMeasured. Used + // when measurement is needed for a line that's not in the viewport. + function updateExternalMeasurement(cm, line) { + line = visualLine(line); + var lineN = lineNo(line); + var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN); + view.lineN = lineN; + var built = view.built = buildLineContent(cm, view); + view.text = built.pre; + removeChildrenAndAdd(cm.display.lineMeasure, built.pre); + return view; + } + + // Get a {top, bottom, left, right} box (in line-local coordinates) + // for a given character. + function measureChar(cm, line, ch, bias) { + return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias); + } + + // Find a line view that corresponds to the given line number. + function findViewForLine(cm, lineN) { + if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo) + return cm.display.view[findViewIndex(cm, lineN)]; + var ext = cm.display.externalMeasured; + if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size) + return ext; + } + + // Measurement can be split in two steps, the set-up work that + // applies to the whole line, and the measurement of the actual + // character. Functions like coordsChar, that need to do a lot of + // measurements in a row, can thus ensure that the set-up work is + // only done once. + function prepareMeasureForLine(cm, line) { + var lineN = lineNo(line); + var view = findViewForLine(cm, lineN); + if (view && !view.text) + view = null; + else if (view && view.changes) + updateLineForChanges(cm, view, lineN, getDimensions(cm)); + if (!view) + view = updateExternalMeasurement(cm, line); + + var info = mapFromLineView(view, line, lineN); + return { + line: line, view: view, rect: null, + map: info.map, cache: info.cache, before: info.before, + hasHeights: false + }; + } + + // Given a prepared measurement object, measures the position of an + // actual character (or fetches it from the cache). + function measureCharPrepared(cm, prepared, ch, bias, varHeight) { + if (prepared.before) ch = -1; + var key = ch + (bias || ""), found; + if (prepared.cache.hasOwnProperty(key)) { + found = prepared.cache[key]; + } else { + if (!prepared.rect) + prepared.rect = prepared.view.text.getBoundingClientRect(); + if (!prepared.hasHeights) { + ensureLineHeights(cm, prepared.view, prepared.rect); + prepared.hasHeights = true; + } + found = measureCharInner(cm, prepared, ch, bias); + if (!found.bogus) prepared.cache[key] = found; + } + return {left: found.left, right: found.right, + top: varHeight ? found.rtop : found.top, + bottom: varHeight ? found.rbottom : found.bottom}; + } + + var nullRect = {left: 0, right: 0, top: 0, bottom: 0}; + + function nodeAndOffsetInLineMap(map, ch, bias) { + var node, start, end, collapse; + // First, search the line map for the text node corresponding to, + // or closest to, the target character. + for (var i = 0; i < map.length; i += 3) { + var mStart = map[i], mEnd = map[i + 1]; + if (ch < mStart) { + start = 0; end = 1; + collapse = "left"; + } else if (ch < mEnd) { + start = ch - mStart; + end = start + 1; + } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) { + end = mEnd - mStart; + start = end - 1; + if (ch >= mEnd) collapse = "right"; + } + if (start != null) { + node = map[i + 2]; + if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) + collapse = bias; + if (bias == "left" && start == 0) + while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) { + node = map[(i -= 3) + 2]; + collapse = "left"; + } + if (bias == "right" && start == mEnd - mStart) + while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) { + node = map[(i += 3) + 2]; + collapse = "right"; + } + break; + } + } + return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd}; + } + + function measureCharInner(cm, prepared, ch, bias) { + var place = nodeAndOffsetInLineMap(prepared.map, ch, bias); + var node = place.node, start = place.start, end = place.end, collapse = place.collapse; + + var rect; + if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. + for (var i = 0; i < 4; i++) { // Retry a maximum of 4 times when nonsense rectangles are returned + while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) --start; + while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) ++end; + if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) { + rect = node.parentNode.getBoundingClientRect(); + } else if (ie && cm.options.lineWrapping) { + var rects = range(node, start, end).getClientRects(); + if (rects.length) + rect = rects[bias == "right" ? rects.length - 1 : 0]; + else + rect = nullRect; + } else { + rect = range(node, start, end).getBoundingClientRect() || nullRect; + } + if (rect.left || rect.right || start == 0) break; + end = start; + start = start - 1; + collapse = "right"; + } + if (ie && ie_version < 11) rect = maybeUpdateRectForZooming(cm.display.measure, rect); + } else { // If it is a widget, simply get the box for the whole widget. + if (start > 0) collapse = bias = "right"; + var rects; + if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1) + rect = rects[bias == "right" ? rects.length - 1 : 0]; + else + rect = node.getBoundingClientRect(); + } + if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) { + var rSpan = node.parentNode.getClientRects()[0]; + if (rSpan) + rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom}; + else + rect = nullRect; + } + + var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top; + var mid = (rtop + rbot) / 2; + var heights = prepared.view.measure.heights; + for (var i = 0; i < heights.length - 1; i++) + if (mid < heights[i]) break; + var top = i ? heights[i - 1] : 0, bot = heights[i]; + var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left, + right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left, + top: top, bottom: bot}; + if (!rect.left && !rect.right) result.bogus = true; + if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; } + + return result; + } + + // Work around problem with bounding client rects on ranges being + // returned incorrectly when zoomed on IE10 and below. + function maybeUpdateRectForZooming(measure, rect) { + if (!window.screen || screen.logicalXDPI == null || + screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure)) + return rect; + var scaleX = screen.logicalXDPI / screen.deviceXDPI; + var scaleY = screen.logicalYDPI / screen.deviceYDPI; + return {left: rect.left * scaleX, right: rect.right * scaleX, + top: rect.top * scaleY, bottom: rect.bottom * scaleY}; + } + + function clearLineMeasurementCacheFor(lineView) { + if (lineView.measure) { + lineView.measure.cache = {}; + lineView.measure.heights = null; + if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) + lineView.measure.caches[i] = {}; + } + } + + function clearLineMeasurementCache(cm) { + cm.display.externalMeasure = null; + removeChildren(cm.display.lineMeasure); + for (var i = 0; i < cm.display.view.length; i++) + clearLineMeasurementCacheFor(cm.display.view[i]); + } + + function clearCaches(cm) { + clearLineMeasurementCache(cm); + cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null; + if (!cm.options.lineWrapping) cm.display.maxLineChanged = true; + cm.display.lineNumChars = null; + } + + function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; } + function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; } + + // Converts a {top, bottom, left, right} box from line-local + // coordinates into another coordinate system. Context may be one of + // "line", "div" (display.lineDiv), "local"/null (editor), "window", + // or "page". + function intoCoordSystem(cm, lineObj, rect, context) { + if (lineObj.widgets) for (var i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) { + var size = widgetHeight(lineObj.widgets[i]); + rect.top += size; rect.bottom += size; + } + if (context == "line") return rect; + if (!context) context = "local"; + var yOff = heightAtLine(lineObj); + if (context == "local") yOff += paddingTop(cm.display); + else yOff -= cm.display.viewOffset; + if (context == "page" || context == "window") { + var lOff = cm.display.lineSpace.getBoundingClientRect(); + yOff += lOff.top + (context == "window" ? 0 : pageScrollY()); + var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()); + rect.left += xOff; rect.right += xOff; + } + rect.top += yOff; rect.bottom += yOff; + return rect; + } + + // Coverts a box from "div" coords to another coordinate system. + // Context may be "window", "page", "div", or "local"/null. + function fromCoordSystem(cm, coords, context) { + if (context == "div") return coords; + var left = coords.left, top = coords.top; + // First move into "page" coordinate system + if (context == "page") { + left -= pageScrollX(); + top -= pageScrollY(); + } else if (context == "local" || !context) { + var localBox = cm.display.sizer.getBoundingClientRect(); + left += localBox.left; + top += localBox.top; + } + + var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect(); + return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top}; + } + + function charCoords(cm, pos, context, lineObj, bias) { + if (!lineObj) lineObj = getLine(cm.doc, pos.line); + return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context); + } + + // Returns a box for a given cursor position, which may have an + // 'other' property containing the position of the secondary cursor + // on a bidi boundary. + function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) { + lineObj = lineObj || getLine(cm.doc, pos.line); + if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj); + function get(ch, right) { + var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight); + if (right) m.left = m.right; else m.right = m.left; + return intoCoordSystem(cm, lineObj, m, context); + } + function getBidi(ch, partPos) { + var part = order[partPos], right = part.level % 2; + if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) { + part = order[--partPos]; + ch = bidiRight(part) - (part.level % 2 ? 0 : 1); + right = true; + } else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) { + part = order[++partPos]; + ch = bidiLeft(part) - part.level % 2; + right = false; + } + if (right && ch == part.to && ch > part.from) return get(ch - 1); + return get(ch, right); + } + var order = getOrder(lineObj), ch = pos.ch; + if (!order) return get(ch); + var partPos = getBidiPartAt(order, ch); + var val = getBidi(ch, partPos); + if (bidiOther != null) val.other = getBidi(ch, bidiOther); + return val; + } + + // Used to cheaply estimate the coordinates for a position. Used for + // intermediate scroll updates. + function estimateCoords(cm, pos) { + var left = 0, pos = clipPos(cm.doc, pos); + if (!cm.options.lineWrapping) left = charWidth(cm.display) * pos.ch; + var lineObj = getLine(cm.doc, pos.line); + var top = heightAtLine(lineObj) + paddingTop(cm.display); + return {left: left, right: left, top: top, bottom: top + lineObj.height}; + } + + // Positions returned by coordsChar contain some extra information. + // xRel is the relative x position of the input coordinates compared + // to the found position (so xRel > 0 means the coordinates are to + // the right of the character position, for example). When outside + // is true, that means the coordinates lie outside the line's + // vertical range. + function PosWithInfo(line, ch, outside, xRel) { + var pos = Pos(line, ch); + pos.xRel = xRel; + if (outside) pos.outside = true; + return pos; + } + + // Compute the character position closest to the given coordinates. + // Input must be lineSpace-local ("div" coordinate system). + function coordsChar(cm, x, y) { + var doc = cm.doc; + y += cm.display.viewOffset; + if (y < 0) return PosWithInfo(doc.first, 0, true, -1); + var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1; + if (lineN > last) + return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1); + if (x < 0) x = 0; + + var lineObj = getLine(doc, lineN); + for (;;) { + var found = coordsCharInner(cm, lineObj, lineN, x, y); + var merged = collapsedSpanAtEnd(lineObj); + var mergedPos = merged && merged.find(0, true); + if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0)) + lineN = lineNo(lineObj = mergedPos.to.line); + else + return found; + } + } + + function coordsCharInner(cm, lineObj, lineNo, x, y) { + var innerOff = y - heightAtLine(lineObj); + var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth; + var preparedMeasure = prepareMeasureForLine(cm, lineObj); + + function getX(ch) { + var sp = cursorCoords(cm, Pos(lineNo, ch), "line", lineObj, preparedMeasure); + wrongLine = true; + if (innerOff > sp.bottom) return sp.left - adjust; + else if (innerOff < sp.top) return sp.left + adjust; + else wrongLine = false; + return sp.left; + } + + var bidi = getOrder(lineObj), dist = lineObj.text.length; + var from = lineLeft(lineObj), to = lineRight(lineObj); + var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine; + + if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1); + // Do a binary search between these bounds. + for (;;) { + if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) { + var ch = x < fromX || x - fromX <= toX - x ? from : to; + var xDiff = x - (ch == from ? fromX : toX); + while (isExtendingChar(lineObj.text.charAt(ch))) ++ch; + var pos = PosWithInfo(lineNo, ch, ch == from ? fromOutside : toOutside, + xDiff < -1 ? -1 : xDiff > 1 ? 1 : 0); + return pos; + } + var step = Math.ceil(dist / 2), middle = from + step; + if (bidi) { + middle = from; + for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1); + } + var middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;} + else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;} + } + } + + var measureText; + // Compute the default text height. + function textHeight(display) { + if (display.cachedTextHeight != null) return display.cachedTextHeight; + if (measureText == null) { + measureText = elt("pre"); + // Measure a bunch of lines, for browsers that compute + // fractional heights. + for (var i = 0; i < 49; ++i) { + measureText.appendChild(document.createTextNode("x")); + measureText.appendChild(elt("br")); + } + measureText.appendChild(document.createTextNode("x")); + } + removeChildrenAndAdd(display.measure, measureText); + var height = measureText.offsetHeight / 50; + if (height > 3) display.cachedTextHeight = height; + removeChildren(display.measure); + return height || 1; + } + + // Compute the default character width. + function charWidth(display) { + if (display.cachedCharWidth != null) return display.cachedCharWidth; + var anchor = elt("span", "xxxxxxxxxx"); + var pre = elt("pre", [anchor]); + removeChildrenAndAdd(display.measure, pre); + var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10; + if (width > 2) display.cachedCharWidth = width; + return width || 10; + } + + // OPERATIONS + + // Operations are used to wrap a series of changes to the editor + // state in such a way that each change won't have to update the + // cursor and display (which would be awkward, slow, and + // error-prone). Instead, display updates are batched and then all + // combined and executed at once. + + var operationGroup = null; + + var nextOpId = 0; + // Start a new operation. + function startOperation(cm) { + cm.curOp = { + cm: cm, + viewChanged: false, // Flag that indicates that lines might need to be redrawn + startHeight: cm.doc.height, // Used to detect need to update scrollbar + forceUpdate: false, // Used to force a redraw + updateInput: null, // Whether to reset the input textarea + typing: false, // Whether this reset should be careful to leave existing text (for compositing) + changeObjs: null, // Accumulated changes, for firing change events + cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on + cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already + selectionChanged: false, // Whether the selection needs to be redrawn + updateMaxLine: false, // Set when the widest line needs to be determined anew + scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet + scrollToPos: null, // Used to scroll to a specific position + focus: false, + id: ++nextOpId // Unique ID + }; + if (operationGroup) { + operationGroup.ops.push(cm.curOp); + } else { + cm.curOp.ownsGroup = operationGroup = { + ops: [cm.curOp], + delayedCallbacks: [] + }; + } + } + + function fireCallbacksForOps(group) { + // Calls delayed callbacks and cursorActivity handlers until no + // new ones appear + var callbacks = group.delayedCallbacks, i = 0; + do { + for (; i < callbacks.length; i++) + callbacks[i](); + for (var j = 0; j < group.ops.length; j++) { + var op = group.ops[j]; + if (op.cursorActivityHandlers) + while (op.cursorActivityCalled < op.cursorActivityHandlers.length) + op.cursorActivityHandlers[op.cursorActivityCalled++](op.cm); + } + } while (i < callbacks.length); + } + + // Finish an operation, updating the display and signalling delayed events + function endOperation(cm) { + var op = cm.curOp, group = op.ownsGroup; + if (!group) return; + + try { fireCallbacksForOps(group); } + finally { + operationGroup = null; + for (var i = 0; i < group.ops.length; i++) + group.ops[i].cm.curOp = null; + endOperations(group); + } + } + + // The DOM updates done when an operation finishes are batched so + // that the minimum number of relayouts are required. + function endOperations(group) { + var ops = group.ops; + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_R1(ops[i]); + for (var i = 0; i < ops.length; i++) // Write DOM (maybe) + endOperation_W1(ops[i]); + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_R2(ops[i]); + for (var i = 0; i < ops.length; i++) // Write DOM (maybe) + endOperation_W2(ops[i]); + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_finish(ops[i]); + } + + function endOperation_R1(op) { + var cm = op.cm, display = cm.display; + maybeClipScrollbars(cm); + if (op.updateMaxLine) findMaxLine(cm); + + op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null || + op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom || + op.scrollToPos.to.line >= display.viewTo) || + display.maxLineChanged && cm.options.lineWrapping; + op.update = op.mustUpdate && + new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate); + } + + function endOperation_W1(op) { + op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update); + } + + function endOperation_R2(op) { + var cm = op.cm, display = cm.display; + if (op.updatedDisplay) updateHeightsInViewport(cm); + + op.barMeasure = measureForScrollbars(cm); + + // If the max line changed since it was last measured, measure it, + // and ensure the document's width matches it. + // updateDisplay_W2 will use these properties to do the actual resizing + if (display.maxLineChanged && !cm.options.lineWrapping) { + op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3; + cm.display.sizerWidth = op.adjustWidthTo; + op.barMeasure.scrollWidth = + Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth); + op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm)); + } + + if (op.updatedDisplay || op.selectionChanged) + op.preparedSelection = display.input.prepareSelection(); + } + + function endOperation_W2(op) { + var cm = op.cm; + + if (op.adjustWidthTo != null) { + cm.display.sizer.style.minWidth = op.adjustWidthTo + "px"; + if (op.maxScrollLeft < cm.doc.scrollLeft) + setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true); + cm.display.maxLineChanged = false; + } + + if (op.preparedSelection) + cm.display.input.showSelection(op.preparedSelection); + if (op.updatedDisplay) + setDocumentHeight(cm, op.barMeasure); + if (op.updatedDisplay || op.startHeight != cm.doc.height) + updateScrollbars(cm, op.barMeasure); + + if (op.selectionChanged) restartBlink(cm); + + if (cm.state.focused && op.updateInput) + cm.display.input.reset(op.typing); + if (op.focus && op.focus == activeElt()) ensureFocus(op.cm); + } + + function endOperation_finish(op) { + var cm = op.cm, display = cm.display, doc = cm.doc; + + if (op.updatedDisplay) postUpdateDisplay(cm, op.update); + + // Abort mouse wheel delta measurement, when scrolling explicitly + if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos)) + display.wheelStartX = display.wheelStartY = null; + + // Propagate the scroll position to the actual DOM scroller + if (op.scrollTop != null && (display.scroller.scrollTop != op.scrollTop || op.forceScroll)) { + doc.scrollTop = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop)); + display.scrollbars.setScrollTop(doc.scrollTop); + display.scroller.scrollTop = doc.scrollTop; + } + if (op.scrollLeft != null && (display.scroller.scrollLeft != op.scrollLeft || op.forceScroll)) { + doc.scrollLeft = Math.max(0, Math.min(display.scroller.scrollWidth - displayWidth(cm), op.scrollLeft)); + display.scrollbars.setScrollLeft(doc.scrollLeft); + display.scroller.scrollLeft = doc.scrollLeft; + alignHorizontally(cm); + } + // If we need to scroll a specific position into view, do so. + if (op.scrollToPos) { + var coords = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from), + clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin); + if (op.scrollToPos.isCursor && cm.state.focused) maybeScrollWindow(cm, coords); + } + + // Fire events for markers that are hidden/unidden by editing or + // undoing + var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; + if (hidden) for (var i = 0; i < hidden.length; ++i) + if (!hidden[i].lines.length) signal(hidden[i], "hide"); + if (unhidden) for (var i = 0; i < unhidden.length; ++i) + if (unhidden[i].lines.length) signal(unhidden[i], "unhide"); + + if (display.wrapper.offsetHeight) + doc.scrollTop = cm.display.scroller.scrollTop; + + // Fire change events, and delayed event handlers + if (op.changeObjs) + signal(cm, "changes", cm, op.changeObjs); + if (op.update) + op.update.finish(); + } + + // Run the given function in an operation + function runInOp(cm, f) { + if (cm.curOp) return f(); + startOperation(cm); + try { return f(); } + finally { endOperation(cm); } + } + // Wraps a function in an operation. Returns the wrapped function. + function operation(cm, f) { + return function() { + if (cm.curOp) return f.apply(cm, arguments); + startOperation(cm); + try { return f.apply(cm, arguments); } + finally { endOperation(cm); } + }; + } + // Used to add methods to editor and doc instances, wrapping them in + // operations. + function methodOp(f) { + return function() { + if (this.curOp) return f.apply(this, arguments); + startOperation(this); + try { return f.apply(this, arguments); } + finally { endOperation(this); } + }; + } + function docMethodOp(f) { + return function() { + var cm = this.cm; + if (!cm || cm.curOp) return f.apply(this, arguments); + startOperation(cm); + try { return f.apply(this, arguments); } + finally { endOperation(cm); } + }; + } + + // VIEW TRACKING + + // These objects are used to represent the visible (currently drawn) + // part of the document. A LineView may correspond to multiple + // logical lines, if those are connected by collapsed ranges. + function LineView(doc, line, lineN) { + // The starting line + this.line = line; + // Continuing lines, if any + this.rest = visualLineContinued(line); + // Number of logical lines in this visual line + this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1; + this.node = this.text = null; + this.hidden = lineIsHidden(doc, line); + } + + // Create a range of LineView objects for the given lines. + function buildViewArray(cm, from, to) { + var array = [], nextPos; + for (var pos = from; pos < to; pos = nextPos) { + var view = new LineView(cm.doc, getLine(cm.doc, pos), pos); + nextPos = pos + view.size; + array.push(view); + } + return array; + } + + // Updates the display.view data structure for a given change to the + // document. From and to are in pre-change coordinates. Lendiff is + // the amount of lines added or subtracted by the change. This is + // used for changes that span multiple lines, or change the way + // lines are divided into visual lines. regLineChange (below) + // registers single-line changes. + function regChange(cm, from, to, lendiff) { + if (from == null) from = cm.doc.first; + if (to == null) to = cm.doc.first + cm.doc.size; + if (!lendiff) lendiff = 0; + + var display = cm.display; + if (lendiff && to < display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers > from)) + display.updateLineNumbers = from; + + cm.curOp.viewChanged = true; + + if (from >= display.viewTo) { // Change after + if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo) + resetView(cm); + } else if (to <= display.viewFrom) { // Change before + if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) { + resetView(cm); + } else { + display.viewFrom += lendiff; + display.viewTo += lendiff; + } + } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap + resetView(cm); + } else if (from <= display.viewFrom) { // Top overlap + var cut = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cut) { + display.view = display.view.slice(cut.index); + display.viewFrom = cut.lineN; + display.viewTo += lendiff; + } else { + resetView(cm); + } + } else if (to >= display.viewTo) { // Bottom overlap + var cut = viewCuttingPoint(cm, from, from, -1); + if (cut) { + display.view = display.view.slice(0, cut.index); + display.viewTo = cut.lineN; + } else { + resetView(cm); + } + } else { // Gap in the middle + var cutTop = viewCuttingPoint(cm, from, from, -1); + var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cutTop && cutBot) { + display.view = display.view.slice(0, cutTop.index) + .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN)) + .concat(display.view.slice(cutBot.index)); + display.viewTo += lendiff; + } else { + resetView(cm); + } + } + + var ext = display.externalMeasured; + if (ext) { + if (to < ext.lineN) + ext.lineN += lendiff; + else if (from < ext.lineN + ext.size) + display.externalMeasured = null; + } + } + + // Register a change to a single line. Type must be one of "text", + // "gutter", "class", "widget" + function regLineChange(cm, line, type) { + cm.curOp.viewChanged = true; + var display = cm.display, ext = cm.display.externalMeasured; + if (ext && line >= ext.lineN && line < ext.lineN + ext.size) + display.externalMeasured = null; + + if (line < display.viewFrom || line >= display.viewTo) return; + var lineView = display.view[findViewIndex(cm, line)]; + if (lineView.node == null) return; + var arr = lineView.changes || (lineView.changes = []); + if (indexOf(arr, type) == -1) arr.push(type); + } + + // Clear the view. + function resetView(cm) { + cm.display.viewFrom = cm.display.viewTo = cm.doc.first; + cm.display.view = []; + cm.display.viewOffset = 0; + } + + // Find the view element corresponding to a given line. Return null + // when the line isn't visible. + function findViewIndex(cm, n) { + if (n >= cm.display.viewTo) return null; + n -= cm.display.viewFrom; + if (n < 0) return null; + var view = cm.display.view; + for (var i = 0; i < view.length; i++) { + n -= view[i].size; + if (n < 0) return i; + } + } + + function viewCuttingPoint(cm, oldN, newN, dir) { + var index = findViewIndex(cm, oldN), diff, view = cm.display.view; + if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size) + return {index: index, lineN: newN}; + for (var i = 0, n = cm.display.viewFrom; i < index; i++) + n += view[i].size; + if (n != oldN) { + if (dir > 0) { + if (index == view.length - 1) return null; + diff = (n + view[index].size) - oldN; + index++; + } else { + diff = n - oldN; + } + oldN += diff; newN += diff; + } + while (visualLineNo(cm.doc, newN) != newN) { + if (index == (dir < 0 ? 0 : view.length - 1)) return null; + newN += dir * view[index - (dir < 0 ? 1 : 0)].size; + index += dir; + } + return {index: index, lineN: newN}; + } + + // Force the view to cover a given range, adding empty view element + // or clipping off existing ones as needed. + function adjustView(cm, from, to) { + var display = cm.display, view = display.view; + if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) { + display.view = buildViewArray(cm, from, to); + display.viewFrom = from; + } else { + if (display.viewFrom > from) + display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view); + else if (display.viewFrom < from) + display.view = display.view.slice(findViewIndex(cm, from)); + display.viewFrom = from; + if (display.viewTo < to) + display.view = display.view.concat(buildViewArray(cm, display.viewTo, to)); + else if (display.viewTo > to) + display.view = display.view.slice(0, findViewIndex(cm, to)); + } + display.viewTo = to; + } + + // Count the number of lines in the view whose DOM representation is + // out of date (or nonexistent). + function countDirtyView(cm) { + var view = cm.display.view, dirty = 0; + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (!lineView.hidden && (!lineView.node || lineView.changes)) ++dirty; + } + return dirty; + } + + // EVENT HANDLERS + + // Attach the necessary event handlers when initializing the editor + function registerEventHandlers(cm) { + var d = cm.display; + on(d.scroller, "mousedown", operation(cm, onMouseDown)); + // Older IE's will not fire a second mousedown for a double click + if (ie && ie_version < 11) + on(d.scroller, "dblclick", operation(cm, function(e) { + if (signalDOMEvent(cm, e)) return; + var pos = posFromMouse(cm, e); + if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return; + e_preventDefault(e); + var word = cm.findWordAt(pos); + extendSelection(cm.doc, word.anchor, word.head); + })); + else + on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); }); + // Some browsers fire contextmenu *after* opening the menu, at + // which point we can't mess with it anymore. Context menu is + // handled in onMouseDown for these browsers. + if (!captureRightClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);}); + + // Used to suppress mouse event handling when a touch happens + var touchFinished, prevTouch = {end: 0}; + function finishTouch() { + if (d.activeTouch) { + touchFinished = setTimeout(function() {d.activeTouch = null;}, 1000); + prevTouch = d.activeTouch; + prevTouch.end = +new Date; + } + }; + function isMouseLikeTouchEvent(e) { + if (e.touches.length != 1) return false; + var touch = e.touches[0]; + return touch.radiusX <= 1 && touch.radiusY <= 1; + } + function farAway(touch, other) { + if (other.left == null) return true; + var dx = other.left - touch.left, dy = other.top - touch.top; + return dx * dx + dy * dy > 20 * 20; + } + on(d.scroller, "touchstart", function(e) { + if (!isMouseLikeTouchEvent(e)) { + clearTimeout(touchFinished); + var now = +new Date; + d.activeTouch = {start: now, moved: false, + prev: now - prevTouch.end <= 300 ? prevTouch : null}; + if (e.touches.length == 1) { + d.activeTouch.left = e.touches[0].pageX; + d.activeTouch.top = e.touches[0].pageY; + } + } + }); + on(d.scroller, "touchmove", function() { + if (d.activeTouch) d.activeTouch.moved = true; + }); + on(d.scroller, "touchend", function(e) { + var touch = d.activeTouch; + if (touch && !eventInWidget(d, e) && touch.left != null && + !touch.moved && new Date - touch.start < 300) { + var pos = cm.coordsChar(d.activeTouch, "page"), range; + if (!touch.prev || farAway(touch, touch.prev)) // Single tap + range = new Range(pos, pos); + else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap + range = cm.findWordAt(pos); + else // Triple tap + range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))); + cm.setSelection(range.anchor, range.head); + cm.focus(); + e_preventDefault(e); + } + finishTouch(); + }); + on(d.scroller, "touchcancel", finishTouch); + + // Sync scrolling between fake scrollbars and real scrollable + // area, ensure viewport is updated when scrolling. + on(d.scroller, "scroll", function() { + if (d.scroller.clientHeight) { + setScrollTop(cm, d.scroller.scrollTop); + setScrollLeft(cm, d.scroller.scrollLeft, true); + signal(cm, "scroll", cm); + } + }); + + // Listen to wheel events in order to try and update the viewport on time. + on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);}); + on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);}); + + // Prevent wrapper from ever scrolling + on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); + + d.dragFunctions = { + simple: function(e) {if (!signalDOMEvent(cm, e)) e_stop(e);}, + start: function(e){onDragStart(cm, e);}, + drop: operation(cm, onDrop) + }; + + var inp = d.input.getField(); + on(inp, "keyup", function(e) { onKeyUp.call(cm, e); }); + on(inp, "keydown", operation(cm, onKeyDown)); + on(inp, "keypress", operation(cm, onKeyPress)); + on(inp, "focus", bind(onFocus, cm)); + on(inp, "blur", bind(onBlur, cm)); + } + + function dragDropChanged(cm, value, old) { + var wasOn = old && old != CodeMirror.Init; + if (!value != !wasOn) { + var funcs = cm.display.dragFunctions; + var toggle = value ? on : off; + toggle(cm.display.scroller, "dragstart", funcs.start); + toggle(cm.display.scroller, "dragenter", funcs.simple); + toggle(cm.display.scroller, "dragover", funcs.simple); + toggle(cm.display.scroller, "drop", funcs.drop); + } + } + + // Called when the window resizes + function onResize(cm) { + var d = cm.display; + if (d.lastWrapHeight == d.wrapper.clientHeight && d.lastWrapWidth == d.wrapper.clientWidth) + return; + // Might be a text scaling operation, clear size caches. + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + d.scrollbarsClipped = false; + cm.setSize(); + } + + // MOUSE EVENTS + + // Return true when the given mouse event happened in a widget + function eventInWidget(display, e) { + for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { + if (!n || (n.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") || + (n.parentNode == display.sizer && n != display.mover)) + return true; + } + } + + // Given a mouse event, find the corresponding position. If liberal + // is false, it checks whether a gutter or scrollbar was clicked, + // and returns null if it was. forRect is used by rectangular + // selections, and tries to estimate a character position even for + // coordinates beyond the right of the text. + function posFromMouse(cm, e, liberal, forRect) { + var display = cm.display; + if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") return null; + + var x, y, space = display.lineSpace.getBoundingClientRect(); + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX - space.left; y = e.clientY - space.top; } + catch (e) { return null; } + var coords = coordsChar(cm, x, y), line; + if (forRect && coords.xRel == 1 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) { + var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length; + coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff)); + } + return coords; + } + + // A mouse down can be a single click, double click, triple click, + // start of selection drag, start of text drag, new cursor + // (ctrl-click), rectangle drag (alt-drag), or xwin + // middle-click-paste. Or it might be a click on something we should + // not interfere with, such as a scrollbar or widget. + function onMouseDown(e) { + var cm = this, display = cm.display; + if (display.activeTouch && display.input.supportsTouch() || signalDOMEvent(cm, e)) return; + display.shift = e.shiftKey; + + if (eventInWidget(display, e)) { + if (!webkit) { + // Briefly turn off draggability, to allow widgets to do + // normal dragging things. + display.scroller.draggable = false; + setTimeout(function(){display.scroller.draggable = true;}, 100); + } + return; + } + if (clickInGutter(cm, e)) return; + var start = posFromMouse(cm, e); + window.focus(); + + switch (e_button(e)) { + case 1: + if (start) + leftButtonDown(cm, e, start); + else if (e_target(e) == display.scroller) + e_preventDefault(e); + break; + case 2: + if (webkit) cm.state.lastMiddleDown = +new Date; + if (start) extendSelection(cm.doc, start); + setTimeout(function() {display.input.focus();}, 20); + e_preventDefault(e); + break; + case 3: + if (captureRightClick) onContextMenu(cm, e); + else delayBlurEvent(cm); + break; + } + } + + var lastClick, lastDoubleClick; + function leftButtonDown(cm, e, start) { + if (ie) setTimeout(bind(ensureFocus, cm), 0); + else cm.curOp.focus = activeElt(); + + var now = +new Date, type; + if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) { + type = "triple"; + } else if (lastClick && lastClick.time > now - 400 && cmp(lastClick.pos, start) == 0) { + type = "double"; + lastDoubleClick = {time: now, pos: start}; + } else { + type = "single"; + lastClick = {time: now, pos: start}; + } + + var sel = cm.doc.sel, modifier = mac ? e.metaKey : e.ctrlKey, contained; + if (cm.options.dragDrop && dragAndDrop && !isReadOnly(cm) && + type == "single" && (contained = sel.contains(start)) > -1 && + !sel.ranges[contained].empty()) + leftButtonStartDrag(cm, e, start, modifier); + else + leftButtonSelect(cm, e, start, type, modifier); + } + + // Start a text drag. When it ends, see if any dragging actually + // happen, and treat as a click if it didn't. + function leftButtonStartDrag(cm, e, start, modifier) { + var display = cm.display; + var dragEnd = operation(cm, function(e2) { + if (webkit) display.scroller.draggable = false; + cm.state.draggingText = false; + off(document, "mouseup", dragEnd); + off(display.scroller, "drop", dragEnd); + if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { + e_preventDefault(e2); + if (!modifier) + extendSelection(cm.doc, start); + // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081) + if (webkit || ie && ie_version == 9) + setTimeout(function() {document.body.focus(); display.input.focus();}, 20); + else + display.input.focus(); + } + }); + // Let the drag handler handle this. + if (webkit) display.scroller.draggable = true; + cm.state.draggingText = dragEnd; + // IE's approach to draggable + if (display.scroller.dragDrop) display.scroller.dragDrop(); + on(document, "mouseup", dragEnd); + on(display.scroller, "drop", dragEnd); + } + + // Normal selection, as opposed to text dragging. + function leftButtonSelect(cm, e, start, type, addNew) { + var display = cm.display, doc = cm.doc; + e_preventDefault(e); + + var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges; + if (addNew && !e.shiftKey) { + ourIndex = doc.sel.contains(start); + if (ourIndex > -1) + ourRange = ranges[ourIndex]; + else + ourRange = new Range(start, start); + } else { + ourRange = doc.sel.primary(); + ourIndex = doc.sel.primIndex; + } + + if (e.altKey) { + type = "rect"; + if (!addNew) ourRange = new Range(start, start); + start = posFromMouse(cm, e, true, true); + ourIndex = -1; + } else if (type == "double") { + var word = cm.findWordAt(start); + if (cm.display.shift || doc.extend) + ourRange = extendRange(doc, ourRange, word.anchor, word.head); + else + ourRange = word; + } else if (type == "triple") { + var line = new Range(Pos(start.line, 0), clipPos(doc, Pos(start.line + 1, 0))); + if (cm.display.shift || doc.extend) + ourRange = extendRange(doc, ourRange, line.anchor, line.head); + else + ourRange = line; + } else { + ourRange = extendRange(doc, ourRange, start); + } + + if (!addNew) { + ourIndex = 0; + setSelection(doc, new Selection([ourRange], 0), sel_mouse); + startSel = doc.sel; + } else if (ourIndex == -1) { + ourIndex = ranges.length; + setSelection(doc, normalizeSelection(ranges.concat([ourRange]), ourIndex), + {scroll: false, origin: "*mouse"}); + } else if (ranges.length > 1 && ranges[ourIndex].empty() && type == "single" && !e.shiftKey) { + setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0)); + startSel = doc.sel; + } else { + replaceOneSelection(doc, ourIndex, ourRange, sel_mouse); + } + + var lastPos = start; + function extendTo(pos) { + if (cmp(lastPos, pos) == 0) return; + lastPos = pos; + + if (type == "rect") { + var ranges = [], tabSize = cm.options.tabSize; + var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize); + var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize); + var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol); + for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line)); + line <= end; line++) { + var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize); + if (left == right) + ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))); + else if (text.length > leftPos) + ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))); + } + if (!ranges.length) ranges.push(new Range(start, start)); + setSelection(doc, normalizeSelection(startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), + {origin: "*mouse", scroll: false}); + cm.scrollIntoView(pos); + } else { + var oldRange = ourRange; + var anchor = oldRange.anchor, head = pos; + if (type != "single") { + if (type == "double") + var range = cm.findWordAt(pos); + else + var range = new Range(Pos(pos.line, 0), clipPos(doc, Pos(pos.line + 1, 0))); + if (cmp(range.anchor, anchor) > 0) { + head = range.head; + anchor = minPos(oldRange.from(), range.anchor); + } else { + head = range.anchor; + anchor = maxPos(oldRange.to(), range.head); + } + } + var ranges = startSel.ranges.slice(0); + ranges[ourIndex] = new Range(clipPos(doc, anchor), head); + setSelection(doc, normalizeSelection(ranges, ourIndex), sel_mouse); + } + } + + var editorSize = display.wrapper.getBoundingClientRect(); + // Used to ensure timeout re-tries don't fire when another extend + // happened in the meantime (clearTimeout isn't reliable -- at + // least on Chrome, the timeouts still happen even when cleared, + // if the clear happens after their scheduled firing time). + var counter = 0; + + function extend(e) { + var curCount = ++counter; + var cur = posFromMouse(cm, e, true, type == "rect"); + if (!cur) return; + if (cmp(cur, lastPos) != 0) { + cm.curOp.focus = activeElt(); + extendTo(cur); + var visible = visibleLines(display, doc); + if (cur.line >= visible.to || cur.line < visible.from) + setTimeout(operation(cm, function(){if (counter == curCount) extend(e);}), 150); + } else { + var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0; + if (outside) setTimeout(operation(cm, function() { + if (counter != curCount) return; + display.scroller.scrollTop += outside; + extend(e); + }), 50); + } + } + + function done(e) { + counter = Infinity; + e_preventDefault(e); + display.input.focus(); + off(document, "mousemove", move); + off(document, "mouseup", up); + doc.history.lastSelOrigin = null; + } + + var move = operation(cm, function(e) { + if (!e_button(e)) done(e); + else extend(e); + }); + var up = operation(cm, done); + on(document, "mousemove", move); + on(document, "mouseup", up); + } + + // Determines whether an event happened in the gutter, and fires the + // handlers for the corresponding event. + function gutterEvent(cm, e, type, prevent, signalfn) { + try { var mX = e.clientX, mY = e.clientY; } + catch(e) { return false; } + if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false; + if (prevent) e_preventDefault(e); + + var display = cm.display; + var lineBox = display.lineDiv.getBoundingClientRect(); + + if (mY > lineBox.bottom || !hasHandler(cm, type)) return e_defaultPrevented(e); + mY -= lineBox.top - display.viewOffset; + + for (var i = 0; i < cm.options.gutters.length; ++i) { + var g = display.gutters.childNodes[i]; + if (g && g.getBoundingClientRect().right >= mX) { + var line = lineAtHeight(cm.doc, mY); + var gutter = cm.options.gutters[i]; + signalfn(cm, type, cm, line, gutter, e); + return e_defaultPrevented(e); + } + } + } + + function clickInGutter(cm, e) { + return gutterEvent(cm, e, "gutterClick", true, signalLater); + } + + // Kludge to work around strange IE behavior where it'll sometimes + // re-fire a series of drag-related events right after the drop (#1551) + var lastDrop = 0; + + function onDrop(e) { + var cm = this; + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) + return; + e_preventDefault(e); + if (ie) lastDrop = +new Date; + var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; + if (!pos || isReadOnly(cm)) return; + // Might be a file drop, in which case we simply extract the text + // and insert it. + if (files && files.length && window.FileReader && window.File) { + var n = files.length, text = Array(n), read = 0; + var loadFile = function(file, i) { + var reader = new FileReader; + reader.onload = operation(cm, function() { + text[i] = reader.result; + if (++read == n) { + pos = clipPos(cm.doc, pos); + var change = {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"}; + makeChange(cm.doc, change); + setSelectionReplaceHistory(cm.doc, simpleSelection(pos, changeEnd(change))); + } + }); + reader.readAsText(file); + }; + for (var i = 0; i < n; ++i) loadFile(files[i], i); + } else { // Normal drop + // Don't do a replace if the drop happened inside of the selected text. + if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) { + cm.state.draggingText(e); + // Ensure the editor is re-focused + setTimeout(function() {cm.display.input.focus();}, 20); + return; + } + try { + var text = e.dataTransfer.getData("Text"); + if (text) { + if (cm.state.draggingText && !(mac ? e.altKey : e.ctrlKey)) + var selected = cm.listSelections(); + setSelectionNoUndo(cm.doc, simpleSelection(pos, pos)); + if (selected) for (var i = 0; i < selected.length; ++i) + replaceRange(cm.doc, "", selected[i].anchor, selected[i].head, "drag"); + cm.replaceSelection(text, "around", "paste"); + cm.display.input.focus(); + } + } + catch(e){} + } + } + + function onDragStart(cm, e) { + if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; } + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return; + + e.dataTransfer.setData("Text", cm.getSelection()); + + // Use dummy image instead of default browsers image. + // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. + if (e.dataTransfer.setDragImage && !safari) { + var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); + img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + if (presto) { + img.width = img.height = 1; + cm.display.wrapper.appendChild(img); + // Force a relayout, or Opera won't use our image for some obscure reason + img._top = img.offsetTop; + } + e.dataTransfer.setDragImage(img, 0, 0); + if (presto) img.parentNode.removeChild(img); + } + } + + // SCROLL EVENTS + + // Sync the scrollable area and scrollbars, ensure the viewport + // covers the visible area. + function setScrollTop(cm, val) { + if (Math.abs(cm.doc.scrollTop - val) < 2) return; + cm.doc.scrollTop = val; + if (!gecko) updateDisplaySimple(cm, {top: val}); + if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val; + cm.display.scrollbars.setScrollTop(val); + if (gecko) updateDisplaySimple(cm); + startWorker(cm, 100); + } + // Sync scroller and scrollbar, ensure the gutter elements are + // aligned. + function setScrollLeft(cm, val, isScroller) { + if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return; + val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth); + cm.doc.scrollLeft = val; + alignHorizontally(cm); + if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val; + cm.display.scrollbars.setScrollLeft(val); + } + + // Since the delta values reported on mouse wheel events are + // unstandardized between browsers and even browser versions, and + // generally horribly unpredictable, this code starts by measuring + // the scroll effect that the first few mouse wheel events have, + // and, from that, detects the way it can convert deltas to pixel + // offsets afterwards. + // + // The reason we want to know the amount a wheel event will scroll + // is that it gives us a chance to update the display before the + // actual scrolling happens, reducing flickering. + + var wheelSamples = 0, wheelPixelsPerUnit = null; + // Fill in a browser-detected starting value on browsers where we + // know one. These don't have to be accurate -- the result of them + // being wrong would just be a slight flicker on the first wheel + // scroll (if it is large enough). + if (ie) wheelPixelsPerUnit = -.53; + else if (gecko) wheelPixelsPerUnit = 15; + else if (chrome) wheelPixelsPerUnit = -.7; + else if (safari) wheelPixelsPerUnit = -1/3; + + var wheelEventDelta = function(e) { + var dx = e.wheelDeltaX, dy = e.wheelDeltaY; + if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail; + if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail; + else if (dy == null) dy = e.wheelDelta; + return {x: dx, y: dy}; + }; + CodeMirror.wheelEventPixels = function(e) { + var delta = wheelEventDelta(e); + delta.x *= wheelPixelsPerUnit; + delta.y *= wheelPixelsPerUnit; + return delta; + }; + + function onScrollWheel(cm, e) { + var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y; + + var display = cm.display, scroll = display.scroller; + // Quit if there's nothing to scroll here + if (!(dx && scroll.scrollWidth > scroll.clientWidth || + dy && scroll.scrollHeight > scroll.clientHeight)) return; + + // Webkit browsers on OS X abort momentum scrolls when the target + // of the scroll event is removed from the scrollable element. + // This hack (see related code in patchDisplay) makes sure the + // element is kept around. + if (dy && mac && webkit) { + outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { + for (var i = 0; i < view.length; i++) { + if (view[i].node == cur) { + cm.display.currentWheelTarget = cur; + break outer; + } + } + } + } + + // On some browsers, horizontal scrolling will cause redraws to + // happen before the gutter has been realigned, causing it to + // wriggle around in a most unseemly way. When we have an + // estimated pixels/delta value, we just handle horizontal + // scrolling entirely here. It'll be slightly off from native, but + // better than glitching out. + if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { + if (dy) + setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight))); + setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth))); + e_preventDefault(e); + display.wheelStartX = null; // Abort measurement, if in progress + return; + } + + // 'Project' the visible viewport to cover the area that is being + // scrolled into view (if we know enough to estimate it). + if (dy && wheelPixelsPerUnit != null) { + var pixels = dy * wheelPixelsPerUnit; + var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; + if (pixels < 0) top = Math.max(0, top + pixels - 50); + else bot = Math.min(cm.doc.height, bot + pixels + 50); + updateDisplaySimple(cm, {top: top, bottom: bot}); + } + + if (wheelSamples < 20) { + if (display.wheelStartX == null) { + display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop; + display.wheelDX = dx; display.wheelDY = dy; + setTimeout(function() { + if (display.wheelStartX == null) return; + var movedX = scroll.scrollLeft - display.wheelStartX; + var movedY = scroll.scrollTop - display.wheelStartY; + var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || + (movedX && display.wheelDX && movedX / display.wheelDX); + display.wheelStartX = display.wheelStartY = null; + if (!sample) return; + wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1); + ++wheelSamples; + }, 200); + } else { + display.wheelDX += dx; display.wheelDY += dy; + } + } + } + + // KEY EVENTS + + // Run a handler that was bound to a key. + function doHandleBinding(cm, bound, dropShift) { + if (typeof bound == "string") { + bound = commands[bound]; + if (!bound) return false; + } + // Ensure previous input has been read, so that the handler sees a + // consistent view of the document + cm.display.input.ensurePolled(); + var prevShift = cm.display.shift, done = false; + try { + if (isReadOnly(cm)) cm.state.suppressEdits = true; + if (dropShift) cm.display.shift = false; + done = bound(cm) != Pass; + } finally { + cm.display.shift = prevShift; + cm.state.suppressEdits = false; + } + return done; + } + + function lookupKeyForEditor(cm, name, handle) { + for (var i = 0; i < cm.state.keyMaps.length; i++) { + var result = lookupKey(name, cm.state.keyMaps[i], handle, cm); + if (result) return result; + } + return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm)) + || lookupKey(name, cm.options.keyMap, handle, cm); + } + + var stopSeq = new Delayed; + function dispatchKey(cm, name, e, handle) { + var seq = cm.state.keySeq; + if (seq) { + if (isModifierKey(name)) return "handled"; + stopSeq.set(50, function() { + if (cm.state.keySeq == seq) { + cm.state.keySeq = null; + cm.display.input.reset(); + } + }); + name = seq + " " + name; + } + var result = lookupKeyForEditor(cm, name, handle); + + if (result == "multi") + cm.state.keySeq = name; + if (result == "handled") + signalLater(cm, "keyHandled", cm, name, e); + + if (result == "handled" || result == "multi") { + e_preventDefault(e); + restartBlink(cm); + } + + if (seq && !result && /\'$/.test(name)) { + e_preventDefault(e); + return true; + } + return !!result; + } + + // Handle a key from the keydown event. + function handleKeyBinding(cm, e) { + var name = keyName(e, true); + if (!name) return false; + + if (e.shiftKey && !cm.state.keySeq) { + // First try to resolve full name (including 'Shift-'). Failing + // that, see if there is a cursor-motion command (starting with + // 'go') bound to the keyname without 'Shift-'. + return dispatchKey(cm, "Shift-" + name, e, function(b) {return doHandleBinding(cm, b, true);}) + || dispatchKey(cm, name, e, function(b) { + if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion) + return doHandleBinding(cm, b); + }); + } else { + return dispatchKey(cm, name, e, function(b) { return doHandleBinding(cm, b); }); + } + } + + // Handle a key from the keypress event + function handleCharBinding(cm, e, ch) { + return dispatchKey(cm, "'" + ch + "'", e, + function(b) { return doHandleBinding(cm, b, true); }); + } + + var lastStoppedKey = null; + function onKeyDown(e) { + var cm = this; + cm.curOp.focus = activeElt(); + if (signalDOMEvent(cm, e)) return; + // IE does strange things with escape. + if (ie && ie_version < 11 && e.keyCode == 27) e.returnValue = false; + var code = e.keyCode; + cm.display.shift = code == 16 || e.shiftKey; + var handled = handleKeyBinding(cm, e); + if (presto) { + lastStoppedKey = handled ? code : null; + // Opera has no cut event... we try to at least catch the key combo + if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) + cm.replaceSelection("", null, "cut"); + } + + // Turn mouse into crosshair when Alt is held on Mac. + if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className)) + showCrossHair(cm); + } + + function showCrossHair(cm) { + var lineDiv = cm.display.lineDiv; + addClass(lineDiv, "CodeMirror-crosshair"); + + function up(e) { + if (e.keyCode == 18 || !e.altKey) { + rmClass(lineDiv, "CodeMirror-crosshair"); + off(document, "keyup", up); + off(document, "mouseover", up); + } + } + on(document, "keyup", up); + on(document, "mouseover", up); + } + + function onKeyUp(e) { + if (e.keyCode == 16) this.doc.sel.shift = false; + signalDOMEvent(this, e); + } + + function onKeyPress(e) { + var cm = this; + if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) return; + var keyCode = e.keyCode, charCode = e.charCode; + if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} + if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) return; + var ch = String.fromCharCode(charCode == null ? keyCode : charCode); + if (handleCharBinding(cm, e, ch)) return; + cm.display.input.onKeyPress(e); + } + + // FOCUS/BLUR EVENTS + + function delayBlurEvent(cm) { + cm.state.delayingBlurEvent = true; + setTimeout(function() { + if (cm.state.delayingBlurEvent) { + cm.state.delayingBlurEvent = false; + onBlur(cm); + } + }, 100); + } + + function onFocus(cm) { + if (cm.state.delayingBlurEvent) cm.state.delayingBlurEvent = false; + + if (cm.options.readOnly == "nocursor") return; + if (!cm.state.focused) { + signal(cm, "focus", cm); + cm.state.focused = true; + addClass(cm.display.wrapper, "CodeMirror-focused"); + // This test prevents this from firing when a context + // menu is closed (since the input reset would kill the + // select-all detection hack) + if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) { + cm.display.input.reset(); + if (webkit) setTimeout(function() { cm.display.input.reset(true); }, 20); // Issue #1730 + } + cm.display.input.receivedFocus(); + } + restartBlink(cm); + } + function onBlur(cm) { + if (cm.state.delayingBlurEvent) return; + + if (cm.state.focused) { + signal(cm, "blur", cm); + cm.state.focused = false; + rmClass(cm.display.wrapper, "CodeMirror-focused"); + } + clearInterval(cm.display.blinker); + setTimeout(function() {if (!cm.state.focused) cm.display.shift = false;}, 150); + } + + // CONTEXT MENU HANDLING + + // To make the context menu work, we need to briefly unhide the + // textarea (making it as unobtrusive as possible) to let the + // right-click take effect on it. + function onContextMenu(cm, e) { + if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) return; + cm.display.input.onContextMenu(e); + } + + function contextMenuInGutter(cm, e) { + if (!hasHandler(cm, "gutterContextMenu")) return false; + return gutterEvent(cm, e, "gutterContextMenu", false, signal); + } + + // UPDATING + + // Compute the position of the end of a change (its 'to' property + // refers to the pre-change end). + var changeEnd = CodeMirror.changeEnd = function(change) { + if (!change.text) return change.to; + return Pos(change.from.line + change.text.length - 1, + lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)); + }; + + // Adjust a position to refer to the post-change position of the + // same text, or the end of the change if the change covers it. + function adjustForChange(pos, change) { + if (cmp(pos, change.from) < 0) return pos; + if (cmp(pos, change.to) <= 0) return changeEnd(change); + + var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; + if (pos.line == change.to.line) ch += changeEnd(change).ch - change.to.ch; + return Pos(line, ch); + } + + function computeSelAfterChange(doc, change) { + var out = []; + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + out.push(new Range(adjustForChange(range.anchor, change), + adjustForChange(range.head, change))); + } + return normalizeSelection(out, doc.sel.primIndex); + } + + function offsetPos(pos, old, nw) { + if (pos.line == old.line) + return Pos(nw.line, pos.ch - old.ch + nw.ch); + else + return Pos(nw.line + (pos.line - old.line), pos.ch); + } + + // Used by replaceSelections to allow moving the selection to the + // start or around the replaced test. Hint may be "start" or "around". + function computeReplacedSel(doc, changes, hint) { + var out = []; + var oldPrev = Pos(doc.first, 0), newPrev = oldPrev; + for (var i = 0; i < changes.length; i++) { + var change = changes[i]; + var from = offsetPos(change.from, oldPrev, newPrev); + var to = offsetPos(changeEnd(change), oldPrev, newPrev); + oldPrev = change.to; + newPrev = to; + if (hint == "around") { + var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0; + out[i] = new Range(inv ? to : from, inv ? from : to); + } else { + out[i] = new Range(from, from); + } + } + return new Selection(out, doc.sel.primIndex); + } + + // Allow "beforeChange" event handlers to influence a change + function filterChange(doc, change, update) { + var obj = { + canceled: false, + from: change.from, + to: change.to, + text: change.text, + origin: change.origin, + cancel: function() { this.canceled = true; } + }; + if (update) obj.update = function(from, to, text, origin) { + if (from) this.from = clipPos(doc, from); + if (to) this.to = clipPos(doc, to); + if (text) this.text = text; + if (origin !== undefined) this.origin = origin; + }; + signal(doc, "beforeChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj); + + if (obj.canceled) return null; + return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin}; + } + + // Apply a change to a document, and add it to the document's + // history, and propagating it to all linked documents. + function makeChange(doc, change, ignoreReadOnly) { + if (doc.cm) { + if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly); + if (doc.cm.state.suppressEdits) return; + } + + if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { + change = filterChange(doc, change, true); + if (!change) return; + } + + // Possibly split or suppress the update based on the presence + // of read-only spans in its range. + var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to); + if (split) { + for (var i = split.length - 1; i >= 0; --i) + makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text}); + } else { + makeChangeInner(doc, change); + } + } + + function makeChangeInner(doc, change) { + if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return; + var selAfter = computeSelAfterChange(doc, change); + addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); + + makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)); + var rebased = []; + + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)); + }); + } + + // Revert a change stored in a document's history. + function makeChangeFromHistory(doc, type, allowSelectionOnly) { + if (doc.cm && doc.cm.state.suppressEdits) return; + + var hist = doc.history, event, selAfter = doc.sel; + var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done; + + // Verify that there is a useable event (so that ctrl-z won't + // needlessly clear selection events) + for (var i = 0; i < source.length; i++) { + event = source[i]; + if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges) + break; + } + if (i == source.length) return; + hist.lastOrigin = hist.lastSelOrigin = null; + + for (;;) { + event = source.pop(); + if (event.ranges) { + pushSelectionToHistory(event, dest); + if (allowSelectionOnly && !event.equals(doc.sel)) { + setSelection(doc, event, {clearRedo: false}); + return; + } + selAfter = event; + } + else break; + } + + // Build up a reverse change object to add to the opposite history + // stack (redo when undoing, and vice versa). + var antiChanges = []; + pushSelectionToHistory(selAfter, dest); + dest.push({changes: antiChanges, generation: hist.generation}); + hist.generation = event.generation || ++hist.maxGeneration; + + var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange"); + + for (var i = event.changes.length - 1; i >= 0; --i) { + var change = event.changes[i]; + change.origin = type; + if (filter && !filterChange(doc, change, false)) { + source.length = 0; + return; + } + + antiChanges.push(historyChangeFromChange(doc, change)); + + var after = i ? computeSelAfterChange(doc, change) : lst(source); + makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); + if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)}); + var rebased = []; + + // Propagate to the linked documents + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)); + }); + } + } + + // Sub-views need their line numbers shifted when text is added + // above or below them in the parent document. + function shiftDoc(doc, distance) { + if (distance == 0) return; + doc.first += distance; + doc.sel = new Selection(map(doc.sel.ranges, function(range) { + return new Range(Pos(range.anchor.line + distance, range.anchor.ch), + Pos(range.head.line + distance, range.head.ch)); + }), doc.sel.primIndex); + if (doc.cm) { + regChange(doc.cm, doc.first, doc.first - distance, distance); + for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++) + regLineChange(doc.cm, l, "gutter"); + } + } + + // More lower-level change function, handling only a single document + // (not linked ones). + function makeChangeSingleDoc(doc, change, selAfter, spans) { + if (doc.cm && !doc.cm.curOp) + return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans); + + if (change.to.line < doc.first) { + shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)); + return; + } + if (change.from.line > doc.lastLine()) return; + + // Clip the change to the size of this doc + if (change.from.line < doc.first) { + var shift = change.text.length - 1 - (doc.first - change.from.line); + shiftDoc(doc, shift); + change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), + text: [lst(change.text)], origin: change.origin}; + } + var last = doc.lastLine(); + if (change.to.line > last) { + change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), + text: [change.text[0]], origin: change.origin}; + } + + change.removed = getBetween(doc, change.from, change.to); + + if (!selAfter) selAfter = computeSelAfterChange(doc, change); + if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans); + else updateDoc(doc, change, spans); + setSelectionNoUndo(doc, selAfter, sel_dontScroll); + } + + // Handle the interaction of a change to a document with the editor + // that this document is part of. + function makeChangeSingleDocInEditor(cm, change, spans) { + var doc = cm.doc, display = cm.display, from = change.from, to = change.to; + + var recomputeMaxLength = false, checkWidthStart = from.line; + if (!cm.options.lineWrapping) { + checkWidthStart = lineNo(visualLine(getLine(doc, from.line))); + doc.iter(checkWidthStart, to.line + 1, function(line) { + if (line == display.maxLine) { + recomputeMaxLength = true; + return true; + } + }); + } + + if (doc.sel.contains(change.from, change.to) > -1) + signalCursorActivity(cm); + + updateDoc(doc, change, spans, estimateHeight(cm)); + + if (!cm.options.lineWrapping) { + doc.iter(checkWidthStart, from.line + change.text.length, function(line) { + var len = lineLength(line); + if (len > display.maxLineLength) { + display.maxLine = line; + display.maxLineLength = len; + display.maxLineChanged = true; + recomputeMaxLength = false; + } + }); + if (recomputeMaxLength) cm.curOp.updateMaxLine = true; + } + + // Adjust frontier, schedule worker + doc.frontier = Math.min(doc.frontier, from.line); + startWorker(cm, 400); + + var lendiff = change.text.length - (to.line - from.line) - 1; + // Remember that these lines changed, for updating the display + if (change.full) + regChange(cm); + else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change)) + regLineChange(cm, from.line, "text"); + else + regChange(cm, from.line, to.line + 1, lendiff); + + var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change"); + if (changeHandler || changesHandler) { + var obj = { + from: from, to: to, + text: change.text, + removed: change.removed, + origin: change.origin + }; + if (changeHandler) signalLater(cm, "change", cm, obj); + if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj); + } + cm.display.selForContextMenu = null; + } + + function replaceRange(doc, code, from, to, origin) { + if (!to) to = from; + if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp; } + if (typeof code == "string") code = splitLines(code); + makeChange(doc, {from: from, to: to, text: code, origin: origin}); + } + + // SCROLLING THINGS INTO VIEW + + // If an editor sits on the top or bottom of the window, partially + // scrolled out of view, this ensures that the cursor is visible. + function maybeScrollWindow(cm, coords) { + if (signalDOMEvent(cm, "scrollCursorIntoView")) return; + + var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null; + if (coords.top + box.top < 0) doScroll = true; + else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false; + if (doScroll != null && !phantom) { + var scrollNode = elt("div", "\u200b", null, "position: absolute; top: " + + (coords.top - display.viewOffset - paddingTop(cm.display)) + "px; height: " + + (coords.bottom - coords.top + scrollGap(cm) + display.barHeight) + "px; left: " + + coords.left + "px; width: 2px;"); + cm.display.lineSpace.appendChild(scrollNode); + scrollNode.scrollIntoView(doScroll); + cm.display.lineSpace.removeChild(scrollNode); + } + } + + // Scroll a given position into view (immediately), verifying that + // it actually became visible (as line heights are accurately + // measured, the position of something may 'drift' during drawing). + function scrollPosIntoView(cm, pos, end, margin) { + if (margin == null) margin = 0; + for (var limit = 0; limit < 5; limit++) { + var changed = false, coords = cursorCoords(cm, pos); + var endCoords = !end || end == pos ? coords : cursorCoords(cm, end); + var scrollPos = calculateScrollPos(cm, Math.min(coords.left, endCoords.left), + Math.min(coords.top, endCoords.top) - margin, + Math.max(coords.left, endCoords.left), + Math.max(coords.bottom, endCoords.bottom) + margin); + var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft; + if (scrollPos.scrollTop != null) { + setScrollTop(cm, scrollPos.scrollTop); + if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true; + } + if (scrollPos.scrollLeft != null) { + setScrollLeft(cm, scrollPos.scrollLeft); + if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true; + } + if (!changed) break; + } + return coords; + } + + // Scroll a given set of coordinates into view (immediately). + function scrollIntoView(cm, x1, y1, x2, y2) { + var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2); + if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop); + if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft); + } + + // Calculate a new scroll position needed to scroll the given + // rectangle into view. Returns an object with scrollTop and + // scrollLeft properties. When these are undefined, the + // vertical/horizontal position does not need to be adjusted. + function calculateScrollPos(cm, x1, y1, x2, y2) { + var display = cm.display, snapMargin = textHeight(cm.display); + if (y1 < 0) y1 = 0; + var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop; + var screen = displayHeight(cm), result = {}; + if (y2 - y1 > screen) y2 = y1 + screen; + var docBottom = cm.doc.height + paddingVert(display); + var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin; + if (y1 < screentop) { + result.scrollTop = atTop ? 0 : y1; + } else if (y2 > screentop + screen) { + var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen); + if (newTop != screentop) result.scrollTop = newTop; + } + + var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft; + var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0); + var tooWide = x2 - x1 > screenw; + if (tooWide) x2 = x1 + screenw; + if (x1 < 10) + result.scrollLeft = 0; + else if (x1 < screenleft) + result.scrollLeft = Math.max(0, x1 - (tooWide ? 0 : 10)); + else if (x2 > screenw + screenleft - 3) + result.scrollLeft = x2 + (tooWide ? 0 : 10) - screenw; + return result; + } + + // Store a relative adjustment to the scroll position in the current + // operation (to be applied when the operation finishes). + function addToScrollPos(cm, left, top) { + if (left != null || top != null) resolveScrollToPos(cm); + if (left != null) + cm.curOp.scrollLeft = (cm.curOp.scrollLeft == null ? cm.doc.scrollLeft : cm.curOp.scrollLeft) + left; + if (top != null) + cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top; + } + + // Make sure that at the end of the operation the current cursor is + // shown. + function ensureCursorVisible(cm) { + resolveScrollToPos(cm); + var cur = cm.getCursor(), from = cur, to = cur; + if (!cm.options.lineWrapping) { + from = cur.ch ? Pos(cur.line, cur.ch - 1) : cur; + to = Pos(cur.line, cur.ch + 1); + } + cm.curOp.scrollToPos = {from: from, to: to, margin: cm.options.cursorScrollMargin, isCursor: true}; + } + + // When an operation has its scrollToPos property set, and another + // scroll action is applied before the end of the operation, this + // 'simulates' scrolling that position into view in a cheap way, so + // that the effect of intermediate scroll commands is not ignored. + function resolveScrollToPos(cm) { + var range = cm.curOp.scrollToPos; + if (range) { + cm.curOp.scrollToPos = null; + var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to); + var sPos = calculateScrollPos(cm, Math.min(from.left, to.left), + Math.min(from.top, to.top) - range.margin, + Math.max(from.right, to.right), + Math.max(from.bottom, to.bottom) + range.margin); + cm.scrollTo(sPos.scrollLeft, sPos.scrollTop); + } + } + + // API UTILITIES + + // Indent the given line. The how parameter can be "smart", + // "add"/null, "subtract", or "prev". When aggressive is false + // (typically set to true for forced single-line indents), empty + // lines are not indented, and places where the mode returns Pass + // are left alone. + function indentLine(cm, n, how, aggressive) { + var doc = cm.doc, state; + if (how == null) how = "add"; + if (how == "smart") { + // Fall back to "prev" when the mode doesn't have an indentation + // method. + if (!doc.mode.indent) how = "prev"; + else state = getStateBefore(cm, n); + } + + var tabSize = cm.options.tabSize; + var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); + if (line.stateAfter) line.stateAfter = null; + var curSpaceString = line.text.match(/^\s*/)[0], indentation; + if (!aggressive && !/\S/.test(line.text)) { + indentation = 0; + how = "not"; + } else if (how == "smart") { + indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text); + if (indentation == Pass || indentation > 150) { + if (!aggressive) return; + how = "prev"; + } + } + if (how == "prev") { + if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize); + else indentation = 0; + } else if (how == "add") { + indentation = curSpace + cm.options.indentUnit; + } else if (how == "subtract") { + indentation = curSpace - cm.options.indentUnit; + } else if (typeof how == "number") { + indentation = curSpace + how; + } + indentation = Math.max(0, indentation); + + var indentString = "", pos = 0; + if (cm.options.indentWithTabs) + for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} + if (pos < indentation) indentString += spaceStr(indentation - pos); + + if (indentString != curSpaceString) { + replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); + } else { + // Ensure that, if the cursor was in the whitespace at the start + // of the line, it is moved to the end of that space. + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + if (range.head.line == n && range.head.ch < curSpaceString.length) { + var pos = Pos(n, curSpaceString.length); + replaceOneSelection(doc, i, new Range(pos, pos)); + break; + } + } + } + line.stateAfter = null; + } + + // Utility for applying a change to a line by handle or number, + // returning the number and optionally registering the line as + // changed. + function changeLine(doc, handle, changeType, op) { + var no = handle, line = handle; + if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle)); + else no = lineNo(handle); + if (no == null) return null; + if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType); + return line; + } + + // Helper for deleting text near the selection(s), used to implement + // backspace, delete, and similar functionality. + function deleteNearSelection(cm, compute) { + var ranges = cm.doc.sel.ranges, kill = []; + // Build up a set of ranges to kill first, merging overlapping + // ranges. + for (var i = 0; i < ranges.length; i++) { + var toKill = compute(ranges[i]); + while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) { + var replaced = kill.pop(); + if (cmp(replaced.from, toKill.from) < 0) { + toKill.from = replaced.from; + break; + } + } + kill.push(toKill); + } + // Next, remove those actual ranges. + runInOp(cm, function() { + for (var i = kill.length - 1; i >= 0; i--) + replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete"); + ensureCursorVisible(cm); + }); + } + + // Used for horizontal relative motion. Dir is -1 or 1 (left or + // right), unit can be "char", "column" (like char, but doesn't + // cross line boundaries), "word" (across next word), or "group" (to + // the start of next group of word or non-word-non-whitespace + // chars). The visually param controls whether, in right-to-left + // text, direction 1 means to move towards the next index in the + // string, or towards the character to the right of the current + // position. The resulting position will have a hitSide=true + // property if it reached the end of the document. + function findPosH(doc, pos, dir, unit, visually) { + var line = pos.line, ch = pos.ch, origDir = dir; + var lineObj = getLine(doc, line); + var possible = true; + function findNextLine() { + var l = line + dir; + if (l < doc.first || l >= doc.first + doc.size) return (possible = false); + line = l; + return lineObj = getLine(doc, l); + } + function moveOnce(boundToLine) { + var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true); + if (next == null) { + if (!boundToLine && findNextLine()) { + if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj); + else ch = dir < 0 ? lineObj.text.length : 0; + } else return (possible = false); + } else ch = next; + return true; + } + + if (unit == "char") moveOnce(); + else if (unit == "column") moveOnce(true); + else if (unit == "word" || unit == "group") { + var sawType = null, group = unit == "group"; + var helper = doc.cm && doc.cm.getHelper(pos, "wordChars"); + for (var first = true;; first = false) { + if (dir < 0 && !moveOnce(!first)) break; + var cur = lineObj.text.charAt(ch) || "\n"; + var type = isWordChar(cur, helper) ? "w" + : group && cur == "\n" ? "n" + : !group || /\s/.test(cur) ? null + : "p"; + if (group && !first && !type) type = "s"; + if (sawType && sawType != type) { + if (dir < 0) {dir = 1; moveOnce();} + break; + } + + if (type) sawType = type; + if (dir > 0 && !moveOnce(!first)) break; + } + } + var result = skipAtomic(doc, Pos(line, ch), origDir, true); + if (!possible) result.hitSide = true; + return result; + } + + // For relative vertical movement. Dir may be -1 or 1. Unit can be + // "page" or "line". The resulting position will have a hitSide=true + // property if it reached the end of the document. + function findPosV(cm, pos, dir, unit) { + var doc = cm.doc, x = pos.left, y; + if (unit == "page") { + var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight); + y = pos.top + dir * (pageSize - (dir < 0 ? 1.5 : .5) * textHeight(cm.display)); + } else if (unit == "line") { + y = dir > 0 ? pos.bottom + 3 : pos.top - 3; + } + for (;;) { + var target = coordsChar(cm, x, y); + if (!target.outside) break; + if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; } + y += dir * 5; + } + return target; + } + + // EDITOR METHODS + + // The publicly visible API. Note that methodOp(f) means + // 'wrap f in an operation, performed on its `this` parameter'. + + // This is not the complete set of editor methods. Most of the + // methods defined on the Doc type are also injected into + // CodeMirror.prototype, for backwards compatibility and + // convenience. + + CodeMirror.prototype = { + constructor: CodeMirror, + focus: function(){window.focus(); this.display.input.focus();}, + + setOption: function(option, value) { + var options = this.options, old = options[option]; + if (options[option] == value && option != "mode") return; + options[option] = value; + if (optionHandlers.hasOwnProperty(option)) + operation(this, optionHandlers[option])(this, value, old); + }, + + getOption: function(option) {return this.options[option];}, + getDoc: function() {return this.doc;}, + + addKeyMap: function(map, bottom) { + this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map)); + }, + removeKeyMap: function(map) { + var maps = this.state.keyMaps; + for (var i = 0; i < maps.length; ++i) + if (maps[i] == map || maps[i].name == map) { + maps.splice(i, 1); + return true; + } + }, + + addOverlay: methodOp(function(spec, options) { + var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec); + if (mode.startState) throw new Error("Overlays may not be stateful."); + this.state.overlays.push({mode: mode, modeSpec: spec, opaque: options && options.opaque}); + this.state.modeGen++; + regChange(this); + }), + removeOverlay: methodOp(function(spec) { + var overlays = this.state.overlays; + for (var i = 0; i < overlays.length; ++i) { + var cur = overlays[i].modeSpec; + if (cur == spec || typeof spec == "string" && cur.name == spec) { + overlays.splice(i, 1); + this.state.modeGen++; + regChange(this); + return; + } + } + }), + + indentLine: methodOp(function(n, dir, aggressive) { + if (typeof dir != "string" && typeof dir != "number") { + if (dir == null) dir = this.options.smartIndent ? "smart" : "prev"; + else dir = dir ? "add" : "subtract"; + } + if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive); + }), + indentSelection: methodOp(function(how) { + var ranges = this.doc.sel.ranges, end = -1; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + if (!range.empty()) { + var from = range.from(), to = range.to(); + var start = Math.max(end, from.line); + end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1; + for (var j = start; j < end; ++j) + indentLine(this, j, how); + var newRanges = this.doc.sel.ranges; + if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0) + replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll); + } else if (range.head.line > end) { + indentLine(this, range.head.line, how, true); + end = range.head.line; + if (i == this.doc.sel.primIndex) ensureCursorVisible(this); + } + } + }), + + // Fetch the parser token for a given character. Useful for hacks + // that want to inspect the mode state (say, for completion). + getTokenAt: function(pos, precise) { + return takeToken(this, pos, precise); + }, + + getLineTokens: function(line, precise) { + return takeToken(this, Pos(line), precise, true); + }, + + getTokenTypeAt: function(pos) { + pos = clipPos(this.doc, pos); + var styles = getLineStyles(this, getLine(this.doc, pos.line)); + var before = 0, after = (styles.length - 1) / 2, ch = pos.ch; + var type; + if (ch == 0) type = styles[2]; + else for (;;) { + var mid = (before + after) >> 1; + if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid; + else if (styles[mid * 2 + 1] < ch) before = mid + 1; + else { type = styles[mid * 2 + 2]; break; } + } + var cut = type ? type.indexOf("cm-overlay ") : -1; + return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1); + }, + + getModeAt: function(pos) { + var mode = this.doc.mode; + if (!mode.innerMode) return mode; + return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode; + }, + + getHelper: function(pos, type) { + return this.getHelpers(pos, type)[0]; + }, + + getHelpers: function(pos, type) { + var found = []; + if (!helpers.hasOwnProperty(type)) return found; + var help = helpers[type], mode = this.getModeAt(pos); + if (typeof mode[type] == "string") { + if (help[mode[type]]) found.push(help[mode[type]]); + } else if (mode[type]) { + for (var i = 0; i < mode[type].length; i++) { + var val = help[mode[type][i]]; + if (val) found.push(val); + } + } else if (mode.helperType && help[mode.helperType]) { + found.push(help[mode.helperType]); + } else if (help[mode.name]) { + found.push(help[mode.name]); + } + for (var i = 0; i < help._global.length; i++) { + var cur = help._global[i]; + if (cur.pred(mode, this) && indexOf(found, cur.val) == -1) + found.push(cur.val); + } + return found; + }, + + getStateAfter: function(line, precise) { + var doc = this.doc; + line = clipLine(doc, line == null ? doc.first + doc.size - 1: line); + return getStateBefore(this, line + 1, precise); + }, + + cursorCoords: function(start, mode) { + var pos, range = this.doc.sel.primary(); + if (start == null) pos = range.head; + else if (typeof start == "object") pos = clipPos(this.doc, start); + else pos = start ? range.from() : range.to(); + return cursorCoords(this, pos, mode || "page"); + }, + + charCoords: function(pos, mode) { + return charCoords(this, clipPos(this.doc, pos), mode || "page"); + }, + + coordsChar: function(coords, mode) { + coords = fromCoordSystem(this, coords, mode || "page"); + return coordsChar(this, coords.left, coords.top); + }, + + lineAtHeight: function(height, mode) { + height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top; + return lineAtHeight(this.doc, height + this.display.viewOffset); + }, + heightAtLine: function(line, mode) { + var end = false, lineObj; + if (typeof line == "number") { + var last = this.doc.first + this.doc.size - 1; + if (line < this.doc.first) line = this.doc.first; + else if (line > last) { line = last; end = true; } + lineObj = getLine(this.doc, line); + } else { + lineObj = line; + } + return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page").top + + (end ? this.doc.height - heightAtLine(lineObj) : 0); + }, + + defaultTextHeight: function() { return textHeight(this.display); }, + defaultCharWidth: function() { return charWidth(this.display); }, + + setGutterMarker: methodOp(function(line, gutterID, value) { + return changeLine(this.doc, line, "gutter", function(line) { + var markers = line.gutterMarkers || (line.gutterMarkers = {}); + markers[gutterID] = value; + if (!value && isEmpty(markers)) line.gutterMarkers = null; + return true; + }); + }), + + clearGutter: methodOp(function(gutterID) { + var cm = this, doc = cm.doc, i = doc.first; + doc.iter(function(line) { + if (line.gutterMarkers && line.gutterMarkers[gutterID]) { + line.gutterMarkers[gutterID] = null; + regLineChange(cm, i, "gutter"); + if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null; + } + ++i; + }); + }), + + lineInfo: function(line) { + if (typeof line == "number") { + if (!isLine(this.doc, line)) return null; + var n = line; + line = getLine(this.doc, line); + if (!line) return null; + } else { + var n = lineNo(line); + if (n == null) return null; + } + return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, + textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, + widgets: line.widgets}; + }, + + getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo};}, + + addWidget: function(pos, node, scroll, vert, horiz) { + var display = this.display; + pos = cursorCoords(this, clipPos(this.doc, pos)); + var top = pos.bottom, left = pos.left; + node.style.position = "absolute"; + node.setAttribute("cm-ignore-events", "true"); + this.display.input.setUneditable(node); + display.sizer.appendChild(node); + if (vert == "over") { + top = pos.top; + } else if (vert == "above" || vert == "near") { + var vspace = Math.max(display.wrapper.clientHeight, this.doc.height), + hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth); + // Default to positioning above (if specified and possible); otherwise default to positioning below + if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) + top = pos.top - node.offsetHeight; + else if (pos.bottom + node.offsetHeight <= vspace) + top = pos.bottom; + if (left + node.offsetWidth > hspace) + left = hspace - node.offsetWidth; + } + node.style.top = top + "px"; + node.style.left = node.style.right = ""; + if (horiz == "right") { + left = display.sizer.clientWidth - node.offsetWidth; + node.style.right = "0px"; + } else { + if (horiz == "left") left = 0; + else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2; + node.style.left = left + "px"; + } + if (scroll) + scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight); + }, + + triggerOnKeyDown: methodOp(onKeyDown), + triggerOnKeyPress: methodOp(onKeyPress), + triggerOnKeyUp: onKeyUp, + + execCommand: function(cmd) { + if (commands.hasOwnProperty(cmd)) + return commands[cmd](this); + }, + + findPosH: function(from, amount, unit, visually) { + var dir = 1; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + cur = findPosH(this.doc, cur, dir, unit, visually); + if (cur.hitSide) break; + } + return cur; + }, + + moveH: methodOp(function(dir, unit) { + var cm = this; + cm.extendSelectionsBy(function(range) { + if (cm.display.shift || cm.doc.extend || range.empty()) + return findPosH(cm.doc, range.head, dir, unit, cm.options.rtlMoveVisually); + else + return dir < 0 ? range.from() : range.to(); + }, sel_move); + }), + + deleteH: methodOp(function(dir, unit) { + var sel = this.doc.sel, doc = this.doc; + if (sel.somethingSelected()) + doc.replaceSelection("", null, "+delete"); + else + deleteNearSelection(this, function(range) { + var other = findPosH(doc, range.head, dir, unit, false); + return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other}; + }); + }), + + findPosV: function(from, amount, unit, goalColumn) { + var dir = 1, x = goalColumn; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + var coords = cursorCoords(this, cur, "div"); + if (x == null) x = coords.left; + else coords.left = x; + cur = findPosV(this, coords, dir, unit); + if (cur.hitSide) break; + } + return cur; + }, + + moveV: methodOp(function(dir, unit) { + var cm = this, doc = this.doc, goals = []; + var collapse = !cm.display.shift && !doc.extend && doc.sel.somethingSelected(); + doc.extendSelectionsBy(function(range) { + if (collapse) + return dir < 0 ? range.from() : range.to(); + var headPos = cursorCoords(cm, range.head, "div"); + if (range.goalColumn != null) headPos.left = range.goalColumn; + goals.push(headPos.left); + var pos = findPosV(cm, headPos, dir, unit); + if (unit == "page" && range == doc.sel.primary()) + addToScrollPos(cm, null, charCoords(cm, pos, "div").top - headPos.top); + return pos; + }, sel_move); + if (goals.length) for (var i = 0; i < doc.sel.ranges.length; i++) + doc.sel.ranges[i].goalColumn = goals[i]; + }), + + // Find the word at the given position (as returned by coordsChar). + findWordAt: function(pos) { + var doc = this.doc, line = getLine(doc, pos.line).text; + var start = pos.ch, end = pos.ch; + if (line) { + var helper = this.getHelper(pos, "wordChars"); + if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end; + var startChar = line.charAt(start); + var check = isWordChar(startChar, helper) + ? function(ch) { return isWordChar(ch, helper); } + : /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);} + : function(ch) {return !/\s/.test(ch) && !isWordChar(ch);}; + while (start > 0 && check(line.charAt(start - 1))) --start; + while (end < line.length && check(line.charAt(end))) ++end; + } + return new Range(Pos(pos.line, start), Pos(pos.line, end)); + }, + + toggleOverwrite: function(value) { + if (value != null && value == this.state.overwrite) return; + if (this.state.overwrite = !this.state.overwrite) + addClass(this.display.cursorDiv, "CodeMirror-overwrite"); + else + rmClass(this.display.cursorDiv, "CodeMirror-overwrite"); + + signal(this, "overwriteToggle", this, this.state.overwrite); + }, + hasFocus: function() { return this.display.input.getField() == activeElt(); }, + + scrollTo: methodOp(function(x, y) { + if (x != null || y != null) resolveScrollToPos(this); + if (x != null) this.curOp.scrollLeft = x; + if (y != null) this.curOp.scrollTop = y; + }), + getScrollInfo: function() { + var scroller = this.display.scroller; + return {left: scroller.scrollLeft, top: scroller.scrollTop, + height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight, + width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth, + clientHeight: displayHeight(this), clientWidth: displayWidth(this)}; + }, + + scrollIntoView: methodOp(function(range, margin) { + if (range == null) { + range = {from: this.doc.sel.primary().head, to: null}; + if (margin == null) margin = this.options.cursorScrollMargin; + } else if (typeof range == "number") { + range = {from: Pos(range, 0), to: null}; + } else if (range.from == null) { + range = {from: range, to: null}; + } + if (!range.to) range.to = range.from; + range.margin = margin || 0; + + if (range.from.line != null) { + resolveScrollToPos(this); + this.curOp.scrollToPos = range; + } else { + var sPos = calculateScrollPos(this, Math.min(range.from.left, range.to.left), + Math.min(range.from.top, range.to.top) - range.margin, + Math.max(range.from.right, range.to.right), + Math.max(range.from.bottom, range.to.bottom) + range.margin); + this.scrollTo(sPos.scrollLeft, sPos.scrollTop); + } + }), + + setSize: methodOp(function(width, height) { + var cm = this; + function interpret(val) { + return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; + } + if (width != null) cm.display.wrapper.style.width = interpret(width); + if (height != null) cm.display.wrapper.style.height = interpret(height); + if (cm.options.lineWrapping) clearLineMeasurementCache(this); + var lineNo = cm.display.viewFrom; + cm.doc.iter(lineNo, cm.display.viewTo, function(line) { + if (line.widgets) for (var i = 0; i < line.widgets.length; i++) + if (line.widgets[i].noHScroll) { regLineChange(cm, lineNo, "widget"); break; } + ++lineNo; + }); + cm.curOp.forceUpdate = true; + signal(cm, "refresh", this); + }), + + operation: function(f){return runInOp(this, f);}, + + refresh: methodOp(function() { + var oldHeight = this.display.cachedTextHeight; + regChange(this); + this.curOp.forceUpdate = true; + clearCaches(this); + this.scrollTo(this.doc.scrollLeft, this.doc.scrollTop); + updateGutterSpace(this); + if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5) + estimateLineHeights(this); + signal(this, "refresh", this); + }), + + swapDoc: methodOp(function(doc) { + var old = this.doc; + old.cm = null; + attachDoc(this, doc); + clearCaches(this); + this.display.input.reset(); + this.scrollTo(doc.scrollLeft, doc.scrollTop); + this.curOp.forceScroll = true; + signalLater(this, "swapDoc", this, old); + return old; + }), + + getInputField: function(){return this.display.input.getField();}, + getWrapperElement: function(){return this.display.wrapper;}, + getScrollerElement: function(){return this.display.scroller;}, + getGutterElement: function(){return this.display.gutters;} + }; + eventMixin(CodeMirror); + + // OPTION DEFAULTS + + // The default configuration options. + var defaults = CodeMirror.defaults = {}; + // Functions to run when options are changed. + var optionHandlers = CodeMirror.optionHandlers = {}; + + function option(name, deflt, handle, notOnInit) { + CodeMirror.defaults[name] = deflt; + if (handle) optionHandlers[name] = + notOnInit ? function(cm, val, old) {if (old != Init) handle(cm, val, old);} : handle; + } + + // Passed to option handlers when there is no old value. + var Init = CodeMirror.Init = {toString: function(){return "CodeMirror.Init";}}; + + // These two are, on init, called from the constructor because they + // have to be initialized before the editor can start at all. + option("value", "", function(cm, val) { + cm.setValue(val); + }, true); + option("mode", null, function(cm, val) { + cm.doc.modeOption = val; + loadMode(cm); + }, true); + + option("indentUnit", 2, loadMode, true); + option("indentWithTabs", false); + option("smartIndent", true); + option("tabSize", 4, function(cm) { + resetModeState(cm); + clearCaches(cm); + regChange(cm); + }, true); + option("specialChars", /[\t\u0000-\u0019\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g, function(cm, val, old) { + cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); + if (old != CodeMirror.Init) cm.refresh(); + }); + option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function(cm) {cm.refresh();}, true); + option("electricChars", true); + option("inputStyle", mobile ? "contenteditable" : "textarea", function() { + throw new Error("inputStyle can not (yet) be changed in a running editor"); // FIXME + }, true); + option("rtlMoveVisually", !windows); + option("wholeLineUpdateBefore", true); + + option("theme", "default", function(cm) { + themeChanged(cm); + guttersChanged(cm); + }, true); + option("keyMap", "default", function(cm, val, old) { + var next = getKeyMap(val); + var prev = old != CodeMirror.Init && getKeyMap(old); + if (prev && prev.detach) prev.detach(cm, next); + if (next.attach) next.attach(cm, prev || null); + }); + option("extraKeys", null); + + option("lineWrapping", false, wrappingChanged, true); + option("gutters", [], function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("fixedGutter", true, function(cm, val) { + cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; + cm.refresh(); + }, true); + option("coverGutterNextToScrollbar", false, function(cm) {updateScrollbars(cm);}, true); + option("scrollbarStyle", "native", function(cm) { + initScrollbars(cm); + updateScrollbars(cm); + cm.display.scrollbars.setScrollTop(cm.doc.scrollTop); + cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft); + }, true); + option("lineNumbers", false, function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("firstLineNumber", 1, guttersChanged, true); + option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true); + option("showCursorWhenSelecting", false, updateSelection, true); + + option("resetSelectionOnContextMenu", true); + option("lineWiseCopyCut", true); + + option("readOnly", false, function(cm, val) { + if (val == "nocursor") { + onBlur(cm); + cm.display.input.blur(); + cm.display.disabled = true; + } else { + cm.display.disabled = false; + if (!val) cm.display.input.reset(); + } + }); + option("disableInput", false, function(cm, val) {if (!val) cm.display.input.reset();}, true); + option("dragDrop", true, dragDropChanged); + + option("cursorBlinkRate", 530); + option("cursorScrollMargin", 0); + option("cursorHeight", 1, updateSelection, true); + option("singleCursorHeightPerLine", true, updateSelection, true); + option("workTime", 100); + option("workDelay", 100); + option("flattenSpans", true, resetModeState, true); + option("addModeClass", false, resetModeState, true); + option("pollInterval", 100); + option("undoDepth", 200, function(cm, val){cm.doc.history.undoDepth = val;}); + option("historyEventDelay", 1250); + option("viewportMargin", 10, function(cm){cm.refresh();}, true); + option("maxHighlightLength", 10000, resetModeState, true); + option("moveInputWithCursor", true, function(cm, val) { + if (!val) cm.display.input.resetPosition(); + }); + + option("tabindex", null, function(cm, val) { + cm.display.input.getField().tabIndex = val || ""; + }); + option("autofocus", null); + + // MODE DEFINITION AND QUERYING + + // Known modes, by name and by MIME + var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {}; + + // Extra arguments are stored as the mode's dependencies, which is + // used by (legacy) mechanisms like loadmode.js to automatically + // load a mode. (Preferred mechanism is the require/define calls.) + CodeMirror.defineMode = function(name, mode) { + if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name; + if (arguments.length > 2) + mode.dependencies = Array.prototype.slice.call(arguments, 2); + modes[name] = mode; + }; + + CodeMirror.defineMIME = function(mime, spec) { + mimeModes[mime] = spec; + }; + + // Given a MIME type, a {name, ...options} config object, or a name + // string, return a mode config object. + CodeMirror.resolveMode = function(spec) { + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { + spec = mimeModes[spec]; + } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { + var found = mimeModes[spec.name]; + if (typeof found == "string") found = {name: found}; + spec = createObj(found, spec); + spec.name = found.name; + } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { + return CodeMirror.resolveMode("application/xml"); + } + if (typeof spec == "string") return {name: spec}; + else return spec || {name: "null"}; + }; + + // Given a mode spec (anything that resolveMode accepts), find and + // initialize an actual mode object. + CodeMirror.getMode = function(options, spec) { + var spec = CodeMirror.resolveMode(spec); + var mfactory = modes[spec.name]; + if (!mfactory) return CodeMirror.getMode(options, "text/plain"); + var modeObj = mfactory(options, spec); + if (modeExtensions.hasOwnProperty(spec.name)) { + var exts = modeExtensions[spec.name]; + for (var prop in exts) { + if (!exts.hasOwnProperty(prop)) continue; + if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop]; + modeObj[prop] = exts[prop]; + } + } + modeObj.name = spec.name; + if (spec.helperType) modeObj.helperType = spec.helperType; + if (spec.modeProps) for (var prop in spec.modeProps) + modeObj[prop] = spec.modeProps[prop]; + + return modeObj; + }; + + // Minimal default mode. + CodeMirror.defineMode("null", function() { + return {token: function(stream) {stream.skipToEnd();}}; + }); + CodeMirror.defineMIME("text/plain", "null"); + + // This can be used to attach properties to mode objects from + // outside the actual mode definition. + var modeExtensions = CodeMirror.modeExtensions = {}; + CodeMirror.extendMode = function(mode, properties) { + var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); + copyObj(properties, exts); + }; + + // EXTENSIONS + + CodeMirror.defineExtension = function(name, func) { + CodeMirror.prototype[name] = func; + }; + CodeMirror.defineDocExtension = function(name, func) { + Doc.prototype[name] = func; + }; + CodeMirror.defineOption = option; + + var initHooks = []; + CodeMirror.defineInitHook = function(f) {initHooks.push(f);}; + + var helpers = CodeMirror.helpers = {}; + CodeMirror.registerHelper = function(type, name, value) { + if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []}; + helpers[type][name] = value; + }; + CodeMirror.registerGlobalHelper = function(type, name, predicate, value) { + CodeMirror.registerHelper(type, name, value); + helpers[type]._global.push({pred: predicate, val: value}); + }; + + // MODE STATE HANDLING + + // Utility functions for working with state. Exported because nested + // modes need to do this for their inner modes. + + var copyState = CodeMirror.copyState = function(mode, state) { + if (state === true) return state; + if (mode.copyState) return mode.copyState(state); + var nstate = {}; + for (var n in state) { + var val = state[n]; + if (val instanceof Array) val = val.concat([]); + nstate[n] = val; + } + return nstate; + }; + + var startState = CodeMirror.startState = function(mode, a1, a2) { + return mode.startState ? mode.startState(a1, a2) : true; + }; + + // Given a mode and a state (for that mode), find the inner mode and + // state at the position that the state refers to. + CodeMirror.innerMode = function(mode, state) { + while (mode.innerMode) { + var info = mode.innerMode(state); + if (!info || info.mode == mode) break; + state = info.state; + mode = info.mode; + } + return info || {mode: mode, state: state}; + }; + + // STANDARD COMMANDS + + // Commands are parameter-less actions that can be performed on an + // editor, mostly used for keybindings. + var commands = CodeMirror.commands = { + selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll);}, + singleSelection: function(cm) { + cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll); + }, + killLine: function(cm) { + deleteNearSelection(cm, function(range) { + if (range.empty()) { + var len = getLine(cm.doc, range.head.line).text.length; + if (range.head.ch == len && range.head.line < cm.lastLine()) + return {from: range.head, to: Pos(range.head.line + 1, 0)}; + else + return {from: range.head, to: Pos(range.head.line, len)}; + } else { + return {from: range.from(), to: range.to()}; + } + }); + }, + deleteLine: function(cm) { + deleteNearSelection(cm, function(range) { + return {from: Pos(range.from().line, 0), + to: clipPos(cm.doc, Pos(range.to().line + 1, 0))}; + }); + }, + delLineLeft: function(cm) { + deleteNearSelection(cm, function(range) { + return {from: Pos(range.from().line, 0), to: range.from()}; + }); + }, + delWrappedLineLeft: function(cm) { + deleteNearSelection(cm, function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var leftPos = cm.coordsChar({left: 0, top: top}, "div"); + return {from: leftPos, to: range.from()}; + }); + }, + delWrappedLineRight: function(cm) { + deleteNearSelection(cm, function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + return {from: range.from(), to: rightPos }; + }); + }, + undo: function(cm) {cm.undo();}, + redo: function(cm) {cm.redo();}, + undoSelection: function(cm) {cm.undoSelection();}, + redoSelection: function(cm) {cm.redoSelection();}, + goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));}, + goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));}, + goLineStart: function(cm) { + cm.extendSelectionsBy(function(range) { return lineStart(cm, range.head.line); }, + {origin: "+move", bias: 1}); + }, + goLineStartSmart: function(cm) { + cm.extendSelectionsBy(function(range) { + return lineStartSmart(cm, range.head); + }, {origin: "+move", bias: 1}); + }, + goLineEnd: function(cm) { + cm.extendSelectionsBy(function(range) { return lineEnd(cm, range.head.line); }, + {origin: "+move", bias: -1}); + }, + goLineRight: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + }, sel_move); + }, + goLineLeft: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + return cm.coordsChar({left: 0, top: top}, "div"); + }, sel_move); + }, + goLineLeftSmart: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var pos = cm.coordsChar({left: 0, top: top}, "div"); + if (pos.ch < cm.getLine(pos.line).search(/\S/)) return lineStartSmart(cm, range.head); + return pos; + }, sel_move); + }, + goLineUp: function(cm) {cm.moveV(-1, "line");}, + goLineDown: function(cm) {cm.moveV(1, "line");}, + goPageUp: function(cm) {cm.moveV(-1, "page");}, + goPageDown: function(cm) {cm.moveV(1, "page");}, + goCharLeft: function(cm) {cm.moveH(-1, "char");}, + goCharRight: function(cm) {cm.moveH(1, "char");}, + goColumnLeft: function(cm) {cm.moveH(-1, "column");}, + goColumnRight: function(cm) {cm.moveH(1, "column");}, + goWordLeft: function(cm) {cm.moveH(-1, "word");}, + goGroupRight: function(cm) {cm.moveH(1, "group");}, + goGroupLeft: function(cm) {cm.moveH(-1, "group");}, + goWordRight: function(cm) {cm.moveH(1, "word");}, + delCharBefore: function(cm) {cm.deleteH(-1, "char");}, + delCharAfter: function(cm) {cm.deleteH(1, "char");}, + delWordBefore: function(cm) {cm.deleteH(-1, "word");}, + delWordAfter: function(cm) {cm.deleteH(1, "word");}, + delGroupBefore: function(cm) {cm.deleteH(-1, "group");}, + delGroupAfter: function(cm) {cm.deleteH(1, "group");}, + indentAuto: function(cm) {cm.indentSelection("smart");}, + indentMore: function(cm) {cm.indentSelection("add");}, + indentLess: function(cm) {cm.indentSelection("subtract");}, + insertTab: function(cm) {cm.replaceSelection("\t");}, + insertSoftTab: function(cm) { + var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize; + for (var i = 0; i < ranges.length; i++) { + var pos = ranges[i].from(); + var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize); + spaces.push(new Array(tabSize - col % tabSize + 1).join(" ")); + } + cm.replaceSelections(spaces); + }, + defaultTab: function(cm) { + if (cm.somethingSelected()) cm.indentSelection("add"); + else cm.execCommand("insertTab"); + }, + transposeChars: function(cm) { + runInOp(cm, function() { + var ranges = cm.listSelections(), newSel = []; + for (var i = 0; i < ranges.length; i++) { + var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text; + if (line) { + if (cur.ch == line.length) cur = new Pos(cur.line, cur.ch - 1); + if (cur.ch > 0) { + cur = new Pos(cur.line, cur.ch + 1); + cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2), + Pos(cur.line, cur.ch - 2), cur, "+transpose"); + } else if (cur.line > cm.doc.first) { + var prev = getLine(cm.doc, cur.line - 1).text; + if (prev) + cm.replaceRange(line.charAt(0) + "\n" + prev.charAt(prev.length - 1), + Pos(cur.line - 1, prev.length - 1), Pos(cur.line, 1), "+transpose"); + } + } + newSel.push(new Range(cur, cur)); + } + cm.setSelections(newSel); + }); + }, + newlineAndIndent: function(cm) { + runInOp(cm, function() { + var len = cm.listSelections().length; + for (var i = 0; i < len; i++) { + var range = cm.listSelections()[i]; + cm.replaceRange("\n", range.anchor, range.head, "+input"); + cm.indentLine(range.from().line + 1, null, true); + ensureCursorVisible(cm); + } + }); + }, + toggleOverwrite: function(cm) {cm.toggleOverwrite();} + }; + + + // STANDARD KEYMAPS + + var keyMap = CodeMirror.keyMap = {}; + + keyMap.basic = { + "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", + "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", + "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", + "Tab": "defaultTab", "Shift-Tab": "indentAuto", + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite", + "Esc": "singleSelection" + }; + // Note that the save and find-related commands aren't defined by + // default. User code or addons can define them. Unknown commands + // are simply ignored. + keyMap.pcDefault = { + "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", + "Ctrl-Home": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown", + "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", + "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", + "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", + "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", + "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection", + fallthrough: "basic" + }; + // Very basic readline/emacs-style bindings, which are standard on Mac. + keyMap.emacsy = { + "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", + "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", + "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars" + }; + keyMap.macDefault = { + "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", + "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft", + "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore", + "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", + "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", + "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight", + "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd", + fallthrough: ["basic", "emacsy"] + }; + keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + + // KEYMAP DISPATCH + + function normalizeKeyName(name) { + var parts = name.split(/-(?!$)/), name = parts[parts.length - 1]; + var alt, ctrl, shift, cmd; + for (var i = 0; i < parts.length - 1; i++) { + var mod = parts[i]; + if (/^(cmd|meta|m)$/i.test(mod)) cmd = true; + else if (/^a(lt)?$/i.test(mod)) alt = true; + else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true; + else if (/^s(hift)$/i.test(mod)) shift = true; + else throw new Error("Unrecognized modifier name: " + mod); + } + if (alt) name = "Alt-" + name; + if (ctrl) name = "Ctrl-" + name; + if (cmd) name = "Cmd-" + name; + if (shift) name = "Shift-" + name; + return name; + } + + // This is a kludge to keep keymaps mostly working as raw objects + // (backwards compatibility) while at the same time support features + // like normalization and multi-stroke key bindings. It compiles a + // new normalized keymap, and then updates the old object to reflect + // this. + CodeMirror.normalizeKeyMap = function(keymap) { + var copy = {}; + for (var keyname in keymap) if (keymap.hasOwnProperty(keyname)) { + var value = keymap[keyname]; + if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) continue; + if (value == "...") { delete keymap[keyname]; continue; } + + var keys = map(keyname.split(" "), normalizeKeyName); + for (var i = 0; i < keys.length; i++) { + var val, name; + if (i == keys.length - 1) { + name = keyname; + val = value; + } else { + name = keys.slice(0, i + 1).join(" "); + val = "..."; + } + var prev = copy[name]; + if (!prev) copy[name] = val; + else if (prev != val) throw new Error("Inconsistent bindings for " + name); + } + delete keymap[keyname]; + } + for (var prop in copy) keymap[prop] = copy[prop]; + return keymap; + }; + + var lookupKey = CodeMirror.lookupKey = function(key, map, handle, context) { + map = getKeyMap(map); + var found = map.call ? map.call(key, context) : map[key]; + if (found === false) return "nothing"; + if (found === "...") return "multi"; + if (found != null && handle(found)) return "handled"; + + if (map.fallthrough) { + if (Object.prototype.toString.call(map.fallthrough) != "[object Array]") + return lookupKey(key, map.fallthrough, handle, context); + for (var i = 0; i < map.fallthrough.length; i++) { + var result = lookupKey(key, map.fallthrough[i], handle, context); + if (result) return result; + } + } + }; + + // Modifier key presses don't count as 'real' key presses for the + // purpose of keymap fallthrough. + var isModifierKey = CodeMirror.isModifierKey = function(value) { + var name = typeof value == "string" ? value : keyNames[value.keyCode]; + return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod"; + }; + + // Look up the name of a key as indicated by an event object. + var keyName = CodeMirror.keyName = function(event, noShift) { + if (presto && event.keyCode == 34 && event["char"]) return false; + var base = keyNames[event.keyCode], name = base; + if (name == null || event.altGraphKey) return false; + if (event.altKey && base != "Alt") name = "Alt-" + name; + if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") name = "Ctrl-" + name; + if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Cmd") name = "Cmd-" + name; + if (!noShift && event.shiftKey && base != "Shift") name = "Shift-" + name; + return name; + }; + + function getKeyMap(val) { + return typeof val == "string" ? keyMap[val] : val; + } + + // FROMTEXTAREA + + CodeMirror.fromTextArea = function(textarea, options) { + options = options ? copyObj(options) : {}; + options.value = textarea.value; + if (!options.tabindex && textarea.tabIndex) + options.tabindex = textarea.tabIndex; + if (!options.placeholder && textarea.placeholder) + options.placeholder = textarea.placeholder; + // Set autofocus to true if this textarea is focused, or if it has + // autofocus and no other element is focused. + if (options.autofocus == null) { + var hasFocus = activeElt(); + options.autofocus = hasFocus == textarea || + textarea.getAttribute("autofocus") != null && hasFocus == document.body; + } + + function save() {textarea.value = cm.getValue();} + if (textarea.form) { + on(textarea.form, "submit", save); + // Deplorable hack to make the submit method do the right thing. + if (!options.leaveSubmitMethodAlone) { + var form = textarea.form, realSubmit = form.submit; + try { + var wrappedSubmit = form.submit = function() { + save(); + form.submit = realSubmit; + form.submit(); + form.submit = wrappedSubmit; + }; + } catch(e) {} + } + } + + options.finishInit = function(cm) { + cm.save = save; + cm.getTextArea = function() { return textarea; }; + cm.toTextArea = function() { + cm.toTextArea = isNaN; // Prevent this from being ran twice + save(); + textarea.parentNode.removeChild(cm.getWrapperElement()); + textarea.style.display = ""; + if (textarea.form) { + off(textarea.form, "submit", save); + if (typeof textarea.form.submit == "function") + textarea.form.submit = realSubmit; + } + }; + }; + + textarea.style.display = "none"; + var cm = CodeMirror(function(node) { + textarea.parentNode.insertBefore(node, textarea.nextSibling); + }, options); + return cm; + }; + + // STRING STREAM + + // Fed to the mode parsers, provides helper functions to make + // parsers more succinct. + + var StringStream = CodeMirror.StringStream = function(string, tabSize) { + this.pos = this.start = 0; + this.string = string; + this.tabSize = tabSize || 8; + this.lastColumnPos = this.lastColumnValue = 0; + this.lineStart = 0; + }; + + StringStream.prototype = { + eol: function() {return this.pos >= this.string.length;}, + sol: function() {return this.pos == this.lineStart;}, + peek: function() {return this.string.charAt(this.pos) || undefined;}, + next: function() { + if (this.pos < this.string.length) + return this.string.charAt(this.pos++); + }, + eat: function(match) { + var ch = this.string.charAt(this.pos); + if (typeof match == "string") var ok = ch == match; + else var ok = ch && (match.test ? match.test(ch) : match(ch)); + if (ok) {++this.pos; return ch;} + }, + eatWhile: function(match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start; + }, + eatSpace: function() { + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos; + return this.pos > start; + }, + skipToEnd: function() {this.pos = this.string.length;}, + skipTo: function(ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true;} + }, + backUp: function(n) {this.pos -= n;}, + column: function() { + if (this.lastColumnPos < this.start) { + this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); + this.lastColumnPos = this.start; + } + return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); + }, + indentation: function() { + return countColumn(this.string, null, this.tabSize) - + (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); + }, + match: function(pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;}; + var substr = this.string.substr(this.pos, pattern.length); + if (cased(substr) == cased(pattern)) { + if (consume !== false) this.pos += pattern.length; + return true; + } + } else { + var match = this.string.slice(this.pos).match(pattern); + if (match && match.index > 0) return null; + if (match && consume !== false) this.pos += match[0].length; + return match; + } + }, + current: function(){return this.string.slice(this.start, this.pos);}, + hideFirstChars: function(n, inner) { + this.lineStart += n; + try { return inner(); } + finally { this.lineStart -= n; } + } + }; + + // TEXTMARKERS + + // Created with markText and setBookmark methods. A TextMarker is a + // handle that can be used to clear or find a marked position in the + // document. Line objects hold arrays (markedSpans) containing + // {from, to, marker} object pointing to such marker objects, and + // indicating that such a marker is present on that line. Multiple + // lines may point to the same marker when it spans across lines. + // The spans will have null for their from/to properties when the + // marker continues beyond the start/end of the line. Markers have + // links back to the lines they currently touch. + + var nextMarkerId = 0; + + var TextMarker = CodeMirror.TextMarker = function(doc, type) { + this.lines = []; + this.type = type; + this.doc = doc; + this.id = ++nextMarkerId; + }; + eventMixin(TextMarker); + + // Clear the marker. + TextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + var cm = this.doc.cm, withOp = cm && !cm.curOp; + if (withOp) startOperation(cm); + if (hasHandler(this, "clear")) { + var found = this.find(); + if (found) signalLater(this, "clear", found.from, found.to); + } + var min = null, max = null; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (cm && !this.collapsed) regLineChange(cm, lineNo(line), "text"); + else if (cm) { + if (span.to != null) max = lineNo(line); + if (span.from != null) min = lineNo(line); + } + line.markedSpans = removeMarkedSpan(line.markedSpans, span); + if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm) + updateLineHeight(line, textHeight(cm.display)); + } + if (cm && this.collapsed && !cm.options.lineWrapping) for (var i = 0; i < this.lines.length; ++i) { + var visual = visualLine(this.lines[i]), len = lineLength(visual); + if (len > cm.display.maxLineLength) { + cm.display.maxLine = visual; + cm.display.maxLineLength = len; + cm.display.maxLineChanged = true; + } + } + + if (min != null && cm && this.collapsed) regChange(cm, min, max + 1); + this.lines.length = 0; + this.explicitlyCleared = true; + if (this.atomic && this.doc.cantEdit) { + this.doc.cantEdit = false; + if (cm) reCheckSelection(cm.doc); + } + if (cm) signalLater(cm, "markerCleared", cm, this); + if (withOp) endOperation(cm); + if (this.parent) this.parent.clear(); + }; + + // Find the position of the marker in the document. Returns a {from, + // to} object by default. Side can be passed to get a specific side + // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the + // Pos objects returned contain a line object, rather than a line + // number (used to prevent looking up the same line twice). + TextMarker.prototype.find = function(side, lineObj) { + if (side == null && this.type == "bookmark") side = 1; + var from, to; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (span.from != null) { + from = Pos(lineObj ? line : lineNo(line), span.from); + if (side == -1) return from; + } + if (span.to != null) { + to = Pos(lineObj ? line : lineNo(line), span.to); + if (side == 1) return to; + } + } + return from && {from: from, to: to}; + }; + + // Signals that the marker's widget changed, and surrounding layout + // should be recomputed. + TextMarker.prototype.changed = function() { + var pos = this.find(-1, true), widget = this, cm = this.doc.cm; + if (!pos || !cm) return; + runInOp(cm, function() { + var line = pos.line, lineN = lineNo(pos.line); + var view = findViewForLine(cm, lineN); + if (view) { + clearLineMeasurementCacheFor(view); + cm.curOp.selectionChanged = cm.curOp.forceUpdate = true; + } + cm.curOp.updateMaxLine = true; + if (!lineIsHidden(widget.doc, line) && widget.height != null) { + var oldHeight = widget.height; + widget.height = null; + var dHeight = widgetHeight(widget) - oldHeight; + if (dHeight) + updateLineHeight(line, line.height + dHeight); + } + }); + }; + + TextMarker.prototype.attachLine = function(line) { + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) + (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this); + } + this.lines.push(line); + }; + TextMarker.prototype.detachLine = function(line) { + this.lines.splice(indexOf(this.lines, line), 1); + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + (op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this); + } + }; + + // Collapsed markers have unique ids, in order to be able to order + // them, which is needed for uniquely determining an outer marker + // when they overlap (they may nest, but not partially overlap). + var nextMarkerId = 0; + + // Create a marker, wire it up to the right lines, and + function markText(doc, from, to, options, type) { + // Shared markers (across linked documents) are handled separately + // (markTextShared will call out to this again, once per + // document). + if (options && options.shared) return markTextShared(doc, from, to, options, type); + // Ensure we are in an operation. + if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type); + + var marker = new TextMarker(doc, type), diff = cmp(from, to); + if (options) copyObj(options, marker, false); + // Don't connect empty markers unless clearWhenEmpty is false + if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false) + return marker; + if (marker.replacedWith) { + // Showing up as a widget implies collapsed (widget replaces text) + marker.collapsed = true; + marker.widgetNode = elt("span", [marker.replacedWith], "CodeMirror-widget"); + if (!options.handleMouseEvents) marker.widgetNode.setAttribute("cm-ignore-events", "true"); + if (options.insertLeft) marker.widgetNode.insertLeft = true; + } + if (marker.collapsed) { + if (conflictingCollapsedRange(doc, from.line, from, to, marker) || + from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker)) + throw new Error("Inserting collapsed marker partially overlapping an existing one"); + sawCollapsedSpans = true; + } + + if (marker.addToHistory) + addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN); + + var curLine = from.line, cm = doc.cm, updateMaxLine; + doc.iter(curLine, to.line + 1, function(line) { + if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine) + updateMaxLine = true; + if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0); + addMarkedSpan(line, new MarkedSpan(marker, + curLine == from.line ? from.ch : null, + curLine == to.line ? to.ch : null)); + ++curLine; + }); + // lineIsHidden depends on the presence of the spans, so needs a second pass + if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) { + if (lineIsHidden(doc, line)) updateLineHeight(line, 0); + }); + + if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); }); + + if (marker.readOnly) { + sawReadOnlySpans = true; + if (doc.history.done.length || doc.history.undone.length) + doc.clearHistory(); + } + if (marker.collapsed) { + marker.id = ++nextMarkerId; + marker.atomic = true; + } + if (cm) { + // Sync editor state + if (updateMaxLine) cm.curOp.updateMaxLine = true; + if (marker.collapsed) + regChange(cm, from.line, to.line + 1); + else if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.css) + for (var i = from.line; i <= to.line; i++) regLineChange(cm, i, "text"); + if (marker.atomic) reCheckSelection(cm.doc); + signalLater(cm, "markerAdded", cm, marker); + } + return marker; + } + + // SHARED TEXTMARKERS + + // A shared marker spans multiple linked documents. It is + // implemented as a meta-marker-object controlling multiple normal + // markers. + var SharedTextMarker = CodeMirror.SharedTextMarker = function(markers, primary) { + this.markers = markers; + this.primary = primary; + for (var i = 0; i < markers.length; ++i) + markers[i].parent = this; + }; + eventMixin(SharedTextMarker); + + SharedTextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + this.explicitlyCleared = true; + for (var i = 0; i < this.markers.length; ++i) + this.markers[i].clear(); + signalLater(this, "clear"); + }; + SharedTextMarker.prototype.find = function(side, lineObj) { + return this.primary.find(side, lineObj); + }; + + function markTextShared(doc, from, to, options, type) { + options = copyObj(options); + options.shared = false; + var markers = [markText(doc, from, to, options, type)], primary = markers[0]; + var widget = options.widgetNode; + linkedDocs(doc, function(doc) { + if (widget) options.widgetNode = widget.cloneNode(true); + markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)); + for (var i = 0; i < doc.linked.length; ++i) + if (doc.linked[i].isParent) return; + primary = lst(markers); + }); + return new SharedTextMarker(markers, primary); + } + + function findSharedMarkers(doc) { + return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), + function(m) { return m.parent; }); + } + + function copySharedMarkers(doc, markers) { + for (var i = 0; i < markers.length; i++) { + var marker = markers[i], pos = marker.find(); + var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to); + if (cmp(mFrom, mTo)) { + var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type); + marker.markers.push(subMark); + subMark.parent = marker; + } + } + } + + function detachSharedMarkers(markers) { + for (var i = 0; i < markers.length; i++) { + var marker = markers[i], linked = [marker.primary.doc];; + linkedDocs(marker.primary.doc, function(d) { linked.push(d); }); + for (var j = 0; j < marker.markers.length; j++) { + var subMarker = marker.markers[j]; + if (indexOf(linked, subMarker.doc) == -1) { + subMarker.parent = null; + marker.markers.splice(j--, 1); + } + } + } + } + + // TEXTMARKER SPANS + + function MarkedSpan(marker, from, to) { + this.marker = marker; + this.from = from; this.to = to; + } + + // Search an array of spans for a span matching the given marker. + function getMarkedSpanFor(spans, marker) { + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.marker == marker) return span; + } + } + // Remove a span from an array, returning undefined if no spans are + // left (we don't store arrays for lines without spans). + function removeMarkedSpan(spans, span) { + for (var r, i = 0; i < spans.length; ++i) + if (spans[i] != span) (r || (r = [])).push(spans[i]); + return r; + } + // Add a span to a line. + function addMarkedSpan(line, span) { + line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; + span.marker.attachLine(line); + } + + // Used for the algorithm that adjusts markers for a change in the + // document. These functions cut an array of spans at a given + // character position, returning an array of remaining chunks (or + // undefined if nothing remains). + function markedSpansBefore(old, startCh, isInsert) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); + if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh); + (nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)); + } + } + return nw; + } + function markedSpansAfter(old, endCh, isInsert) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); + if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh); + (nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, + span.to == null ? null : span.to - endCh)); + } + } + return nw; + } + + // Given a change object, compute the new set of marker spans that + // cover the line in which the change took place. Removes spans + // entirely within the change, reconnects spans belonging to the + // same marker that appear on both sides of the change, and cuts off + // spans partially within the change. Returns an array of span + // arrays with one element for each line in (after) the change. + function stretchSpansOverChange(doc, change) { + if (change.full) return null; + var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans; + var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans; + if (!oldFirst && !oldLast) return null; + + var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0; + // Get the spans that 'stick out' on both sides + var first = markedSpansBefore(oldFirst, startCh, isInsert); + var last = markedSpansAfter(oldLast, endCh, isInsert); + + // Next, merge those two ends + var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0); + if (first) { + // Fix up .to properties of first + for (var i = 0; i < first.length; ++i) { + var span = first[i]; + if (span.to == null) { + var found = getMarkedSpanFor(last, span.marker); + if (!found) span.to = startCh; + else if (sameLine) span.to = found.to == null ? null : found.to + offset; + } + } + } + if (last) { + // Fix up .from in last (or move them into first in case of sameLine) + for (var i = 0; i < last.length; ++i) { + var span = last[i]; + if (span.to != null) span.to += offset; + if (span.from == null) { + var found = getMarkedSpanFor(first, span.marker); + if (!found) { + span.from = offset; + if (sameLine) (first || (first = [])).push(span); + } + } else { + span.from += offset; + if (sameLine) (first || (first = [])).push(span); + } + } + } + // Make sure we didn't create any zero-length spans + if (first) first = clearEmptySpans(first); + if (last && last != first) last = clearEmptySpans(last); + + var newMarkers = [first]; + if (!sameLine) { + // Fill gap with whole-line-spans + var gap = change.text.length - 2, gapMarkers; + if (gap > 0 && first) + for (var i = 0; i < first.length; ++i) + if (first[i].to == null) + (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i].marker, null, null)); + for (var i = 0; i < gap; ++i) + newMarkers.push(gapMarkers); + newMarkers.push(last); + } + return newMarkers; + } + + // Remove spans that are empty and don't have a clearWhenEmpty + // option of false. + function clearEmptySpans(spans) { + for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false) + spans.splice(i--, 1); + } + if (!spans.length) return null; + return spans; + } + + // Used for un/re-doing changes from the history. Combines the + // result of computing the existing spans with the set of spans that + // existed in the history (so that deleting around a span and then + // undoing brings back the span). + function mergeOldSpans(doc, change) { + var old = getOldSpans(doc, change); + var stretched = stretchSpansOverChange(doc, change); + if (!old) return stretched; + if (!stretched) return old; + + for (var i = 0; i < old.length; ++i) { + var oldCur = old[i], stretchCur = stretched[i]; + if (oldCur && stretchCur) { + spans: for (var j = 0; j < stretchCur.length; ++j) { + var span = stretchCur[j]; + for (var k = 0; k < oldCur.length; ++k) + if (oldCur[k].marker == span.marker) continue spans; + oldCur.push(span); + } + } else if (stretchCur) { + old[i] = stretchCur; + } + } + return old; + } + + // Used to 'clip' out readOnly ranges when making a change. + function removeReadOnlyRanges(doc, from, to) { + var markers = null; + doc.iter(from.line, to.line + 1, function(line) { + if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) { + var mark = line.markedSpans[i].marker; + if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) + (markers || (markers = [])).push(mark); + } + }); + if (!markers) return null; + var parts = [{from: from, to: to}]; + for (var i = 0; i < markers.length; ++i) { + var mk = markers[i], m = mk.find(0); + for (var j = 0; j < parts.length; ++j) { + var p = parts[j]; + if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) continue; + var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to); + if (dfrom < 0 || !mk.inclusiveLeft && !dfrom) + newParts.push({from: p.from, to: m.from}); + if (dto > 0 || !mk.inclusiveRight && !dto) + newParts.push({from: m.to, to: p.to}); + parts.splice.apply(parts, newParts); + j += newParts.length - 1; + } + } + return parts; + } + + // Connect or disconnect spans from a line. + function detachMarkedSpans(line) { + var spans = line.markedSpans; + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.detachLine(line); + line.markedSpans = null; + } + function attachMarkedSpans(line, spans) { + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.attachLine(line); + line.markedSpans = spans; + } + + // Helpers used when computing which overlapping collapsed span + // counts as the larger one. + function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0; } + function extraRight(marker) { return marker.inclusiveRight ? 1 : 0; } + + // Returns a number indicating which of two overlapping collapsed + // spans is larger (and thus includes the other). Falls back to + // comparing ids when the spans cover exactly the same range. + function compareCollapsedMarkers(a, b) { + var lenDiff = a.lines.length - b.lines.length; + if (lenDiff != 0) return lenDiff; + var aPos = a.find(), bPos = b.find(); + var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b); + if (fromCmp) return -fromCmp; + var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b); + if (toCmp) return toCmp; + return b.id - a.id; + } + + // Find out whether a line ends or starts in a collapsed span. If + // so, return the marker for that span. + function collapsedSpanAtSide(line, start) { + var sps = sawCollapsedSpans && line.markedSpans, found; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (sp.marker.collapsed && (start ? sp.from : sp.to) == null && + (!found || compareCollapsedMarkers(found, sp.marker) < 0)) + found = sp.marker; + } + return found; + } + function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true); } + function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false); } + + // Test whether there exists a collapsed span that partially + // overlaps (covers the start or end, but not both) of a new span. + // Such overlap is not allowed. + function conflictingCollapsedRange(doc, lineNo, from, to, marker) { + var line = getLine(doc, lineNo); + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) for (var i = 0; i < sps.length; ++i) { + var sp = sps[i]; + if (!sp.marker.collapsed) continue; + var found = sp.marker.find(0); + var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker); + var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker); + if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue; + if (fromCmp <= 0 && (cmp(found.to, from) > 0 || (sp.marker.inclusiveRight && marker.inclusiveLeft)) || + fromCmp >= 0 && (cmp(found.from, to) < 0 || (sp.marker.inclusiveLeft && marker.inclusiveRight))) + return true; + } + } + + // A visual line is a line as drawn on the screen. Folding, for + // example, can cause multiple logical lines to appear on the same + // visual line. This finds the start of the visual line that the + // given line is part of (usually that is the line itself). + function visualLine(line) { + var merged; + while (merged = collapsedSpanAtStart(line)) + line = merged.find(-1, true).line; + return line; + } + + // Returns an array of logical lines that continue the visual line + // started by the argument, or undefined if there are no such lines. + function visualLineContinued(line) { + var merged, lines; + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line; + (lines || (lines = [])).push(line); + } + return lines; + } + + // Get the line number of the start of the visual line that the + // given line number is part of. + function visualLineNo(doc, lineN) { + var line = getLine(doc, lineN), vis = visualLine(line); + if (line == vis) return lineN; + return lineNo(vis); + } + // Get the line number of the start of the next visual line after + // the given line. + function visualLineEndNo(doc, lineN) { + if (lineN > doc.lastLine()) return lineN; + var line = getLine(doc, lineN), merged; + if (!lineIsHidden(doc, line)) return lineN; + while (merged = collapsedSpanAtEnd(line)) + line = merged.find(1, true).line; + return lineNo(line) + 1; + } + + // Compute whether a line is hidden. Lines count as hidden when they + // are part of a visual line that starts with another line, or when + // they are entirely covered by collapsed, non-widget span. + function lineIsHidden(doc, line) { + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (!sp.marker.collapsed) continue; + if (sp.from == null) return true; + if (sp.marker.widgetNode) continue; + if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) + return true; + } + } + function lineIsHiddenInner(doc, line, span) { + if (span.to == null) { + var end = span.marker.find(1, true); + return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker)); + } + if (span.marker.inclusiveRight && span.to == line.text.length) + return true; + for (var sp, i = 0; i < line.markedSpans.length; ++i) { + sp = line.markedSpans[i]; + if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to && + (sp.to == null || sp.to != span.from) && + (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && + lineIsHiddenInner(doc, line, sp)) return true; + } + } + + // LINE WIDGETS + + // Line widgets are block elements displayed above or below a line. + + var LineWidget = CodeMirror.LineWidget = function(doc, node, options) { + if (options) for (var opt in options) if (options.hasOwnProperty(opt)) + this[opt] = options[opt]; + this.doc = doc; + this.node = node; + }; + eventMixin(LineWidget); + + function adjustScrollWhenAboveVisible(cm, line, diff) { + if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) + addToScrollPos(cm, null, diff); + } + + LineWidget.prototype.clear = function() { + var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line); + if (no == null || !ws) return; + for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1); + if (!ws.length) line.widgets = null; + var height = widgetHeight(this); + updateLineHeight(line, Math.max(0, line.height - height)); + if (cm) runInOp(cm, function() { + adjustScrollWhenAboveVisible(cm, line, -height); + regLineChange(cm, no, "widget"); + }); + }; + LineWidget.prototype.changed = function() { + var oldH = this.height, cm = this.doc.cm, line = this.line; + this.height = null; + var diff = widgetHeight(this) - oldH; + if (!diff) return; + updateLineHeight(line, line.height + diff); + if (cm) runInOp(cm, function() { + cm.curOp.forceUpdate = true; + adjustScrollWhenAboveVisible(cm, line, diff); + }); + }; + + function widgetHeight(widget) { + if (widget.height != null) return widget.height; + var cm = widget.doc.cm; + if (!cm) return 0; + if (!contains(document.body, widget.node)) { + var parentStyle = "position: relative;"; + if (widget.coverGutter) + parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;"; + if (widget.noHScroll) + parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;"; + removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle)); + } + return widget.height = widget.node.offsetHeight; + } + + function addLineWidget(doc, handle, node, options) { + var widget = new LineWidget(doc, node, options); + var cm = doc.cm; + if (cm && widget.noHScroll) cm.display.alignWidgets = true; + changeLine(doc, handle, "widget", function(line) { + var widgets = line.widgets || (line.widgets = []); + if (widget.insertAt == null) widgets.push(widget); + else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget); + widget.line = line; + if (cm && !lineIsHidden(doc, line)) { + var aboveVisible = heightAtLine(line) < doc.scrollTop; + updateLineHeight(line, line.height + widgetHeight(widget)); + if (aboveVisible) addToScrollPos(cm, null, widget.height); + cm.curOp.forceUpdate = true; + } + return true; + }); + return widget; + } + + // LINE DATA STRUCTURE + + // Line objects. These hold state related to a line, including + // highlighting info (the styles array). + var Line = CodeMirror.Line = function(text, markedSpans, estimateHeight) { + this.text = text; + attachMarkedSpans(this, markedSpans); + this.height = estimateHeight ? estimateHeight(this) : 1; + }; + eventMixin(Line); + Line.prototype.lineNo = function() { return lineNo(this); }; + + // Change the content (text, markers) of a line. Automatically + // invalidates cached information and tries to re-estimate the + // line's height. + function updateLine(line, text, markedSpans, estimateHeight) { + line.text = text; + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + if (line.order != null) line.order = null; + detachMarkedSpans(line); + attachMarkedSpans(line, markedSpans); + var estHeight = estimateHeight ? estimateHeight(line) : 1; + if (estHeight != line.height) updateLineHeight(line, estHeight); + } + + // Detach a line from the document tree and its markers. + function cleanUpLine(line) { + line.parent = null; + detachMarkedSpans(line); + } + + function extractLineClasses(type, output) { + if (type) for (;;) { + var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/); + if (!lineClass) break; + type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length); + var prop = lineClass[1] ? "bgClass" : "textClass"; + if (output[prop] == null) + output[prop] = lineClass[2]; + else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop])) + output[prop] += " " + lineClass[2]; + } + return type; + } + + function callBlankLine(mode, state) { + if (mode.blankLine) return mode.blankLine(state); + if (!mode.innerMode) return; + var inner = CodeMirror.innerMode(mode, state); + if (inner.mode.blankLine) return inner.mode.blankLine(inner.state); + } + + function readToken(mode, stream, state, inner) { + for (var i = 0; i < 10; i++) { + if (inner) inner[0] = CodeMirror.innerMode(mode, state).mode; + var style = mode.token(stream, state); + if (stream.pos > stream.start) return style; + } + throw new Error("Mode " + mode.name + " failed to advance stream."); + } + + // Utility for getTokenAt and getLineTokens + function takeToken(cm, pos, precise, asArray) { + function getObj(copy) { + return {start: stream.start, end: stream.pos, + string: stream.current(), + type: style || null, + state: copy ? copyState(doc.mode, state) : state}; + } + + var doc = cm.doc, mode = doc.mode, style; + pos = clipPos(doc, pos); + var line = getLine(doc, pos.line), state = getStateBefore(cm, pos.line, precise); + var stream = new StringStream(line.text, cm.options.tabSize), tokens; + if (asArray) tokens = []; + while ((asArray || stream.pos < pos.ch) && !stream.eol()) { + stream.start = stream.pos; + style = readToken(mode, stream, state); + if (asArray) tokens.push(getObj(true)); + } + return asArray ? tokens : getObj(); + } + + // Run the given mode's parser over a line, calling f for each token. + function runMode(cm, text, mode, state, f, lineClasses, forceToEnd) { + var flattenSpans = mode.flattenSpans; + if (flattenSpans == null) flattenSpans = cm.options.flattenSpans; + var curStart = 0, curStyle = null; + var stream = new StringStream(text, cm.options.tabSize), style; + var inner = cm.options.addModeClass && [null]; + if (text == "") extractLineClasses(callBlankLine(mode, state), lineClasses); + while (!stream.eol()) { + if (stream.pos > cm.options.maxHighlightLength) { + flattenSpans = false; + if (forceToEnd) processLine(cm, text, state, stream.pos); + stream.pos = text.length; + style = null; + } else { + style = extractLineClasses(readToken(mode, stream, state, inner), lineClasses); + } + if (inner) { + var mName = inner[0].name; + if (mName) style = "m-" + (style ? mName + " " + style : mName); + } + if (!flattenSpans || curStyle != style) { + while (curStart < stream.start) { + curStart = Math.min(stream.start, curStart + 50000); + f(curStart, curStyle); + } + curStyle = style; + } + stream.start = stream.pos; + } + while (curStart < stream.pos) { + // Webkit seems to refuse to render text nodes longer than 57444 characters + var pos = Math.min(stream.pos, curStart + 50000); + f(pos, curStyle); + curStart = pos; + } + } + + // Compute a style array (an array starting with a mode generation + // -- for invalidation -- followed by pairs of end positions and + // style strings), which is used to highlight the tokens on the + // line. + function highlightLine(cm, line, state, forceToEnd) { + // A styles array always starts with a number identifying the + // mode/overlays that it is based on (for easy invalidation). + var st = [cm.state.modeGen], lineClasses = {}; + // Compute the base array of styles + runMode(cm, line.text, cm.doc.mode, state, function(end, style) { + st.push(end, style); + }, lineClasses, forceToEnd); + + // Run overlays, adjust style array. + for (var o = 0; o < cm.state.overlays.length; ++o) { + var overlay = cm.state.overlays[o], i = 1, at = 0; + runMode(cm, line.text, overlay.mode, true, function(end, style) { + var start = i; + // Ensure there's a token end at the current position, and that i points at it + while (at < end) { + var i_end = st[i]; + if (i_end > end) + st.splice(i, 1, end, st[i+1], i_end); + i += 2; + at = Math.min(end, i_end); + } + if (!style) return; + if (overlay.opaque) { + st.splice(start, i - start, end, "cm-overlay " + style); + i = start + 2; + } else { + for (; start < i; start += 2) { + var cur = st[start+1]; + st[start+1] = (cur ? cur + " " : "") + "cm-overlay " + style; + } + } + }, lineClasses); + } + + return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null}; + } + + function getLineStyles(cm, line, updateFrontier) { + if (!line.styles || line.styles[0] != cm.state.modeGen) { + var result = highlightLine(cm, line, line.stateAfter = getStateBefore(cm, lineNo(line))); + line.styles = result.styles; + if (result.classes) line.styleClasses = result.classes; + else if (line.styleClasses) line.styleClasses = null; + if (updateFrontier === cm.doc.frontier) cm.doc.frontier++; + } + return line.styles; + } + + // Lightweight form of highlight -- proceed over this line and + // update state, but don't save a style array. Used for lines that + // aren't currently visible. + function processLine(cm, text, state, startAt) { + var mode = cm.doc.mode; + var stream = new StringStream(text, cm.options.tabSize); + stream.start = stream.pos = startAt || 0; + if (text == "") callBlankLine(mode, state); + while (!stream.eol() && stream.pos <= cm.options.maxHighlightLength) { + readToken(mode, stream, state); + stream.start = stream.pos; + } + } + + // Convert a style as returned by a mode (either null, or a string + // containing one or more styles) to a CSS style. This is cached, + // and also looks for line-wide styles. + var styleToClassCache = {}, styleToClassCacheWithMode = {}; + function interpretTokenStyle(style, options) { + if (!style || /^\s*$/.test(style)) return null; + var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache; + return cache[style] || + (cache[style] = style.replace(/\S+/g, "cm-$&")); + } + + // Render the DOM representation of the text of a line. Also builds + // up a 'line map', which points at the DOM nodes that represent + // specific stretches of text, and is used by the measuring code. + // The returned object contains the DOM node, this map, and + // information about line-wide styles that were set by the mode. + function buildLineContent(cm, lineView) { + // The padding-right forces the element to have a 'border', which + // is needed on Webkit to be able to get line-level bounding + // rectangles for it (in measureChar). + var content = elt("span", null, null, webkit ? "padding-right: .1px" : null); + var builder = {pre: elt("pre", [content]), content: content, + col: 0, pos: 0, cm: cm, + splitSpaces: (ie || webkit) && cm.getOption("lineWrapping")}; + lineView.measure = {}; + + // Iterate over the logical lines that make up this visual line. + for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) { + var line = i ? lineView.rest[i - 1] : lineView.line, order; + builder.pos = 0; + builder.addToken = buildToken; + // Optionally wire in some hacks into the token-rendering + // algorithm, to deal with browser quirks. + if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line))) + builder.addToken = buildTokenBadBidi(builder.addToken, order); + builder.map = []; + var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line); + insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate)); + if (line.styleClasses) { + if (line.styleClasses.bgClass) + builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || ""); + if (line.styleClasses.textClass) + builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || ""); + } + + // Ensure at least a single node is present, for measuring. + if (builder.map.length == 0) + builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure))); + + // Store the map and a cache object for the current logical line + if (i == 0) { + lineView.measure.map = builder.map; + lineView.measure.cache = {}; + } else { + (lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map); + (lineView.measure.caches || (lineView.measure.caches = [])).push({}); + } + } + + // See issue #2901 + if (webkit && /\bcm-tab\b/.test(builder.content.lastChild.className)) + builder.content.className = "cm-tab-wrap-hack"; + + signal(cm, "renderLine", cm, lineView.line, builder.pre); + if (builder.pre.className) + builder.textClass = joinClasses(builder.pre.className, builder.textClass || ""); + + return builder; + } + + function defaultSpecialCharPlaceholder(ch) { + var token = elt("span", "\u2022", "cm-invalidchar"); + token.title = "\\u" + ch.charCodeAt(0).toString(16); + token.setAttribute("aria-label", token.title); + return token; + } + + // Build up the DOM representation for a single token, and add it to + // the line map. Takes care to render special characters separately. + function buildToken(builder, text, style, startStyle, endStyle, title, css) { + if (!text) return; + var displayText = builder.splitSpaces ? text.replace(/ {3,}/g, splitSpaces) : text; + var special = builder.cm.state.specialChars, mustWrap = false; + if (!special.test(text)) { + builder.col += text.length; + var content = document.createTextNode(displayText); + builder.map.push(builder.pos, builder.pos + text.length, content); + if (ie && ie_version < 9) mustWrap = true; + builder.pos += text.length; + } else { + var content = document.createDocumentFragment(), pos = 0; + while (true) { + special.lastIndex = pos; + var m = special.exec(text); + var skipped = m ? m.index - pos : text.length - pos; + if (skipped) { + var txt = document.createTextNode(displayText.slice(pos, pos + skipped)); + if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); + else content.appendChild(txt); + builder.map.push(builder.pos, builder.pos + skipped, txt); + builder.col += skipped; + builder.pos += skipped; + } + if (!m) break; + pos += skipped + 1; + if (m[0] == "\t") { + var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; + var txt = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); + txt.setAttribute("role", "presentation"); + txt.setAttribute("cm-text", "\t"); + builder.col += tabWidth; + } else { + var txt = builder.cm.options.specialCharPlaceholder(m[0]); + txt.setAttribute("cm-text", m[0]); + if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); + else content.appendChild(txt); + builder.col += 1; + } + builder.map.push(builder.pos, builder.pos + 1, txt); + builder.pos++; + } + } + if (style || startStyle || endStyle || mustWrap || css) { + var fullStyle = style || ""; + if (startStyle) fullStyle += startStyle; + if (endStyle) fullStyle += endStyle; + var token = elt("span", [content], fullStyle, css); + if (title) token.title = title; + return builder.content.appendChild(token); + } + builder.content.appendChild(content); + } + + function splitSpaces(old) { + var out = " "; + for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0"; + out += " "; + return out; + } + + // Work around nonsense dimensions being reported for stretches of + // right-to-left text. + function buildTokenBadBidi(inner, order) { + return function(builder, text, style, startStyle, endStyle, title, css) { + style = style ? style + " cm-force-border" : "cm-force-border"; + var start = builder.pos, end = start + text.length; + for (;;) { + // Find the part that overlaps with the start of this text + for (var i = 0; i < order.length; i++) { + var part = order[i]; + if (part.to > start && part.from <= start) break; + } + if (part.to >= end) return inner(builder, text, style, startStyle, endStyle, title, css); + inner(builder, text.slice(0, part.to - start), style, startStyle, null, title, css); + startStyle = null; + text = text.slice(part.to - start); + start = part.to; + } + }; + } + + function buildCollapsedSpan(builder, size, marker, ignoreWidget) { + var widget = !ignoreWidget && marker.widgetNode; + if (widget) builder.map.push(builder.pos, builder.pos + size, widget); + if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) { + if (!widget) + widget = builder.content.appendChild(document.createElement("span")); + widget.setAttribute("cm-marker", marker.id); + } + if (widget) { + builder.cm.display.input.setUneditable(widget); + builder.content.appendChild(widget); + } + builder.pos += size; + } + + // Outputs a number of spans to make up a line, taking highlighting + // and marked text into account. + function insertLineContent(line, builder, styles) { + var spans = line.markedSpans, allText = line.text, at = 0; + if (!spans) { + for (var i = 1; i < styles.length; i+=2) + builder.addToken(builder, allText.slice(at, at = styles[i]), interpretTokenStyle(styles[i+1], builder.cm.options)); + return; + } + + var len = allText.length, pos = 0, i = 1, text = "", style, css; + var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed; + for (;;) { + if (nextChange == pos) { // Update current marker set + spanStyle = spanEndStyle = spanStartStyle = title = css = ""; + collapsed = null; nextChange = Infinity; + var foundBookmarks = []; + for (var j = 0; j < spans.length; ++j) { + var sp = spans[j], m = sp.marker; + if (sp.from <= pos && (sp.to == null || sp.to > pos)) { + if (sp.to != null && nextChange > sp.to) { nextChange = sp.to; spanEndStyle = ""; } + if (m.className) spanStyle += " " + m.className; + if (m.css) css = m.css; + if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle; + if (m.endStyle && sp.to == nextChange) spanEndStyle += " " + m.endStyle; + if (m.title && !title) title = m.title; + if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0)) + collapsed = sp; + } else if (sp.from > pos && nextChange > sp.from) { + nextChange = sp.from; + } + if (m.type == "bookmark" && sp.from == pos && m.widgetNode) foundBookmarks.push(m); + } + if (collapsed && (collapsed.from || 0) == pos) { + buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos, + collapsed.marker, collapsed.from == null); + if (collapsed.to == null) return; + } + if (!collapsed && foundBookmarks.length) for (var j = 0; j < foundBookmarks.length; ++j) + buildCollapsedSpan(builder, 0, foundBookmarks[j]); + } + if (pos >= len) break; + + var upto = Math.min(len, nextChange); + while (true) { + if (text) { + var end = pos + text.length; + if (!collapsed) { + var tokenText = end > upto ? text.slice(0, upto - pos) : text; + builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle, + spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title, css); + } + if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;} + pos = end; + spanStartStyle = ""; + } + text = allText.slice(at, at = styles[i++]); + style = interpretTokenStyle(styles[i++], builder.cm.options); + } + } + } + + // DOCUMENT DATA STRUCTURE + + // By default, updates that start and end at the beginning of a line + // are treated specially, in order to make the association of line + // widgets and marker elements with the text behave more intuitive. + function isWholeLineUpdate(doc, change) { + return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" && + (!doc.cm || doc.cm.options.wholeLineUpdateBefore); + } + + // Perform a change on the document data structure. + function updateDoc(doc, change, markedSpans, estimateHeight) { + function spansFor(n) {return markedSpans ? markedSpans[n] : null;} + function update(line, text, spans) { + updateLine(line, text, spans, estimateHeight); + signalLater(line, "change", line, change); + } + function linesFor(start, end) { + for (var i = start, result = []; i < end; ++i) + result.push(new Line(text[i], spansFor(i), estimateHeight)); + return result; + } + + var from = change.from, to = change.to, text = change.text; + var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line); + var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; + + // Adjust the line structure + if (change.full) { + doc.insert(0, linesFor(0, text.length)); + doc.remove(text.length, doc.size - text.length); + } else if (isWholeLineUpdate(doc, change)) { + // This is a whole-line replace. Treated specially to make + // sure line objects move the way they are supposed to. + var added = linesFor(0, text.length - 1); + update(lastLine, lastLine.text, lastSpans); + if (nlines) doc.remove(from.line, nlines); + if (added.length) doc.insert(from.line, added); + } else if (firstLine == lastLine) { + if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans); + } else { + var added = linesFor(1, text.length - 1); + added.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight)); + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + doc.insert(from.line + 1, added); + } + } else if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)); + doc.remove(from.line + 1, nlines); + } else { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans); + var added = linesFor(1, text.length - 1); + if (nlines > 1) doc.remove(from.line + 1, nlines - 1); + doc.insert(from.line + 1, added); + } + + signalLater(doc, "change", doc, change); + } + + // The document is represented as a BTree consisting of leaves, with + // chunk of lines in them, and branches, with up to ten leaves or + // other branch nodes below them. The top node is always a branch + // node, and is the document object itself (meaning it has + // additional methods and properties). + // + // All nodes have parent links. The tree is used both to go from + // line numbers to line objects, and to go from objects to numbers. + // It also indexes by height, and is used to convert between height + // and line object, and to find the total height of the document. + // + // See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html + + function LeafChunk(lines) { + this.lines = lines; + this.parent = null; + for (var i = 0, height = 0; i < lines.length; ++i) { + lines[i].parent = this; + height += lines[i].height; + } + this.height = height; + } + + LeafChunk.prototype = { + chunkSize: function() { return this.lines.length; }, + // Remove the n lines at offset 'at'. + removeInner: function(at, n) { + for (var i = at, e = at + n; i < e; ++i) { + var line = this.lines[i]; + this.height -= line.height; + cleanUpLine(line); + signalLater(line, "delete"); + } + this.lines.splice(at, n); + }, + // Helper used to collapse a small branch into a single leaf. + collapse: function(lines) { + lines.push.apply(lines, this.lines); + }, + // Insert the given array of lines at offset 'at', count them as + // having the given height. + insertInner: function(at, lines, height) { + this.height += height; + this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); + for (var i = 0; i < lines.length; ++i) lines[i].parent = this; + }, + // Used to iterate over a part of the tree. + iterN: function(at, n, op) { + for (var e = at + n; at < e; ++at) + if (op(this.lines[at])) return true; + } + }; + + function BranchChunk(children) { + this.children = children; + var size = 0, height = 0; + for (var i = 0; i < children.length; ++i) { + var ch = children[i]; + size += ch.chunkSize(); height += ch.height; + ch.parent = this; + } + this.size = size; + this.height = height; + this.parent = null; + } + + BranchChunk.prototype = { + chunkSize: function() { return this.size; }, + removeInner: function(at, n) { + this.size -= n; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var rm = Math.min(n, sz - at), oldHeight = child.height; + child.removeInner(at, rm); + this.height -= oldHeight - child.height; + if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } + if ((n -= rm) == 0) break; + at = 0; + } else at -= sz; + } + // If the result is smaller than 25 lines, ensure that it is a + // single leaf node. + if (this.size - n < 25 && + (this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) { + var lines = []; + this.collapse(lines); + this.children = [new LeafChunk(lines)]; + this.children[0].parent = this; + } + }, + collapse: function(lines) { + for (var i = 0; i < this.children.length; ++i) this.children[i].collapse(lines); + }, + insertInner: function(at, lines, height) { + this.size += lines.length; + this.height += height; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at <= sz) { + child.insertInner(at, lines, height); + if (child.lines && child.lines.length > 50) { + while (child.lines.length > 50) { + var spilled = child.lines.splice(child.lines.length - 25, 25); + var newleaf = new LeafChunk(spilled); + child.height -= newleaf.height; + this.children.splice(i + 1, 0, newleaf); + newleaf.parent = this; + } + this.maybeSpill(); + } + break; + } + at -= sz; + } + }, + // When a node has grown, check whether it should be split. + maybeSpill: function() { + if (this.children.length <= 10) return; + var me = this; + do { + var spilled = me.children.splice(me.children.length - 5, 5); + var sibling = new BranchChunk(spilled); + if (!me.parent) { // Become the parent node + var copy = new BranchChunk(me.children); + copy.parent = me; + me.children = [copy, sibling]; + me = copy; + } else { + me.size -= sibling.size; + me.height -= sibling.height; + var myIndex = indexOf(me.parent.children, me); + me.parent.children.splice(myIndex + 1, 0, sibling); + } + sibling.parent = me.parent; + } while (me.children.length > 10); + me.parent.maybeSpill(); + }, + iterN: function(at, n, op) { + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var used = Math.min(n, sz - at); + if (child.iterN(at, used, op)) return true; + if ((n -= used) == 0) break; + at = 0; + } else at -= sz; + } + } + }; + + var nextDocId = 0; + var Doc = CodeMirror.Doc = function(text, mode, firstLine) { + if (!(this instanceof Doc)) return new Doc(text, mode, firstLine); + if (firstLine == null) firstLine = 0; + + BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); + this.first = firstLine; + this.scrollTop = this.scrollLeft = 0; + this.cantEdit = false; + this.cleanGeneration = 1; + this.frontier = firstLine; + var start = Pos(firstLine, 0); + this.sel = simpleSelection(start); + this.history = new History(null); + this.id = ++nextDocId; + this.modeOption = mode; + + if (typeof text == "string") text = splitLines(text); + updateDoc(this, {from: start, to: start, text: text}); + setSelection(this, simpleSelection(start), sel_dontScroll); + }; + + Doc.prototype = createObj(BranchChunk.prototype, { + constructor: Doc, + // Iterate over the document. Supports two forms -- with only one + // argument, it calls that for each line in the document. With + // three, it iterates over the range given by the first two (with + // the second being non-inclusive). + iter: function(from, to, op) { + if (op) this.iterN(from - this.first, to - from, op); + else this.iterN(this.first, this.first + this.size, from); + }, + + // Non-public interface for adding and removing lines. + insert: function(at, lines) { + var height = 0; + for (var i = 0; i < lines.length; ++i) height += lines[i].height; + this.insertInner(at - this.first, lines, height); + }, + remove: function(at, n) { this.removeInner(at - this.first, n); }, + + // From here, the methods are part of the public interface. Most + // are also available from CodeMirror (editor) instances. + + getValue: function(lineSep) { + var lines = getLines(this, this.first, this.first + this.size); + if (lineSep === false) return lines; + return lines.join(lineSep || "\n"); + }, + setValue: docMethodOp(function(code) { + var top = Pos(this.first, 0), last = this.first + this.size - 1; + makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), + text: splitLines(code), origin: "setValue", full: true}, true); + setSelection(this, simpleSelection(top)); + }), + replaceRange: function(code, from, to, origin) { + from = clipPos(this, from); + to = to ? clipPos(this, to) : from; + replaceRange(this, code, from, to, origin); + }, + getRange: function(from, to, lineSep) { + var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); + if (lineSep === false) return lines; + return lines.join(lineSep || "\n"); + }, + + getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;}, + + getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line);}, + getLineNumber: function(line) {return lineNo(line);}, + + getLineHandleVisualStart: function(line) { + if (typeof line == "number") line = getLine(this, line); + return visualLine(line); + }, + + lineCount: function() {return this.size;}, + firstLine: function() {return this.first;}, + lastLine: function() {return this.first + this.size - 1;}, + + clipPos: function(pos) {return clipPos(this, pos);}, + + getCursor: function(start) { + var range = this.sel.primary(), pos; + if (start == null || start == "head") pos = range.head; + else if (start == "anchor") pos = range.anchor; + else if (start == "end" || start == "to" || start === false) pos = range.to(); + else pos = range.from(); + return pos; + }, + listSelections: function() { return this.sel.ranges; }, + somethingSelected: function() {return this.sel.somethingSelected();}, + + setCursor: docMethodOp(function(line, ch, options) { + setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options); + }), + setSelection: docMethodOp(function(anchor, head, options) { + setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options); + }), + extendSelection: docMethodOp(function(head, other, options) { + extendSelection(this, clipPos(this, head), other && clipPos(this, other), options); + }), + extendSelections: docMethodOp(function(heads, options) { + extendSelections(this, clipPosArray(this, heads, options)); + }), + extendSelectionsBy: docMethodOp(function(f, options) { + extendSelections(this, map(this.sel.ranges, f), options); + }), + setSelections: docMethodOp(function(ranges, primary, options) { + if (!ranges.length) return; + for (var i = 0, out = []; i < ranges.length; i++) + out[i] = new Range(clipPos(this, ranges[i].anchor), + clipPos(this, ranges[i].head)); + if (primary == null) primary = Math.min(ranges.length - 1, this.sel.primIndex); + setSelection(this, normalizeSelection(out, primary), options); + }), + addSelection: docMethodOp(function(anchor, head, options) { + var ranges = this.sel.ranges.slice(0); + ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor))); + setSelection(this, normalizeSelection(ranges, ranges.length - 1), options); + }), + + getSelection: function(lineSep) { + var ranges = this.sel.ranges, lines; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + lines = lines ? lines.concat(sel) : sel; + } + if (lineSep === false) return lines; + else return lines.join(lineSep || "\n"); + }, + getSelections: function(lineSep) { + var parts = [], ranges = this.sel.ranges; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + if (lineSep !== false) sel = sel.join(lineSep || "\n"); + parts[i] = sel; + } + return parts; + }, + replaceSelection: function(code, collapse, origin) { + var dup = []; + for (var i = 0; i < this.sel.ranges.length; i++) + dup[i] = code; + this.replaceSelections(dup, collapse, origin || "+input"); + }, + replaceSelections: docMethodOp(function(code, collapse, origin) { + var changes = [], sel = this.sel; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + changes[i] = {from: range.from(), to: range.to(), text: splitLines(code[i]), origin: origin}; + } + var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse); + for (var i = changes.length - 1; i >= 0; i--) + makeChange(this, changes[i]); + if (newSel) setSelectionReplaceHistory(this, newSel); + else if (this.cm) ensureCursorVisible(this.cm); + }), + undo: docMethodOp(function() {makeChangeFromHistory(this, "undo");}), + redo: docMethodOp(function() {makeChangeFromHistory(this, "redo");}), + undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true);}), + redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true);}), + + setExtending: function(val) {this.extend = val;}, + getExtending: function() {return this.extend;}, + + historySize: function() { + var hist = this.history, done = 0, undone = 0; + for (var i = 0; i < hist.done.length; i++) if (!hist.done[i].ranges) ++done; + for (var i = 0; i < hist.undone.length; i++) if (!hist.undone[i].ranges) ++undone; + return {undo: done, redo: undone}; + }, + clearHistory: function() {this.history = new History(this.history.maxGeneration);}, + + markClean: function() { + this.cleanGeneration = this.changeGeneration(true); + }, + changeGeneration: function(forceSplit) { + if (forceSplit) + this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null; + return this.history.generation; + }, + isClean: function (gen) { + return this.history.generation == (gen || this.cleanGeneration); + }, + + getHistory: function() { + return {done: copyHistoryArray(this.history.done), + undone: copyHistoryArray(this.history.undone)}; + }, + setHistory: function(histData) { + var hist = this.history = new History(this.history.maxGeneration); + hist.done = copyHistoryArray(histData.done.slice(0), null, true); + hist.undone = copyHistoryArray(histData.undone.slice(0), null, true); + }, + + addLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + if (!line[prop]) line[prop] = cls; + else if (classTest(cls).test(line[prop])) return false; + else line[prop] += " " + cls; + return true; + }); + }), + removeLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + var cur = line[prop]; + if (!cur) return false; + else if (cls == null) line[prop] = null; + else { + var found = cur.match(classTest(cls)); + if (!found) return false; + var end = found.index + found[0].length; + line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null; + } + return true; + }); + }), + + addLineWidget: docMethodOp(function(handle, node, options) { + return addLineWidget(this, handle, node, options); + }), + removeLineWidget: function(widget) { widget.clear(); }, + + markText: function(from, to, options) { + return markText(this, clipPos(this, from), clipPos(this, to), options, "range"); + }, + setBookmark: function(pos, options) { + var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), + insertLeft: options && options.insertLeft, + clearWhenEmpty: false, shared: options && options.shared, + handleMouseEvents: options && options.handleMouseEvents}; + pos = clipPos(this, pos); + return markText(this, pos, pos, realOpts, "bookmark"); + }, + findMarksAt: function(pos) { + pos = clipPos(this, pos); + var markers = [], spans = getLine(this, pos.line).markedSpans; + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if ((span.from == null || span.from <= pos.ch) && + (span.to == null || span.to >= pos.ch)) + markers.push(span.marker.parent || span.marker); + } + return markers; + }, + findMarks: function(from, to, filter) { + from = clipPos(this, from); to = clipPos(this, to); + var found = [], lineNo = from.line; + this.iter(from.line, to.line + 1, function(line) { + var spans = line.markedSpans; + if (spans) for (var i = 0; i < spans.length; i++) { + var span = spans[i]; + if (!(lineNo == from.line && from.ch > span.to || + span.from == null && lineNo != from.line|| + lineNo == to.line && span.from > to.ch) && + (!filter || filter(span.marker))) + found.push(span.marker.parent || span.marker); + } + ++lineNo; + }); + return found; + }, + getAllMarks: function() { + var markers = []; + this.iter(function(line) { + var sps = line.markedSpans; + if (sps) for (var i = 0; i < sps.length; ++i) + if (sps[i].from != null) markers.push(sps[i].marker); + }); + return markers; + }, + + posFromIndex: function(off) { + var ch, lineNo = this.first; + this.iter(function(line) { + var sz = line.text.length + 1; + if (sz > off) { ch = off; return true; } + off -= sz; + ++lineNo; + }); + return clipPos(this, Pos(lineNo, ch)); + }, + indexFromPos: function (coords) { + coords = clipPos(this, coords); + var index = coords.ch; + if (coords.line < this.first || coords.ch < 0) return 0; + this.iter(this.first, coords.line, function (line) { + index += line.text.length + 1; + }); + return index; + }, + + copy: function(copyHistory) { + var doc = new Doc(getLines(this, this.first, this.first + this.size), this.modeOption, this.first); + doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; + doc.sel = this.sel; + doc.extend = false; + if (copyHistory) { + doc.history.undoDepth = this.history.undoDepth; + doc.setHistory(this.getHistory()); + } + return doc; + }, + + linkedDoc: function(options) { + if (!options) options = {}; + var from = this.first, to = this.first + this.size; + if (options.from != null && options.from > from) from = options.from; + if (options.to != null && options.to < to) to = options.to; + var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from); + if (options.sharedHist) copy.history = this.history; + (this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); + copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; + copySharedMarkers(copy, findSharedMarkers(this)); + return copy; + }, + unlinkDoc: function(other) { + if (other instanceof CodeMirror) other = other.doc; + if (this.linked) for (var i = 0; i < this.linked.length; ++i) { + var link = this.linked[i]; + if (link.doc != other) continue; + this.linked.splice(i, 1); + other.unlinkDoc(this); + detachSharedMarkers(findSharedMarkers(this)); + break; + } + // If the histories were shared, split them again + if (other.history == this.history) { + var splitIds = [other.id]; + linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true); + other.history = new History(null); + other.history.done = copyHistoryArray(this.history.done, splitIds); + other.history.undone = copyHistoryArray(this.history.undone, splitIds); + } + }, + iterLinkedDocs: function(f) {linkedDocs(this, f);}, + + getMode: function() {return this.mode;}, + getEditor: function() {return this.cm;} + }); + + // Public alias. + Doc.prototype.eachLine = Doc.prototype.iter; + + // Set up methods on CodeMirror's prototype to redirect to the editor's document. + var dontDelegate = "iter insert remove copy getEditor".split(" "); + for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) + CodeMirror.prototype[prop] = (function(method) { + return function() {return method.apply(this.doc, arguments);}; + })(Doc.prototype[prop]); + + eventMixin(Doc); + + // Call f for all linked documents. + function linkedDocs(doc, f, sharedHistOnly) { + function propagate(doc, skip, sharedHist) { + if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) { + var rel = doc.linked[i]; + if (rel.doc == skip) continue; + var shared = sharedHist && rel.sharedHist; + if (sharedHistOnly && !shared) continue; + f(rel.doc, shared); + propagate(rel.doc, doc, shared); + } + } + propagate(doc, null, true); + } + + // Attach a document to an editor. + function attachDoc(cm, doc) { + if (doc.cm) throw new Error("This document is already in use."); + cm.doc = doc; + doc.cm = cm; + estimateLineHeights(cm); + loadMode(cm); + if (!cm.options.lineWrapping) findMaxLine(cm); + cm.options.mode = doc.modeOption; + regChange(cm); + } + + // LINE UTILITIES + + // Find the line object corresponding to the given line number. + function getLine(doc, n) { + n -= doc.first; + if (n < 0 || n >= doc.size) throw new Error("There is no line " + (n + doc.first) + " in the document."); + for (var chunk = doc; !chunk.lines;) { + for (var i = 0;; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; break; } + n -= sz; + } + } + return chunk.lines[n]; + } + + // Get the part of a document between two positions, as an array of + // strings. + function getBetween(doc, start, end) { + var out = [], n = start.line; + doc.iter(start.line, end.line + 1, function(line) { + var text = line.text; + if (n == end.line) text = text.slice(0, end.ch); + if (n == start.line) text = text.slice(start.ch); + out.push(text); + ++n; + }); + return out; + } + // Get the lines between from and to, as array of strings. + function getLines(doc, from, to) { + var out = []; + doc.iter(from, to, function(line) { out.push(line.text); }); + return out; + } + + // Update the height of a line, propagating the height change + // upwards to parent nodes. + function updateLineHeight(line, height) { + var diff = height - line.height; + if (diff) for (var n = line; n; n = n.parent) n.height += diff; + } + + // Given a line object, find its line number by walking up through + // its parent links. + function lineNo(line) { + if (line.parent == null) return null; + var cur = line.parent, no = indexOf(cur.lines, line); + for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { + for (var i = 0;; ++i) { + if (chunk.children[i] == cur) break; + no += chunk.children[i].chunkSize(); + } + } + return no + cur.first; + } + + // Find the line at the given vertical position, using the height + // information in the document tree. + function lineAtHeight(chunk, h) { + var n = chunk.first; + outer: do { + for (var i = 0; i < chunk.children.length; ++i) { + var child = chunk.children[i], ch = child.height; + if (h < ch) { chunk = child; continue outer; } + h -= ch; + n += child.chunkSize(); + } + return n; + } while (!chunk.lines); + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i], lh = line.height; + if (h < lh) break; + h -= lh; + } + return n + i; + } + + + // Find the height above the given line. + function heightAtLine(lineObj) { + lineObj = visualLine(lineObj); + + var h = 0, chunk = lineObj.parent; + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i]; + if (line == lineObj) break; + else h += line.height; + } + for (var p = chunk.parent; p; chunk = p, p = chunk.parent) { + for (var i = 0; i < p.children.length; ++i) { + var cur = p.children[i]; + if (cur == chunk) break; + else h += cur.height; + } + } + return h; + } + + // Get the bidi ordering for the given line (and cache it). Returns + // false for lines that are fully left-to-right, and an array of + // BidiSpan objects otherwise. + function getOrder(line) { + var order = line.order; + if (order == null) order = line.order = bidiOrdering(line.text); + return order; + } + + // HISTORY + + function History(startGen) { + // Arrays of change events and selections. Doing something adds an + // event to done and clears undo. Undoing moves events from done + // to undone, redoing moves them in the other direction. + this.done = []; this.undone = []; + this.undoDepth = Infinity; + // Used to track when changes can be merged into a single undo + // event + this.lastModTime = this.lastSelTime = 0; + this.lastOp = this.lastSelOp = null; + this.lastOrigin = this.lastSelOrigin = null; + // Used by the isClean() method + this.generation = this.maxGeneration = startGen || 1; + } + + // Create a history change event from an updateDoc-style change + // object. + function historyChangeFromChange(doc, change) { + var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; + attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); + linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true); + return histChange; + } + + // Pop all selection events off the end of a history array. Stop at + // a change event. + function clearSelectionEvents(array) { + while (array.length) { + var last = lst(array); + if (last.ranges) array.pop(); + else break; + } + } + + // Find the top change event in the history. Pop off selection + // events that are in the way. + function lastChangeEvent(hist, force) { + if (force) { + clearSelectionEvents(hist.done); + return lst(hist.done); + } else if (hist.done.length && !lst(hist.done).ranges) { + return lst(hist.done); + } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) { + hist.done.pop(); + return lst(hist.done); + } + } + + // Register a change in the history. Merges changes that are within + // a single operation, ore are close together with an origin that + // allows merging (starting with "+") into a single event. + function addChangeToHistory(doc, change, selAfter, opId) { + var hist = doc.history; + hist.undone.length = 0; + var time = +new Date, cur; + + if ((hist.lastOp == opId || + hist.lastOrigin == change.origin && change.origin && + ((change.origin.charAt(0) == "+" && doc.cm && hist.lastModTime > time - doc.cm.options.historyEventDelay) || + change.origin.charAt(0) == "*")) && + (cur = lastChangeEvent(hist, hist.lastOp == opId))) { + // Merge this change into the last event + var last = lst(cur.changes); + if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) { + // Optimized case for simple insertion -- don't want to add + // new changesets for every character typed + last.to = changeEnd(change); + } else { + // Add new sub-event + cur.changes.push(historyChangeFromChange(doc, change)); + } + } else { + // Can not be merged, start a new event. + var before = lst(hist.done); + if (!before || !before.ranges) + pushSelectionToHistory(doc.sel, hist.done); + cur = {changes: [historyChangeFromChange(doc, change)], + generation: hist.generation}; + hist.done.push(cur); + while (hist.done.length > hist.undoDepth) { + hist.done.shift(); + if (!hist.done[0].ranges) hist.done.shift(); + } + } + hist.done.push(selAfter); + hist.generation = ++hist.maxGeneration; + hist.lastModTime = hist.lastSelTime = time; + hist.lastOp = hist.lastSelOp = opId; + hist.lastOrigin = hist.lastSelOrigin = change.origin; + + if (!last) signal(doc, "historyAdded"); + } + + function selectionEventCanBeMerged(doc, origin, prev, sel) { + var ch = origin.charAt(0); + return ch == "*" || + ch == "+" && + prev.ranges.length == sel.ranges.length && + prev.somethingSelected() == sel.somethingSelected() && + new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500); + } + + // Called whenever the selection changes, sets the new selection as + // the pending selection in the history, and pushes the old pending + // selection into the 'done' array when it was significantly + // different (in number of selected ranges, emptiness, or time). + function addSelectionToHistory(doc, sel, opId, options) { + var hist = doc.history, origin = options && options.origin; + + // A new event is started when the previous origin does not match + // the current, or the origins don't allow matching. Origins + // starting with * are always merged, those starting with + are + // merged when similar and close together in time. + if (opId == hist.lastSelOp || + (origin && hist.lastSelOrigin == origin && + (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin || + selectionEventCanBeMerged(doc, origin, lst(hist.done), sel)))) + hist.done[hist.done.length - 1] = sel; + else + pushSelectionToHistory(sel, hist.done); + + hist.lastSelTime = +new Date; + hist.lastSelOrigin = origin; + hist.lastSelOp = opId; + if (options && options.clearRedo !== false) + clearSelectionEvents(hist.undone); + } + + function pushSelectionToHistory(sel, dest) { + var top = lst(dest); + if (!(top && top.ranges && top.equals(sel))) + dest.push(sel); + } + + // Used to store marked span information in the history. + function attachLocalSpans(doc, change, from, to) { + var existing = change["spans_" + doc.id], n = 0; + doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function(line) { + if (line.markedSpans) + (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans; + ++n; + }); + } + + // When un/re-doing restores text containing marked spans, those + // that have been explicitly cleared should not be restored. + function removeClearedSpans(spans) { + if (!spans) return null; + for (var i = 0, out; i < spans.length; ++i) { + if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); } + else if (out) out.push(spans[i]); + } + return !out ? spans : out.length ? out : null; + } + + // Retrieve and filter the old marked spans stored in a change event. + function getOldSpans(doc, change) { + var found = change["spans_" + doc.id]; + if (!found) return null; + for (var i = 0, nw = []; i < change.text.length; ++i) + nw.push(removeClearedSpans(found[i])); + return nw; + } + + // Used both to provide a JSON-safe object in .getHistory, and, when + // detaching a document, to split the history in two + function copyHistoryArray(events, newGroup, instantiateSel) { + for (var i = 0, copy = []; i < events.length; ++i) { + var event = events[i]; + if (event.ranges) { + copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event); + continue; + } + var changes = event.changes, newChanges = []; + copy.push({changes: newChanges}); + for (var j = 0; j < changes.length; ++j) { + var change = changes[j], m; + newChanges.push({from: change.from, to: change.to, text: change.text}); + if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) { + if (indexOf(newGroup, Number(m[1])) > -1) { + lst(newChanges)[prop] = change[prop]; + delete change[prop]; + } + } + } + } + return copy; + } + + // Rebasing/resetting history to deal with externally-sourced changes + + function rebaseHistSelSingle(pos, from, to, diff) { + if (to < pos.line) { + pos.line += diff; + } else if (from < pos.line) { + pos.line = from; + pos.ch = 0; + } + } + + // Tries to rebase an array of history events given a change in the + // document. If the change touches the same lines as the event, the + // event, and everything 'behind' it, is discarded. If the change is + // before the event, the event's positions are updated. Uses a + // copy-on-write scheme for the positions, to avoid having to + // reallocate them all on every rebase, but also avoid problems with + // shared position objects being unsafely updated. + function rebaseHistArray(array, from, to, diff) { + for (var i = 0; i < array.length; ++i) { + var sub = array[i], ok = true; + if (sub.ranges) { + if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true; } + for (var j = 0; j < sub.ranges.length; j++) { + rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff); + rebaseHistSelSingle(sub.ranges[j].head, from, to, diff); + } + continue; + } + for (var j = 0; j < sub.changes.length; ++j) { + var cur = sub.changes[j]; + if (to < cur.from.line) { + cur.from = Pos(cur.from.line + diff, cur.from.ch); + cur.to = Pos(cur.to.line + diff, cur.to.ch); + } else if (from <= cur.to.line) { + ok = false; + break; + } + } + if (!ok) { + array.splice(0, i + 1); + i = 0; + } + } + } + + function rebaseHist(hist, change) { + var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1; + rebaseHistArray(hist.done, from, to, diff); + rebaseHistArray(hist.undone, from, to, diff); + } + + // EVENT UTILITIES + + // Due to the fact that we still support jurassic IE versions, some + // compatibility wrappers are needed. + + var e_preventDefault = CodeMirror.e_preventDefault = function(e) { + if (e.preventDefault) e.preventDefault(); + else e.returnValue = false; + }; + var e_stopPropagation = CodeMirror.e_stopPropagation = function(e) { + if (e.stopPropagation) e.stopPropagation(); + else e.cancelBubble = true; + }; + function e_defaultPrevented(e) { + return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false; + } + var e_stop = CodeMirror.e_stop = function(e) {e_preventDefault(e); e_stopPropagation(e);}; + + function e_target(e) {return e.target || e.srcElement;} + function e_button(e) { + var b = e.which; + if (b == null) { + if (e.button & 1) b = 1; + else if (e.button & 2) b = 3; + else if (e.button & 4) b = 2; + } + if (mac && e.ctrlKey && b == 1) b = 3; + return b; + } + + // EVENT HANDLING + + // Lightweight event framework. on/off also work on DOM nodes, + // registering native DOM handlers. + + var on = CodeMirror.on = function(emitter, type, f) { + if (emitter.addEventListener) + emitter.addEventListener(type, f, false); + else if (emitter.attachEvent) + emitter.attachEvent("on" + type, f); + else { + var map = emitter._handlers || (emitter._handlers = {}); + var arr = map[type] || (map[type] = []); + arr.push(f); + } + }; + + var off = CodeMirror.off = function(emitter, type, f) { + if (emitter.removeEventListener) + emitter.removeEventListener(type, f, false); + else if (emitter.detachEvent) + emitter.detachEvent("on" + type, f); + else { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + for (var i = 0; i < arr.length; ++i) + if (arr[i] == f) { arr.splice(i, 1); break; } + } + }; + + var signal = CodeMirror.signal = function(emitter, type /*, values...*/) { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + var args = Array.prototype.slice.call(arguments, 2); + for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args); + }; + + var orphanDelayedCallbacks = null; + + // Often, we want to signal events at a point where we are in the + // middle of some work, but don't want the handler to start calling + // other methods on the editor, which might be in an inconsistent + // state or simply not expect any other events to happen. + // signalLater looks whether there are any handlers, and schedules + // them to be executed when the last operation ends, or, if no + // operation is active, when a timeout fires. + function signalLater(emitter, type /*, values...*/) { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + var args = Array.prototype.slice.call(arguments, 2), list; + if (operationGroup) { + list = operationGroup.delayedCallbacks; + } else if (orphanDelayedCallbacks) { + list = orphanDelayedCallbacks; + } else { + list = orphanDelayedCallbacks = []; + setTimeout(fireOrphanDelayed, 0); + } + function bnd(f) {return function(){f.apply(null, args);};}; + for (var i = 0; i < arr.length; ++i) + list.push(bnd(arr[i])); + } + + function fireOrphanDelayed() { + var delayed = orphanDelayedCallbacks; + orphanDelayedCallbacks = null; + for (var i = 0; i < delayed.length; ++i) delayed[i](); + } + + // The DOM events that CodeMirror handles can be overridden by + // registering a (non-DOM) handler on the editor for the event name, + // and preventDefault-ing the event in that handler. + function signalDOMEvent(cm, e, override) { + if (typeof e == "string") + e = {type: e, preventDefault: function() { this.defaultPrevented = true; }}; + signal(cm, override || e.type, cm, e); + return e_defaultPrevented(e) || e.codemirrorIgnore; + } + + function signalCursorActivity(cm) { + var arr = cm._handlers && cm._handlers.cursorActivity; + if (!arr) return; + var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []); + for (var i = 0; i < arr.length; ++i) if (indexOf(set, arr[i]) == -1) + set.push(arr[i]); + } + + function hasHandler(emitter, type) { + var arr = emitter._handlers && emitter._handlers[type]; + return arr && arr.length > 0; + } + + // Add on and off methods to a constructor's prototype, to make + // registering events on such objects more convenient. + function eventMixin(ctor) { + ctor.prototype.on = function(type, f) {on(this, type, f);}; + ctor.prototype.off = function(type, f) {off(this, type, f);}; + } + + // MISC UTILITIES + + // Number of pixels added to scroller and sizer to hide scrollbar + var scrollerGap = 30; + + // Returned or thrown by various protocols to signal 'I'm not + // handling this'. + var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}}; + + // Reused option objects for setSelection & friends + var sel_dontScroll = {scroll: false}, sel_mouse = {origin: "*mouse"}, sel_move = {origin: "+move"}; + + function Delayed() {this.id = null;} + Delayed.prototype.set = function(ms, f) { + clearTimeout(this.id); + this.id = setTimeout(f, ms); + }; + + // Counts the column offset in a string, taking tabs into account. + // Used mostly to find indentation. + var countColumn = CodeMirror.countColumn = function(string, end, tabSize, startIndex, startValue) { + if (end == null) { + end = string.search(/[^\s\u00a0]/); + if (end == -1) end = string.length; + } + for (var i = startIndex || 0, n = startValue || 0;;) { + var nextTab = string.indexOf("\t", i); + if (nextTab < 0 || nextTab >= end) + return n + (end - i); + n += nextTab - i; + n += tabSize - (n % tabSize); + i = nextTab + 1; + } + }; + + // The inverse of countColumn -- find the offset that corresponds to + // a particular column. + function findColumn(string, goal, tabSize) { + for (var pos = 0, col = 0;;) { + var nextTab = string.indexOf("\t", pos); + if (nextTab == -1) nextTab = string.length; + var skipped = nextTab - pos; + if (nextTab == string.length || col + skipped >= goal) + return pos + Math.min(skipped, goal - col); + col += nextTab - pos; + col += tabSize - (col % tabSize); + pos = nextTab + 1; + if (col >= goal) return pos; + } + } + + var spaceStrs = [""]; + function spaceStr(n) { + while (spaceStrs.length <= n) + spaceStrs.push(lst(spaceStrs) + " "); + return spaceStrs[n]; + } + + function lst(arr) { return arr[arr.length-1]; } + + var selectInput = function(node) { node.select(); }; + if (ios) // Mobile Safari apparently has a bug where select() is broken. + selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length; }; + else if (ie) // Suppress mysterious IE10 errors + selectInput = function(node) { try { node.select(); } catch(_e) {} }; + + function indexOf(array, elt) { + for (var i = 0; i < array.length; ++i) + if (array[i] == elt) return i; + return -1; + } + function map(array, f) { + var out = []; + for (var i = 0; i < array.length; i++) out[i] = f(array[i], i); + return out; + } + + function nothing() {} + + function createObj(base, props) { + var inst; + if (Object.create) { + inst = Object.create(base); + } else { + nothing.prototype = base; + inst = new nothing(); + } + if (props) copyObj(props, inst); + return inst; + }; + + function copyObj(obj, target, overwrite) { + if (!target) target = {}; + for (var prop in obj) + if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop))) + target[prop] = obj[prop]; + return target; + } + + function bind(f) { + var args = Array.prototype.slice.call(arguments, 1); + return function(){return f.apply(null, args);}; + } + + var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; + var isWordCharBasic = CodeMirror.isWordChar = function(ch) { + return /\w/.test(ch) || ch > "\x80" && + (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)); + }; + function isWordChar(ch, helper) { + if (!helper) return isWordCharBasic(ch); + if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) return true; + return helper.test(ch); + } + + function isEmpty(obj) { + for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false; + return true; + } + + // Extending unicode characters. A series of a non-extending char + + // any number of extending chars is treated as a single unit as far + // as editing and measuring is concerned. This is not fully correct, + // since some scripts/fonts/browsers also treat other configurations + // of code points as a group. + var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/; + function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch); } + + // DOM UTILITIES + + function elt(tag, content, className, style) { + var e = document.createElement(tag); + if (className) e.className = className; + if (style) e.style.cssText = style; + if (typeof content == "string") e.appendChild(document.createTextNode(content)); + else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]); + return e; + } + + var range; + if (document.createRange) range = function(node, start, end, endNode) { + var r = document.createRange(); + r.setEnd(endNode || node, end); + r.setStart(node, start); + return r; + }; + else range = function(node, start, end) { + var r = document.body.createTextRange(); + try { r.moveToElementText(node.parentNode); } + catch(e) { return r; } + r.collapse(true); + r.moveEnd("character", end); + r.moveStart("character", start); + return r; + }; + + function removeChildren(e) { + for (var count = e.childNodes.length; count > 0; --count) + e.removeChild(e.firstChild); + return e; + } + + function removeChildrenAndAdd(parent, e) { + return removeChildren(parent).appendChild(e); + } + + var contains = CodeMirror.contains = function(parent, child) { + if (child.nodeType == 3) // Android browser always returns false when child is a textnode + child = child.parentNode; + if (parent.contains) + return parent.contains(child); + do { + if (child.nodeType == 11) child = child.host; + if (child == parent) return true; + } while (child = child.parentNode); + }; + + function activeElt() { return document.activeElement; } + // Older versions of IE throws unspecified error when touching + // document.activeElement in some cases (during loading, in iframe) + if (ie && ie_version < 11) activeElt = function() { + try { return document.activeElement; } + catch(e) { return document.body; } + }; + + function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*"); } + var rmClass = CodeMirror.rmClass = function(node, cls) { + var current = node.className; + var match = classTest(cls).exec(current); + if (match) { + var after = current.slice(match.index + match[0].length); + node.className = current.slice(0, match.index) + (after ? match[1] + after : ""); + } + }; + var addClass = CodeMirror.addClass = function(node, cls) { + var current = node.className; + if (!classTest(cls).test(current)) node.className += (current ? " " : "") + cls; + }; + function joinClasses(a, b) { + var as = a.split(" "); + for (var i = 0; i < as.length; i++) + if (as[i] && !classTest(as[i]).test(b)) b += " " + as[i]; + return b; + } + + // WINDOW-WIDE EVENTS + + // These must be handled carefully, because naively registering a + // handler for each editor will cause the editors to never be + // garbage collected. + + function forEachCodeMirror(f) { + if (!document.body.getElementsByClassName) return; + var byClass = document.body.getElementsByClassName("CodeMirror"); + for (var i = 0; i < byClass.length; i++) { + var cm = byClass[i].CodeMirror; + if (cm) f(cm); + } + } + + var globalsRegistered = false; + function ensureGlobalHandlers() { + if (globalsRegistered) return; + registerGlobalHandlers(); + globalsRegistered = true; + } + function registerGlobalHandlers() { + // When the window resizes, we need to refresh active editors. + var resizeTimer; + on(window, "resize", function() { + if (resizeTimer == null) resizeTimer = setTimeout(function() { + resizeTimer = null; + forEachCodeMirror(onResize); + }, 100); + }); + // When the window loses focus, we want to show the editor as blurred + on(window, "blur", function() { + forEachCodeMirror(onBlur); + }); + } + + // FEATURE DETECTION + + // Detect drag-and-drop + var dragAndDrop = function() { + // There is *some* kind of drag-and-drop support in IE6-8, but I + // couldn't get it to work yet. + if (ie && ie_version < 9) return false; + var div = elt('div'); + return "draggable" in div || "dragDrop" in div; + }(); + + var zwspSupported; + function zeroWidthElement(measure) { + if (zwspSupported == null) { + var test = elt("span", "\u200b"); + removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])); + if (measure.firstChild.offsetHeight != 0) + zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8); + } + var node = zwspSupported ? elt("span", "\u200b") : + elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); + node.setAttribute("cm-text", ""); + return node; + } + + // Feature-detect IE's crummy client rect reporting for bidi text + var badBidiRects; + function hasBadBidiRects(measure) { + if (badBidiRects != null) return badBidiRects; + var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA")); + var r0 = range(txt, 0, 1).getBoundingClientRect(); + if (!r0 || r0.left == r0.right) return false; // Safari returns null in some cases (#2780) + var r1 = range(txt, 1, 2).getBoundingClientRect(); + return badBidiRects = (r1.right - r0.right < 3); + } + + // See if "".split is the broken IE version, if so, provide an + // alternative way to split lines. + var splitLines = CodeMirror.splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { + var pos = 0, result = [], l = string.length; + while (pos <= l) { + var nl = string.indexOf("\n", pos); + if (nl == -1) nl = string.length; + var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl); + var rt = line.indexOf("\r"); + if (rt != -1) { + result.push(line.slice(0, rt)); + pos += rt + 1; + } else { + result.push(line); + pos = nl + 1; + } + } + return result; + } : function(string){return string.split(/\r\n?|\n/);}; + + var hasSelection = window.getSelection ? function(te) { + try { return te.selectionStart != te.selectionEnd; } + catch(e) { return false; } + } : function(te) { + try {var range = te.ownerDocument.selection.createRange();} + catch(e) {} + if (!range || range.parentElement() != te) return false; + return range.compareEndPoints("StartToEnd", range) != 0; + }; + + var hasCopyEvent = (function() { + var e = elt("div"); + if ("oncopy" in e) return true; + e.setAttribute("oncopy", "return;"); + return typeof e.oncopy == "function"; + })(); + + var badZoomedRects = null; + function hasBadZoomedRects(measure) { + if (badZoomedRects != null) return badZoomedRects; + var node = removeChildrenAndAdd(measure, elt("span", "x")); + var normal = node.getBoundingClientRect(); + var fromRange = range(node, 0, 1).getBoundingClientRect(); + return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1; + } + + // KEY NAMES + + var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", + 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", + 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", + 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", 107: "=", 109: "-", 127: "Delete", + 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", + 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", + 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"}; + CodeMirror.keyNames = keyNames; + (function() { + // Number keys + for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i); + // Alphabetic keys + for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i); + // Function keys + for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i; + })(); + + // BIDI HELPERS + + function iterateBidiSections(order, from, to, f) { + if (!order) return f(from, to, "ltr"); + var found = false; + for (var i = 0; i < order.length; ++i) { + var part = order[i]; + if (part.from < to && part.to > from || from == to && part.to == from) { + f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr"); + found = true; + } + } + if (!found) f(from, to, "ltr"); + } + + function bidiLeft(part) { return part.level % 2 ? part.to : part.from; } + function bidiRight(part) { return part.level % 2 ? part.from : part.to; } + + function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; } + function lineRight(line) { + var order = getOrder(line); + if (!order) return line.text.length; + return bidiRight(lst(order)); + } + + function lineStart(cm, lineN) { + var line = getLine(cm.doc, lineN); + var visual = visualLine(line); + if (visual != line) lineN = lineNo(visual); + var order = getOrder(visual); + var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual); + return Pos(lineN, ch); + } + function lineEnd(cm, lineN) { + var merged, line = getLine(cm.doc, lineN); + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line; + lineN = null; + } + var order = getOrder(line); + var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line); + return Pos(lineN == null ? lineNo(line) : lineN, ch); + } + function lineStartSmart(cm, pos) { + var start = lineStart(cm, pos.line); + var line = getLine(cm.doc, start.line); + var order = getOrder(line); + if (!order || order[0].level == 0) { + var firstNonWS = Math.max(0, line.text.search(/\S/)); + var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch; + return Pos(start.line, inWS ? 0 : firstNonWS); + } + return start; + } + + function compareBidiLevel(order, a, b) { + var linedir = order[0].level; + if (a == linedir) return true; + if (b == linedir) return false; + return a < b; + } + var bidiOther; + function getBidiPartAt(order, pos) { + bidiOther = null; + for (var i = 0, found; i < order.length; ++i) { + var cur = order[i]; + if (cur.from < pos && cur.to > pos) return i; + if ((cur.from == pos || cur.to == pos)) { + if (found == null) { + found = i; + } else if (compareBidiLevel(order, cur.level, order[found].level)) { + if (cur.from != cur.to) bidiOther = found; + return i; + } else { + if (cur.from != cur.to) bidiOther = i; + return found; + } + } + } + return found; + } + + function moveInLine(line, pos, dir, byUnit) { + if (!byUnit) return pos + dir; + do pos += dir; + while (pos > 0 && isExtendingChar(line.text.charAt(pos))); + return pos; + } + + // This is needed in order to move 'visually' through bi-directional + // text -- i.e., pressing left should make the cursor go left, even + // when in RTL text. The tricky part is the 'jumps', where RTL and + // LTR text touch each other. This often requires the cursor offset + // to move more than one unit, in order to visually move one unit. + function moveVisually(line, start, dir, byUnit) { + var bidi = getOrder(line); + if (!bidi) return moveLogically(line, start, dir, byUnit); + var pos = getBidiPartAt(bidi, start), part = bidi[pos]; + var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit); + + for (;;) { + if (target > part.from && target < part.to) return target; + if (target == part.from || target == part.to) { + if (getBidiPartAt(bidi, target) == pos) return target; + part = bidi[pos += dir]; + return (dir > 0) == part.level % 2 ? part.to : part.from; + } else { + part = bidi[pos += dir]; + if (!part) return null; + if ((dir > 0) == part.level % 2) + target = moveInLine(line, part.to, -1, byUnit); + else + target = moveInLine(line, part.from, 1, byUnit); + } + } + } + + function moveLogically(line, start, dir, byUnit) { + var target = start + dir; + if (byUnit) while (target > 0 && isExtendingChar(line.text.charAt(target))) target += dir; + return target < 0 || target > line.text.length ? null : target; + } + + // Bidirectional ordering algorithm + // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm + // that this (partially) implements. + + // One-char codes used for character types: + // L (L): Left-to-Right + // R (R): Right-to-Left + // r (AL): Right-to-Left Arabic + // 1 (EN): European Number + // + (ES): European Number Separator + // % (ET): European Number Terminator + // n (AN): Arabic Number + // , (CS): Common Number Separator + // m (NSM): Non-Spacing Mark + // b (BN): Boundary Neutral + // s (B): Paragraph Separator + // t (S): Segment Separator + // w (WS): Whitespace + // N (ON): Other Neutrals + + // Returns null if characters are ordered as they appear + // (left-to-right), or an array of sections ({from, to, level} + // objects) in the order in which they occur visually. + var bidiOrdering = (function() { + // Character types for codepoints 0 to 0xff + var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"; + // Character types for codepoints 0x600 to 0x6ff + var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm"; + function charType(code) { + if (code <= 0xf7) return lowTypes.charAt(code); + else if (0x590 <= code && code <= 0x5f4) return "R"; + else if (0x600 <= code && code <= 0x6ed) return arabicTypes.charAt(code - 0x600); + else if (0x6ee <= code && code <= 0x8ac) return "r"; + else if (0x2000 <= code && code <= 0x200b) return "w"; + else if (code == 0x200c) return "b"; + else return "L"; + } + + var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/; + var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/; + // Browsers seem to always treat the boundaries of block elements as being L. + var outerType = "L"; + + function BidiSpan(level, from, to) { + this.level = level; + this.from = from; this.to = to; + } + + return function(str) { + if (!bidiRE.test(str)) return false; + var len = str.length, types = []; + for (var i = 0, type; i < len; ++i) + types.push(type = charType(str.charCodeAt(i))); + + // W1. Examine each non-spacing mark (NSM) in the level run, and + // change the type of the NSM to the type of the previous + // character. If the NSM is at the start of the level run, it will + // get the type of sor. + for (var i = 0, prev = outerType; i < len; ++i) { + var type = types[i]; + if (type == "m") types[i] = prev; + else prev = type; + } + + // W2. Search backwards from each instance of a European number + // until the first strong type (R, L, AL, or sor) is found. If an + // AL is found, change the type of the European number to Arabic + // number. + // W3. Change all ALs to R. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (type == "1" && cur == "r") types[i] = "n"; + else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; } + } + + // W4. A single European separator between two European numbers + // changes to a European number. A single common separator between + // two numbers of the same type changes to that type. + for (var i = 1, prev = types[0]; i < len - 1; ++i) { + var type = types[i]; + if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1"; + else if (type == "," && prev == types[i+1] && + (prev == "1" || prev == "n")) types[i] = prev; + prev = type; + } + + // W5. A sequence of European terminators adjacent to European + // numbers changes to all European numbers. + // W6. Otherwise, separators and terminators change to Other + // Neutral. + for (var i = 0; i < len; ++i) { + var type = types[i]; + if (type == ",") types[i] = "N"; + else if (type == "%") { + for (var end = i + 1; end < len && types[end] == "%"; ++end) {} + var replace = (i && types[i-1] == "!") || (end < len && types[end] == "1") ? "1" : "N"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // W7. Search backwards from each instance of a European number + // until the first strong type (R, L, or sor) is found. If an L is + // found, then change the type of the European number to L. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (cur == "L" && type == "1") types[i] = "L"; + else if (isStrong.test(type)) cur = type; + } + + // N1. A sequence of neutrals takes the direction of the + // surrounding strong text if the text on both sides has the same + // direction. European and Arabic numbers act as if they were R in + // terms of their influence on neutrals. Start-of-level-run (sor) + // and end-of-level-run (eor) are used at level run boundaries. + // N2. Any remaining neutrals take the embedding direction. + for (var i = 0; i < len; ++i) { + if (isNeutral.test(types[i])) { + for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {} + var before = (i ? types[i-1] : outerType) == "L"; + var after = (end < len ? types[end] : outerType) == "L"; + var replace = before || after ? "L" : "R"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // Here we depart from the documented algorithm, in order to avoid + // building up an actual levels array. Since there are only three + // levels (0, 1, 2) in an implementation that doesn't take + // explicit embedding into account, we can build up the order on + // the fly, without following the level-based algorithm. + var order = [], m; + for (var i = 0; i < len;) { + if (countsAsLeft.test(types[i])) { + var start = i; + for (++i; i < len && countsAsLeft.test(types[i]); ++i) {} + order.push(new BidiSpan(0, start, i)); + } else { + var pos = i, at = order.length; + for (++i; i < len && types[i] != "L"; ++i) {} + for (var j = pos; j < i;) { + if (countsAsNum.test(types[j])) { + if (pos < j) order.splice(at, 0, new BidiSpan(1, pos, j)); + var nstart = j; + for (++j; j < i && countsAsNum.test(types[j]); ++j) {} + order.splice(at, 0, new BidiSpan(2, nstart, j)); + pos = j; + } else ++j; + } + if (pos < i) order.splice(at, 0, new BidiSpan(1, pos, i)); + } + } + if (order[0].level == 1 && (m = str.match(/^\s+/))) { + order[0].from = m[0].length; + order.unshift(new BidiSpan(0, 0, m[0].length)); + } + if (lst(order).level == 1 && (m = str.match(/\s+$/))) { + lst(order).to -= m[0].length; + order.push(new BidiSpan(0, len - m[0].length, len)); + } + if (order[0].level != lst(order).level) + order.push(new BidiSpan(order[0].level, len, len)); + + return order; + }; + })(); + + // THE END + + CodeMirror.version = "5.1.1"; + + return CodeMirror; +}); \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/public/js/policy-view.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/public/js/policy-view.js new file mode 100644 index 000000000..67e2c7f8a --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/public/js/policy-view.js @@ -0,0 +1,128 @@ +/* + * 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. + */ + +var skipStep = {}; +var policy = {}; + + +skipStep["policy-platform"] = function (policyPayloadObj) { + console.log(policyPayloadObj); + policy["name"] = policyPayloadObj["policyName"]; + policy["platform"] = policyPayloadObj["profile"]["deviceType"]["name"]; + policy["platformId"] = policyPayloadObj["profile"]["deviceType"]["id"]; + var userRoleInput = $("#user-roles-input"); + var ownershipInput = $("#ownership-input"); + var userInput = $("#users-select-field"); + var actionInput = $("#action-input"); + var policyNameInput = $("#policy-name-input"); + var policyDescriptionInput = $("#policy-description-input"); + userRoleInput.val(policyPayloadObj.roles); + userInput.val(policyPayloadObj.users); + ownershipInput.val(policyPayloadObj.ownershipType); + actionInput.val(policyPayloadObj.compliance); + policyNameInput.val(policyPayloadObj["policyName"]); + policyDescriptionInput.val(policyPayloadObj["description"]); + // updating next-page wizard title with selected platform + $("#policy-heading").text(policy["platform"].toUpperCase() + " POLICY - " + policy["name"].toUpperCase()); + $("#policy-platform").text(policy["platform"].toUpperCase()); + $("#policy-assignment").text(policyPayloadObj.ownershipType); + $("#policy-action").text(policyPayloadObj.compliance.toUpperCase()); + $("#policy-description").text(policyPayloadObj["description"]); + var policyStatus = "Active"; + if(policyPayloadObj["active"] == true && policyPayloadObj["updated"] == true) { + policyStatus = ' Active/Updated'; + } else if(policyPayloadObj["active"] == true && policyPayloadObj["updated"] == false) { + policyStatus = ' Active'; + } else if(policyPayloadObj["active"] == false && policyPayloadObj["updated"] == true) { + policyStatus = ' Inactive/Updated'; + } else if(policyPayloadObj["active"] == false && policyPayloadObj["updated"] == false) { + policyStatus = ' Inactive'; + } + + $("#policy-status").html(policyStatus); + + if(policyPayloadObj.users.length > 0) { + $("#policy-users").text(policyPayloadObj.users.toString().split(",").join(", ")); + } else { + $("#users-row").addClass("hidden"); + } + + if(policyPayloadObj.roles.length > 0) { + $("#policy-roles").text(policyPayloadObj.roles.toString().split(",").join(", ")); + } else { + $("#roles-row").addClass("hidden"); + } + var profileFeaturesList = policyPayloadObj["profile"]["profileFeaturesList"]; + if (profileFeaturesList.length > 0){ + var content = profileFeaturesList[0]["content"]; + var policyDefinitionObj = JSON.parse(content); + window.queryEditor.setValue(policyDefinitionObj["policyDefinition"]); + } +}; + + +// End of functions related to grid-input-view + +/** + * This method will return query parameter value given its name. + * @param name Query parameter name + * @returns {string} Query parameter value + */ +var getParameterByName = function (name) { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), + results = regex.exec(location.search); + return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); +}; + +$(document).ready(function () { + + window.queryEditor = CodeMirror.fromTextArea(document.getElementById('policy-definition-input'), { + mode: MIME_TYPE_SIDDHI_QL, + indentWithTabs: true, + smartIndent: true, + lineNumbers: true, + matchBrackets: true, + autofocus: true, + readOnly: true, + extraKeys: { + "Shift-2": function (cm) { + insertStr(cm, cm.getCursor(), '@'); + CodeMirror.showHint(cm, getAnnotationHints); + }, + "Ctrl-Space": "autocomplete" + } + }); + + var policyPayloadObj; + invokerUtil.get( + "/devicemgt_admin/policies/" + getParameterByName("id"), + // on success + function (data) { + // console.log("success: " + JSON.stringify(data)); + data = JSON.parse(data); + policyPayloadObj = data["responseContent"]; + skipStep["policy-platform"](policyPayloadObj); + }, + // on error + function () { + // should be redirected to an error page + } + ); + +}); \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/public/js/sql.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/public/js/sql.js new file mode 100644 index 000000000..9f92fbc93 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/public/js/sql.js @@ -0,0 +1,310 @@ +/* + * 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. + */ + +/*Annotations, Annotation Names and relevant tokens*/ +var ANNOTATION_IMPORT = "Import"; +var ANNOTATION_EXPORT = "Export"; + +var ANNOTATION_TOKEN_AT = "@"; +var ANNOTATION_TOKEN_OPENING_BRACKET = "("; +var ANNOTATION_TOKEN_CLOSING_BRACKET = ")"; + +var REGEX_LINE_STARTING_WITH_PLAN = /^@Plan.*/g; +var REGEX_LINE_STARTING_WITH_SINGLE_LINE_COMMENT = /^--.*/g; +var REGEX_LINE_STARTING_WITH_MULTI_LINE_COMMENT = /^\/\*.*\*\//g; +var REGEX_LINE_STARTING_WITH_IMPORT_STATEMENT = /^@Import.*/g; + +var SIDDHI_STATEMENT_DELIMETER = ";"; +var SIDDHI_LINE_BREAK = "\n"; +var SIDDHI_LINE_BREAK_CHARACTER = '\n'; +var SIDDHI_SINGLE_QUOTE = "'"; +var SIDDHI_SPACE_LITERAL = " "; + +var SIDDHI_LITERAL_DEFINE_STREAM = "define stream"; + +var MIME_TYPE_SIDDHI_QL = "text/siddhi-ql"; + + +CodeMirror.defineMode("sql", function (config, parserConfig) { + "use strict"; + + var client = parserConfig.client || {}, + atoms = parserConfig.atoms || {"false":true, "true":true, "null":true}, + builtin = parserConfig.builtin || {}, + keywords = parserConfig.keywords || {}, + operatorChars = parserConfig.operatorChars || /^[*+\-%<>!=&|~^]/, + support = parserConfig.support || {}, + hooks = parserConfig.hooks || {}, + dateSQL = parserConfig.dateSQL || {"date":true, "time":true, "timestamp":true}; + + function tokenBase(stream, state) { + var ch = stream.next(); + + // call hooks from the mime type + if (hooks[ch]) { + var result = hooks[ch](stream, state); + if (result !== false) return result; + } + + if (ch.charCodeAt(0) > 47 && ch.charCodeAt(0) < 58) { + // numbers + // ref: http://dev.mysql.com/doc/refman/5.5/en/number-literals.html + stream.match(/^[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?/); + support.decimallessFloat == true && stream.eat('.'); + return "number"; + } else if (ch == "'" || (ch == '"' && support.doubleQuote)) { + // strings + // ref: http://dev.mysql.com/doc/refman/5.5/en/string-literals.html + state.tokenize = tokenLiteral(ch); + return state.tokenize(stream, state); + } else if (/^[\(\),\;\[\]]/.test(ch)) { + // no highlightning + return null; + } else if ((ch == "-" && stream.eat("-") && (!support.commentSpaceRequired || stream.eat(" ")))) { + // 1-line comments + // ref: https://kb.askmonty.org/en/comment-syntax/ + stream.skipToEnd(); + return "comment"; + } else if (ch == "/" && stream.eat("*")) { + // multi-line comments + // ref: https://kb.askmonty.org/en/comment-syntax/ + state.tokenize = tokenComment; + return state.tokenize(stream, state); + } else if (ch == ".") { + // .1 for 0.1 + if (support.zerolessFloat == true && stream.match(/^(?:\d+(?:e[+-]?\d+)?)/i)) { + return "number"; + } + } else { + stream.eatWhile(/^[_\-\w\d]/); /* Character '-' will also be eaten, to prevent the highlight happening in keywords being embedded in non-keyword strings. For example, 'all' in 'all-nonkeyword' */ + var word = stream.current().toLowerCase(); // Added toLowerCase() to highlight keywords in a case insensitive manner. + // dates (standard SQL syntax) + // ref: http://dev.mysql.com/doc/refman/5.5/en/date-and-time-literals.html + if (dateSQL.hasOwnProperty(word) && (stream.match(/^( )+'[^']*'/) || stream.match(/^( )+"[^"]*"/))) + return "number"; + if (atoms.hasOwnProperty(word)) return "atom"; + if (builtin.hasOwnProperty(word)) return "builtin"; + if (keywords.hasOwnProperty(word)) return "keyword"; + if (client.hasOwnProperty(word)) return "string-2"; + return null; + } + } + + // 'string', with char specified in quote escaped by '\' + function tokenLiteral(quote) { + return function (stream, state) { + var escaped = false, ch; + while ((ch = stream.next()) != null) { + if (ch == quote && !escaped) { + state.tokenize = tokenBase; + break; + } + escaped = !escaped && ch == "\\"; + } + return "string"; + }; + } + + function tokenComment(stream, state) { + while (true) { + if (stream.skipTo("*")) { + stream.next(); + if (stream.eat("/")) { + state.tokenize = tokenBase; + break; + } + } else { + stream.skipToEnd(); + break; + } + } + return "comment"; + } + + function pushContext(stream, state, type) { + state.context = { + prev:state.context, + indent:stream.indentation(), + col:stream.column(), + type:type + }; + } + + function popContext(state) { + state.indent = state.context.indent; + state.context = state.context.prev; + } + + return { + startState:function () { + return {tokenize:tokenBase, context:null}; + }, + + token:function (stream, state) { + if (stream.sol()) { + if (state.context && state.context.align == null) + state.context.align = false; + } + if (stream.eatSpace()) return null; + + var style = state.tokenize(stream, state); + if (style == "comment") return style; + + if (state.context && state.context.align == null) + state.context.align = true; + + var tok = stream.current(); + if (tok == "(") + pushContext(stream, state, ")"); + else if (tok == "[") + pushContext(stream, state, "]"); + else if (state.context && state.context.type == tok) + popContext(state); + return style; + }, + + indent:function (state, textAfter) { + var cx = state.context; + if (!cx) return CodeMirror.Pass; + var closing = textAfter.charAt(0) == cx.type; + if (cx.align) return cx.col + (closing ? 0 : 1); + else return cx.indent + (closing ? 0 : config.indentUnit); + }, + + blockCommentStart: "/*", + blockCommentEnd: "*/", + lineComment: "--" + }; +}); + +(function () { + "use strict"; + + // `identifier` + function hookIdentifier(stream) { + // MySQL/MariaDB identifiers + // ref: http://dev.mysql.com/doc/refman/5.6/en/identifier-qualifiers.html + var ch; + while ((ch = stream.next()) != null) { + if (ch == "`" && !stream.eat("`")) return "variable-2"; + } + stream.backUp(stream.current().length - 1); + return stream.eatWhile(/\w/) ? "variable-2" : null; + } + + // variable token + function hookVar(stream) { + // variables + // @@prefix.varName @varName + // varName can be quoted with ` or ' or " + // ref: http://dev.mysql.com/doc/refman/5.5/en/user-variables.html + if (stream.eat("@")) { + stream.match(/^session\./); + stream.match(/^local\./); + stream.match(/^global\./); + } + + if (stream.eat("'")) { + stream.match(/^.*'/); + return "variable-2"; + } else if (stream.eat('"')) { + stream.match(/^.*"/); + return "variable-2"; + } else if (stream.eat("`")) { + stream.match(/^.*`/); + return "variable-2"; + } else if (stream.match(/^[0-9a-zA-Z$\.\_]+/)) { + return "variable-2"; + } + return null; + } + + ; + + // short client keyword token + function hookClient(stream) { + // \N means NULL + // ref: http://dev.mysql.com/doc/refman/5.5/en/null-values.html + if (stream.eat("N")) { + return "atom"; + } + // \g, etc + // ref: http://dev.mysql.com/doc/refman/5.5/en/mysql-commands.html + return stream.match(/^[a-zA-Z.#!?]/) ? "variable-2" : null; + } + + // these keywords are used by all SQL dialects (however, a mode can still overwrite it) + var sqlKeywordsWithoutSymbols = "all and as begin by contains define delete end events " + + "every first for from full group having inner insert into join last " + + "left not of on or outer output partition raw return right select snapshot stream table "; + var sqlKeywords = ", : ? # ( ) " + sqlKeywordsWithoutSymbols; + var builtIn = "bool double float int long object string "; + var atoms = "false true null "; + var dateSQL = "days hours milliseconds minutes months seconds "; + var allSqlSuggestions = sqlKeywordsWithoutSymbols + builtIn + atoms + dateSQL; + + // turn a space-separated list into an array + function set(str) { + var obj = {}, words = str.split(" "); + for (var i = 0; i < words.length; ++i) obj[words[i]] = true; + return obj; + } + + // A generic SQL Mode. It's not a standard, it just try to support what is generally supported + CodeMirror.defineMIME(MIME_TYPE_SIDDHI_QL, { + name:"sql", + keywords:set(sqlKeywords), + builtin:set(builtIn), + atoms:set(atoms), + operatorChars:/^[*+%<>!=/]/, + dateSQL:set(dateSQL), + support:set("doubleQuote "), + allSqlSuggestions:set(allSqlSuggestions) + }); +}()); + +/* + How Properties of Mime Types are used by SQL Mode + ================================================= + + keywords: + A list of keywords you want to be highlighted. + functions: + A list of function names you want to be highlighted. + builtin: + A list of builtin types you want to be highlighted (if you want types to be of class "builtin" instead of "keyword"). + operatorChars: + All characters that must be handled as operators. + client: + Commands parsed and executed by the client (not the server). + support: + A list of supported syntaxes which are not common, but are supported by more than 1 DBMS. + * ODBCdotTable: .tableName + * zerolessFloat: .1 + * doubleQuote + * nCharCast: N'string' + * charsetCast: _utf8'string' + * commentHash: use # char for comments + * commentSlashSlash: use // for comments + * commentSpaceRequired: require a space after -- for comments + atoms: + Keywords that must be highlighted as atoms,. Some DBMS's support more atoms than others: + UNKNOWN, INFINITY, UNDERFLOW, NaN... + dateSQL: + Used for date/time SQL standard syntax, because not all DBMS's support same temporal types. + */ \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/view.hbs b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/view.hbs new file mode 100644 index 000000000..6cf20df0e --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/view.hbs @@ -0,0 +1,95 @@ +{{#zone "topCss"}} + {{css "css/codemirror.css"}} +{{/zone}} +{{#zone "contentTitle"}} +
      +
      + +
      +
      +{{/zone}} + + +
      + +
      +
      +
      +
      +
      + Policy Overview +
      + {{#defineZone "policy-detail-properties"}} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + Platform +
      Assignment Group +
      Action upon + non-compliance +
      Status
      Assigned Users
      Assigned Roles
      + {{/defineZone}} +
      + Description +
      +
      +
      +
      +
      + +
      + Profile Information +
      +
      + +
      +
      +
      + +
      +
      +
      +
      +
      +
      +
      + +
      +
      + +
      +{{#zone "bottomJs"}} + {{js "js/codemirror.js"}} + {{js "js/sql.js"}} + {{js "js/policy-view.js"}} +{{/zone}} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/view.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/view.js new file mode 100644 index 000000000..36b130ca0 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/view.js @@ -0,0 +1,25 @@ +/* + * 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. + */ + +function onRequest(context) { + var log = new Log("policy-view-edit-unit backend js"); + log.debug("calling policy-view-edit-unit"); + var userModule = require("/app/modules/user.js").userModule; + context.roles = userModule.getRoles().content; + return context; +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/view.json b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/view.json new file mode 100644 index 000000000..fd2590129 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.view/view.json @@ -0,0 +1,3 @@ +{ + "version" : "1.0.0" +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/public/css/codemirror.css b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/public/css/codemirror.css new file mode 100644 index 000000000..e749a5211 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/public/css/codemirror.css @@ -0,0 +1,342 @@ +/* + * 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. + */ + +/* BASICS */ + +.CodeMirror { + /* Set height, width, borders, and global font properties here */ + font-family: monospace; + height: 300px; + color: black; +} + +/* PADDING */ + +.CodeMirror-lines { + padding: 4px 0; /* Vertical padding around content */ +} +.CodeMirror pre { + padding: 0 4px; /* Horizontal padding of content */ +} + +.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + background-color: white; /* The little square between H and V scrollbars */ +} + +/* GUTTER */ + +.CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; +} +.CodeMirror-linenumbers {} +.CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; + white-space: nowrap; + left: -30px; +} + +.CodeMirror-guttermarker { color: black; } +.CodeMirror-guttermarker-subtle { color: #999; } + +/* CURSOR */ + +.CodeMirror div.CodeMirror-cursor { + border-left: 1px solid black; +} +/* Shown when moving in bi-directional text */ +.CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; +} +.CodeMirror.cm-fat-cursor div.CodeMirror-cursor { + width: auto; + border: 0; + background: #7e7; +} +.CodeMirror.cm-fat-cursor div.CodeMirror-cursors { + z-index: 1; +} + +.cm-animate-fat-cursor { + width: auto; + border: 0; + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; +} +@-moz-keyframes blink { + 0% { background: #7e7; } + 50% { background: none; } + 100% { background: #7e7; } +} +@-webkit-keyframes blink { + 0% { background: #7e7; } + 50% { background: none; } + 100% { background: #7e7; } +} +@keyframes blink { + 0% { background: #7e7; } + 50% { background: none; } + 100% { background: #7e7; } +} + +/* Can style cursor different in overwrite (non-insert) mode */ +div.CodeMirror-overwrite div.CodeMirror-cursor {} + +.cm-tab { display: inline-block; text-decoration: inherit; } + +.CodeMirror-ruler { + border-left: 1px solid #ccc; + position: absolute; +} + +/* DEFAULT THEME */ + +.cm-s-default .cm-keyword {color: #708;} +.cm-s-default .cm-atom {color: #219;} +.cm-s-default .cm-number {color: #164;} +.cm-s-default .cm-def {color: #00f;} +.cm-s-default .cm-variable, +.cm-s-default .cm-punctuation, +.cm-s-default .cm-property, +.cm-s-default .cm-operator {} +.cm-s-default .cm-variable-2 {color: #05a;} +.cm-s-default .cm-variable-3 {color: #085;} +.cm-s-default .cm-comment {color: #a50;} +.cm-s-default .cm-string {color: #a11;} +.cm-s-default .cm-string-2 {color: #f50;} +.cm-s-default .cm-meta {color: #555;} +.cm-s-default .cm-qualifier {color: #555;} +.cm-s-default .cm-builtin {color: #30a;} +.cm-s-default .cm-bracket {color: #997;} +.cm-s-default .cm-tag {color: #170;} +.cm-s-default .cm-attribute {color: #00c;} +.cm-s-default .cm-header {color: blue;} +.cm-s-default .cm-quote {color: #090;} +.cm-s-default .cm-hr {color: #999;} +.cm-s-default .cm-link {color: #00c;} + +.cm-negative {color: #d44;} +.cm-positive {color: #292;} +.cm-header, .cm-strong {font-weight: bold;} +.cm-em {font-style: italic;} +.cm-link {text-decoration: underline;} +.cm-strikethrough {text-decoration: line-through;} + +.cm-s-default .cm-error {color: #f00;} +.cm-invalidchar {color: #f00;} + +/* Default styles for common addons */ + +div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} +.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } +.CodeMirror-activeline-background {background: #e8f2ff;} + +/* STOP */ + +/* The rest of this file contains styles related to the mechanics of + the editor. You probably shouldn't touch them. */ + +.CodeMirror { + position: relative; + overflow: hidden; + background: white; +} + +.CodeMirror-scroll { + overflow: scroll !important; /* Things will break if this is overridden */ + /* 30px is the magic margin used to hide the element's real scrollbars */ + /* See overflow: hidden in .CodeMirror */ + margin-bottom: -30px; margin-right: -30px; + padding-bottom: 30px; + height: 100%; + outline: none; /* Prevent dragging from highlighting the element */ + position: relative; +} +.CodeMirror-sizer { + position: relative; + border-right: 30px solid transparent; +} + +/* The fake, visible scrollbars. Used to force redraw during scrolling + before actuall scrolling happens, thus preventing shaking and + flickering artifacts. */ +.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + position: absolute; + z-index: 6; + display: none; +} +.CodeMirror-vscrollbar { + right: 0; top: 0; + overflow-x: hidden; + overflow-y: scroll; +} +.CodeMirror-hscrollbar { + bottom: 0; left: 0; + overflow-y: hidden; + overflow-x: scroll; +} +.CodeMirror-scrollbar-filler { + right: 0; bottom: 0; +} +.CodeMirror-gutter-filler { + left: 0; bottom: 0; +} + +.CodeMirror-gutters { + position: absolute; left: 0; top: 0; + z-index: 3; +} +.CodeMirror-gutter { + white-space: normal; + height: 100%; + display: inline-block; + /* Hack to make IE7 behave */ + *zoom:1; + *display:inline; +} +.CodeMirror-gutter-wrapper { + position: absolute; + z-index: 4; + height: 100%; +} +.CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4; + left: -30px; +} +.CodeMirror-gutter-wrapper { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.CodeMirror-lines { + cursor: text; + min-height: 1px; /* prevents collapsing before first draw */ +} +.CodeMirror pre { + /* Reset some styles that the rest of the page might have set */ + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; + -webkit-tap-highlight-color: transparent; +} +.CodeMirror-wrap pre { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} + +.CodeMirror-linebackground { + position: absolute; + left: 0; right: 0; top: 0; bottom: 0; + z-index: 0; +} + +.CodeMirror-linewidget { + position: relative; + z-index: 2; + overflow: auto; +} + +.CodeMirror-widget {} + +.CodeMirror-code { + outline: none; +} + +/* Force content-box sizing for the elements where we expect it */ +.CodeMirror-scroll, +.CodeMirror-sizer, +.CodeMirror-gutter, +.CodeMirror-gutters, +.CodeMirror-linenumber { + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +.CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; +} +.CodeMirror-measure pre { position: static; } + +.CodeMirror div.CodeMirror-cursor { + position: absolute; + border-right: none; + width: 0; +} + +div.CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 3; +} +.CodeMirror-focused div.CodeMirror-cursors { + visibility: visible; +} + +.CodeMirror-selected { background: #d9d9d9; } +.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } +.CodeMirror-crosshair { cursor: crosshair; } +.CodeMirror ::selection { background: #d7d4f0; } +.CodeMirror ::-moz-selection { background: #d7d4f0; } + +.cm-searching { + background: #ffa; + background: rgba(255, 255, 0, .4); +} + +/* IE7 hack to prevent it from returning funny offsetTops on the spans */ +.CodeMirror span { *vertical-align: text-bottom; } + +/* Used to force a border model for a node */ +.cm-force-border { padding-right: .1px; } + +@media print { + /* Hide the cursor when printing */ + .CodeMirror div.CodeMirror-cursors { + visibility: hidden; + } +} + +/* See issue #2901 */ +.cm-tab-wrap-hack:after { content: ''; } + +/* Help users use markselection to safely style text background */ +span.CodeMirror-selectedtext { background: none; } \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/public/js/codemirror.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/public/js/codemirror.js new file mode 100644 index 000000000..20f3f95ed --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/public/js/codemirror.js @@ -0,0 +1,8720 @@ +/* + * 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. + */ + +// This is CodeMirror (http://codemirror.net), a code editor +// implemented in JavaScript on top of the browser's DOM. +// +// You can find some technical background for some of the code below +// at http://marijnhaverbeke.nl/blog/#cm-internals . + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + module.exports = mod(); + else if (typeof define == "function" && define.amd) // AMD + return define([], mod); + else // Plain browser env + this.CodeMirror = mod(); +})(function() { + "use strict"; + + // BROWSER SNIFFING + + // Kludges for bugs and behavior differences that can't be feature + // detected are enabled based on userAgent etc sniffing. + + var gecko = /gecko\/\d/i.test(navigator.userAgent); + var ie_upto10 = /MSIE \d/.test(navigator.userAgent); + var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent); + var ie = ie_upto10 || ie_11up; + var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]); + var webkit = /WebKit\//.test(navigator.userAgent); + var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent); + var chrome = /Chrome\//.test(navigator.userAgent); + var presto = /Opera\//.test(navigator.userAgent); + var safari = /Apple Computer/.test(navigator.vendor); + var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent); + var phantom = /PhantomJS/.test(navigator.userAgent); + + var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent); + // This is woefully incomplete. Suggestions for alternative methods welcome. + var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent); + var mac = ios || /Mac/.test(navigator.platform); + var windows = /win/i.test(navigator.platform); + + var presto_version = presto && navigator.userAgent.match(/Version\/(\d*\.\d*)/); + if (presto_version) presto_version = Number(presto_version[1]); + if (presto_version && presto_version >= 15) { presto = false; webkit = true; } + // Some browsers use the wrong event properties to signal cmd/ctrl on OS X + var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11)); + var captureRightClick = gecko || (ie && ie_version >= 9); + + // Optimize some code when these features are not used. + var sawReadOnlySpans = false, sawCollapsedSpans = false; + + // EDITOR CONSTRUCTOR + + // A CodeMirror instance represents an editor. This is the object + // that user code is usually dealing with. + + function CodeMirror(place, options) { + if (!(this instanceof CodeMirror)) return new CodeMirror(place, options); + + this.options = options = options ? copyObj(options) : {}; + // Determine effective options based on given values and defaults. + copyObj(defaults, options, false); + setGuttersForLineNumbers(options); + + var doc = options.value; + if (typeof doc == "string") doc = new Doc(doc, options.mode); + this.doc = doc; + + var input = new CodeMirror.inputStyles[options.inputStyle](this); + var display = this.display = new Display(place, doc, input); + display.wrapper.CodeMirror = this; + updateGutters(this); + themeChanged(this); + if (options.lineWrapping) + this.display.wrapper.className += " CodeMirror-wrap"; + if (options.autofocus && !mobile) display.input.focus(); + initScrollbars(this); + + this.state = { + keyMaps: [], // stores maps added by addKeyMap + overlays: [], // highlighting overlays, as added by addOverlay + modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info + overwrite: false, + delayingBlurEvent: false, + focused: false, + suppressEdits: false, // used to disable editing during key handlers when in readOnly mode + pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in input.poll + draggingText: false, + highlight: new Delayed(), // stores highlight worker timeout + keySeq: null, // Unfinished key sequence + specialChars: null + }; + + var cm = this; + + // Override magic textarea content restore that IE sometimes does + // on our hidden textarea on reload + if (ie && ie_version < 11) setTimeout(function() { cm.display.input.reset(true); }, 20); + + registerEventHandlers(this); + ensureGlobalHandlers(); + + startOperation(this); + this.curOp.forceUpdate = true; + attachDoc(this, doc); + + if ((options.autofocus && !mobile) || cm.hasFocus()) + setTimeout(bind(onFocus, this), 20); + else + onBlur(this); + + for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt)) + optionHandlers[opt](this, options[opt], Init); + maybeUpdateLineNumberWidth(this); + if (options.finishInit) options.finishInit(this); + for (var i = 0; i < initHooks.length; ++i) initHooks[i](this); + endOperation(this); + // Suppress optimizelegibility in Webkit, since it breaks text + // measuring on line wrapping boundaries. + if (webkit && options.lineWrapping && + getComputedStyle(display.lineDiv).textRendering == "optimizelegibility") + display.lineDiv.style.textRendering = "auto"; + } + + // DISPLAY CONSTRUCTOR + + // The display handles the DOM integration, both for input reading + // and content drawing. It holds references to DOM nodes and + // display-related state. + + function Display(place, doc, input) { + var d = this; + this.input = input; + + // Covers bottom-right square when both scrollbars are present. + d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); + d.scrollbarFiller.setAttribute("cm-not-content", "true"); + // Covers bottom of gutter when coverGutterNextToScrollbar is on + // and h scrollbar is present. + d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); + d.gutterFiller.setAttribute("cm-not-content", "true"); + // Will contain the actual code, positioned to cover the viewport. + d.lineDiv = elt("div", null, "CodeMirror-code"); + // Elements are added to these to represent selection and cursors. + d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); + d.cursorDiv = elt("div", null, "CodeMirror-cursors"); + // A visibility: hidden element used to find the size of things. + d.measure = elt("div", null, "CodeMirror-measure"); + // When lines outside of the viewport are measured, they are drawn in this. + d.lineMeasure = elt("div", null, "CodeMirror-measure"); + // Wraps everything that needs to exist inside the vertically-padded coordinate system + d.lineSpace = elt("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], + null, "position: relative; outline: none"); + // Moved around its parent to cover visible view. + d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative"); + // Set to the height of the document, allowing scrolling. + d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); + d.sizerWidth = null; + // Behavior of elts with overflow: auto and padding is + // inconsistent across browsers. This is used to ensure the + // scrollable area is big enough. + d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;"); + // Will contain the gutters, if any. + d.gutters = elt("div", null, "CodeMirror-gutters"); + d.lineGutter = null; + // Actual scrollable element. + d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); + d.scroller.setAttribute("tabIndex", "-1"); + // The element in which the editor lives. + d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); + + // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) + if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } + if (!webkit && !(gecko && mobile)) d.scroller.draggable = true; + + if (place) { + if (place.appendChild) place.appendChild(d.wrapper); + else place(d.wrapper); + } + + // Current rendered range (may be bigger than the view window). + d.viewFrom = d.viewTo = doc.first; + d.reportedViewFrom = d.reportedViewTo = doc.first; + // Information about the rendered lines. + d.view = []; + d.renderedView = null; + // Holds info about a single rendered line when it was rendered + // for measurement, while not in view. + d.externalMeasured = null; + // Empty space (in pixels) above the view + d.viewOffset = 0; + d.lastWrapHeight = d.lastWrapWidth = 0; + d.updateLineNumbers = null; + + d.nativeBarWidth = d.barHeight = d.barWidth = 0; + d.scrollbarsClipped = false; + + // Used to only resize the line number gutter when necessary (when + // the amount of lines crosses a boundary that makes its width change) + d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; + // Set to true when a non-horizontal-scrolling line widget is + // added. As an optimization, line widget aligning is skipped when + // this is false. + d.alignWidgets = false; + + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + + // Tracks the maximum line length so that the horizontal scrollbar + // can be kept static when scrolling. + d.maxLine = null; + d.maxLineLength = 0; + d.maxLineChanged = false; + + // Used for measuring wheel scrolling granularity + d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; + + // True when shift is held down. + d.shift = false; + + // Used to track whether anything happened since the context menu + // was opened. + d.selForContextMenu = null; + + d.activeTouch = null; + + input.init(d); + } + + // STATE UPDATES + + // Used to get the editor into a consistent state again when options change. + + function loadMode(cm) { + cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption); + resetModeState(cm); + } + + function resetModeState(cm) { + cm.doc.iter(function(line) { + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + }); + cm.doc.frontier = cm.doc.first; + startWorker(cm, 100); + cm.state.modeGen++; + if (cm.curOp) regChange(cm); + } + + function wrappingChanged(cm) { + if (cm.options.lineWrapping) { + addClass(cm.display.wrapper, "CodeMirror-wrap"); + cm.display.sizer.style.minWidth = ""; + cm.display.sizerWidth = null; + } else { + rmClass(cm.display.wrapper, "CodeMirror-wrap"); + findMaxLine(cm); + } + estimateLineHeights(cm); + regChange(cm); + clearCaches(cm); + setTimeout(function(){updateScrollbars(cm);}, 100); + } + + // Returns a function that estimates the height of a line, to use as + // first approximation until the line becomes visible (and is thus + // properly measurable). + function estimateHeight(cm) { + var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; + var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); + return function(line) { + if (lineIsHidden(cm.doc, line)) return 0; + + var widgetsHeight = 0; + if (line.widgets) for (var i = 0; i < line.widgets.length; i++) { + if (line.widgets[i].height) widgetsHeight += line.widgets[i].height; + } + + if (wrapping) + return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th; + else + return widgetsHeight + th; + }; + } + + function estimateLineHeights(cm) { + var doc = cm.doc, est = estimateHeight(cm); + doc.iter(function(line) { + var estHeight = est(line); + if (estHeight != line.height) updateLineHeight(line, estHeight); + }); + } + + function themeChanged(cm) { + cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + + cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-"); + clearCaches(cm); + } + + function guttersChanged(cm) { + updateGutters(cm); + regChange(cm); + setTimeout(function(){alignHorizontally(cm);}, 20); + } + + // Rebuild the gutter elements, ensure the margin to the left of the + // code matches their width. + function updateGutters(cm) { + var gutters = cm.display.gutters, specs = cm.options.gutters; + removeChildren(gutters); + for (var i = 0; i < specs.length; ++i) { + var gutterClass = specs[i]; + var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass)); + if (gutterClass == "CodeMirror-linenumbers") { + cm.display.lineGutter = gElt; + gElt.style.width = (cm.display.lineNumWidth || 1) + "px"; + } + } + gutters.style.display = i ? "" : "none"; + updateGutterSpace(cm); + } + + function updateGutterSpace(cm) { + var width = cm.display.gutters.offsetWidth; + cm.display.sizer.style.marginLeft = width + "px"; + } + + // Compute the character length of a line, taking into account + // collapsed ranges (see markText) that might hide parts, and join + // other lines onto it. + function lineLength(line) { + if (line.height == 0) return 0; + var len = line.text.length, merged, cur = line; + while (merged = collapsedSpanAtStart(cur)) { + var found = merged.find(0, true); + cur = found.from.line; + len += found.from.ch - found.to.ch; + } + cur = line; + while (merged = collapsedSpanAtEnd(cur)) { + var found = merged.find(0, true); + len -= cur.text.length - found.from.ch; + cur = found.to.line; + len += cur.text.length - found.to.ch; + } + return len; + } + + // Find the longest line in the document. + function findMaxLine(cm) { + var d = cm.display, doc = cm.doc; + d.maxLine = getLine(doc, doc.first); + d.maxLineLength = lineLength(d.maxLine); + d.maxLineChanged = true; + doc.iter(function(line) { + var len = lineLength(line); + if (len > d.maxLineLength) { + d.maxLineLength = len; + d.maxLine = line; + } + }); + } + + // Make sure the gutters options contains the element + // "CodeMirror-linenumbers" when the lineNumbers option is true. + function setGuttersForLineNumbers(options) { + var found = indexOf(options.gutters, "CodeMirror-linenumbers"); + if (found == -1 && options.lineNumbers) { + options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]); + } else if (found > -1 && !options.lineNumbers) { + options.gutters = options.gutters.slice(0); + options.gutters.splice(found, 1); + } + } + + // SCROLLBARS + + // Prepare DOM reads needed to update the scrollbars. Done in one + // shot to minimize update/measure roundtrips. + function measureForScrollbars(cm) { + var d = cm.display, gutterW = d.gutters.offsetWidth; + var docH = Math.round(cm.doc.height + paddingVert(cm.display)); + return { + clientHeight: d.scroller.clientHeight, + viewHeight: d.wrapper.clientHeight, + scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth, + viewWidth: d.wrapper.clientWidth, + barLeft: cm.options.fixedGutter ? gutterW : 0, + docHeight: docH, + scrollHeight: docH + scrollGap(cm) + d.barHeight, + nativeBarWidth: d.nativeBarWidth, + gutterWidth: gutterW + }; + } + + function NativeScrollbars(place, scroll, cm) { + this.cm = cm; + var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); + var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); + place(vert); place(horiz); + + on(vert, "scroll", function() { + if (vert.clientHeight) scroll(vert.scrollTop, "vertical"); + }); + on(horiz, "scroll", function() { + if (horiz.clientWidth) scroll(horiz.scrollLeft, "horizontal"); + }); + + this.checkedOverlay = false; + // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). + if (ie && ie_version < 8) this.horiz.style.minHeight = this.vert.style.minWidth = "18px"; + } + + NativeScrollbars.prototype = copyObj({ + update: function(measure) { + var needsH = measure.scrollWidth > measure.clientWidth + 1; + var needsV = measure.scrollHeight > measure.clientHeight + 1; + var sWidth = measure.nativeBarWidth; + + if (needsV) { + this.vert.style.display = "block"; + this.vert.style.bottom = needsH ? sWidth + "px" : "0"; + var totalHeight = measure.viewHeight - (needsH ? sWidth : 0); + // A bug in IE8 can cause this value to be negative, so guard it. + this.vert.firstChild.style.height = + Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px"; + } else { + this.vert.style.display = ""; + this.vert.firstChild.style.height = "0"; + } + + if (needsH) { + this.horiz.style.display = "block"; + this.horiz.style.right = needsV ? sWidth + "px" : "0"; + this.horiz.style.left = measure.barLeft + "px"; + var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0); + this.horiz.firstChild.style.width = + (measure.scrollWidth - measure.clientWidth + totalWidth) + "px"; + } else { + this.horiz.style.display = ""; + this.horiz.firstChild.style.width = "0"; + } + + if (!this.checkedOverlay && measure.clientHeight > 0) { + if (sWidth == 0) this.overlayHack(); + this.checkedOverlay = true; + } + + return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0}; + }, + setScrollLeft: function(pos) { + if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos; + }, + setScrollTop: function(pos) { + if (this.vert.scrollTop != pos) this.vert.scrollTop = pos; + }, + overlayHack: function() { + var w = mac && !mac_geMountainLion ? "12px" : "18px"; + this.horiz.style.minHeight = this.vert.style.minWidth = w; + var self = this; + var barMouseDown = function(e) { + if (e_target(e) != self.vert && e_target(e) != self.horiz) + operation(self.cm, onMouseDown)(e); + }; + on(this.vert, "mousedown", barMouseDown); + on(this.horiz, "mousedown", barMouseDown); + }, + clear: function() { + var parent = this.horiz.parentNode; + parent.removeChild(this.horiz); + parent.removeChild(this.vert); + } + }, NativeScrollbars.prototype); + + function NullScrollbars() {} + + NullScrollbars.prototype = copyObj({ + update: function() { return {bottom: 0, right: 0}; }, + setScrollLeft: function() {}, + setScrollTop: function() {}, + clear: function() {} + }, NullScrollbars.prototype); + + CodeMirror.scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars}; + + function initScrollbars(cm) { + if (cm.display.scrollbars) { + cm.display.scrollbars.clear(); + if (cm.display.scrollbars.addClass) + rmClass(cm.display.wrapper, cm.display.scrollbars.addClass); + } + + cm.display.scrollbars = new CodeMirror.scrollbarModel[cm.options.scrollbarStyle](function(node) { + cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller); + // Prevent clicks in the scrollbars from killing focus + on(node, "mousedown", function() { + if (cm.state.focused) setTimeout(function() { cm.display.input.focus(); }, 0); + }); + node.setAttribute("cm-not-content", "true"); + }, function(pos, axis) { + if (axis == "horizontal") setScrollLeft(cm, pos); + else setScrollTop(cm, pos); + }, cm); + if (cm.display.scrollbars.addClass) + addClass(cm.display.wrapper, cm.display.scrollbars.addClass); + } + + function updateScrollbars(cm, measure) { + if (!measure) measure = measureForScrollbars(cm); + var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight; + updateScrollbarsInner(cm, measure); + for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) { + if (startWidth != cm.display.barWidth && cm.options.lineWrapping) + updateHeightsInViewport(cm); + updateScrollbarsInner(cm, measureForScrollbars(cm)); + startWidth = cm.display.barWidth; startHeight = cm.display.barHeight; + } + } + + // Re-synchronize the fake scrollbars with the actual size of the + // content. + function updateScrollbarsInner(cm, measure) { + var d = cm.display; + var sizes = d.scrollbars.update(measure); + + d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px"; + d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px"; + + if (sizes.right && sizes.bottom) { + d.scrollbarFiller.style.display = "block"; + d.scrollbarFiller.style.height = sizes.bottom + "px"; + d.scrollbarFiller.style.width = sizes.right + "px"; + } else d.scrollbarFiller.style.display = ""; + if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { + d.gutterFiller.style.display = "block"; + d.gutterFiller.style.height = sizes.bottom + "px"; + d.gutterFiller.style.width = measure.gutterWidth + "px"; + } else d.gutterFiller.style.display = ""; + } + + // Compute the lines that are visible in a given viewport (defaults + // the the current scroll position). viewport may contain top, + // height, and ensure (see op.scrollToPos) properties. + function visibleLines(display, doc, viewport) { + var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop; + top = Math.floor(top - paddingTop(display)); + var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight; + + var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom); + // Ensure is a {from: {line, ch}, to: {line, ch}} object, and + // forces those lines into the viewport (if possible). + if (viewport && viewport.ensure) { + var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line; + if (ensureFrom < from) { + from = ensureFrom; + to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight); + } else if (Math.min(ensureTo, doc.lastLine()) >= to) { + from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight); + to = ensureTo; + } + } + return {from: from, to: Math.max(to, from + 1)}; + } + + // LINE NUMBERS + + // Re-align line numbers and gutter marks to compensate for + // horizontal scrolling. + function alignHorizontally(cm) { + var display = cm.display, view = display.view; + if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return; + var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; + var gutterW = display.gutters.offsetWidth, left = comp + "px"; + for (var i = 0; i < view.length; i++) if (!view[i].hidden) { + if (cm.options.fixedGutter && view[i].gutter) + view[i].gutter.style.left = left; + var align = view[i].alignable; + if (align) for (var j = 0; j < align.length; j++) + align[j].style.left = left; + } + if (cm.options.fixedGutter) + display.gutters.style.left = (comp + gutterW) + "px"; + } + + // Used to ensure that the line number gutter is still the right + // size for the current document size. Returns true when an update + // is needed. + function maybeUpdateLineNumberWidth(cm) { + if (!cm.options.lineNumbers) return false; + var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; + if (last.length != display.lineNumChars) { + var test = display.measure.appendChild(elt("div", [elt("div", last)], + "CodeMirror-linenumber CodeMirror-gutter-elt")); + var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; + display.lineGutter.style.width = ""; + display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1; + display.lineNumWidth = display.lineNumInnerWidth + padding; + display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; + display.lineGutter.style.width = display.lineNumWidth + "px"; + updateGutterSpace(cm); + return true; + } + return false; + } + + function lineNumberFor(options, i) { + return String(options.lineNumberFormatter(i + options.firstLineNumber)); + } + + // Computes display.scroller.scrollLeft + display.gutters.offsetWidth, + // but using getBoundingClientRect to get a sub-pixel-accurate + // result. + function compensateForHScroll(display) { + return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left; + } + + // DISPLAY DRAWING + + function DisplayUpdate(cm, viewport, force) { + var display = cm.display; + + this.viewport = viewport; + // Store some values that we'll need later (but don't want to force a relayout for) + this.visible = visibleLines(display, cm.doc, viewport); + this.editorIsHidden = !display.wrapper.offsetWidth; + this.wrapperHeight = display.wrapper.clientHeight; + this.wrapperWidth = display.wrapper.clientWidth; + this.oldDisplayWidth = displayWidth(cm); + this.force = force; + this.dims = getDimensions(cm); + this.events = []; + } + + DisplayUpdate.prototype.signal = function(emitter, type) { + if (hasHandler(emitter, type)) + this.events.push(arguments); + }; + DisplayUpdate.prototype.finish = function() { + for (var i = 0; i < this.events.length; i++) + signal.apply(null, this.events[i]); + }; + + function maybeClipScrollbars(cm) { + var display = cm.display; + if (!display.scrollbarsClipped && display.scroller.offsetWidth) { + display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth; + display.heightForcer.style.height = scrollGap(cm) + "px"; + display.sizer.style.marginBottom = -display.nativeBarWidth + "px"; + display.sizer.style.borderRightWidth = scrollGap(cm) + "px"; + display.scrollbarsClipped = true; + } + } + + // Does the actual updating of the line display. Bails out + // (returning false) when there is nothing to be done and forced is + // false. + function updateDisplayIfNeeded(cm, update) { + var display = cm.display, doc = cm.doc; + + if (update.editorIsHidden) { + resetView(cm); + return false; + } + + // Bail out if the visible area is already rendered and nothing changed. + if (!update.force && + update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) && + display.renderedView == display.view && countDirtyView(cm) == 0) + return false; + + if (maybeUpdateLineNumberWidth(cm)) { + resetView(cm); + update.dims = getDimensions(cm); + } + + // Compute a suitable new viewport (from & to) + var end = doc.first + doc.size; + var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first); + var to = Math.min(end, update.visible.to + cm.options.viewportMargin); + if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom); + if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo); + if (sawCollapsedSpans) { + from = visualLineNo(cm.doc, from); + to = visualLineEndNo(cm.doc, to); + } + + var different = from != display.viewFrom || to != display.viewTo || + display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth; + adjustView(cm, from, to); + + display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)); + // Position the mover div to align with the current scroll position + cm.display.mover.style.top = display.viewOffset + "px"; + + var toUpdate = countDirtyView(cm); + if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo)) + return false; + + // For big changes, we hide the enclosing element during the + // update, since that speeds up the operations on most browsers. + var focused = activeElt(); + if (toUpdate > 4) display.lineDiv.style.display = "none"; + patchDisplay(cm, display.updateLineNumbers, update.dims); + if (toUpdate > 4) display.lineDiv.style.display = ""; + display.renderedView = display.view; + // There might have been a widget with a focused element that got + // hidden or updated, if so re-focus it. + if (focused && activeElt() != focused && focused.offsetHeight) focused.focus(); + + // Prevent selection and cursors from interfering with the scroll + // width and height. + removeChildren(display.cursorDiv); + removeChildren(display.selectionDiv); + display.gutters.style.height = 0; + + if (different) { + display.lastWrapHeight = update.wrapperHeight; + display.lastWrapWidth = update.wrapperWidth; + startWorker(cm, 400); + } + + display.updateLineNumbers = null; + + return true; + } + + function postUpdateDisplay(cm, update) { + var force = update.force, viewport = update.viewport; + for (var first = true;; first = false) { + if (first && cm.options.lineWrapping && update.oldDisplayWidth != displayWidth(cm)) { + force = true; + } else { + force = false; + // Clip forced viewport to actual scrollable area. + if (viewport && viewport.top != null) + viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)}; + // Updated line heights might result in the drawn area not + // actually covering the viewport. Keep looping until it does. + update.visible = visibleLines(cm.display, cm.doc, viewport); + if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo) + break; + } + if (!updateDisplayIfNeeded(cm, update)) break; + updateHeightsInViewport(cm); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + setDocumentHeight(cm, barMeasure); + updateScrollbars(cm, barMeasure); + } + + update.signal(cm, "update", cm); + if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) { + update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); + cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo; + } + } + + function updateDisplaySimple(cm, viewport) { + var update = new DisplayUpdate(cm, viewport); + if (updateDisplayIfNeeded(cm, update)) { + updateHeightsInViewport(cm); + postUpdateDisplay(cm, update); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + setDocumentHeight(cm, barMeasure); + updateScrollbars(cm, barMeasure); + update.finish(); + } + } + + function setDocumentHeight(cm, measure) { + cm.display.sizer.style.minHeight = measure.docHeight + "px"; + var total = measure.docHeight + cm.display.barHeight; + cm.display.heightForcer.style.top = total + "px"; + cm.display.gutters.style.height = Math.max(total + scrollGap(cm), measure.clientHeight) + "px"; + } + + // Read the actual heights of the rendered lines, and update their + // stored heights to match. + function updateHeightsInViewport(cm) { + var display = cm.display; + var prevBottom = display.lineDiv.offsetTop; + for (var i = 0; i < display.view.length; i++) { + var cur = display.view[i], height; + if (cur.hidden) continue; + if (ie && ie_version < 8) { + var bot = cur.node.offsetTop + cur.node.offsetHeight; + height = bot - prevBottom; + prevBottom = bot; + } else { + var box = cur.node.getBoundingClientRect(); + height = box.bottom - box.top; + } + var diff = cur.line.height - height; + if (height < 2) height = textHeight(display); + if (diff > .001 || diff < -.001) { + updateLineHeight(cur.line, height); + updateWidgetHeight(cur.line); + if (cur.rest) for (var j = 0; j < cur.rest.length; j++) + updateWidgetHeight(cur.rest[j]); + } + } + } + + // Read and store the height of line widgets associated with the + // given line. + function updateWidgetHeight(line) { + if (line.widgets) for (var i = 0; i < line.widgets.length; ++i) + line.widgets[i].height = line.widgets[i].node.offsetHeight; + } + + // Do a bulk-read of the DOM positions and sizes needed to draw the + // view, so that we don't interleave reading and writing to the DOM. + function getDimensions(cm) { + var d = cm.display, left = {}, width = {}; + var gutterLeft = d.gutters.clientLeft; + for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { + left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft; + width[cm.options.gutters[i]] = n.clientWidth; + } + return {fixedPos: compensateForHScroll(d), + gutterTotalWidth: d.gutters.offsetWidth, + gutterLeft: left, + gutterWidth: width, + wrapperWidth: d.wrapper.clientWidth}; + } + + // Sync the actual display DOM structure with display.view, removing + // nodes for lines that are no longer in view, and creating the ones + // that are not there yet, and updating the ones that are out of + // date. + function patchDisplay(cm, updateNumbersFrom, dims) { + var display = cm.display, lineNumbers = cm.options.lineNumbers; + var container = display.lineDiv, cur = container.firstChild; + + function rm(node) { + var next = node.nextSibling; + // Works around a throw-scroll bug in OS X Webkit + if (webkit && mac && cm.display.currentWheelTarget == node) + node.style.display = "none"; + else + node.parentNode.removeChild(node); + return next; + } + + var view = display.view, lineN = display.viewFrom; + // Loop over the elements in the view, syncing cur (the DOM nodes + // in display.lineDiv) with the view as we go. + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (lineView.hidden) { + } else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet + var node = buildLineElement(cm, lineView, lineN, dims); + container.insertBefore(node, cur); + } else { // Already drawn + while (cur != lineView.node) cur = rm(cur); + var updateNumber = lineNumbers && updateNumbersFrom != null && + updateNumbersFrom <= lineN && lineView.lineNumber; + if (lineView.changes) { + if (indexOf(lineView.changes, "gutter") > -1) updateNumber = false; + updateLineForChanges(cm, lineView, lineN, dims); + } + if (updateNumber) { + removeChildren(lineView.lineNumber); + lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN))); + } + cur = lineView.node.nextSibling; + } + lineN += lineView.size; + } + while (cur) cur = rm(cur); + } + + // When an aspect of a line changes, a string is added to + // lineView.changes. This updates the relevant part of the line's + // DOM structure. + function updateLineForChanges(cm, lineView, lineN, dims) { + for (var j = 0; j < lineView.changes.length; j++) { + var type = lineView.changes[j]; + if (type == "text") updateLineText(cm, lineView); + else if (type == "gutter") updateLineGutter(cm, lineView, lineN, dims); + else if (type == "class") updateLineClasses(lineView); + else if (type == "widget") updateLineWidgets(cm, lineView, dims); + } + lineView.changes = null; + } + + // Lines with gutter elements, widgets or a background class need to + // be wrapped, and have the extra elements added to the wrapper div + function ensureLineWrapped(lineView) { + if (lineView.node == lineView.text) { + lineView.node = elt("div", null, null, "position: relative"); + if (lineView.text.parentNode) + lineView.text.parentNode.replaceChild(lineView.node, lineView.text); + lineView.node.appendChild(lineView.text); + if (ie && ie_version < 8) lineView.node.style.zIndex = 2; + } + return lineView.node; + } + + function updateLineBackground(lineView) { + var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass; + if (cls) cls += " CodeMirror-linebackground"; + if (lineView.background) { + if (cls) lineView.background.className = cls; + else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; } + } else if (cls) { + var wrap = ensureLineWrapped(lineView); + lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild); + } + } + + // Wrapper around buildLineContent which will reuse the structure + // in display.externalMeasured when possible. + function getLineContent(cm, lineView) { + var ext = cm.display.externalMeasured; + if (ext && ext.line == lineView.line) { + cm.display.externalMeasured = null; + lineView.measure = ext.measure; + return ext.built; + } + return buildLineContent(cm, lineView); + } + + // Redraw the line's text. Interacts with the background and text + // classes because the mode may output tokens that influence these + // classes. + function updateLineText(cm, lineView) { + var cls = lineView.text.className; + var built = getLineContent(cm, lineView); + if (lineView.text == lineView.node) lineView.node = built.pre; + lineView.text.parentNode.replaceChild(built.pre, lineView.text); + lineView.text = built.pre; + if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) { + lineView.bgClass = built.bgClass; + lineView.textClass = built.textClass; + updateLineClasses(lineView); + } else if (cls) { + lineView.text.className = cls; + } + } + + function updateLineClasses(lineView) { + updateLineBackground(lineView); + if (lineView.line.wrapClass) + ensureLineWrapped(lineView).className = lineView.line.wrapClass; + else if (lineView.node != lineView.text) + lineView.node.className = ""; + var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass; + lineView.text.className = textClass || ""; + } + + function updateLineGutter(cm, lineView, lineN, dims) { + if (lineView.gutter) { + lineView.node.removeChild(lineView.gutter); + lineView.gutter = null; + } + var markers = lineView.line.gutterMarkers; + if (cm.options.lineNumbers || markers) { + var wrap = ensureLineWrapped(lineView); + var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", "left: " + + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + + "px; width: " + dims.gutterTotalWidth + "px"); + cm.display.input.setUneditable(gutterWrap); + wrap.insertBefore(gutterWrap, lineView.text); + if (lineView.line.gutterClass) + gutterWrap.className += " " + lineView.line.gutterClass; + if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) + lineView.lineNumber = gutterWrap.appendChild( + elt("div", lineNumberFor(cm.options, lineN), + "CodeMirror-linenumber CodeMirror-gutter-elt", + "left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: " + + cm.display.lineNumInnerWidth + "px")); + if (markers) for (var k = 0; k < cm.options.gutters.length; ++k) { + var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id]; + if (found) + gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " + + dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px")); + } + } + } + + function updateLineWidgets(cm, lineView, dims) { + if (lineView.alignable) lineView.alignable = null; + for (var node = lineView.node.firstChild, next; node; node = next) { + var next = node.nextSibling; + if (node.className == "CodeMirror-linewidget") + lineView.node.removeChild(node); + } + insertLineWidgets(cm, lineView, dims); + } + + // Build a line's DOM representation from scratch + function buildLineElement(cm, lineView, lineN, dims) { + var built = getLineContent(cm, lineView); + lineView.text = lineView.node = built.pre; + if (built.bgClass) lineView.bgClass = built.bgClass; + if (built.textClass) lineView.textClass = built.textClass; + + updateLineClasses(lineView); + updateLineGutter(cm, lineView, lineN, dims); + insertLineWidgets(cm, lineView, dims); + return lineView.node; + } + + // A lineView may contain multiple logical lines (when merged by + // collapsed spans). The widgets for all of them need to be drawn. + function insertLineWidgets(cm, lineView, dims) { + insertLineWidgetsFor(cm, lineView.line, lineView, dims, true); + if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) + insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false); + } + + function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) { + if (!line.widgets) return; + var wrap = ensureLineWrapped(lineView); + for (var i = 0, ws = line.widgets; i < ws.length; ++i) { + var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget"); + if (!widget.handleMouseEvents) node.setAttribute("cm-ignore-events", "true"); + positionLineWidget(widget, node, lineView, dims); + cm.display.input.setUneditable(node); + if (allowAbove && widget.above) + wrap.insertBefore(node, lineView.gutter || lineView.text); + else + wrap.appendChild(node); + signalLater(widget, "redraw"); + } + } + + function positionLineWidget(widget, node, lineView, dims) { + if (widget.noHScroll) { + (lineView.alignable || (lineView.alignable = [])).push(node); + var width = dims.wrapperWidth; + node.style.left = dims.fixedPos + "px"; + if (!widget.coverGutter) { + width -= dims.gutterTotalWidth; + node.style.paddingLeft = dims.gutterTotalWidth + "px"; + } + node.style.width = width + "px"; + } + if (widget.coverGutter) { + node.style.zIndex = 5; + node.style.position = "relative"; + if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px"; + } + } + + // POSITION OBJECT + + // A Pos instance represents a position within the text. + var Pos = CodeMirror.Pos = function(line, ch) { + if (!(this instanceof Pos)) return new Pos(line, ch); + this.line = line; this.ch = ch; + }; + + // Compare two positions, return 0 if they are the same, a negative + // number when a is less, and a positive number otherwise. + var cmp = CodeMirror.cmpPos = function(a, b) { return a.line - b.line || a.ch - b.ch; }; + + function copyPos(x) {return Pos(x.line, x.ch);} + function maxPos(a, b) { return cmp(a, b) < 0 ? b : a; } + function minPos(a, b) { return cmp(a, b) < 0 ? a : b; } + + // INPUT HANDLING + + function ensureFocus(cm) { + if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm); } + } + + function isReadOnly(cm) { + return cm.options.readOnly || cm.doc.cantEdit; + } + + // This will be set to an array of strings when copying, so that, + // when pasting, we know what kind of selections the copied text + // was made out of. + var lastCopied = null; + + function applyTextInput(cm, inserted, deleted, sel) { + var doc = cm.doc; + cm.display.shift = false; + if (!sel) sel = doc.sel; + + var textLines = splitLines(inserted), multiPaste = null; + // When pasing N lines into N selections, insert one line per selection + if (cm.state.pasteIncoming && sel.ranges.length > 1) { + if (lastCopied && lastCopied.join("\n") == inserted) + multiPaste = sel.ranges.length % lastCopied.length == 0 && map(lastCopied, splitLines); + else if (textLines.length == sel.ranges.length) + multiPaste = map(textLines, function(l) { return [l]; }); + } + + // Normal behavior is to insert the new text into every selection + for (var i = sel.ranges.length - 1; i >= 0; i--) { + var range = sel.ranges[i]; + var from = range.from(), to = range.to(); + if (range.empty()) { + if (deleted && deleted > 0) // Handle deletion + from = Pos(from.line, from.ch - deleted); + else if (cm.state.overwrite && !cm.state.pasteIncoming) // Handle overwrite + to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)); + } + var updateInput = cm.curOp.updateInput; + var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i % multiPaste.length] : textLines, + origin: cm.state.pasteIncoming ? "paste" : cm.state.cutIncoming ? "cut" : "+input"}; + makeChange(cm.doc, changeEvent); + signalLater(cm, "inputRead", cm, changeEvent); + // When an 'electric' character is inserted, immediately trigger a reindent + if (inserted && !cm.state.pasteIncoming && cm.options.electricChars && + cm.options.smartIndent && range.head.ch < 100 && + (!i || sel.ranges[i - 1].head.line != range.head.line)) { + var mode = cm.getModeAt(range.head); + var end = changeEnd(changeEvent); + if (mode.electricChars) { + for (var j = 0; j < mode.electricChars.length; j++) + if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) { + indentLine(cm, end.line, "smart"); + break; + } + } else if (mode.electricInput) { + if (mode.electricInput.test(getLine(doc, end.line).text.slice(0, end.ch))) + indentLine(cm, end.line, "smart"); + } + } + } + ensureCursorVisible(cm); + cm.curOp.updateInput = updateInput; + cm.curOp.typing = true; + cm.state.pasteIncoming = cm.state.cutIncoming = false; + } + + function copyableRanges(cm) { + var text = [], ranges = []; + for (var i = 0; i < cm.doc.sel.ranges.length; i++) { + var line = cm.doc.sel.ranges[i].head.line; + var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)}; + ranges.push(lineRange); + text.push(cm.getRange(lineRange.anchor, lineRange.head)); + } + return {text: text, ranges: ranges}; + } + + function disableBrowserMagic(field) { + field.setAttribute("autocorrect", "off"); + field.setAttribute("autocapitalize", "off"); + field.setAttribute("spellcheck", "false"); + } + + // TEXTAREA INPUT STYLE + + function TextareaInput(cm) { + this.cm = cm; + // See input.poll and input.reset + this.prevInput = ""; + + // Flag that indicates whether we expect input to appear real soon + // now (after some event like 'keypress' or 'input') and are + // polling intensively. + this.pollingFast = false; + // Self-resetting timeout for the poller + this.polling = new Delayed(); + // Tracks when input.reset has punted to just putting a short + // string into the textarea instead of the full selection. + this.inaccurateSelection = false; + // Used to work around IE issue with selection being forgotten when focus moves away from textarea + this.hasSelection = false; + }; + + function hiddenTextarea() { + var te = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none"); + var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); + // The textarea is kept positioned near the cursor to prevent the + // fact that it'll be scrolled into view on input from scrolling + // our fake cursor out of view. On webkit, when wrap=off, paste is + // very slow. So make the area wide instead. + if (webkit) te.style.width = "1000px"; + else te.setAttribute("wrap", "off"); + // If border: 0; -- iOS fails to open keyboard (issue #1287) + if (ios) te.style.border = "1px solid black"; + disableBrowserMagic(te); + return div; + } + + TextareaInput.prototype = copyObj({ + init: function(display) { + var input = this, cm = this.cm; + + // Wraps and hides input textarea + var div = this.wrapper = hiddenTextarea(); + // The semihidden textarea that is focused when the editor is + // focused, and receives input. + var te = this.textarea = div.firstChild; + display.wrapper.insertBefore(div, display.wrapper.firstChild); + + // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore) + if (ios) te.style.width = "0px"; + + on(te, "input", function() { + if (ie && ie_version >= 9 && input.hasSelection) input.hasSelection = null; + input.poll(); + }); + + on(te, "paste", function() { + // Workaround for webkit bug https://bugs.webkit.org/show_bug.cgi?id=90206 + // Add a char to the end of textarea before paste occur so that + // selection doesn't span to the end of textarea. + if (webkit && !cm.state.fakedLastChar && !(new Date - cm.state.lastMiddleDown < 200)) { + var start = te.selectionStart, end = te.selectionEnd; + te.value += "$"; + // The selection end needs to be set before the start, otherwise there + // can be an intermediate non-empty selection between the two, which + // can override the middle-click paste buffer on linux and cause the + // wrong thing to get pasted. + te.selectionEnd = end; + te.selectionStart = start; + cm.state.fakedLastChar = true; + } + cm.state.pasteIncoming = true; + input.fastPoll(); + }); + + function prepareCopyCut(e) { + if (cm.somethingSelected()) { + lastCopied = cm.getSelections(); + if (input.inaccurateSelection) { + input.prevInput = ""; + input.inaccurateSelection = false; + te.value = lastCopied.join("\n"); + selectInput(te); + } + } else if (!cm.options.lineWiseCopyCut) { + return; + } else { + var ranges = copyableRanges(cm); + lastCopied = ranges.text; + if (e.type == "cut") { + cm.setSelections(ranges.ranges, null, sel_dontScroll); + } else { + input.prevInput = ""; + te.value = ranges.text.join("\n"); + selectInput(te); + } + } + if (e.type == "cut") cm.state.cutIncoming = true; + } + on(te, "cut", prepareCopyCut); + on(te, "copy", prepareCopyCut); + + on(display.scroller, "paste", function(e) { + if (eventInWidget(display, e)) return; + cm.state.pasteIncoming = true; + input.focus(); + }); + + // Prevent normal selection in the editor (we handle our own) + on(display.lineSpace, "selectstart", function(e) { + if (!eventInWidget(display, e)) e_preventDefault(e); + }); + }, + + prepareSelection: function() { + // Redraw the selection and/or cursor + var cm = this.cm, display = cm.display, doc = cm.doc; + var result = prepareSelection(cm); + + // Move the hidden textarea near the cursor to prevent scrolling artifacts + if (cm.options.moveInputWithCursor) { + var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); + var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); + result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, + headPos.top + lineOff.top - wrapOff.top)); + result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, + headPos.left + lineOff.left - wrapOff.left)); + } + + return result; + }, + + showSelection: function(drawn) { + var cm = this.cm, display = cm.display; + removeChildrenAndAdd(display.cursorDiv, drawn.cursors); + removeChildrenAndAdd(display.selectionDiv, drawn.selection); + if (drawn.teTop != null) { + this.wrapper.style.top = drawn.teTop + "px"; + this.wrapper.style.left = drawn.teLeft + "px"; + } + }, + + // Reset the input to correspond to the selection (or to be empty, + // when not typing and nothing is selected) + reset: function(typing) { + if (this.contextMenuPending) return; + var minimal, selected, cm = this.cm, doc = cm.doc; + if (cm.somethingSelected()) { + this.prevInput = ""; + var range = doc.sel.primary(); + minimal = hasCopyEvent && + (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000); + var content = minimal ? "-" : selected || cm.getSelection(); + this.textarea.value = content; + if (cm.state.focused) selectInput(this.textarea); + if (ie && ie_version >= 9) this.hasSelection = content; + } else if (!typing) { + this.prevInput = this.textarea.value = ""; + if (ie && ie_version >= 9) this.hasSelection = null; + } + this.inaccurateSelection = minimal; + }, + + getField: function() { return this.textarea; }, + + supportsTouch: function() { return false; }, + + focus: function() { + if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { + try { this.textarea.focus(); } + catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM + } + }, + + blur: function() { this.textarea.blur(); }, + + resetPosition: function() { + this.wrapper.style.top = this.wrapper.style.left = 0; + }, + + receivedFocus: function() { this.slowPoll(); }, + + // Poll for input changes, using the normal rate of polling. This + // runs as long as the editor is focused. + slowPoll: function() { + var input = this; + if (input.pollingFast) return; + input.polling.set(this.cm.options.pollInterval, function() { + input.poll(); + if (input.cm.state.focused) input.slowPoll(); + }); + }, + + // When an event has just come in that is likely to add or change + // something in the input textarea, we poll faster, to ensure that + // the change appears on the screen quickly. + fastPoll: function() { + var missed = false, input = this; + input.pollingFast = true; + function p() { + var changed = input.poll(); + if (!changed && !missed) {missed = true; input.polling.set(60, p);} + else {input.pollingFast = false; input.slowPoll();} + } + input.polling.set(20, p); + }, + + // Read input from the textarea, and update the document to match. + // When something is selected, it is present in the textarea, and + // selected (unless it is huge, in which case a placeholder is + // used). When nothing is selected, the cursor sits after previously + // seen text (can be empty), which is stored in prevInput (we must + // not reset the textarea when typing, because that breaks IME). + poll: function() { + var cm = this.cm, input = this.textarea, prevInput = this.prevInput; + // Since this is called a *lot*, try to bail out as cheaply as + // possible when it is clear that nothing happened. hasSelection + // will be the case when there is a lot of text in the textarea, + // in which case reading its value would be expensive. + if (!cm.state.focused || (hasSelection(input) && !prevInput) || + isReadOnly(cm) || cm.options.disableInput || cm.state.keySeq) + return false; + // See paste handler for more on the fakedLastChar kludge + if (cm.state.pasteIncoming && cm.state.fakedLastChar) { + input.value = input.value.substring(0, input.value.length - 1); + cm.state.fakedLastChar = false; + } + var text = input.value; + // If nothing changed, bail. + if (text == prevInput && !cm.somethingSelected()) return false; + // Work around nonsensical selection resetting in IE9/10, and + // inexplicable appearance of private area unicode characters on + // some key combos in Mac (#2689). + if (ie && ie_version >= 9 && this.hasSelection === text || + mac && /[\uf700-\uf7ff]/.test(text)) { + cm.display.input.reset(); + return false; + } + + if (cm.doc.sel == cm.display.selForContextMenu) { + var first = text.charCodeAt(0); + if (first == 0x200b && !prevInput) prevInput = "\u200b"; + if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo"); } + } + // Find the part of the input that is actually new + var same = 0, l = Math.min(prevInput.length, text.length); + while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same; + + var self = this; + runInOp(cm, function() { + applyTextInput(cm, text.slice(same), prevInput.length - same); + + // Don't leave long text in the textarea, since it makes further polling slow + if (text.length > 1000 || text.indexOf("\n") > -1) input.value = self.prevInput = ""; + else self.prevInput = text; + }); + return true; + }, + + ensurePolled: function() { + if (this.pollingFast && this.poll()) this.pollingFast = false; + }, + + onKeyPress: function() { + if (ie && ie_version >= 9) this.hasSelection = null; + this.fastPoll(); + }, + + onContextMenu: function(e) { + var input = this, cm = input.cm, display = cm.display, te = input.textarea; + var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; + if (!pos || presto) return; // Opera is difficult. + + // Reset the current text selection only if the click is done outside of the selection + // and 'resetSelectionOnContextMenu' option is true. + var reset = cm.options.resetSelectionOnContextMenu; + if (reset && cm.doc.sel.contains(pos) == -1) + operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll); + + var oldCSS = te.style.cssText; + input.wrapper.style.position = "absolute"; + te.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + + "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: " + + (ie ? "rgba(255, 255, 255, .05)" : "transparent") + + "; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; + if (webkit) var oldScrollY = window.scrollY; // Work around Chrome issue (#2712) + display.input.focus(); + if (webkit) window.scrollTo(null, oldScrollY); + display.input.reset(); + // Adds "Select all" to context menu in FF + if (!cm.somethingSelected()) te.value = input.prevInput = " "; + input.contextMenuPending = true; + display.selForContextMenu = cm.doc.sel; + clearTimeout(display.detectingSelectAll); + + // Select-all will be greyed out if there's nothing to select, so + // this adds a zero-width space so that we can later check whether + // it got selected. + function prepareSelectAllHack() { + if (te.selectionStart != null) { + var selected = cm.somethingSelected(); + var extval = "\u200b" + (selected ? te.value : ""); + te.value = "\u21da"; // Used to catch context-menu undo + te.value = extval; + input.prevInput = selected ? "" : "\u200b"; + te.selectionStart = 1; te.selectionEnd = extval.length; + // Re-set this, in case some other handler touched the + // selection in the meantime. + display.selForContextMenu = cm.doc.sel; + } + } + function rehide() { + input.contextMenuPending = false; + input.wrapper.style.position = "relative"; + te.style.cssText = oldCSS; + if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos); + + // Try to detect the user choosing select-all + if (te.selectionStart != null) { + if (!ie || (ie && ie_version < 9)) prepareSelectAllHack(); + var i = 0, poll = function() { + if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 && + te.selectionEnd > 0 && input.prevInput == "\u200b") + operation(cm, commands.selectAll)(cm); + else if (i++ < 10) display.detectingSelectAll = setTimeout(poll, 500); + else display.input.reset(); + }; + display.detectingSelectAll = setTimeout(poll, 200); + } + } + + if (ie && ie_version >= 9) prepareSelectAllHack(); + if (captureRightClick) { + e_stop(e); + var mouseup = function() { + off(window, "mouseup", mouseup); + setTimeout(rehide, 20); + }; + on(window, "mouseup", mouseup); + } else { + setTimeout(rehide, 50); + } + }, + + setUneditable: nothing, + + needsContentAttribute: false + }, TextareaInput.prototype); + + // CONTENTEDITABLE INPUT STYLE + + function ContentEditableInput(cm) { + this.cm = cm; + this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null; + this.polling = new Delayed(); + this.gracePeriod = false; + } + + ContentEditableInput.prototype = copyObj({ + init: function(display) { + var input = this, cm = input.cm; + var div = input.div = display.lineDiv; + div.contentEditable = "true"; + disableBrowserMagic(div); + + on(div, "paste", function(e) { + var pasted = e.clipboardData && e.clipboardData.getData("text/plain"); + if (pasted) { + e.preventDefault(); + cm.replaceSelection(pasted, null, "paste"); + } + }); + + on(div, "compositionstart", function(e) { + var data = e.data; + input.composing = {sel: cm.doc.sel, data: data, startData: data}; + if (!data) return; + var prim = cm.doc.sel.primary(); + var line = cm.getLine(prim.head.line); + var found = line.indexOf(data, Math.max(0, prim.head.ch - data.length)); + if (found > -1 && found <= prim.head.ch) + input.composing.sel = simpleSelection(Pos(prim.head.line, found), + Pos(prim.head.line, found + data.length)); + }); + on(div, "compositionupdate", function(e) { + input.composing.data = e.data; + }); + on(div, "compositionend", function(e) { + var ours = input.composing; + if (!ours) return; + if (e.data != ours.startData && !/\u200b/.test(e.data)) + ours.data = e.data; + // Need a small delay to prevent other code (input event, + // selection polling) from doing damage when fired right after + // compositionend. + setTimeout(function() { + if (!ours.handled) + input.applyComposition(ours); + if (input.composing == ours) + input.composing = null; + }, 50); + }); + + on(div, "touchstart", function() { + input.forceCompositionEnd(); + }); + + on(div, "input", function() { + if (input.composing) return; + if (!input.pollContent()) + runInOp(input.cm, function() {regChange(cm);}); + }); + + function onCopyCut(e) { + if (cm.somethingSelected()) { + lastCopied = cm.getSelections(); + if (e.type == "cut") cm.replaceSelection("", null, "cut"); + } else if (!cm.options.lineWiseCopyCut) { + return; + } else { + var ranges = copyableRanges(cm); + lastCopied = ranges.text; + if (e.type == "cut") { + cm.operation(function() { + cm.setSelections(ranges.ranges, 0, sel_dontScroll); + cm.replaceSelection("", null, "cut"); + }); + } + } + // iOS exposes the clipboard API, but seems to discard content inserted into it + if (e.clipboardData && !ios) { + e.preventDefault(); + e.clipboardData.clearData(); + e.clipboardData.setData("text/plain", lastCopied.join("\n")); + } else { + // Old-fashioned briefly-focus-a-textarea hack + var kludge = hiddenTextarea(), te = kludge.firstChild; + cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild); + te.value = lastCopied.join("\n"); + var hadFocus = document.activeElement; + selectInput(te); + setTimeout(function() { + cm.display.lineSpace.removeChild(kludge); + hadFocus.focus(); + }, 50); + } + } + on(div, "copy", onCopyCut); + on(div, "cut", onCopyCut); + }, + + prepareSelection: function() { + var result = prepareSelection(this.cm, false); + result.focus = this.cm.state.focused; + return result; + }, + + showSelection: function(info) { + if (!info || !this.cm.display.view.length) return; + if (info.focus) this.showPrimarySelection(); + this.showMultipleSelections(info); + }, + + showPrimarySelection: function() { + var sel = window.getSelection(), prim = this.cm.doc.sel.primary(); + var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset); + var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset); + if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad && + cmp(minPos(curAnchor, curFocus), prim.from()) == 0 && + cmp(maxPos(curAnchor, curFocus), prim.to()) == 0) + return; + + var start = posToDOM(this.cm, prim.from()); + var end = posToDOM(this.cm, prim.to()); + if (!start && !end) return; + + var view = this.cm.display.view; + var old = sel.rangeCount && sel.getRangeAt(0); + if (!start) { + start = {node: view[0].measure.map[2], offset: 0}; + } else if (!end) { // FIXME dangerously hacky + var measure = view[view.length - 1].measure; + var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map; + end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]}; + } + + try { var rng = range(start.node, start.offset, end.offset, end.node); } + catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible + if (rng) { + sel.removeAllRanges(); + sel.addRange(rng); + if (old && sel.anchorNode == null) sel.addRange(old); + else if (gecko) this.startGracePeriod(); + } + this.rememberSelection(); + }, + + startGracePeriod: function() { + var input = this; + clearTimeout(this.gracePeriod); + this.gracePeriod = setTimeout(function() { + input.gracePeriod = false; + if (input.selectionChanged()) + input.cm.operation(function() { input.cm.curOp.selectionChanged = true; }); + }, 20); + }, + + showMultipleSelections: function(info) { + removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors); + removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection); + }, + + rememberSelection: function() { + var sel = window.getSelection(); + this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset; + this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset; + }, + + selectionInEditor: function() { + var sel = window.getSelection(); + if (!sel.rangeCount) return false; + var node = sel.getRangeAt(0).commonAncestorContainer; + return contains(this.div, node); + }, + + focus: function() { + if (this.cm.options.readOnly != "nocursor") this.div.focus(); + }, + blur: function() { this.div.blur(); }, + getField: function() { return this.div; }, + + supportsTouch: function() { return true; }, + + receivedFocus: function() { + var input = this; + if (this.selectionInEditor()) + this.pollSelection(); + else + runInOp(this.cm, function() { input.cm.curOp.selectionChanged = true; }); + + function poll() { + if (input.cm.state.focused) { + input.pollSelection(); + input.polling.set(input.cm.options.pollInterval, poll); + } + } + this.polling.set(this.cm.options.pollInterval, poll); + }, + + selectionChanged: function() { + var sel = window.getSelection(); + return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || + sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset; + }, + + pollSelection: function() { + if (!this.composing && !this.gracePeriod && this.selectionChanged()) { + var sel = window.getSelection(), cm = this.cm; + this.rememberSelection(); + var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); + var head = domToPos(cm, sel.focusNode, sel.focusOffset); + if (anchor && head) runInOp(cm, function() { + setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll); + if (anchor.bad || head.bad) cm.curOp.selectionChanged = true; + }); + } + }, + + pollContent: function() { + var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary(); + var from = sel.from(), to = sel.to(); + if (from.line < display.viewFrom || to.line > display.viewTo - 1) return false; + + var fromIndex; + if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) { + var fromLine = lineNo(display.view[0].line); + var fromNode = display.view[0].node; + } else { + var fromLine = lineNo(display.view[fromIndex].line); + var fromNode = display.view[fromIndex - 1].node.nextSibling; + } + var toIndex = findViewIndex(cm, to.line); + if (toIndex == display.view.length - 1) { + var toLine = display.viewTo - 1; + var toNode = display.view[toIndex].node; + } else { + var toLine = lineNo(display.view[toIndex + 1].line) - 1; + var toNode = display.view[toIndex + 1].node.previousSibling; + } + + var newText = splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine)); + var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length)); + while (newText.length > 1 && oldText.length > 1) { + if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; } + else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; } + else break; + } + + var cutFront = 0, cutEnd = 0; + var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length); + while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront)) + ++cutFront; + var newBot = lst(newText), oldBot = lst(oldText); + var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0), + oldBot.length - (oldText.length == 1 ? cutFront : 0)); + while (cutEnd < maxCutEnd && + newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) + ++cutEnd; + + newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd); + newText[0] = newText[0].slice(cutFront); + + var chFrom = Pos(fromLine, cutFront); + var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0); + if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) { + replaceRange(cm.doc, newText, chFrom, chTo, "+input"); + return true; + } + }, + + ensurePolled: function() { + this.forceCompositionEnd(); + }, + reset: function() { + this.forceCompositionEnd(); + }, + forceCompositionEnd: function() { + if (!this.composing || this.composing.handled) return; + this.applyComposition(this.composing); + this.composing.handled = true; + this.div.blur(); + this.div.focus(); + }, + applyComposition: function(composing) { + if (composing.data && composing.data != composing.startData) + operation(this.cm, applyTextInput)(this.cm, composing.data, 0, composing.sel); + }, + + setUneditable: function(node) { + node.setAttribute("contenteditable", "false"); + }, + + onKeyPress: function(e) { + e.preventDefault(); + operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); + }, + + onContextMenu: nothing, + resetPosition: nothing, + + needsContentAttribute: true + }, ContentEditableInput.prototype); + + function posToDOM(cm, pos) { + var view = findViewForLine(cm, pos.line); + if (!view || view.hidden) return null; + var line = getLine(cm.doc, pos.line); + var info = mapFromLineView(view, line, pos.line); + + var order = getOrder(line), side = "left"; + if (order) { + var partPos = getBidiPartAt(order, pos.ch); + side = partPos % 2 ? "right" : "left"; + } + var result = nodeAndOffsetInLineMap(info.map, pos.ch, "left"); + result.offset = result.collapse == "right" ? result.end : result.start; + return result; + } + + function badPos(pos, bad) { if (bad) pos.bad = true; return pos; } + + function domToPos(cm, node, offset) { + var lineNode; + if (node == cm.display.lineDiv) { + lineNode = cm.display.lineDiv.childNodes[offset]; + if (!lineNode) return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true); + node = null; offset = 0; + } else { + for (lineNode = node;; lineNode = lineNode.parentNode) { + if (!lineNode || lineNode == cm.display.lineDiv) return null; + if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) break; + } + } + for (var i = 0; i < cm.display.view.length; i++) { + var lineView = cm.display.view[i]; + if (lineView.node == lineNode) + return locateNodeInLineView(lineView, node, offset); + } + } + + function locateNodeInLineView(lineView, node, offset) { + var wrapper = lineView.text.firstChild, bad = false; + if (!node || !contains(wrapper, node)) return badPos(Pos(lineNo(lineView.line), 0), true); + if (node == wrapper) { + bad = true; + node = wrapper.childNodes[offset]; + offset = 0; + if (!node) { + var line = lineView.rest ? lst(lineView.rest) : lineView.line; + return badPos(Pos(lineNo(line), line.text.length), bad); + } + } + + var textNode = node.nodeType == 3 ? node : null, topNode = node; + if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) { + textNode = node.firstChild; + if (offset) offset = textNode.nodeValue.length; + } + while (topNode.parentNode != wrapper) topNode = topNode.parentNode; + var measure = lineView.measure, maps = measure.maps; + + function find(textNode, topNode, offset) { + for (var i = -1; i < (maps ? maps.length : 0); i++) { + var map = i < 0 ? measure.map : maps[i]; + for (var j = 0; j < map.length; j += 3) { + var curNode = map[j + 2]; + if (curNode == textNode || curNode == topNode) { + var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]); + var ch = map[j] + offset; + if (offset < 0 || curNode != textNode) ch = map[j + (offset ? 1 : 0)]; + return Pos(line, ch); + } + } + } + } + var found = find(textNode, topNode, offset); + if (found) return badPos(found, bad); + + // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems + for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) { + found = find(after, after.firstChild, 0); + if (found) + return badPos(Pos(found.line, found.ch - dist), bad); + else + dist += after.textContent.length; + } + for (var before = topNode.previousSibling, dist = offset; before; before = before.previousSibling) { + found = find(before, before.firstChild, -1); + if (found) + return badPos(Pos(found.line, found.ch + dist), bad); + else + dist += after.textContent.length; + } + } + + function domTextBetween(cm, from, to, fromLine, toLine) { + var text = "", closing = false; + function recognizeMarker(id) { return function(marker) { return marker.id == id; }; } + function walk(node) { + if (node.nodeType == 1) { + var cmText = node.getAttribute("cm-text"); + if (cmText != null) { + if (cmText == "") cmText = node.textContent.replace(/\u200b/g, ""); + text += cmText; + return; + } + var markerID = node.getAttribute("cm-marker"), range; + if (markerID) { + var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID)); + if (found.length && (range = found[0].find())) + text += getBetween(cm.doc, range.from, range.to).join("\n"); + return; + } + if (node.getAttribute("contenteditable") == "false") return; + for (var i = 0; i < node.childNodes.length; i++) + walk(node.childNodes[i]); + if (/^(pre|div|p)$/i.test(node.nodeName)) + closing = true; + } else if (node.nodeType == 3) { + var val = node.nodeValue; + if (!val) return; + if (closing) { + text += "\n"; + closing = false; + } + text += val; + } + } + for (;;) { + walk(from); + if (from == to) break; + from = from.nextSibling; + } + return text; + } + + CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; + + // SELECTION / CURSOR + + // Selection objects are immutable. A new one is created every time + // the selection changes. A selection is one or more non-overlapping + // (and non-touching) ranges, sorted, and an integer that indicates + // which one is the primary selection (the one that's scrolled into + // view, that getCursor returns, etc). + function Selection(ranges, primIndex) { + this.ranges = ranges; + this.primIndex = primIndex; + } + + Selection.prototype = { + primary: function() { return this.ranges[this.primIndex]; }, + equals: function(other) { + if (other == this) return true; + if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) return false; + for (var i = 0; i < this.ranges.length; i++) { + var here = this.ranges[i], there = other.ranges[i]; + if (cmp(here.anchor, there.anchor) != 0 || cmp(here.head, there.head) != 0) return false; + } + return true; + }, + deepCopy: function() { + for (var out = [], i = 0; i < this.ranges.length; i++) + out[i] = new Range(copyPos(this.ranges[i].anchor), copyPos(this.ranges[i].head)); + return new Selection(out, this.primIndex); + }, + somethingSelected: function() { + for (var i = 0; i < this.ranges.length; i++) + if (!this.ranges[i].empty()) return true; + return false; + }, + contains: function(pos, end) { + if (!end) end = pos; + for (var i = 0; i < this.ranges.length; i++) { + var range = this.ranges[i]; + if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) + return i; + } + return -1; + } + }; + + function Range(anchor, head) { + this.anchor = anchor; this.head = head; + } + + Range.prototype = { + from: function() { return minPos(this.anchor, this.head); }, + to: function() { return maxPos(this.anchor, this.head); }, + empty: function() { + return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch; + } + }; + + // Take an unsorted, potentially overlapping set of ranges, and + // build a selection out of it. 'Consumes' ranges array (modifying + // it). + function normalizeSelection(ranges, primIndex) { + var prim = ranges[primIndex]; + ranges.sort(function(a, b) { return cmp(a.from(), b.from()); }); + primIndex = indexOf(ranges, prim); + for (var i = 1; i < ranges.length; i++) { + var cur = ranges[i], prev = ranges[i - 1]; + if (cmp(prev.to(), cur.from()) >= 0) { + var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to()); + var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head; + if (i <= primIndex) --primIndex; + ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to)); + } + } + return new Selection(ranges, primIndex); + } + + function simpleSelection(anchor, head) { + return new Selection([new Range(anchor, head || anchor)], 0); + } + + // Most of the external API clips given positions to make sure they + // actually exist within the document. + function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));} + function clipPos(doc, pos) { + if (pos.line < doc.first) return Pos(doc.first, 0); + var last = doc.first + doc.size - 1; + if (pos.line > last) return Pos(last, getLine(doc, last).text.length); + return clipToLen(pos, getLine(doc, pos.line).text.length); + } + function clipToLen(pos, linelen) { + var ch = pos.ch; + if (ch == null || ch > linelen) return Pos(pos.line, linelen); + else if (ch < 0) return Pos(pos.line, 0); + else return pos; + } + function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;} + function clipPosArray(doc, array) { + for (var out = [], i = 0; i < array.length; i++) out[i] = clipPos(doc, array[i]); + return out; + } + + // SELECTION UPDATES + + // The 'scroll' parameter given to many of these indicated whether + // the new cursor position should be scrolled into view after + // modifying the selection. + + // If shift is held or the extend flag is set, extends a range to + // include a given position (and optionally a second position). + // Otherwise, simply returns the range between the given positions. + // Used for cursor motion and such. + function extendRange(doc, range, head, other) { + if (doc.cm && doc.cm.display.shift || doc.extend) { + var anchor = range.anchor; + if (other) { + var posBefore = cmp(head, anchor) < 0; + if (posBefore != (cmp(other, anchor) < 0)) { + anchor = head; + head = other; + } else if (posBefore != (cmp(head, other) < 0)) { + head = other; + } + } + return new Range(anchor, head); + } else { + return new Range(other || head, head); + } + } + + // Extend the primary selection range, discard the rest. + function extendSelection(doc, head, other, options) { + setSelection(doc, new Selection([extendRange(doc, doc.sel.primary(), head, other)], 0), options); + } + + // Extend all selections (pos is an array of selections with length + // equal the number of selections) + function extendSelections(doc, heads, options) { + for (var out = [], i = 0; i < doc.sel.ranges.length; i++) + out[i] = extendRange(doc, doc.sel.ranges[i], heads[i], null); + var newSel = normalizeSelection(out, doc.sel.primIndex); + setSelection(doc, newSel, options); + } + + // Updates a single range in the selection. + function replaceOneSelection(doc, i, range, options) { + var ranges = doc.sel.ranges.slice(0); + ranges[i] = range; + setSelection(doc, normalizeSelection(ranges, doc.sel.primIndex), options); + } + + // Reset the selection to a single range. + function setSimpleSelection(doc, anchor, head, options) { + setSelection(doc, simpleSelection(anchor, head), options); + } + + // Give beforeSelectionChange handlers a change to influence a + // selection update. + function filterSelectionChange(doc, sel) { + var obj = { + ranges: sel.ranges, + update: function(ranges) { + this.ranges = []; + for (var i = 0; i < ranges.length; i++) + this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), + clipPos(doc, ranges[i].head)); + } + }; + signal(doc, "beforeSelectionChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj); + if (obj.ranges != sel.ranges) return normalizeSelection(obj.ranges, obj.ranges.length - 1); + else return sel; + } + + function setSelectionReplaceHistory(doc, sel, options) { + var done = doc.history.done, last = lst(done); + if (last && last.ranges) { + done[done.length - 1] = sel; + setSelectionNoUndo(doc, sel, options); + } else { + setSelection(doc, sel, options); + } + } + + // Set a new selection. + function setSelection(doc, sel, options) { + setSelectionNoUndo(doc, sel, options); + addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options); + } + + function setSelectionNoUndo(doc, sel, options) { + if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) + sel = filterSelectionChange(doc, sel); + + var bias = options && options.bias || + (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1); + setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)); + + if (!(options && options.scroll === false) && doc.cm) + ensureCursorVisible(doc.cm); + } + + function setSelectionInner(doc, sel) { + if (sel.equals(doc.sel)) return; + + doc.sel = sel; + + if (doc.cm) { + doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = true; + signalCursorActivity(doc.cm); + } + signalLater(doc, "cursorActivity", doc); + } + + // Verify that the selection does not partially select any atomic + // marked ranges. + function reCheckSelection(doc) { + setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false), sel_dontScroll); + } + + // Return a selection that does not partially select any atomic + // ranges. + function skipAtomicInSelection(doc, sel, bias, mayClear) { + var out; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + var newAnchor = skipAtomic(doc, range.anchor, bias, mayClear); + var newHead = skipAtomic(doc, range.head, bias, mayClear); + if (out || newAnchor != range.anchor || newHead != range.head) { + if (!out) out = sel.ranges.slice(0, i); + out[i] = new Range(newAnchor, newHead); + } + } + return out ? normalizeSelection(out, sel.primIndex) : sel; + } + + // Ensure a given position is not inside an atomic range. + function skipAtomic(doc, pos, bias, mayClear) { + var flipped = false, curPos = pos; + var dir = bias || 1; + doc.cantEdit = false; + search: for (;;) { + var line = getLine(doc, curPos.line); + if (line.markedSpans) { + for (var i = 0; i < line.markedSpans.length; ++i) { + var sp = line.markedSpans[i], m = sp.marker; + if ((sp.from == null || (m.inclusiveLeft ? sp.from <= curPos.ch : sp.from < curPos.ch)) && + (sp.to == null || (m.inclusiveRight ? sp.to >= curPos.ch : sp.to > curPos.ch))) { + if (mayClear) { + signal(m, "beforeCursorEnter"); + if (m.explicitlyCleared) { + if (!line.markedSpans) break; + else {--i; continue;} + } + } + if (!m.atomic) continue; + var newPos = m.find(dir < 0 ? -1 : 1); + if (cmp(newPos, curPos) == 0) { + newPos.ch += dir; + if (newPos.ch < 0) { + if (newPos.line > doc.first) newPos = clipPos(doc, Pos(newPos.line - 1)); + else newPos = null; + } else if (newPos.ch > line.text.length) { + if (newPos.line < doc.first + doc.size - 1) newPos = Pos(newPos.line + 1, 0); + else newPos = null; + } + if (!newPos) { + if (flipped) { + // Driven in a corner -- no valid cursor position found at all + // -- try again *with* clearing, if we didn't already + if (!mayClear) return skipAtomic(doc, pos, bias, true); + // Otherwise, turn off editing until further notice, and return the start of the doc + doc.cantEdit = true; + return Pos(doc.first, 0); + } + flipped = true; newPos = pos; dir = -dir; + } + } + curPos = newPos; + continue search; + } + } + } + return curPos; + } + } + + // SELECTION DRAWING + + function updateSelection(cm) { + cm.display.input.showSelection(cm.display.input.prepareSelection()); + } + + function prepareSelection(cm, primary) { + var doc = cm.doc, result = {}; + var curFragment = result.cursors = document.createDocumentFragment(); + var selFragment = result.selection = document.createDocumentFragment(); + + for (var i = 0; i < doc.sel.ranges.length; i++) { + if (primary === false && i == doc.sel.primIndex) continue; + var range = doc.sel.ranges[i]; + var collapsed = range.empty(); + if (collapsed || cm.options.showCursorWhenSelecting) + drawSelectionCursor(cm, range, curFragment); + if (!collapsed) + drawSelectionRange(cm, range, selFragment); + } + return result; + } + + // Draws a cursor for the given range + function drawSelectionCursor(cm, range, output) { + var pos = cursorCoords(cm, range.head, "div", null, null, !cm.options.singleCursorHeightPerLine); + + var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")); + cursor.style.left = pos.left + "px"; + cursor.style.top = pos.top + "px"; + cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; + + if (pos.other) { + // Secondary cursor, shown when on a 'jump' in bi-directional text + var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor")); + otherCursor.style.display = ""; + otherCursor.style.left = pos.other.left + "px"; + otherCursor.style.top = pos.other.top + "px"; + otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; + } + } + + // Draws the given range as a highlighted selection + function drawSelectionRange(cm, range, output) { + var display = cm.display, doc = cm.doc; + var fragment = document.createDocumentFragment(); + var padding = paddingH(cm.display), leftSide = padding.left; + var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right; + + function add(left, top, width, bottom) { + if (top < 0) top = 0; + top = Math.round(top); + bottom = Math.round(bottom); + fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left + + "px; top: " + top + "px; width: " + (width == null ? rightSide - left : width) + + "px; height: " + (bottom - top) + "px")); + } + + function drawForLine(line, fromArg, toArg) { + var lineObj = getLine(doc, line); + var lineLen = lineObj.text.length; + var start, end; + function coords(ch, bias) { + return charCoords(cm, Pos(line, ch), "div", lineObj, bias); + } + + iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) { + var leftPos = coords(from, "left"), rightPos, left, right; + if (from == to) { + rightPos = leftPos; + left = right = leftPos.left; + } else { + rightPos = coords(to - 1, "right"); + if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; } + left = leftPos.left; + right = rightPos.right; + } + if (fromArg == null && from == 0) left = leftSide; + if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part + add(left, leftPos.top, null, leftPos.bottom); + left = leftSide; + if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top); + } + if (toArg == null && to == lineLen) right = rightSide; + if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left) + start = leftPos; + if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right) + end = rightPos; + if (left < leftSide + 1) left = leftSide; + add(left, rightPos.top, right - left, rightPos.bottom); + }); + return {start: start, end: end}; + } + + var sFrom = range.from(), sTo = range.to(); + if (sFrom.line == sTo.line) { + drawForLine(sFrom.line, sFrom.ch, sTo.ch); + } else { + var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line); + var singleVLine = visualLine(fromLine) == visualLine(toLine); + var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end; + var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start; + if (singleVLine) { + if (leftEnd.top < rightStart.top - 2) { + add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); + add(leftSide, rightStart.top, rightStart.left, rightStart.bottom); + } else { + add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); + } + } + if (leftEnd.bottom < rightStart.top) + add(leftSide, leftEnd.bottom, null, rightStart.top); + } + + output.appendChild(fragment); + } + + // Cursor-blinking + function restartBlink(cm) { + if (!cm.state.focused) return; + var display = cm.display; + clearInterval(display.blinker); + var on = true; + display.cursorDiv.style.visibility = ""; + if (cm.options.cursorBlinkRate > 0) + display.blinker = setInterval(function() { + display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden"; + }, cm.options.cursorBlinkRate); + else if (cm.options.cursorBlinkRate < 0) + display.cursorDiv.style.visibility = "hidden"; + } + + // HIGHLIGHT WORKER + + function startWorker(cm, time) { + if (cm.doc.mode.startState && cm.doc.frontier < cm.display.viewTo) + cm.state.highlight.set(time, bind(highlightWorker, cm)); + } + + function highlightWorker(cm) { + var doc = cm.doc; + if (doc.frontier < doc.first) doc.frontier = doc.first; + if (doc.frontier >= cm.display.viewTo) return; + var end = +new Date + cm.options.workTime; + var state = copyState(doc.mode, getStateBefore(cm, doc.frontier)); + var changedLines = []; + + doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function(line) { + if (doc.frontier >= cm.display.viewFrom) { // Visible + var oldStyles = line.styles; + var highlighted = highlightLine(cm, line, state, true); + line.styles = highlighted.styles; + var oldCls = line.styleClasses, newCls = highlighted.classes; + if (newCls) line.styleClasses = newCls; + else if (oldCls) line.styleClasses = null; + var ischange = !oldStyles || oldStyles.length != line.styles.length || + oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass); + for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i]; + if (ischange) changedLines.push(doc.frontier); + line.stateAfter = copyState(doc.mode, state); + } else { + processLine(cm, line.text, state); + line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null; + } + ++doc.frontier; + if (+new Date > end) { + startWorker(cm, cm.options.workDelay); + return true; + } + }); + if (changedLines.length) runInOp(cm, function() { + for (var i = 0; i < changedLines.length; i++) + regLineChange(cm, changedLines[i], "text"); + }); + } + + // Finds the line to start with when starting a parse. Tries to + // find a line with a stateAfter, so that it can start with a + // valid state. If that fails, it returns the line with the + // smallest indentation, which tends to need the least context to + // parse correctly. + function findStartLine(cm, n, precise) { + var minindent, minline, doc = cm.doc; + var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100); + for (var search = n; search > lim; --search) { + if (search <= doc.first) return doc.first; + var line = getLine(doc, search - 1); + if (line.stateAfter && (!precise || search <= doc.frontier)) return search; + var indented = countColumn(line.text, null, cm.options.tabSize); + if (minline == null || minindent > indented) { + minline = search - 1; + minindent = indented; + } + } + return minline; + } + + function getStateBefore(cm, n, precise) { + var doc = cm.doc, display = cm.display; + if (!doc.mode.startState) return true; + var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter; + if (!state) state = startState(doc.mode); + else state = copyState(doc.mode, state); + doc.iter(pos, n, function(line) { + processLine(cm, line.text, state); + var save = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo; + line.stateAfter = save ? copyState(doc.mode, state) : null; + ++pos; + }); + if (precise) doc.frontier = pos; + return state; + } + + // POSITION MEASUREMENT + + function paddingTop(display) {return display.lineSpace.offsetTop;} + function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;} + function paddingH(display) { + if (display.cachedPaddingH) return display.cachedPaddingH; + var e = removeChildrenAndAdd(display.measure, elt("pre", "x")); + var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle; + var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)}; + if (!isNaN(data.left) && !isNaN(data.right)) display.cachedPaddingH = data; + return data; + } + + function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth; } + function displayWidth(cm) { + return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth; + } + function displayHeight(cm) { + return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight; + } + + // Ensure the lineView.wrapping.heights array is populated. This is + // an array of bottom offsets for the lines that make up a drawn + // line. When lineWrapping is on, there might be more than one + // height. + function ensureLineHeights(cm, lineView, rect) { + var wrapping = cm.options.lineWrapping; + var curWidth = wrapping && displayWidth(cm); + if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) { + var heights = lineView.measure.heights = []; + if (wrapping) { + lineView.measure.width = curWidth; + var rects = lineView.text.firstChild.getClientRects(); + for (var i = 0; i < rects.length - 1; i++) { + var cur = rects[i], next = rects[i + 1]; + if (Math.abs(cur.bottom - next.bottom) > 2) + heights.push((cur.bottom + next.top) / 2 - rect.top); + } + } + heights.push(rect.bottom - rect.top); + } + } + + // Find a line map (mapping character offsets to text nodes) and a + // measurement cache for the given line number. (A line view might + // contain multiple lines when collapsed ranges are present.) + function mapFromLineView(lineView, line, lineN) { + if (lineView.line == line) + return {map: lineView.measure.map, cache: lineView.measure.cache}; + for (var i = 0; i < lineView.rest.length; i++) + if (lineView.rest[i] == line) + return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]}; + for (var i = 0; i < lineView.rest.length; i++) + if (lineNo(lineView.rest[i]) > lineN) + return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i], before: true}; + } + + // Render a line into the hidden node display.externalMeasured. Used + // when measurement is needed for a line that's not in the viewport. + function updateExternalMeasurement(cm, line) { + line = visualLine(line); + var lineN = lineNo(line); + var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN); + view.lineN = lineN; + var built = view.built = buildLineContent(cm, view); + view.text = built.pre; + removeChildrenAndAdd(cm.display.lineMeasure, built.pre); + return view; + } + + // Get a {top, bottom, left, right} box (in line-local coordinates) + // for a given character. + function measureChar(cm, line, ch, bias) { + return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias); + } + + // Find a line view that corresponds to the given line number. + function findViewForLine(cm, lineN) { + if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo) + return cm.display.view[findViewIndex(cm, lineN)]; + var ext = cm.display.externalMeasured; + if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size) + return ext; + } + + // Measurement can be split in two steps, the set-up work that + // applies to the whole line, and the measurement of the actual + // character. Functions like coordsChar, that need to do a lot of + // measurements in a row, can thus ensure that the set-up work is + // only done once. + function prepareMeasureForLine(cm, line) { + var lineN = lineNo(line); + var view = findViewForLine(cm, lineN); + if (view && !view.text) + view = null; + else if (view && view.changes) + updateLineForChanges(cm, view, lineN, getDimensions(cm)); + if (!view) + view = updateExternalMeasurement(cm, line); + + var info = mapFromLineView(view, line, lineN); + return { + line: line, view: view, rect: null, + map: info.map, cache: info.cache, before: info.before, + hasHeights: false + }; + } + + // Given a prepared measurement object, measures the position of an + // actual character (or fetches it from the cache). + function measureCharPrepared(cm, prepared, ch, bias, varHeight) { + if (prepared.before) ch = -1; + var key = ch + (bias || ""), found; + if (prepared.cache.hasOwnProperty(key)) { + found = prepared.cache[key]; + } else { + if (!prepared.rect) + prepared.rect = prepared.view.text.getBoundingClientRect(); + if (!prepared.hasHeights) { + ensureLineHeights(cm, prepared.view, prepared.rect); + prepared.hasHeights = true; + } + found = measureCharInner(cm, prepared, ch, bias); + if (!found.bogus) prepared.cache[key] = found; + } + return {left: found.left, right: found.right, + top: varHeight ? found.rtop : found.top, + bottom: varHeight ? found.rbottom : found.bottom}; + } + + var nullRect = {left: 0, right: 0, top: 0, bottom: 0}; + + function nodeAndOffsetInLineMap(map, ch, bias) { + var node, start, end, collapse; + // First, search the line map for the text node corresponding to, + // or closest to, the target character. + for (var i = 0; i < map.length; i += 3) { + var mStart = map[i], mEnd = map[i + 1]; + if (ch < mStart) { + start = 0; end = 1; + collapse = "left"; + } else if (ch < mEnd) { + start = ch - mStart; + end = start + 1; + } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) { + end = mEnd - mStart; + start = end - 1; + if (ch >= mEnd) collapse = "right"; + } + if (start != null) { + node = map[i + 2]; + if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) + collapse = bias; + if (bias == "left" && start == 0) + while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) { + node = map[(i -= 3) + 2]; + collapse = "left"; + } + if (bias == "right" && start == mEnd - mStart) + while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) { + node = map[(i += 3) + 2]; + collapse = "right"; + } + break; + } + } + return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd}; + } + + function measureCharInner(cm, prepared, ch, bias) { + var place = nodeAndOffsetInLineMap(prepared.map, ch, bias); + var node = place.node, start = place.start, end = place.end, collapse = place.collapse; + + var rect; + if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. + for (var i = 0; i < 4; i++) { // Retry a maximum of 4 times when nonsense rectangles are returned + while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) --start; + while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) ++end; + if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) { + rect = node.parentNode.getBoundingClientRect(); + } else if (ie && cm.options.lineWrapping) { + var rects = range(node, start, end).getClientRects(); + if (rects.length) + rect = rects[bias == "right" ? rects.length - 1 : 0]; + else + rect = nullRect; + } else { + rect = range(node, start, end).getBoundingClientRect() || nullRect; + } + if (rect.left || rect.right || start == 0) break; + end = start; + start = start - 1; + collapse = "right"; + } + if (ie && ie_version < 11) rect = maybeUpdateRectForZooming(cm.display.measure, rect); + } else { // If it is a widget, simply get the box for the whole widget. + if (start > 0) collapse = bias = "right"; + var rects; + if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1) + rect = rects[bias == "right" ? rects.length - 1 : 0]; + else + rect = node.getBoundingClientRect(); + } + if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) { + var rSpan = node.parentNode.getClientRects()[0]; + if (rSpan) + rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom}; + else + rect = nullRect; + } + + var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top; + var mid = (rtop + rbot) / 2; + var heights = prepared.view.measure.heights; + for (var i = 0; i < heights.length - 1; i++) + if (mid < heights[i]) break; + var top = i ? heights[i - 1] : 0, bot = heights[i]; + var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left, + right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left, + top: top, bottom: bot}; + if (!rect.left && !rect.right) result.bogus = true; + if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; } + + return result; + } + + // Work around problem with bounding client rects on ranges being + // returned incorrectly when zoomed on IE10 and below. + function maybeUpdateRectForZooming(measure, rect) { + if (!window.screen || screen.logicalXDPI == null || + screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure)) + return rect; + var scaleX = screen.logicalXDPI / screen.deviceXDPI; + var scaleY = screen.logicalYDPI / screen.deviceYDPI; + return {left: rect.left * scaleX, right: rect.right * scaleX, + top: rect.top * scaleY, bottom: rect.bottom * scaleY}; + } + + function clearLineMeasurementCacheFor(lineView) { + if (lineView.measure) { + lineView.measure.cache = {}; + lineView.measure.heights = null; + if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) + lineView.measure.caches[i] = {}; + } + } + + function clearLineMeasurementCache(cm) { + cm.display.externalMeasure = null; + removeChildren(cm.display.lineMeasure); + for (var i = 0; i < cm.display.view.length; i++) + clearLineMeasurementCacheFor(cm.display.view[i]); + } + + function clearCaches(cm) { + clearLineMeasurementCache(cm); + cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null; + if (!cm.options.lineWrapping) cm.display.maxLineChanged = true; + cm.display.lineNumChars = null; + } + + function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; } + function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; } + + // Converts a {top, bottom, left, right} box from line-local + // coordinates into another coordinate system. Context may be one of + // "line", "div" (display.lineDiv), "local"/null (editor), "window", + // or "page". + function intoCoordSystem(cm, lineObj, rect, context) { + if (lineObj.widgets) for (var i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) { + var size = widgetHeight(lineObj.widgets[i]); + rect.top += size; rect.bottom += size; + } + if (context == "line") return rect; + if (!context) context = "local"; + var yOff = heightAtLine(lineObj); + if (context == "local") yOff += paddingTop(cm.display); + else yOff -= cm.display.viewOffset; + if (context == "page" || context == "window") { + var lOff = cm.display.lineSpace.getBoundingClientRect(); + yOff += lOff.top + (context == "window" ? 0 : pageScrollY()); + var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()); + rect.left += xOff; rect.right += xOff; + } + rect.top += yOff; rect.bottom += yOff; + return rect; + } + + // Coverts a box from "div" coords to another coordinate system. + // Context may be "window", "page", "div", or "local"/null. + function fromCoordSystem(cm, coords, context) { + if (context == "div") return coords; + var left = coords.left, top = coords.top; + // First move into "page" coordinate system + if (context == "page") { + left -= pageScrollX(); + top -= pageScrollY(); + } else if (context == "local" || !context) { + var localBox = cm.display.sizer.getBoundingClientRect(); + left += localBox.left; + top += localBox.top; + } + + var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect(); + return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top}; + } + + function charCoords(cm, pos, context, lineObj, bias) { + if (!lineObj) lineObj = getLine(cm.doc, pos.line); + return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context); + } + + // Returns a box for a given cursor position, which may have an + // 'other' property containing the position of the secondary cursor + // on a bidi boundary. + function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) { + lineObj = lineObj || getLine(cm.doc, pos.line); + if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj); + function get(ch, right) { + var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight); + if (right) m.left = m.right; else m.right = m.left; + return intoCoordSystem(cm, lineObj, m, context); + } + function getBidi(ch, partPos) { + var part = order[partPos], right = part.level % 2; + if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) { + part = order[--partPos]; + ch = bidiRight(part) - (part.level % 2 ? 0 : 1); + right = true; + } else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) { + part = order[++partPos]; + ch = bidiLeft(part) - part.level % 2; + right = false; + } + if (right && ch == part.to && ch > part.from) return get(ch - 1); + return get(ch, right); + } + var order = getOrder(lineObj), ch = pos.ch; + if (!order) return get(ch); + var partPos = getBidiPartAt(order, ch); + var val = getBidi(ch, partPos); + if (bidiOther != null) val.other = getBidi(ch, bidiOther); + return val; + } + + // Used to cheaply estimate the coordinates for a position. Used for + // intermediate scroll updates. + function estimateCoords(cm, pos) { + var left = 0, pos = clipPos(cm.doc, pos); + if (!cm.options.lineWrapping) left = charWidth(cm.display) * pos.ch; + var lineObj = getLine(cm.doc, pos.line); + var top = heightAtLine(lineObj) + paddingTop(cm.display); + return {left: left, right: left, top: top, bottom: top + lineObj.height}; + } + + // Positions returned by coordsChar contain some extra information. + // xRel is the relative x position of the input coordinates compared + // to the found position (so xRel > 0 means the coordinates are to + // the right of the character position, for example). When outside + // is true, that means the coordinates lie outside the line's + // vertical range. + function PosWithInfo(line, ch, outside, xRel) { + var pos = Pos(line, ch); + pos.xRel = xRel; + if (outside) pos.outside = true; + return pos; + } + + // Compute the character position closest to the given coordinates. + // Input must be lineSpace-local ("div" coordinate system). + function coordsChar(cm, x, y) { + var doc = cm.doc; + y += cm.display.viewOffset; + if (y < 0) return PosWithInfo(doc.first, 0, true, -1); + var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1; + if (lineN > last) + return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1); + if (x < 0) x = 0; + + var lineObj = getLine(doc, lineN); + for (;;) { + var found = coordsCharInner(cm, lineObj, lineN, x, y); + var merged = collapsedSpanAtEnd(lineObj); + var mergedPos = merged && merged.find(0, true); + if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0)) + lineN = lineNo(lineObj = mergedPos.to.line); + else + return found; + } + } + + function coordsCharInner(cm, lineObj, lineNo, x, y) { + var innerOff = y - heightAtLine(lineObj); + var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth; + var preparedMeasure = prepareMeasureForLine(cm, lineObj); + + function getX(ch) { + var sp = cursorCoords(cm, Pos(lineNo, ch), "line", lineObj, preparedMeasure); + wrongLine = true; + if (innerOff > sp.bottom) return sp.left - adjust; + else if (innerOff < sp.top) return sp.left + adjust; + else wrongLine = false; + return sp.left; + } + + var bidi = getOrder(lineObj), dist = lineObj.text.length; + var from = lineLeft(lineObj), to = lineRight(lineObj); + var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine; + + if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1); + // Do a binary search between these bounds. + for (;;) { + if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) { + var ch = x < fromX || x - fromX <= toX - x ? from : to; + var xDiff = x - (ch == from ? fromX : toX); + while (isExtendingChar(lineObj.text.charAt(ch))) ++ch; + var pos = PosWithInfo(lineNo, ch, ch == from ? fromOutside : toOutside, + xDiff < -1 ? -1 : xDiff > 1 ? 1 : 0); + return pos; + } + var step = Math.ceil(dist / 2), middle = from + step; + if (bidi) { + middle = from; + for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1); + } + var middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;} + else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;} + } + } + + var measureText; + // Compute the default text height. + function textHeight(display) { + if (display.cachedTextHeight != null) return display.cachedTextHeight; + if (measureText == null) { + measureText = elt("pre"); + // Measure a bunch of lines, for browsers that compute + // fractional heights. + for (var i = 0; i < 49; ++i) { + measureText.appendChild(document.createTextNode("x")); + measureText.appendChild(elt("br")); + } + measureText.appendChild(document.createTextNode("x")); + } + removeChildrenAndAdd(display.measure, measureText); + var height = measureText.offsetHeight / 50; + if (height > 3) display.cachedTextHeight = height; + removeChildren(display.measure); + return height || 1; + } + + // Compute the default character width. + function charWidth(display) { + if (display.cachedCharWidth != null) return display.cachedCharWidth; + var anchor = elt("span", "xxxxxxxxxx"); + var pre = elt("pre", [anchor]); + removeChildrenAndAdd(display.measure, pre); + var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10; + if (width > 2) display.cachedCharWidth = width; + return width || 10; + } + + // OPERATIONS + + // Operations are used to wrap a series of changes to the editor + // state in such a way that each change won't have to update the + // cursor and display (which would be awkward, slow, and + // error-prone). Instead, display updates are batched and then all + // combined and executed at once. + + var operationGroup = null; + + var nextOpId = 0; + // Start a new operation. + function startOperation(cm) { + cm.curOp = { + cm: cm, + viewChanged: false, // Flag that indicates that lines might need to be redrawn + startHeight: cm.doc.height, // Used to detect need to update scrollbar + forceUpdate: false, // Used to force a redraw + updateInput: null, // Whether to reset the input textarea + typing: false, // Whether this reset should be careful to leave existing text (for compositing) + changeObjs: null, // Accumulated changes, for firing change events + cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on + cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already + selectionChanged: false, // Whether the selection needs to be redrawn + updateMaxLine: false, // Set when the widest line needs to be determined anew + scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet + scrollToPos: null, // Used to scroll to a specific position + focus: false, + id: ++nextOpId // Unique ID + }; + if (operationGroup) { + operationGroup.ops.push(cm.curOp); + } else { + cm.curOp.ownsGroup = operationGroup = { + ops: [cm.curOp], + delayedCallbacks: [] + }; + } + } + + function fireCallbacksForOps(group) { + // Calls delayed callbacks and cursorActivity handlers until no + // new ones appear + var callbacks = group.delayedCallbacks, i = 0; + do { + for (; i < callbacks.length; i++) + callbacks[i](); + for (var j = 0; j < group.ops.length; j++) { + var op = group.ops[j]; + if (op.cursorActivityHandlers) + while (op.cursorActivityCalled < op.cursorActivityHandlers.length) + op.cursorActivityHandlers[op.cursorActivityCalled++](op.cm); + } + } while (i < callbacks.length); + } + + // Finish an operation, updating the display and signalling delayed events + function endOperation(cm) { + var op = cm.curOp, group = op.ownsGroup; + if (!group) return; + + try { fireCallbacksForOps(group); } + finally { + operationGroup = null; + for (var i = 0; i < group.ops.length; i++) + group.ops[i].cm.curOp = null; + endOperations(group); + } + } + + // The DOM updates done when an operation finishes are batched so + // that the minimum number of relayouts are required. + function endOperations(group) { + var ops = group.ops; + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_R1(ops[i]); + for (var i = 0; i < ops.length; i++) // Write DOM (maybe) + endOperation_W1(ops[i]); + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_R2(ops[i]); + for (var i = 0; i < ops.length; i++) // Write DOM (maybe) + endOperation_W2(ops[i]); + for (var i = 0; i < ops.length; i++) // Read DOM + endOperation_finish(ops[i]); + } + + function endOperation_R1(op) { + var cm = op.cm, display = cm.display; + maybeClipScrollbars(cm); + if (op.updateMaxLine) findMaxLine(cm); + + op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null || + op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom || + op.scrollToPos.to.line >= display.viewTo) || + display.maxLineChanged && cm.options.lineWrapping; + op.update = op.mustUpdate && + new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate); + } + + function endOperation_W1(op) { + op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update); + } + + function endOperation_R2(op) { + var cm = op.cm, display = cm.display; + if (op.updatedDisplay) updateHeightsInViewport(cm); + + op.barMeasure = measureForScrollbars(cm); + + // If the max line changed since it was last measured, measure it, + // and ensure the document's width matches it. + // updateDisplay_W2 will use these properties to do the actual resizing + if (display.maxLineChanged && !cm.options.lineWrapping) { + op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3; + cm.display.sizerWidth = op.adjustWidthTo; + op.barMeasure.scrollWidth = + Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth); + op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm)); + } + + if (op.updatedDisplay || op.selectionChanged) + op.preparedSelection = display.input.prepareSelection(); + } + + function endOperation_W2(op) { + var cm = op.cm; + + if (op.adjustWidthTo != null) { + cm.display.sizer.style.minWidth = op.adjustWidthTo + "px"; + if (op.maxScrollLeft < cm.doc.scrollLeft) + setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true); + cm.display.maxLineChanged = false; + } + + if (op.preparedSelection) + cm.display.input.showSelection(op.preparedSelection); + if (op.updatedDisplay) + setDocumentHeight(cm, op.barMeasure); + if (op.updatedDisplay || op.startHeight != cm.doc.height) + updateScrollbars(cm, op.barMeasure); + + if (op.selectionChanged) restartBlink(cm); + + if (cm.state.focused && op.updateInput) + cm.display.input.reset(op.typing); + if (op.focus && op.focus == activeElt()) ensureFocus(op.cm); + } + + function endOperation_finish(op) { + var cm = op.cm, display = cm.display, doc = cm.doc; + + if (op.updatedDisplay) postUpdateDisplay(cm, op.update); + + // Abort mouse wheel delta measurement, when scrolling explicitly + if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos)) + display.wheelStartX = display.wheelStartY = null; + + // Propagate the scroll position to the actual DOM scroller + if (op.scrollTop != null && (display.scroller.scrollTop != op.scrollTop || op.forceScroll)) { + doc.scrollTop = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, op.scrollTop)); + display.scrollbars.setScrollTop(doc.scrollTop); + display.scroller.scrollTop = doc.scrollTop; + } + if (op.scrollLeft != null && (display.scroller.scrollLeft != op.scrollLeft || op.forceScroll)) { + doc.scrollLeft = Math.max(0, Math.min(display.scroller.scrollWidth - displayWidth(cm), op.scrollLeft)); + display.scrollbars.setScrollLeft(doc.scrollLeft); + display.scroller.scrollLeft = doc.scrollLeft; + alignHorizontally(cm); + } + // If we need to scroll a specific position into view, do so. + if (op.scrollToPos) { + var coords = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from), + clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin); + if (op.scrollToPos.isCursor && cm.state.focused) maybeScrollWindow(cm, coords); + } + + // Fire events for markers that are hidden/unidden by editing or + // undoing + var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; + if (hidden) for (var i = 0; i < hidden.length; ++i) + if (!hidden[i].lines.length) signal(hidden[i], "hide"); + if (unhidden) for (var i = 0; i < unhidden.length; ++i) + if (unhidden[i].lines.length) signal(unhidden[i], "unhide"); + + if (display.wrapper.offsetHeight) + doc.scrollTop = cm.display.scroller.scrollTop; + + // Fire change events, and delayed event handlers + if (op.changeObjs) + signal(cm, "changes", cm, op.changeObjs); + if (op.update) + op.update.finish(); + } + + // Run the given function in an operation + function runInOp(cm, f) { + if (cm.curOp) return f(); + startOperation(cm); + try { return f(); } + finally { endOperation(cm); } + } + // Wraps a function in an operation. Returns the wrapped function. + function operation(cm, f) { + return function() { + if (cm.curOp) return f.apply(cm, arguments); + startOperation(cm); + try { return f.apply(cm, arguments); } + finally { endOperation(cm); } + }; + } + // Used to add methods to editor and doc instances, wrapping them in + // operations. + function methodOp(f) { + return function() { + if (this.curOp) return f.apply(this, arguments); + startOperation(this); + try { return f.apply(this, arguments); } + finally { endOperation(this); } + }; + } + function docMethodOp(f) { + return function() { + var cm = this.cm; + if (!cm || cm.curOp) return f.apply(this, arguments); + startOperation(cm); + try { return f.apply(this, arguments); } + finally { endOperation(cm); } + }; + } + + // VIEW TRACKING + + // These objects are used to represent the visible (currently drawn) + // part of the document. A LineView may correspond to multiple + // logical lines, if those are connected by collapsed ranges. + function LineView(doc, line, lineN) { + // The starting line + this.line = line; + // Continuing lines, if any + this.rest = visualLineContinued(line); + // Number of logical lines in this visual line + this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1; + this.node = this.text = null; + this.hidden = lineIsHidden(doc, line); + } + + // Create a range of LineView objects for the given lines. + function buildViewArray(cm, from, to) { + var array = [], nextPos; + for (var pos = from; pos < to; pos = nextPos) { + var view = new LineView(cm.doc, getLine(cm.doc, pos), pos); + nextPos = pos + view.size; + array.push(view); + } + return array; + } + + // Updates the display.view data structure for a given change to the + // document. From and to are in pre-change coordinates. Lendiff is + // the amount of lines added or subtracted by the change. This is + // used for changes that span multiple lines, or change the way + // lines are divided into visual lines. regLineChange (below) + // registers single-line changes. + function regChange(cm, from, to, lendiff) { + if (from == null) from = cm.doc.first; + if (to == null) to = cm.doc.first + cm.doc.size; + if (!lendiff) lendiff = 0; + + var display = cm.display; + if (lendiff && to < display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers > from)) + display.updateLineNumbers = from; + + cm.curOp.viewChanged = true; + + if (from >= display.viewTo) { // Change after + if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo) + resetView(cm); + } else if (to <= display.viewFrom) { // Change before + if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) { + resetView(cm); + } else { + display.viewFrom += lendiff; + display.viewTo += lendiff; + } + } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap + resetView(cm); + } else if (from <= display.viewFrom) { // Top overlap + var cut = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cut) { + display.view = display.view.slice(cut.index); + display.viewFrom = cut.lineN; + display.viewTo += lendiff; + } else { + resetView(cm); + } + } else if (to >= display.viewTo) { // Bottom overlap + var cut = viewCuttingPoint(cm, from, from, -1); + if (cut) { + display.view = display.view.slice(0, cut.index); + display.viewTo = cut.lineN; + } else { + resetView(cm); + } + } else { // Gap in the middle + var cutTop = viewCuttingPoint(cm, from, from, -1); + var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cutTop && cutBot) { + display.view = display.view.slice(0, cutTop.index) + .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN)) + .concat(display.view.slice(cutBot.index)); + display.viewTo += lendiff; + } else { + resetView(cm); + } + } + + var ext = display.externalMeasured; + if (ext) { + if (to < ext.lineN) + ext.lineN += lendiff; + else if (from < ext.lineN + ext.size) + display.externalMeasured = null; + } + } + + // Register a change to a single line. Type must be one of "text", + // "gutter", "class", "widget" + function regLineChange(cm, line, type) { + cm.curOp.viewChanged = true; + var display = cm.display, ext = cm.display.externalMeasured; + if (ext && line >= ext.lineN && line < ext.lineN + ext.size) + display.externalMeasured = null; + + if (line < display.viewFrom || line >= display.viewTo) return; + var lineView = display.view[findViewIndex(cm, line)]; + if (lineView.node == null) return; + var arr = lineView.changes || (lineView.changes = []); + if (indexOf(arr, type) == -1) arr.push(type); + } + + // Clear the view. + function resetView(cm) { + cm.display.viewFrom = cm.display.viewTo = cm.doc.first; + cm.display.view = []; + cm.display.viewOffset = 0; + } + + // Find the view element corresponding to a given line. Return null + // when the line isn't visible. + function findViewIndex(cm, n) { + if (n >= cm.display.viewTo) return null; + n -= cm.display.viewFrom; + if (n < 0) return null; + var view = cm.display.view; + for (var i = 0; i < view.length; i++) { + n -= view[i].size; + if (n < 0) return i; + } + } + + function viewCuttingPoint(cm, oldN, newN, dir) { + var index = findViewIndex(cm, oldN), diff, view = cm.display.view; + if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size) + return {index: index, lineN: newN}; + for (var i = 0, n = cm.display.viewFrom; i < index; i++) + n += view[i].size; + if (n != oldN) { + if (dir > 0) { + if (index == view.length - 1) return null; + diff = (n + view[index].size) - oldN; + index++; + } else { + diff = n - oldN; + } + oldN += diff; newN += diff; + } + while (visualLineNo(cm.doc, newN) != newN) { + if (index == (dir < 0 ? 0 : view.length - 1)) return null; + newN += dir * view[index - (dir < 0 ? 1 : 0)].size; + index += dir; + } + return {index: index, lineN: newN}; + } + + // Force the view to cover a given range, adding empty view element + // or clipping off existing ones as needed. + function adjustView(cm, from, to) { + var display = cm.display, view = display.view; + if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) { + display.view = buildViewArray(cm, from, to); + display.viewFrom = from; + } else { + if (display.viewFrom > from) + display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view); + else if (display.viewFrom < from) + display.view = display.view.slice(findViewIndex(cm, from)); + display.viewFrom = from; + if (display.viewTo < to) + display.view = display.view.concat(buildViewArray(cm, display.viewTo, to)); + else if (display.viewTo > to) + display.view = display.view.slice(0, findViewIndex(cm, to)); + } + display.viewTo = to; + } + + // Count the number of lines in the view whose DOM representation is + // out of date (or nonexistent). + function countDirtyView(cm) { + var view = cm.display.view, dirty = 0; + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (!lineView.hidden && (!lineView.node || lineView.changes)) ++dirty; + } + return dirty; + } + + // EVENT HANDLERS + + // Attach the necessary event handlers when initializing the editor + function registerEventHandlers(cm) { + var d = cm.display; + on(d.scroller, "mousedown", operation(cm, onMouseDown)); + // Older IE's will not fire a second mousedown for a double click + if (ie && ie_version < 11) + on(d.scroller, "dblclick", operation(cm, function(e) { + if (signalDOMEvent(cm, e)) return; + var pos = posFromMouse(cm, e); + if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return; + e_preventDefault(e); + var word = cm.findWordAt(pos); + extendSelection(cm.doc, word.anchor, word.head); + })); + else + on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); }); + // Some browsers fire contextmenu *after* opening the menu, at + // which point we can't mess with it anymore. Context menu is + // handled in onMouseDown for these browsers. + if (!captureRightClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);}); + + // Used to suppress mouse event handling when a touch happens + var touchFinished, prevTouch = {end: 0}; + function finishTouch() { + if (d.activeTouch) { + touchFinished = setTimeout(function() {d.activeTouch = null;}, 1000); + prevTouch = d.activeTouch; + prevTouch.end = +new Date; + } + }; + function isMouseLikeTouchEvent(e) { + if (e.touches.length != 1) return false; + var touch = e.touches[0]; + return touch.radiusX <= 1 && touch.radiusY <= 1; + } + function farAway(touch, other) { + if (other.left == null) return true; + var dx = other.left - touch.left, dy = other.top - touch.top; + return dx * dx + dy * dy > 20 * 20; + } + on(d.scroller, "touchstart", function(e) { + if (!isMouseLikeTouchEvent(e)) { + clearTimeout(touchFinished); + var now = +new Date; + d.activeTouch = {start: now, moved: false, + prev: now - prevTouch.end <= 300 ? prevTouch : null}; + if (e.touches.length == 1) { + d.activeTouch.left = e.touches[0].pageX; + d.activeTouch.top = e.touches[0].pageY; + } + } + }); + on(d.scroller, "touchmove", function() { + if (d.activeTouch) d.activeTouch.moved = true; + }); + on(d.scroller, "touchend", function(e) { + var touch = d.activeTouch; + if (touch && !eventInWidget(d, e) && touch.left != null && + !touch.moved && new Date - touch.start < 300) { + var pos = cm.coordsChar(d.activeTouch, "page"), range; + if (!touch.prev || farAway(touch, touch.prev)) // Single tap + range = new Range(pos, pos); + else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap + range = cm.findWordAt(pos); + else // Triple tap + range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))); + cm.setSelection(range.anchor, range.head); + cm.focus(); + e_preventDefault(e); + } + finishTouch(); + }); + on(d.scroller, "touchcancel", finishTouch); + + // Sync scrolling between fake scrollbars and real scrollable + // area, ensure viewport is updated when scrolling. + on(d.scroller, "scroll", function() { + if (d.scroller.clientHeight) { + setScrollTop(cm, d.scroller.scrollTop); + setScrollLeft(cm, d.scroller.scrollLeft, true); + signal(cm, "scroll", cm); + } + }); + + // Listen to wheel events in order to try and update the viewport on time. + on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);}); + on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);}); + + // Prevent wrapper from ever scrolling + on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); + + d.dragFunctions = { + simple: function(e) {if (!signalDOMEvent(cm, e)) e_stop(e);}, + start: function(e){onDragStart(cm, e);}, + drop: operation(cm, onDrop) + }; + + var inp = d.input.getField(); + on(inp, "keyup", function(e) { onKeyUp.call(cm, e); }); + on(inp, "keydown", operation(cm, onKeyDown)); + on(inp, "keypress", operation(cm, onKeyPress)); + on(inp, "focus", bind(onFocus, cm)); + on(inp, "blur", bind(onBlur, cm)); + } + + function dragDropChanged(cm, value, old) { + var wasOn = old && old != CodeMirror.Init; + if (!value != !wasOn) { + var funcs = cm.display.dragFunctions; + var toggle = value ? on : off; + toggle(cm.display.scroller, "dragstart", funcs.start); + toggle(cm.display.scroller, "dragenter", funcs.simple); + toggle(cm.display.scroller, "dragover", funcs.simple); + toggle(cm.display.scroller, "drop", funcs.drop); + } + } + + // Called when the window resizes + function onResize(cm) { + var d = cm.display; + if (d.lastWrapHeight == d.wrapper.clientHeight && d.lastWrapWidth == d.wrapper.clientWidth) + return; + // Might be a text scaling operation, clear size caches. + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + d.scrollbarsClipped = false; + cm.setSize(); + } + + // MOUSE EVENTS + + // Return true when the given mouse event happened in a widget + function eventInWidget(display, e) { + for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { + if (!n || (n.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") || + (n.parentNode == display.sizer && n != display.mover)) + return true; + } + } + + // Given a mouse event, find the corresponding position. If liberal + // is false, it checks whether a gutter or scrollbar was clicked, + // and returns null if it was. forRect is used by rectangular + // selections, and tries to estimate a character position even for + // coordinates beyond the right of the text. + function posFromMouse(cm, e, liberal, forRect) { + var display = cm.display; + if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") return null; + + var x, y, space = display.lineSpace.getBoundingClientRect(); + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX - space.left; y = e.clientY - space.top; } + catch (e) { return null; } + var coords = coordsChar(cm, x, y), line; + if (forRect && coords.xRel == 1 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) { + var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length; + coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff)); + } + return coords; + } + + // A mouse down can be a single click, double click, triple click, + // start of selection drag, start of text drag, new cursor + // (ctrl-click), rectangle drag (alt-drag), or xwin + // middle-click-paste. Or it might be a click on something we should + // not interfere with, such as a scrollbar or widget. + function onMouseDown(e) { + var cm = this, display = cm.display; + if (display.activeTouch && display.input.supportsTouch() || signalDOMEvent(cm, e)) return; + display.shift = e.shiftKey; + + if (eventInWidget(display, e)) { + if (!webkit) { + // Briefly turn off draggability, to allow widgets to do + // normal dragging things. + display.scroller.draggable = false; + setTimeout(function(){display.scroller.draggable = true;}, 100); + } + return; + } + if (clickInGutter(cm, e)) return; + var start = posFromMouse(cm, e); + window.focus(); + + switch (e_button(e)) { + case 1: + if (start) + leftButtonDown(cm, e, start); + else if (e_target(e) == display.scroller) + e_preventDefault(e); + break; + case 2: + if (webkit) cm.state.lastMiddleDown = +new Date; + if (start) extendSelection(cm.doc, start); + setTimeout(function() {display.input.focus();}, 20); + e_preventDefault(e); + break; + case 3: + if (captureRightClick) onContextMenu(cm, e); + else delayBlurEvent(cm); + break; + } + } + + var lastClick, lastDoubleClick; + function leftButtonDown(cm, e, start) { + if (ie) setTimeout(bind(ensureFocus, cm), 0); + else cm.curOp.focus = activeElt(); + + var now = +new Date, type; + if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) { + type = "triple"; + } else if (lastClick && lastClick.time > now - 400 && cmp(lastClick.pos, start) == 0) { + type = "double"; + lastDoubleClick = {time: now, pos: start}; + } else { + type = "single"; + lastClick = {time: now, pos: start}; + } + + var sel = cm.doc.sel, modifier = mac ? e.metaKey : e.ctrlKey, contained; + if (cm.options.dragDrop && dragAndDrop && !isReadOnly(cm) && + type == "single" && (contained = sel.contains(start)) > -1 && + !sel.ranges[contained].empty()) + leftButtonStartDrag(cm, e, start, modifier); + else + leftButtonSelect(cm, e, start, type, modifier); + } + + // Start a text drag. When it ends, see if any dragging actually + // happen, and treat as a click if it didn't. + function leftButtonStartDrag(cm, e, start, modifier) { + var display = cm.display; + var dragEnd = operation(cm, function(e2) { + if (webkit) display.scroller.draggable = false; + cm.state.draggingText = false; + off(document, "mouseup", dragEnd); + off(display.scroller, "drop", dragEnd); + if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { + e_preventDefault(e2); + if (!modifier) + extendSelection(cm.doc, start); + // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081) + if (webkit || ie && ie_version == 9) + setTimeout(function() {document.body.focus(); display.input.focus();}, 20); + else + display.input.focus(); + } + }); + // Let the drag handler handle this. + if (webkit) display.scroller.draggable = true; + cm.state.draggingText = dragEnd; + // IE's approach to draggable + if (display.scroller.dragDrop) display.scroller.dragDrop(); + on(document, "mouseup", dragEnd); + on(display.scroller, "drop", dragEnd); + } + + // Normal selection, as opposed to text dragging. + function leftButtonSelect(cm, e, start, type, addNew) { + var display = cm.display, doc = cm.doc; + e_preventDefault(e); + + var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges; + if (addNew && !e.shiftKey) { + ourIndex = doc.sel.contains(start); + if (ourIndex > -1) + ourRange = ranges[ourIndex]; + else + ourRange = new Range(start, start); + } else { + ourRange = doc.sel.primary(); + ourIndex = doc.sel.primIndex; + } + + if (e.altKey) { + type = "rect"; + if (!addNew) ourRange = new Range(start, start); + start = posFromMouse(cm, e, true, true); + ourIndex = -1; + } else if (type == "double") { + var word = cm.findWordAt(start); + if (cm.display.shift || doc.extend) + ourRange = extendRange(doc, ourRange, word.anchor, word.head); + else + ourRange = word; + } else if (type == "triple") { + var line = new Range(Pos(start.line, 0), clipPos(doc, Pos(start.line + 1, 0))); + if (cm.display.shift || doc.extend) + ourRange = extendRange(doc, ourRange, line.anchor, line.head); + else + ourRange = line; + } else { + ourRange = extendRange(doc, ourRange, start); + } + + if (!addNew) { + ourIndex = 0; + setSelection(doc, new Selection([ourRange], 0), sel_mouse); + startSel = doc.sel; + } else if (ourIndex == -1) { + ourIndex = ranges.length; + setSelection(doc, normalizeSelection(ranges.concat([ourRange]), ourIndex), + {scroll: false, origin: "*mouse"}); + } else if (ranges.length > 1 && ranges[ourIndex].empty() && type == "single" && !e.shiftKey) { + setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0)); + startSel = doc.sel; + } else { + replaceOneSelection(doc, ourIndex, ourRange, sel_mouse); + } + + var lastPos = start; + function extendTo(pos) { + if (cmp(lastPos, pos) == 0) return; + lastPos = pos; + + if (type == "rect") { + var ranges = [], tabSize = cm.options.tabSize; + var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize); + var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize); + var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol); + for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line)); + line <= end; line++) { + var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize); + if (left == right) + ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))); + else if (text.length > leftPos) + ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))); + } + if (!ranges.length) ranges.push(new Range(start, start)); + setSelection(doc, normalizeSelection(startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), + {origin: "*mouse", scroll: false}); + cm.scrollIntoView(pos); + } else { + var oldRange = ourRange; + var anchor = oldRange.anchor, head = pos; + if (type != "single") { + if (type == "double") + var range = cm.findWordAt(pos); + else + var range = new Range(Pos(pos.line, 0), clipPos(doc, Pos(pos.line + 1, 0))); + if (cmp(range.anchor, anchor) > 0) { + head = range.head; + anchor = minPos(oldRange.from(), range.anchor); + } else { + head = range.anchor; + anchor = maxPos(oldRange.to(), range.head); + } + } + var ranges = startSel.ranges.slice(0); + ranges[ourIndex] = new Range(clipPos(doc, anchor), head); + setSelection(doc, normalizeSelection(ranges, ourIndex), sel_mouse); + } + } + + var editorSize = display.wrapper.getBoundingClientRect(); + // Used to ensure timeout re-tries don't fire when another extend + // happened in the meantime (clearTimeout isn't reliable -- at + // least on Chrome, the timeouts still happen even when cleared, + // if the clear happens after their scheduled firing time). + var counter = 0; + + function extend(e) { + var curCount = ++counter; + var cur = posFromMouse(cm, e, true, type == "rect"); + if (!cur) return; + if (cmp(cur, lastPos) != 0) { + cm.curOp.focus = activeElt(); + extendTo(cur); + var visible = visibleLines(display, doc); + if (cur.line >= visible.to || cur.line < visible.from) + setTimeout(operation(cm, function(){if (counter == curCount) extend(e);}), 150); + } else { + var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0; + if (outside) setTimeout(operation(cm, function() { + if (counter != curCount) return; + display.scroller.scrollTop += outside; + extend(e); + }), 50); + } + } + + function done(e) { + counter = Infinity; + e_preventDefault(e); + display.input.focus(); + off(document, "mousemove", move); + off(document, "mouseup", up); + doc.history.lastSelOrigin = null; + } + + var move = operation(cm, function(e) { + if (!e_button(e)) done(e); + else extend(e); + }); + var up = operation(cm, done); + on(document, "mousemove", move); + on(document, "mouseup", up); + } + + // Determines whether an event happened in the gutter, and fires the + // handlers for the corresponding event. + function gutterEvent(cm, e, type, prevent, signalfn) { + try { var mX = e.clientX, mY = e.clientY; } + catch(e) { return false; } + if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false; + if (prevent) e_preventDefault(e); + + var display = cm.display; + var lineBox = display.lineDiv.getBoundingClientRect(); + + if (mY > lineBox.bottom || !hasHandler(cm, type)) return e_defaultPrevented(e); + mY -= lineBox.top - display.viewOffset; + + for (var i = 0; i < cm.options.gutters.length; ++i) { + var g = display.gutters.childNodes[i]; + if (g && g.getBoundingClientRect().right >= mX) { + var line = lineAtHeight(cm.doc, mY); + var gutter = cm.options.gutters[i]; + signalfn(cm, type, cm, line, gutter, e); + return e_defaultPrevented(e); + } + } + } + + function clickInGutter(cm, e) { + return gutterEvent(cm, e, "gutterClick", true, signalLater); + } + + // Kludge to work around strange IE behavior where it'll sometimes + // re-fire a series of drag-related events right after the drop (#1551) + var lastDrop = 0; + + function onDrop(e) { + var cm = this; + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) + return; + e_preventDefault(e); + if (ie) lastDrop = +new Date; + var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; + if (!pos || isReadOnly(cm)) return; + // Might be a file drop, in which case we simply extract the text + // and insert it. + if (files && files.length && window.FileReader && window.File) { + var n = files.length, text = Array(n), read = 0; + var loadFile = function(file, i) { + var reader = new FileReader; + reader.onload = operation(cm, function() { + text[i] = reader.result; + if (++read == n) { + pos = clipPos(cm.doc, pos); + var change = {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"}; + makeChange(cm.doc, change); + setSelectionReplaceHistory(cm.doc, simpleSelection(pos, changeEnd(change))); + } + }); + reader.readAsText(file); + }; + for (var i = 0; i < n; ++i) loadFile(files[i], i); + } else { // Normal drop + // Don't do a replace if the drop happened inside of the selected text. + if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) { + cm.state.draggingText(e); + // Ensure the editor is re-focused + setTimeout(function() {cm.display.input.focus();}, 20); + return; + } + try { + var text = e.dataTransfer.getData("Text"); + if (text) { + if (cm.state.draggingText && !(mac ? e.altKey : e.ctrlKey)) + var selected = cm.listSelections(); + setSelectionNoUndo(cm.doc, simpleSelection(pos, pos)); + if (selected) for (var i = 0; i < selected.length; ++i) + replaceRange(cm.doc, "", selected[i].anchor, selected[i].head, "drag"); + cm.replaceSelection(text, "around", "paste"); + cm.display.input.focus(); + } + } + catch(e){} + } + } + + function onDragStart(cm, e) { + if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; } + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return; + + e.dataTransfer.setData("Text", cm.getSelection()); + + // Use dummy image instead of default browsers image. + // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. + if (e.dataTransfer.setDragImage && !safari) { + var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); + img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + if (presto) { + img.width = img.height = 1; + cm.display.wrapper.appendChild(img); + // Force a relayout, or Opera won't use our image for some obscure reason + img._top = img.offsetTop; + } + e.dataTransfer.setDragImage(img, 0, 0); + if (presto) img.parentNode.removeChild(img); + } + } + + // SCROLL EVENTS + + // Sync the scrollable area and scrollbars, ensure the viewport + // covers the visible area. + function setScrollTop(cm, val) { + if (Math.abs(cm.doc.scrollTop - val) < 2) return; + cm.doc.scrollTop = val; + if (!gecko) updateDisplaySimple(cm, {top: val}); + if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val; + cm.display.scrollbars.setScrollTop(val); + if (gecko) updateDisplaySimple(cm); + startWorker(cm, 100); + } + // Sync scroller and scrollbar, ensure the gutter elements are + // aligned. + function setScrollLeft(cm, val, isScroller) { + if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return; + val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth); + cm.doc.scrollLeft = val; + alignHorizontally(cm); + if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val; + cm.display.scrollbars.setScrollLeft(val); + } + + // Since the delta values reported on mouse wheel events are + // unstandardized between browsers and even browser versions, and + // generally horribly unpredictable, this code starts by measuring + // the scroll effect that the first few mouse wheel events have, + // and, from that, detects the way it can convert deltas to pixel + // offsets afterwards. + // + // The reason we want to know the amount a wheel event will scroll + // is that it gives us a chance to update the display before the + // actual scrolling happens, reducing flickering. + + var wheelSamples = 0, wheelPixelsPerUnit = null; + // Fill in a browser-detected starting value on browsers where we + // know one. These don't have to be accurate -- the result of them + // being wrong would just be a slight flicker on the first wheel + // scroll (if it is large enough). + if (ie) wheelPixelsPerUnit = -.53; + else if (gecko) wheelPixelsPerUnit = 15; + else if (chrome) wheelPixelsPerUnit = -.7; + else if (safari) wheelPixelsPerUnit = -1/3; + + var wheelEventDelta = function(e) { + var dx = e.wheelDeltaX, dy = e.wheelDeltaY; + if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail; + if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail; + else if (dy == null) dy = e.wheelDelta; + return {x: dx, y: dy}; + }; + CodeMirror.wheelEventPixels = function(e) { + var delta = wheelEventDelta(e); + delta.x *= wheelPixelsPerUnit; + delta.y *= wheelPixelsPerUnit; + return delta; + }; + + function onScrollWheel(cm, e) { + var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y; + + var display = cm.display, scroll = display.scroller; + // Quit if there's nothing to scroll here + if (!(dx && scroll.scrollWidth > scroll.clientWidth || + dy && scroll.scrollHeight > scroll.clientHeight)) return; + + // Webkit browsers on OS X abort momentum scrolls when the target + // of the scroll event is removed from the scrollable element. + // This hack (see related code in patchDisplay) makes sure the + // element is kept around. + if (dy && mac && webkit) { + outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { + for (var i = 0; i < view.length; i++) { + if (view[i].node == cur) { + cm.display.currentWheelTarget = cur; + break outer; + } + } + } + } + + // On some browsers, horizontal scrolling will cause redraws to + // happen before the gutter has been realigned, causing it to + // wriggle around in a most unseemly way. When we have an + // estimated pixels/delta value, we just handle horizontal + // scrolling entirely here. It'll be slightly off from native, but + // better than glitching out. + if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { + if (dy) + setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight))); + setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth))); + e_preventDefault(e); + display.wheelStartX = null; // Abort measurement, if in progress + return; + } + + // 'Project' the visible viewport to cover the area that is being + // scrolled into view (if we know enough to estimate it). + if (dy && wheelPixelsPerUnit != null) { + var pixels = dy * wheelPixelsPerUnit; + var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; + if (pixels < 0) top = Math.max(0, top + pixels - 50); + else bot = Math.min(cm.doc.height, bot + pixels + 50); + updateDisplaySimple(cm, {top: top, bottom: bot}); + } + + if (wheelSamples < 20) { + if (display.wheelStartX == null) { + display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop; + display.wheelDX = dx; display.wheelDY = dy; + setTimeout(function() { + if (display.wheelStartX == null) return; + var movedX = scroll.scrollLeft - display.wheelStartX; + var movedY = scroll.scrollTop - display.wheelStartY; + var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || + (movedX && display.wheelDX && movedX / display.wheelDX); + display.wheelStartX = display.wheelStartY = null; + if (!sample) return; + wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1); + ++wheelSamples; + }, 200); + } else { + display.wheelDX += dx; display.wheelDY += dy; + } + } + } + + // KEY EVENTS + + // Run a handler that was bound to a key. + function doHandleBinding(cm, bound, dropShift) { + if (typeof bound == "string") { + bound = commands[bound]; + if (!bound) return false; + } + // Ensure previous input has been read, so that the handler sees a + // consistent view of the document + cm.display.input.ensurePolled(); + var prevShift = cm.display.shift, done = false; + try { + if (isReadOnly(cm)) cm.state.suppressEdits = true; + if (dropShift) cm.display.shift = false; + done = bound(cm) != Pass; + } finally { + cm.display.shift = prevShift; + cm.state.suppressEdits = false; + } + return done; + } + + function lookupKeyForEditor(cm, name, handle) { + for (var i = 0; i < cm.state.keyMaps.length; i++) { + var result = lookupKey(name, cm.state.keyMaps[i], handle, cm); + if (result) return result; + } + return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm)) + || lookupKey(name, cm.options.keyMap, handle, cm); + } + + var stopSeq = new Delayed; + function dispatchKey(cm, name, e, handle) { + var seq = cm.state.keySeq; + if (seq) { + if (isModifierKey(name)) return "handled"; + stopSeq.set(50, function() { + if (cm.state.keySeq == seq) { + cm.state.keySeq = null; + cm.display.input.reset(); + } + }); + name = seq + " " + name; + } + var result = lookupKeyForEditor(cm, name, handle); + + if (result == "multi") + cm.state.keySeq = name; + if (result == "handled") + signalLater(cm, "keyHandled", cm, name, e); + + if (result == "handled" || result == "multi") { + e_preventDefault(e); + restartBlink(cm); + } + + if (seq && !result && /\'$/.test(name)) { + e_preventDefault(e); + return true; + } + return !!result; + } + + // Handle a key from the keydown event. + function handleKeyBinding(cm, e) { + var name = keyName(e, true); + if (!name) return false; + + if (e.shiftKey && !cm.state.keySeq) { + // First try to resolve full name (including 'Shift-'). Failing + // that, see if there is a cursor-motion command (starting with + // 'go') bound to the keyname without 'Shift-'. + return dispatchKey(cm, "Shift-" + name, e, function(b) {return doHandleBinding(cm, b, true);}) + || dispatchKey(cm, name, e, function(b) { + if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion) + return doHandleBinding(cm, b); + }); + } else { + return dispatchKey(cm, name, e, function(b) { return doHandleBinding(cm, b); }); + } + } + + // Handle a key from the keypress event + function handleCharBinding(cm, e, ch) { + return dispatchKey(cm, "'" + ch + "'", e, + function(b) { return doHandleBinding(cm, b, true); }); + } + + var lastStoppedKey = null; + function onKeyDown(e) { + var cm = this; + cm.curOp.focus = activeElt(); + if (signalDOMEvent(cm, e)) return; + // IE does strange things with escape. + if (ie && ie_version < 11 && e.keyCode == 27) e.returnValue = false; + var code = e.keyCode; + cm.display.shift = code == 16 || e.shiftKey; + var handled = handleKeyBinding(cm, e); + if (presto) { + lastStoppedKey = handled ? code : null; + // Opera has no cut event... we try to at least catch the key combo + if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) + cm.replaceSelection("", null, "cut"); + } + + // Turn mouse into crosshair when Alt is held on Mac. + if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className)) + showCrossHair(cm); + } + + function showCrossHair(cm) { + var lineDiv = cm.display.lineDiv; + addClass(lineDiv, "CodeMirror-crosshair"); + + function up(e) { + if (e.keyCode == 18 || !e.altKey) { + rmClass(lineDiv, "CodeMirror-crosshair"); + off(document, "keyup", up); + off(document, "mouseover", up); + } + } + on(document, "keyup", up); + on(document, "mouseover", up); + } + + function onKeyUp(e) { + if (e.keyCode == 16) this.doc.sel.shift = false; + signalDOMEvent(this, e); + } + + function onKeyPress(e) { + var cm = this; + if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) return; + var keyCode = e.keyCode, charCode = e.charCode; + if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} + if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) return; + var ch = String.fromCharCode(charCode == null ? keyCode : charCode); + if (handleCharBinding(cm, e, ch)) return; + cm.display.input.onKeyPress(e); + } + + // FOCUS/BLUR EVENTS + + function delayBlurEvent(cm) { + cm.state.delayingBlurEvent = true; + setTimeout(function() { + if (cm.state.delayingBlurEvent) { + cm.state.delayingBlurEvent = false; + onBlur(cm); + } + }, 100); + } + + function onFocus(cm) { + if (cm.state.delayingBlurEvent) cm.state.delayingBlurEvent = false; + + if (cm.options.readOnly == "nocursor") return; + if (!cm.state.focused) { + signal(cm, "focus", cm); + cm.state.focused = true; + addClass(cm.display.wrapper, "CodeMirror-focused"); + // This test prevents this from firing when a context + // menu is closed (since the input reset would kill the + // select-all detection hack) + if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) { + cm.display.input.reset(); + if (webkit) setTimeout(function() { cm.display.input.reset(true); }, 20); // Issue #1730 + } + cm.display.input.receivedFocus(); + } + restartBlink(cm); + } + function onBlur(cm) { + if (cm.state.delayingBlurEvent) return; + + if (cm.state.focused) { + signal(cm, "blur", cm); + cm.state.focused = false; + rmClass(cm.display.wrapper, "CodeMirror-focused"); + } + clearInterval(cm.display.blinker); + setTimeout(function() {if (!cm.state.focused) cm.display.shift = false;}, 150); + } + + // CONTEXT MENU HANDLING + + // To make the context menu work, we need to briefly unhide the + // textarea (making it as unobtrusive as possible) to let the + // right-click take effect on it. + function onContextMenu(cm, e) { + if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) return; + cm.display.input.onContextMenu(e); + } + + function contextMenuInGutter(cm, e) { + if (!hasHandler(cm, "gutterContextMenu")) return false; + return gutterEvent(cm, e, "gutterContextMenu", false, signal); + } + + // UPDATING + + // Compute the position of the end of a change (its 'to' property + // refers to the pre-change end). + var changeEnd = CodeMirror.changeEnd = function(change) { + if (!change.text) return change.to; + return Pos(change.from.line + change.text.length - 1, + lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)); + }; + + // Adjust a position to refer to the post-change position of the + // same text, or the end of the change if the change covers it. + function adjustForChange(pos, change) { + if (cmp(pos, change.from) < 0) return pos; + if (cmp(pos, change.to) <= 0) return changeEnd(change); + + var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; + if (pos.line == change.to.line) ch += changeEnd(change).ch - change.to.ch; + return Pos(line, ch); + } + + function computeSelAfterChange(doc, change) { + var out = []; + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + out.push(new Range(adjustForChange(range.anchor, change), + adjustForChange(range.head, change))); + } + return normalizeSelection(out, doc.sel.primIndex); + } + + function offsetPos(pos, old, nw) { + if (pos.line == old.line) + return Pos(nw.line, pos.ch - old.ch + nw.ch); + else + return Pos(nw.line + (pos.line - old.line), pos.ch); + } + + // Used by replaceSelections to allow moving the selection to the + // start or around the replaced test. Hint may be "start" or "around". + function computeReplacedSel(doc, changes, hint) { + var out = []; + var oldPrev = Pos(doc.first, 0), newPrev = oldPrev; + for (var i = 0; i < changes.length; i++) { + var change = changes[i]; + var from = offsetPos(change.from, oldPrev, newPrev); + var to = offsetPos(changeEnd(change), oldPrev, newPrev); + oldPrev = change.to; + newPrev = to; + if (hint == "around") { + var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0; + out[i] = new Range(inv ? to : from, inv ? from : to); + } else { + out[i] = new Range(from, from); + } + } + return new Selection(out, doc.sel.primIndex); + } + + // Allow "beforeChange" event handlers to influence a change + function filterChange(doc, change, update) { + var obj = { + canceled: false, + from: change.from, + to: change.to, + text: change.text, + origin: change.origin, + cancel: function() { this.canceled = true; } + }; + if (update) obj.update = function(from, to, text, origin) { + if (from) this.from = clipPos(doc, from); + if (to) this.to = clipPos(doc, to); + if (text) this.text = text; + if (origin !== undefined) this.origin = origin; + }; + signal(doc, "beforeChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj); + + if (obj.canceled) return null; + return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin}; + } + + // Apply a change to a document, and add it to the document's + // history, and propagating it to all linked documents. + function makeChange(doc, change, ignoreReadOnly) { + if (doc.cm) { + if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly); + if (doc.cm.state.suppressEdits) return; + } + + if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { + change = filterChange(doc, change, true); + if (!change) return; + } + + // Possibly split or suppress the update based on the presence + // of read-only spans in its range. + var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to); + if (split) { + for (var i = split.length - 1; i >= 0; --i) + makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text}); + } else { + makeChangeInner(doc, change); + } + } + + function makeChangeInner(doc, change) { + if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) return; + var selAfter = computeSelAfterChange(doc, change); + addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); + + makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)); + var rebased = []; + + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)); + }); + } + + // Revert a change stored in a document's history. + function makeChangeFromHistory(doc, type, allowSelectionOnly) { + if (doc.cm && doc.cm.state.suppressEdits) return; + + var hist = doc.history, event, selAfter = doc.sel; + var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done; + + // Verify that there is a useable event (so that ctrl-z won't + // needlessly clear selection events) + for (var i = 0; i < source.length; i++) { + event = source[i]; + if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges) + break; + } + if (i == source.length) return; + hist.lastOrigin = hist.lastSelOrigin = null; + + for (;;) { + event = source.pop(); + if (event.ranges) { + pushSelectionToHistory(event, dest); + if (allowSelectionOnly && !event.equals(doc.sel)) { + setSelection(doc, event, {clearRedo: false}); + return; + } + selAfter = event; + } + else break; + } + + // Build up a reverse change object to add to the opposite history + // stack (redo when undoing, and vice versa). + var antiChanges = []; + pushSelectionToHistory(selAfter, dest); + dest.push({changes: antiChanges, generation: hist.generation}); + hist.generation = event.generation || ++hist.maxGeneration; + + var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange"); + + for (var i = event.changes.length - 1; i >= 0; --i) { + var change = event.changes[i]; + change.origin = type; + if (filter && !filterChange(doc, change, false)) { + source.length = 0; + return; + } + + antiChanges.push(historyChangeFromChange(doc, change)); + + var after = i ? computeSelAfterChange(doc, change) : lst(source); + makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); + if (!i && doc.cm) doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)}); + var rebased = []; + + // Propagate to the linked documents + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)); + }); + } + } + + // Sub-views need their line numbers shifted when text is added + // above or below them in the parent document. + function shiftDoc(doc, distance) { + if (distance == 0) return; + doc.first += distance; + doc.sel = new Selection(map(doc.sel.ranges, function(range) { + return new Range(Pos(range.anchor.line + distance, range.anchor.ch), + Pos(range.head.line + distance, range.head.ch)); + }), doc.sel.primIndex); + if (doc.cm) { + regChange(doc.cm, doc.first, doc.first - distance, distance); + for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++) + regLineChange(doc.cm, l, "gutter"); + } + } + + // More lower-level change function, handling only a single document + // (not linked ones). + function makeChangeSingleDoc(doc, change, selAfter, spans) { + if (doc.cm && !doc.cm.curOp) + return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans); + + if (change.to.line < doc.first) { + shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)); + return; + } + if (change.from.line > doc.lastLine()) return; + + // Clip the change to the size of this doc + if (change.from.line < doc.first) { + var shift = change.text.length - 1 - (doc.first - change.from.line); + shiftDoc(doc, shift); + change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), + text: [lst(change.text)], origin: change.origin}; + } + var last = doc.lastLine(); + if (change.to.line > last) { + change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), + text: [change.text[0]], origin: change.origin}; + } + + change.removed = getBetween(doc, change.from, change.to); + + if (!selAfter) selAfter = computeSelAfterChange(doc, change); + if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans); + else updateDoc(doc, change, spans); + setSelectionNoUndo(doc, selAfter, sel_dontScroll); + } + + // Handle the interaction of a change to a document with the editor + // that this document is part of. + function makeChangeSingleDocInEditor(cm, change, spans) { + var doc = cm.doc, display = cm.display, from = change.from, to = change.to; + + var recomputeMaxLength = false, checkWidthStart = from.line; + if (!cm.options.lineWrapping) { + checkWidthStart = lineNo(visualLine(getLine(doc, from.line))); + doc.iter(checkWidthStart, to.line + 1, function(line) { + if (line == display.maxLine) { + recomputeMaxLength = true; + return true; + } + }); + } + + if (doc.sel.contains(change.from, change.to) > -1) + signalCursorActivity(cm); + + updateDoc(doc, change, spans, estimateHeight(cm)); + + if (!cm.options.lineWrapping) { + doc.iter(checkWidthStart, from.line + change.text.length, function(line) { + var len = lineLength(line); + if (len > display.maxLineLength) { + display.maxLine = line; + display.maxLineLength = len; + display.maxLineChanged = true; + recomputeMaxLength = false; + } + }); + if (recomputeMaxLength) cm.curOp.updateMaxLine = true; + } + + // Adjust frontier, schedule worker + doc.frontier = Math.min(doc.frontier, from.line); + startWorker(cm, 400); + + var lendiff = change.text.length - (to.line - from.line) - 1; + // Remember that these lines changed, for updating the display + if (change.full) + regChange(cm); + else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change)) + regLineChange(cm, from.line, "text"); + else + regChange(cm, from.line, to.line + 1, lendiff); + + var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change"); + if (changeHandler || changesHandler) { + var obj = { + from: from, to: to, + text: change.text, + removed: change.removed, + origin: change.origin + }; + if (changeHandler) signalLater(cm, "change", cm, obj); + if (changesHandler) (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj); + } + cm.display.selForContextMenu = null; + } + + function replaceRange(doc, code, from, to, origin) { + if (!to) to = from; + if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp; } + if (typeof code == "string") code = splitLines(code); + makeChange(doc, {from: from, to: to, text: code, origin: origin}); + } + + // SCROLLING THINGS INTO VIEW + + // If an editor sits on the top or bottom of the window, partially + // scrolled out of view, this ensures that the cursor is visible. + function maybeScrollWindow(cm, coords) { + if (signalDOMEvent(cm, "scrollCursorIntoView")) return; + + var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null; + if (coords.top + box.top < 0) doScroll = true; + else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false; + if (doScroll != null && !phantom) { + var scrollNode = elt("div", "\u200b", null, "position: absolute; top: " + + (coords.top - display.viewOffset - paddingTop(cm.display)) + "px; height: " + + (coords.bottom - coords.top + scrollGap(cm) + display.barHeight) + "px; left: " + + coords.left + "px; width: 2px;"); + cm.display.lineSpace.appendChild(scrollNode); + scrollNode.scrollIntoView(doScroll); + cm.display.lineSpace.removeChild(scrollNode); + } + } + + // Scroll a given position into view (immediately), verifying that + // it actually became visible (as line heights are accurately + // measured, the position of something may 'drift' during drawing). + function scrollPosIntoView(cm, pos, end, margin) { + if (margin == null) margin = 0; + for (var limit = 0; limit < 5; limit++) { + var changed = false, coords = cursorCoords(cm, pos); + var endCoords = !end || end == pos ? coords : cursorCoords(cm, end); + var scrollPos = calculateScrollPos(cm, Math.min(coords.left, endCoords.left), + Math.min(coords.top, endCoords.top) - margin, + Math.max(coords.left, endCoords.left), + Math.max(coords.bottom, endCoords.bottom) + margin); + var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft; + if (scrollPos.scrollTop != null) { + setScrollTop(cm, scrollPos.scrollTop); + if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true; + } + if (scrollPos.scrollLeft != null) { + setScrollLeft(cm, scrollPos.scrollLeft); + if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true; + } + if (!changed) break; + } + return coords; + } + + // Scroll a given set of coordinates into view (immediately). + function scrollIntoView(cm, x1, y1, x2, y2) { + var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2); + if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop); + if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft); + } + + // Calculate a new scroll position needed to scroll the given + // rectangle into view. Returns an object with scrollTop and + // scrollLeft properties. When these are undefined, the + // vertical/horizontal position does not need to be adjusted. + function calculateScrollPos(cm, x1, y1, x2, y2) { + var display = cm.display, snapMargin = textHeight(cm.display); + if (y1 < 0) y1 = 0; + var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop; + var screen = displayHeight(cm), result = {}; + if (y2 - y1 > screen) y2 = y1 + screen; + var docBottom = cm.doc.height + paddingVert(display); + var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin; + if (y1 < screentop) { + result.scrollTop = atTop ? 0 : y1; + } else if (y2 > screentop + screen) { + var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen); + if (newTop != screentop) result.scrollTop = newTop; + } + + var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft; + var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0); + var tooWide = x2 - x1 > screenw; + if (tooWide) x2 = x1 + screenw; + if (x1 < 10) + result.scrollLeft = 0; + else if (x1 < screenleft) + result.scrollLeft = Math.max(0, x1 - (tooWide ? 0 : 10)); + else if (x2 > screenw + screenleft - 3) + result.scrollLeft = x2 + (tooWide ? 0 : 10) - screenw; + return result; + } + + // Store a relative adjustment to the scroll position in the current + // operation (to be applied when the operation finishes). + function addToScrollPos(cm, left, top) { + if (left != null || top != null) resolveScrollToPos(cm); + if (left != null) + cm.curOp.scrollLeft = (cm.curOp.scrollLeft == null ? cm.doc.scrollLeft : cm.curOp.scrollLeft) + left; + if (top != null) + cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top; + } + + // Make sure that at the end of the operation the current cursor is + // shown. + function ensureCursorVisible(cm) { + resolveScrollToPos(cm); + var cur = cm.getCursor(), from = cur, to = cur; + if (!cm.options.lineWrapping) { + from = cur.ch ? Pos(cur.line, cur.ch - 1) : cur; + to = Pos(cur.line, cur.ch + 1); + } + cm.curOp.scrollToPos = {from: from, to: to, margin: cm.options.cursorScrollMargin, isCursor: true}; + } + + // When an operation has its scrollToPos property set, and another + // scroll action is applied before the end of the operation, this + // 'simulates' scrolling that position into view in a cheap way, so + // that the effect of intermediate scroll commands is not ignored. + function resolveScrollToPos(cm) { + var range = cm.curOp.scrollToPos; + if (range) { + cm.curOp.scrollToPos = null; + var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to); + var sPos = calculateScrollPos(cm, Math.min(from.left, to.left), + Math.min(from.top, to.top) - range.margin, + Math.max(from.right, to.right), + Math.max(from.bottom, to.bottom) + range.margin); + cm.scrollTo(sPos.scrollLeft, sPos.scrollTop); + } + } + + // API UTILITIES + + // Indent the given line. The how parameter can be "smart", + // "add"/null, "subtract", or "prev". When aggressive is false + // (typically set to true for forced single-line indents), empty + // lines are not indented, and places where the mode returns Pass + // are left alone. + function indentLine(cm, n, how, aggressive) { + var doc = cm.doc, state; + if (how == null) how = "add"; + if (how == "smart") { + // Fall back to "prev" when the mode doesn't have an indentation + // method. + if (!doc.mode.indent) how = "prev"; + else state = getStateBefore(cm, n); + } + + var tabSize = cm.options.tabSize; + var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); + if (line.stateAfter) line.stateAfter = null; + var curSpaceString = line.text.match(/^\s*/)[0], indentation; + if (!aggressive && !/\S/.test(line.text)) { + indentation = 0; + how = "not"; + } else if (how == "smart") { + indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text); + if (indentation == Pass || indentation > 150) { + if (!aggressive) return; + how = "prev"; + } + } + if (how == "prev") { + if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize); + else indentation = 0; + } else if (how == "add") { + indentation = curSpace + cm.options.indentUnit; + } else if (how == "subtract") { + indentation = curSpace - cm.options.indentUnit; + } else if (typeof how == "number") { + indentation = curSpace + how; + } + indentation = Math.max(0, indentation); + + var indentString = "", pos = 0; + if (cm.options.indentWithTabs) + for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} + if (pos < indentation) indentString += spaceStr(indentation - pos); + + if (indentString != curSpaceString) { + replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); + } else { + // Ensure that, if the cursor was in the whitespace at the start + // of the line, it is moved to the end of that space. + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + if (range.head.line == n && range.head.ch < curSpaceString.length) { + var pos = Pos(n, curSpaceString.length); + replaceOneSelection(doc, i, new Range(pos, pos)); + break; + } + } + } + line.stateAfter = null; + } + + // Utility for applying a change to a line by handle or number, + // returning the number and optionally registering the line as + // changed. + function changeLine(doc, handle, changeType, op) { + var no = handle, line = handle; + if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle)); + else no = lineNo(handle); + if (no == null) return null; + if (op(line, no) && doc.cm) regLineChange(doc.cm, no, changeType); + return line; + } + + // Helper for deleting text near the selection(s), used to implement + // backspace, delete, and similar functionality. + function deleteNearSelection(cm, compute) { + var ranges = cm.doc.sel.ranges, kill = []; + // Build up a set of ranges to kill first, merging overlapping + // ranges. + for (var i = 0; i < ranges.length; i++) { + var toKill = compute(ranges[i]); + while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) { + var replaced = kill.pop(); + if (cmp(replaced.from, toKill.from) < 0) { + toKill.from = replaced.from; + break; + } + } + kill.push(toKill); + } + // Next, remove those actual ranges. + runInOp(cm, function() { + for (var i = kill.length - 1; i >= 0; i--) + replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete"); + ensureCursorVisible(cm); + }); + } + + // Used for horizontal relative motion. Dir is -1 or 1 (left or + // right), unit can be "char", "column" (like char, but doesn't + // cross line boundaries), "word" (across next word), or "group" (to + // the start of next group of word or non-word-non-whitespace + // chars). The visually param controls whether, in right-to-left + // text, direction 1 means to move towards the next index in the + // string, or towards the character to the right of the current + // position. The resulting position will have a hitSide=true + // property if it reached the end of the document. + function findPosH(doc, pos, dir, unit, visually) { + var line = pos.line, ch = pos.ch, origDir = dir; + var lineObj = getLine(doc, line); + var possible = true; + function findNextLine() { + var l = line + dir; + if (l < doc.first || l >= doc.first + doc.size) return (possible = false); + line = l; + return lineObj = getLine(doc, l); + } + function moveOnce(boundToLine) { + var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true); + if (next == null) { + if (!boundToLine && findNextLine()) { + if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj); + else ch = dir < 0 ? lineObj.text.length : 0; + } else return (possible = false); + } else ch = next; + return true; + } + + if (unit == "char") moveOnce(); + else if (unit == "column") moveOnce(true); + else if (unit == "word" || unit == "group") { + var sawType = null, group = unit == "group"; + var helper = doc.cm && doc.cm.getHelper(pos, "wordChars"); + for (var first = true;; first = false) { + if (dir < 0 && !moveOnce(!first)) break; + var cur = lineObj.text.charAt(ch) || "\n"; + var type = isWordChar(cur, helper) ? "w" + : group && cur == "\n" ? "n" + : !group || /\s/.test(cur) ? null + : "p"; + if (group && !first && !type) type = "s"; + if (sawType && sawType != type) { + if (dir < 0) {dir = 1; moveOnce();} + break; + } + + if (type) sawType = type; + if (dir > 0 && !moveOnce(!first)) break; + } + } + var result = skipAtomic(doc, Pos(line, ch), origDir, true); + if (!possible) result.hitSide = true; + return result; + } + + // For relative vertical movement. Dir may be -1 or 1. Unit can be + // "page" or "line". The resulting position will have a hitSide=true + // property if it reached the end of the document. + function findPosV(cm, pos, dir, unit) { + var doc = cm.doc, x = pos.left, y; + if (unit == "page") { + var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight); + y = pos.top + dir * (pageSize - (dir < 0 ? 1.5 : .5) * textHeight(cm.display)); + } else if (unit == "line") { + y = dir > 0 ? pos.bottom + 3 : pos.top - 3; + } + for (;;) { + var target = coordsChar(cm, x, y); + if (!target.outside) break; + if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; } + y += dir * 5; + } + return target; + } + + // EDITOR METHODS + + // The publicly visible API. Note that methodOp(f) means + // 'wrap f in an operation, performed on its `this` parameter'. + + // This is not the complete set of editor methods. Most of the + // methods defined on the Doc type are also injected into + // CodeMirror.prototype, for backwards compatibility and + // convenience. + + CodeMirror.prototype = { + constructor: CodeMirror, + focus: function(){window.focus(); this.display.input.focus();}, + + setOption: function(option, value) { + var options = this.options, old = options[option]; + if (options[option] == value && option != "mode") return; + options[option] = value; + if (optionHandlers.hasOwnProperty(option)) + operation(this, optionHandlers[option])(this, value, old); + }, + + getOption: function(option) {return this.options[option];}, + getDoc: function() {return this.doc;}, + + addKeyMap: function(map, bottom) { + this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map)); + }, + removeKeyMap: function(map) { + var maps = this.state.keyMaps; + for (var i = 0; i < maps.length; ++i) + if (maps[i] == map || maps[i].name == map) { + maps.splice(i, 1); + return true; + } + }, + + addOverlay: methodOp(function(spec, options) { + var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec); + if (mode.startState) throw new Error("Overlays may not be stateful."); + this.state.overlays.push({mode: mode, modeSpec: spec, opaque: options && options.opaque}); + this.state.modeGen++; + regChange(this); + }), + removeOverlay: methodOp(function(spec) { + var overlays = this.state.overlays; + for (var i = 0; i < overlays.length; ++i) { + var cur = overlays[i].modeSpec; + if (cur == spec || typeof spec == "string" && cur.name == spec) { + overlays.splice(i, 1); + this.state.modeGen++; + regChange(this); + return; + } + } + }), + + indentLine: methodOp(function(n, dir, aggressive) { + if (typeof dir != "string" && typeof dir != "number") { + if (dir == null) dir = this.options.smartIndent ? "smart" : "prev"; + else dir = dir ? "add" : "subtract"; + } + if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive); + }), + indentSelection: methodOp(function(how) { + var ranges = this.doc.sel.ranges, end = -1; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + if (!range.empty()) { + var from = range.from(), to = range.to(); + var start = Math.max(end, from.line); + end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1; + for (var j = start; j < end; ++j) + indentLine(this, j, how); + var newRanges = this.doc.sel.ranges; + if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0) + replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll); + } else if (range.head.line > end) { + indentLine(this, range.head.line, how, true); + end = range.head.line; + if (i == this.doc.sel.primIndex) ensureCursorVisible(this); + } + } + }), + + // Fetch the parser token for a given character. Useful for hacks + // that want to inspect the mode state (say, for completion). + getTokenAt: function(pos, precise) { + return takeToken(this, pos, precise); + }, + + getLineTokens: function(line, precise) { + return takeToken(this, Pos(line), precise, true); + }, + + getTokenTypeAt: function(pos) { + pos = clipPos(this.doc, pos); + var styles = getLineStyles(this, getLine(this.doc, pos.line)); + var before = 0, after = (styles.length - 1) / 2, ch = pos.ch; + var type; + if (ch == 0) type = styles[2]; + else for (;;) { + var mid = (before + after) >> 1; + if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid; + else if (styles[mid * 2 + 1] < ch) before = mid + 1; + else { type = styles[mid * 2 + 2]; break; } + } + var cut = type ? type.indexOf("cm-overlay ") : -1; + return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1); + }, + + getModeAt: function(pos) { + var mode = this.doc.mode; + if (!mode.innerMode) return mode; + return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode; + }, + + getHelper: function(pos, type) { + return this.getHelpers(pos, type)[0]; + }, + + getHelpers: function(pos, type) { + var found = []; + if (!helpers.hasOwnProperty(type)) return found; + var help = helpers[type], mode = this.getModeAt(pos); + if (typeof mode[type] == "string") { + if (help[mode[type]]) found.push(help[mode[type]]); + } else if (mode[type]) { + for (var i = 0; i < mode[type].length; i++) { + var val = help[mode[type][i]]; + if (val) found.push(val); + } + } else if (mode.helperType && help[mode.helperType]) { + found.push(help[mode.helperType]); + } else if (help[mode.name]) { + found.push(help[mode.name]); + } + for (var i = 0; i < help._global.length; i++) { + var cur = help._global[i]; + if (cur.pred(mode, this) && indexOf(found, cur.val) == -1) + found.push(cur.val); + } + return found; + }, + + getStateAfter: function(line, precise) { + var doc = this.doc; + line = clipLine(doc, line == null ? doc.first + doc.size - 1: line); + return getStateBefore(this, line + 1, precise); + }, + + cursorCoords: function(start, mode) { + var pos, range = this.doc.sel.primary(); + if (start == null) pos = range.head; + else if (typeof start == "object") pos = clipPos(this.doc, start); + else pos = start ? range.from() : range.to(); + return cursorCoords(this, pos, mode || "page"); + }, + + charCoords: function(pos, mode) { + return charCoords(this, clipPos(this.doc, pos), mode || "page"); + }, + + coordsChar: function(coords, mode) { + coords = fromCoordSystem(this, coords, mode || "page"); + return coordsChar(this, coords.left, coords.top); + }, + + lineAtHeight: function(height, mode) { + height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top; + return lineAtHeight(this.doc, height + this.display.viewOffset); + }, + heightAtLine: function(line, mode) { + var end = false, lineObj; + if (typeof line == "number") { + var last = this.doc.first + this.doc.size - 1; + if (line < this.doc.first) line = this.doc.first; + else if (line > last) { line = last; end = true; } + lineObj = getLine(this.doc, line); + } else { + lineObj = line; + } + return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page").top + + (end ? this.doc.height - heightAtLine(lineObj) : 0); + }, + + defaultTextHeight: function() { return textHeight(this.display); }, + defaultCharWidth: function() { return charWidth(this.display); }, + + setGutterMarker: methodOp(function(line, gutterID, value) { + return changeLine(this.doc, line, "gutter", function(line) { + var markers = line.gutterMarkers || (line.gutterMarkers = {}); + markers[gutterID] = value; + if (!value && isEmpty(markers)) line.gutterMarkers = null; + return true; + }); + }), + + clearGutter: methodOp(function(gutterID) { + var cm = this, doc = cm.doc, i = doc.first; + doc.iter(function(line) { + if (line.gutterMarkers && line.gutterMarkers[gutterID]) { + line.gutterMarkers[gutterID] = null; + regLineChange(cm, i, "gutter"); + if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null; + } + ++i; + }); + }), + + lineInfo: function(line) { + if (typeof line == "number") { + if (!isLine(this.doc, line)) return null; + var n = line; + line = getLine(this.doc, line); + if (!line) return null; + } else { + var n = lineNo(line); + if (n == null) return null; + } + return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, + textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, + widgets: line.widgets}; + }, + + getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo};}, + + addWidget: function(pos, node, scroll, vert, horiz) { + var display = this.display; + pos = cursorCoords(this, clipPos(this.doc, pos)); + var top = pos.bottom, left = pos.left; + node.style.position = "absolute"; + node.setAttribute("cm-ignore-events", "true"); + this.display.input.setUneditable(node); + display.sizer.appendChild(node); + if (vert == "over") { + top = pos.top; + } else if (vert == "above" || vert == "near") { + var vspace = Math.max(display.wrapper.clientHeight, this.doc.height), + hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth); + // Default to positioning above (if specified and possible); otherwise default to positioning below + if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) + top = pos.top - node.offsetHeight; + else if (pos.bottom + node.offsetHeight <= vspace) + top = pos.bottom; + if (left + node.offsetWidth > hspace) + left = hspace - node.offsetWidth; + } + node.style.top = top + "px"; + node.style.left = node.style.right = ""; + if (horiz == "right") { + left = display.sizer.clientWidth - node.offsetWidth; + node.style.right = "0px"; + } else { + if (horiz == "left") left = 0; + else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2; + node.style.left = left + "px"; + } + if (scroll) + scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight); + }, + + triggerOnKeyDown: methodOp(onKeyDown), + triggerOnKeyPress: methodOp(onKeyPress), + triggerOnKeyUp: onKeyUp, + + execCommand: function(cmd) { + if (commands.hasOwnProperty(cmd)) + return commands[cmd](this); + }, + + findPosH: function(from, amount, unit, visually) { + var dir = 1; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + cur = findPosH(this.doc, cur, dir, unit, visually); + if (cur.hitSide) break; + } + return cur; + }, + + moveH: methodOp(function(dir, unit) { + var cm = this; + cm.extendSelectionsBy(function(range) { + if (cm.display.shift || cm.doc.extend || range.empty()) + return findPosH(cm.doc, range.head, dir, unit, cm.options.rtlMoveVisually); + else + return dir < 0 ? range.from() : range.to(); + }, sel_move); + }), + + deleteH: methodOp(function(dir, unit) { + var sel = this.doc.sel, doc = this.doc; + if (sel.somethingSelected()) + doc.replaceSelection("", null, "+delete"); + else + deleteNearSelection(this, function(range) { + var other = findPosH(doc, range.head, dir, unit, false); + return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other}; + }); + }), + + findPosV: function(from, amount, unit, goalColumn) { + var dir = 1, x = goalColumn; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + var coords = cursorCoords(this, cur, "div"); + if (x == null) x = coords.left; + else coords.left = x; + cur = findPosV(this, coords, dir, unit); + if (cur.hitSide) break; + } + return cur; + }, + + moveV: methodOp(function(dir, unit) { + var cm = this, doc = this.doc, goals = []; + var collapse = !cm.display.shift && !doc.extend && doc.sel.somethingSelected(); + doc.extendSelectionsBy(function(range) { + if (collapse) + return dir < 0 ? range.from() : range.to(); + var headPos = cursorCoords(cm, range.head, "div"); + if (range.goalColumn != null) headPos.left = range.goalColumn; + goals.push(headPos.left); + var pos = findPosV(cm, headPos, dir, unit); + if (unit == "page" && range == doc.sel.primary()) + addToScrollPos(cm, null, charCoords(cm, pos, "div").top - headPos.top); + return pos; + }, sel_move); + if (goals.length) for (var i = 0; i < doc.sel.ranges.length; i++) + doc.sel.ranges[i].goalColumn = goals[i]; + }), + + // Find the word at the given position (as returned by coordsChar). + findWordAt: function(pos) { + var doc = this.doc, line = getLine(doc, pos.line).text; + var start = pos.ch, end = pos.ch; + if (line) { + var helper = this.getHelper(pos, "wordChars"); + if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end; + var startChar = line.charAt(start); + var check = isWordChar(startChar, helper) + ? function(ch) { return isWordChar(ch, helper); } + : /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);} + : function(ch) {return !/\s/.test(ch) && !isWordChar(ch);}; + while (start > 0 && check(line.charAt(start - 1))) --start; + while (end < line.length && check(line.charAt(end))) ++end; + } + return new Range(Pos(pos.line, start), Pos(pos.line, end)); + }, + + toggleOverwrite: function(value) { + if (value != null && value == this.state.overwrite) return; + if (this.state.overwrite = !this.state.overwrite) + addClass(this.display.cursorDiv, "CodeMirror-overwrite"); + else + rmClass(this.display.cursorDiv, "CodeMirror-overwrite"); + + signal(this, "overwriteToggle", this, this.state.overwrite); + }, + hasFocus: function() { return this.display.input.getField() == activeElt(); }, + + scrollTo: methodOp(function(x, y) { + if (x != null || y != null) resolveScrollToPos(this); + if (x != null) this.curOp.scrollLeft = x; + if (y != null) this.curOp.scrollTop = y; + }), + getScrollInfo: function() { + var scroller = this.display.scroller; + return {left: scroller.scrollLeft, top: scroller.scrollTop, + height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight, + width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth, + clientHeight: displayHeight(this), clientWidth: displayWidth(this)}; + }, + + scrollIntoView: methodOp(function(range, margin) { + if (range == null) { + range = {from: this.doc.sel.primary().head, to: null}; + if (margin == null) margin = this.options.cursorScrollMargin; + } else if (typeof range == "number") { + range = {from: Pos(range, 0), to: null}; + } else if (range.from == null) { + range = {from: range, to: null}; + } + if (!range.to) range.to = range.from; + range.margin = margin || 0; + + if (range.from.line != null) { + resolveScrollToPos(this); + this.curOp.scrollToPos = range; + } else { + var sPos = calculateScrollPos(this, Math.min(range.from.left, range.to.left), + Math.min(range.from.top, range.to.top) - range.margin, + Math.max(range.from.right, range.to.right), + Math.max(range.from.bottom, range.to.bottom) + range.margin); + this.scrollTo(sPos.scrollLeft, sPos.scrollTop); + } + }), + + setSize: methodOp(function(width, height) { + var cm = this; + function interpret(val) { + return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; + } + if (width != null) cm.display.wrapper.style.width = interpret(width); + if (height != null) cm.display.wrapper.style.height = interpret(height); + if (cm.options.lineWrapping) clearLineMeasurementCache(this); + var lineNo = cm.display.viewFrom; + cm.doc.iter(lineNo, cm.display.viewTo, function(line) { + if (line.widgets) for (var i = 0; i < line.widgets.length; i++) + if (line.widgets[i].noHScroll) { regLineChange(cm, lineNo, "widget"); break; } + ++lineNo; + }); + cm.curOp.forceUpdate = true; + signal(cm, "refresh", this); + }), + + operation: function(f){return runInOp(this, f);}, + + refresh: methodOp(function() { + var oldHeight = this.display.cachedTextHeight; + regChange(this); + this.curOp.forceUpdate = true; + clearCaches(this); + this.scrollTo(this.doc.scrollLeft, this.doc.scrollTop); + updateGutterSpace(this); + if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5) + estimateLineHeights(this); + signal(this, "refresh", this); + }), + + swapDoc: methodOp(function(doc) { + var old = this.doc; + old.cm = null; + attachDoc(this, doc); + clearCaches(this); + this.display.input.reset(); + this.scrollTo(doc.scrollLeft, doc.scrollTop); + this.curOp.forceScroll = true; + signalLater(this, "swapDoc", this, old); + return old; + }), + + getInputField: function(){return this.display.input.getField();}, + getWrapperElement: function(){return this.display.wrapper;}, + getScrollerElement: function(){return this.display.scroller;}, + getGutterElement: function(){return this.display.gutters;} + }; + eventMixin(CodeMirror); + + // OPTION DEFAULTS + + // The default configuration options. + var defaults = CodeMirror.defaults = {}; + // Functions to run when options are changed. + var optionHandlers = CodeMirror.optionHandlers = {}; + + function option(name, deflt, handle, notOnInit) { + CodeMirror.defaults[name] = deflt; + if (handle) optionHandlers[name] = + notOnInit ? function(cm, val, old) {if (old != Init) handle(cm, val, old);} : handle; + } + + // Passed to option handlers when there is no old value. + var Init = CodeMirror.Init = {toString: function(){return "CodeMirror.Init";}}; + + // These two are, on init, called from the constructor because they + // have to be initialized before the editor can start at all. + option("value", "", function(cm, val) { + cm.setValue(val); + }, true); + option("mode", null, function(cm, val) { + cm.doc.modeOption = val; + loadMode(cm); + }, true); + + option("indentUnit", 2, loadMode, true); + option("indentWithTabs", false); + option("smartIndent", true); + option("tabSize", 4, function(cm) { + resetModeState(cm); + clearCaches(cm); + regChange(cm); + }, true); + option("specialChars", /[\t\u0000-\u0019\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g, function(cm, val, old) { + cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); + if (old != CodeMirror.Init) cm.refresh(); + }); + option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function(cm) {cm.refresh();}, true); + option("electricChars", true); + option("inputStyle", mobile ? "contenteditable" : "textarea", function() { + throw new Error("inputStyle can not (yet) be changed in a running editor"); // FIXME + }, true); + option("rtlMoveVisually", !windows); + option("wholeLineUpdateBefore", true); + + option("theme", "default", function(cm) { + themeChanged(cm); + guttersChanged(cm); + }, true); + option("keyMap", "default", function(cm, val, old) { + var next = getKeyMap(val); + var prev = old != CodeMirror.Init && getKeyMap(old); + if (prev && prev.detach) prev.detach(cm, next); + if (next.attach) next.attach(cm, prev || null); + }); + option("extraKeys", null); + + option("lineWrapping", false, wrappingChanged, true); + option("gutters", [], function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("fixedGutter", true, function(cm, val) { + cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; + cm.refresh(); + }, true); + option("coverGutterNextToScrollbar", false, function(cm) {updateScrollbars(cm);}, true); + option("scrollbarStyle", "native", function(cm) { + initScrollbars(cm); + updateScrollbars(cm); + cm.display.scrollbars.setScrollTop(cm.doc.scrollTop); + cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft); + }, true); + option("lineNumbers", false, function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("firstLineNumber", 1, guttersChanged, true); + option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true); + option("showCursorWhenSelecting", false, updateSelection, true); + + option("resetSelectionOnContextMenu", true); + option("lineWiseCopyCut", true); + + option("readOnly", false, function(cm, val) { + if (val == "nocursor") { + onBlur(cm); + cm.display.input.blur(); + cm.display.disabled = true; + } else { + cm.display.disabled = false; + if (!val) cm.display.input.reset(); + } + }); + option("disableInput", false, function(cm, val) {if (!val) cm.display.input.reset();}, true); + option("dragDrop", true, dragDropChanged); + + option("cursorBlinkRate", 530); + option("cursorScrollMargin", 0); + option("cursorHeight", 1, updateSelection, true); + option("singleCursorHeightPerLine", true, updateSelection, true); + option("workTime", 100); + option("workDelay", 100); + option("flattenSpans", true, resetModeState, true); + option("addModeClass", false, resetModeState, true); + option("pollInterval", 100); + option("undoDepth", 200, function(cm, val){cm.doc.history.undoDepth = val;}); + option("historyEventDelay", 1250); + option("viewportMargin", 10, function(cm){cm.refresh();}, true); + option("maxHighlightLength", 10000, resetModeState, true); + option("moveInputWithCursor", true, function(cm, val) { + if (!val) cm.display.input.resetPosition(); + }); + + option("tabindex", null, function(cm, val) { + cm.display.input.getField().tabIndex = val || ""; + }); + option("autofocus", null); + + // MODE DEFINITION AND QUERYING + + // Known modes, by name and by MIME + var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {}; + + // Extra arguments are stored as the mode's dependencies, which is + // used by (legacy) mechanisms like loadmode.js to automatically + // load a mode. (Preferred mechanism is the require/define calls.) + CodeMirror.defineMode = function(name, mode) { + if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name; + if (arguments.length > 2) + mode.dependencies = Array.prototype.slice.call(arguments, 2); + modes[name] = mode; + }; + + CodeMirror.defineMIME = function(mime, spec) { + mimeModes[mime] = spec; + }; + + // Given a MIME type, a {name, ...options} config object, or a name + // string, return a mode config object. + CodeMirror.resolveMode = function(spec) { + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { + spec = mimeModes[spec]; + } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { + var found = mimeModes[spec.name]; + if (typeof found == "string") found = {name: found}; + spec = createObj(found, spec); + spec.name = found.name; + } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { + return CodeMirror.resolveMode("application/xml"); + } + if (typeof spec == "string") return {name: spec}; + else return spec || {name: "null"}; + }; + + // Given a mode spec (anything that resolveMode accepts), find and + // initialize an actual mode object. + CodeMirror.getMode = function(options, spec) { + var spec = CodeMirror.resolveMode(spec); + var mfactory = modes[spec.name]; + if (!mfactory) return CodeMirror.getMode(options, "text/plain"); + var modeObj = mfactory(options, spec); + if (modeExtensions.hasOwnProperty(spec.name)) { + var exts = modeExtensions[spec.name]; + for (var prop in exts) { + if (!exts.hasOwnProperty(prop)) continue; + if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop]; + modeObj[prop] = exts[prop]; + } + } + modeObj.name = spec.name; + if (spec.helperType) modeObj.helperType = spec.helperType; + if (spec.modeProps) for (var prop in spec.modeProps) + modeObj[prop] = spec.modeProps[prop]; + + return modeObj; + }; + + // Minimal default mode. + CodeMirror.defineMode("null", function() { + return {token: function(stream) {stream.skipToEnd();}}; + }); + CodeMirror.defineMIME("text/plain", "null"); + + // This can be used to attach properties to mode objects from + // outside the actual mode definition. + var modeExtensions = CodeMirror.modeExtensions = {}; + CodeMirror.extendMode = function(mode, properties) { + var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); + copyObj(properties, exts); + }; + + // EXTENSIONS + + CodeMirror.defineExtension = function(name, func) { + CodeMirror.prototype[name] = func; + }; + CodeMirror.defineDocExtension = function(name, func) { + Doc.prototype[name] = func; + }; + CodeMirror.defineOption = option; + + var initHooks = []; + CodeMirror.defineInitHook = function(f) {initHooks.push(f);}; + + var helpers = CodeMirror.helpers = {}; + CodeMirror.registerHelper = function(type, name, value) { + if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []}; + helpers[type][name] = value; + }; + CodeMirror.registerGlobalHelper = function(type, name, predicate, value) { + CodeMirror.registerHelper(type, name, value); + helpers[type]._global.push({pred: predicate, val: value}); + }; + + // MODE STATE HANDLING + + // Utility functions for working with state. Exported because nested + // modes need to do this for their inner modes. + + var copyState = CodeMirror.copyState = function(mode, state) { + if (state === true) return state; + if (mode.copyState) return mode.copyState(state); + var nstate = {}; + for (var n in state) { + var val = state[n]; + if (val instanceof Array) val = val.concat([]); + nstate[n] = val; + } + return nstate; + }; + + var startState = CodeMirror.startState = function(mode, a1, a2) { + return mode.startState ? mode.startState(a1, a2) : true; + }; + + // Given a mode and a state (for that mode), find the inner mode and + // state at the position that the state refers to. + CodeMirror.innerMode = function(mode, state) { + while (mode.innerMode) { + var info = mode.innerMode(state); + if (!info || info.mode == mode) break; + state = info.state; + mode = info.mode; + } + return info || {mode: mode, state: state}; + }; + + // STANDARD COMMANDS + + // Commands are parameter-less actions that can be performed on an + // editor, mostly used for keybindings. + var commands = CodeMirror.commands = { + selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll);}, + singleSelection: function(cm) { + cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll); + }, + killLine: function(cm) { + deleteNearSelection(cm, function(range) { + if (range.empty()) { + var len = getLine(cm.doc, range.head.line).text.length; + if (range.head.ch == len && range.head.line < cm.lastLine()) + return {from: range.head, to: Pos(range.head.line + 1, 0)}; + else + return {from: range.head, to: Pos(range.head.line, len)}; + } else { + return {from: range.from(), to: range.to()}; + } + }); + }, + deleteLine: function(cm) { + deleteNearSelection(cm, function(range) { + return {from: Pos(range.from().line, 0), + to: clipPos(cm.doc, Pos(range.to().line + 1, 0))}; + }); + }, + delLineLeft: function(cm) { + deleteNearSelection(cm, function(range) { + return {from: Pos(range.from().line, 0), to: range.from()}; + }); + }, + delWrappedLineLeft: function(cm) { + deleteNearSelection(cm, function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var leftPos = cm.coordsChar({left: 0, top: top}, "div"); + return {from: leftPos, to: range.from()}; + }); + }, + delWrappedLineRight: function(cm) { + deleteNearSelection(cm, function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + return {from: range.from(), to: rightPos }; + }); + }, + undo: function(cm) {cm.undo();}, + redo: function(cm) {cm.redo();}, + undoSelection: function(cm) {cm.undoSelection();}, + redoSelection: function(cm) {cm.redoSelection();}, + goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));}, + goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));}, + goLineStart: function(cm) { + cm.extendSelectionsBy(function(range) { return lineStart(cm, range.head.line); }, + {origin: "+move", bias: 1}); + }, + goLineStartSmart: function(cm) { + cm.extendSelectionsBy(function(range) { + return lineStartSmart(cm, range.head); + }, {origin: "+move", bias: 1}); + }, + goLineEnd: function(cm) { + cm.extendSelectionsBy(function(range) { return lineEnd(cm, range.head.line); }, + {origin: "+move", bias: -1}); + }, + goLineRight: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + }, sel_move); + }, + goLineLeft: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + return cm.coordsChar({left: 0, top: top}, "div"); + }, sel_move); + }, + goLineLeftSmart: function(cm) { + cm.extendSelectionsBy(function(range) { + var top = cm.charCoords(range.head, "div").top + 5; + var pos = cm.coordsChar({left: 0, top: top}, "div"); + if (pos.ch < cm.getLine(pos.line).search(/\S/)) return lineStartSmart(cm, range.head); + return pos; + }, sel_move); + }, + goLineUp: function(cm) {cm.moveV(-1, "line");}, + goLineDown: function(cm) {cm.moveV(1, "line");}, + goPageUp: function(cm) {cm.moveV(-1, "page");}, + goPageDown: function(cm) {cm.moveV(1, "page");}, + goCharLeft: function(cm) {cm.moveH(-1, "char");}, + goCharRight: function(cm) {cm.moveH(1, "char");}, + goColumnLeft: function(cm) {cm.moveH(-1, "column");}, + goColumnRight: function(cm) {cm.moveH(1, "column");}, + goWordLeft: function(cm) {cm.moveH(-1, "word");}, + goGroupRight: function(cm) {cm.moveH(1, "group");}, + goGroupLeft: function(cm) {cm.moveH(-1, "group");}, + goWordRight: function(cm) {cm.moveH(1, "word");}, + delCharBefore: function(cm) {cm.deleteH(-1, "char");}, + delCharAfter: function(cm) {cm.deleteH(1, "char");}, + delWordBefore: function(cm) {cm.deleteH(-1, "word");}, + delWordAfter: function(cm) {cm.deleteH(1, "word");}, + delGroupBefore: function(cm) {cm.deleteH(-1, "group");}, + delGroupAfter: function(cm) {cm.deleteH(1, "group");}, + indentAuto: function(cm) {cm.indentSelection("smart");}, + indentMore: function(cm) {cm.indentSelection("add");}, + indentLess: function(cm) {cm.indentSelection("subtract");}, + insertTab: function(cm) {cm.replaceSelection("\t");}, + insertSoftTab: function(cm) { + var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize; + for (var i = 0; i < ranges.length; i++) { + var pos = ranges[i].from(); + var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize); + spaces.push(new Array(tabSize - col % tabSize + 1).join(" ")); + } + cm.replaceSelections(spaces); + }, + defaultTab: function(cm) { + if (cm.somethingSelected()) cm.indentSelection("add"); + else cm.execCommand("insertTab"); + }, + transposeChars: function(cm) { + runInOp(cm, function() { + var ranges = cm.listSelections(), newSel = []; + for (var i = 0; i < ranges.length; i++) { + var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text; + if (line) { + if (cur.ch == line.length) cur = new Pos(cur.line, cur.ch - 1); + if (cur.ch > 0) { + cur = new Pos(cur.line, cur.ch + 1); + cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2), + Pos(cur.line, cur.ch - 2), cur, "+transpose"); + } else if (cur.line > cm.doc.first) { + var prev = getLine(cm.doc, cur.line - 1).text; + if (prev) + cm.replaceRange(line.charAt(0) + "\n" + prev.charAt(prev.length - 1), + Pos(cur.line - 1, prev.length - 1), Pos(cur.line, 1), "+transpose"); + } + } + newSel.push(new Range(cur, cur)); + } + cm.setSelections(newSel); + }); + }, + newlineAndIndent: function(cm) { + runInOp(cm, function() { + var len = cm.listSelections().length; + for (var i = 0; i < len; i++) { + var range = cm.listSelections()[i]; + cm.replaceRange("\n", range.anchor, range.head, "+input"); + cm.indentLine(range.from().line + 1, null, true); + ensureCursorVisible(cm); + } + }); + }, + toggleOverwrite: function(cm) {cm.toggleOverwrite();} + }; + + + // STANDARD KEYMAPS + + var keyMap = CodeMirror.keyMap = {}; + + keyMap.basic = { + "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", + "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", + "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", + "Tab": "defaultTab", "Shift-Tab": "indentAuto", + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite", + "Esc": "singleSelection" + }; + // Note that the save and find-related commands aren't defined by + // default. User code or addons can define them. Unknown commands + // are simply ignored. + keyMap.pcDefault = { + "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", + "Ctrl-Home": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown", + "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", + "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", + "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", + "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", + "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection", + fallthrough: "basic" + }; + // Very basic readline/emacs-style bindings, which are standard on Mac. + keyMap.emacsy = { + "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", + "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", + "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars" + }; + keyMap.macDefault = { + "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", + "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft", + "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore", + "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", + "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", + "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight", + "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd", + fallthrough: ["basic", "emacsy"] + }; + keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + + // KEYMAP DISPATCH + + function normalizeKeyName(name) { + var parts = name.split(/-(?!$)/), name = parts[parts.length - 1]; + var alt, ctrl, shift, cmd; + for (var i = 0; i < parts.length - 1; i++) { + var mod = parts[i]; + if (/^(cmd|meta|m)$/i.test(mod)) cmd = true; + else if (/^a(lt)?$/i.test(mod)) alt = true; + else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true; + else if (/^s(hift)$/i.test(mod)) shift = true; + else throw new Error("Unrecognized modifier name: " + mod); + } + if (alt) name = "Alt-" + name; + if (ctrl) name = "Ctrl-" + name; + if (cmd) name = "Cmd-" + name; + if (shift) name = "Shift-" + name; + return name; + } + + // This is a kludge to keep keymaps mostly working as raw objects + // (backwards compatibility) while at the same time support features + // like normalization and multi-stroke key bindings. It compiles a + // new normalized keymap, and then updates the old object to reflect + // this. + CodeMirror.normalizeKeyMap = function(keymap) { + var copy = {}; + for (var keyname in keymap) if (keymap.hasOwnProperty(keyname)) { + var value = keymap[keyname]; + if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) continue; + if (value == "...") { delete keymap[keyname]; continue; } + + var keys = map(keyname.split(" "), normalizeKeyName); + for (var i = 0; i < keys.length; i++) { + var val, name; + if (i == keys.length - 1) { + name = keyname; + val = value; + } else { + name = keys.slice(0, i + 1).join(" "); + val = "..."; + } + var prev = copy[name]; + if (!prev) copy[name] = val; + else if (prev != val) throw new Error("Inconsistent bindings for " + name); + } + delete keymap[keyname]; + } + for (var prop in copy) keymap[prop] = copy[prop]; + return keymap; + }; + + var lookupKey = CodeMirror.lookupKey = function(key, map, handle, context) { + map = getKeyMap(map); + var found = map.call ? map.call(key, context) : map[key]; + if (found === false) return "nothing"; + if (found === "...") return "multi"; + if (found != null && handle(found)) return "handled"; + + if (map.fallthrough) { + if (Object.prototype.toString.call(map.fallthrough) != "[object Array]") + return lookupKey(key, map.fallthrough, handle, context); + for (var i = 0; i < map.fallthrough.length; i++) { + var result = lookupKey(key, map.fallthrough[i], handle, context); + if (result) return result; + } + } + }; + + // Modifier key presses don't count as 'real' key presses for the + // purpose of keymap fallthrough. + var isModifierKey = CodeMirror.isModifierKey = function(value) { + var name = typeof value == "string" ? value : keyNames[value.keyCode]; + return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod"; + }; + + // Look up the name of a key as indicated by an event object. + var keyName = CodeMirror.keyName = function(event, noShift) { + if (presto && event.keyCode == 34 && event["char"]) return false; + var base = keyNames[event.keyCode], name = base; + if (name == null || event.altGraphKey) return false; + if (event.altKey && base != "Alt") name = "Alt-" + name; + if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") name = "Ctrl-" + name; + if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Cmd") name = "Cmd-" + name; + if (!noShift && event.shiftKey && base != "Shift") name = "Shift-" + name; + return name; + }; + + function getKeyMap(val) { + return typeof val == "string" ? keyMap[val] : val; + } + + // FROMTEXTAREA + + CodeMirror.fromTextArea = function(textarea, options) { + options = options ? copyObj(options) : {}; + options.value = textarea.value; + if (!options.tabindex && textarea.tabIndex) + options.tabindex = textarea.tabIndex; + if (!options.placeholder && textarea.placeholder) + options.placeholder = textarea.placeholder; + // Set autofocus to true if this textarea is focused, or if it has + // autofocus and no other element is focused. + if (options.autofocus == null) { + var hasFocus = activeElt(); + options.autofocus = hasFocus == textarea || + textarea.getAttribute("autofocus") != null && hasFocus == document.body; + } + + function save() {textarea.value = cm.getValue();} + if (textarea.form) { + on(textarea.form, "submit", save); + // Deplorable hack to make the submit method do the right thing. + if (!options.leaveSubmitMethodAlone) { + var form = textarea.form, realSubmit = form.submit; + try { + var wrappedSubmit = form.submit = function() { + save(); + form.submit = realSubmit; + form.submit(); + form.submit = wrappedSubmit; + }; + } catch(e) {} + } + } + + options.finishInit = function(cm) { + cm.save = save; + cm.getTextArea = function() { return textarea; }; + cm.toTextArea = function() { + cm.toTextArea = isNaN; // Prevent this from being ran twice + save(); + textarea.parentNode.removeChild(cm.getWrapperElement()); + textarea.style.display = ""; + if (textarea.form) { + off(textarea.form, "submit", save); + if (typeof textarea.form.submit == "function") + textarea.form.submit = realSubmit; + } + }; + }; + + textarea.style.display = "none"; + var cm = CodeMirror(function(node) { + textarea.parentNode.insertBefore(node, textarea.nextSibling); + }, options); + return cm; + }; + + // STRING STREAM + + // Fed to the mode parsers, provides helper functions to make + // parsers more succinct. + + var StringStream = CodeMirror.StringStream = function(string, tabSize) { + this.pos = this.start = 0; + this.string = string; + this.tabSize = tabSize || 8; + this.lastColumnPos = this.lastColumnValue = 0; + this.lineStart = 0; + }; + + StringStream.prototype = { + eol: function() {return this.pos >= this.string.length;}, + sol: function() {return this.pos == this.lineStart;}, + peek: function() {return this.string.charAt(this.pos) || undefined;}, + next: function() { + if (this.pos < this.string.length) + return this.string.charAt(this.pos++); + }, + eat: function(match) { + var ch = this.string.charAt(this.pos); + if (typeof match == "string") var ok = ch == match; + else var ok = ch && (match.test ? match.test(ch) : match(ch)); + if (ok) {++this.pos; return ch;} + }, + eatWhile: function(match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start; + }, + eatSpace: function() { + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos; + return this.pos > start; + }, + skipToEnd: function() {this.pos = this.string.length;}, + skipTo: function(ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true;} + }, + backUp: function(n) {this.pos -= n;}, + column: function() { + if (this.lastColumnPos < this.start) { + this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); + this.lastColumnPos = this.start; + } + return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); + }, + indentation: function() { + return countColumn(this.string, null, this.tabSize) - + (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); + }, + match: function(pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;}; + var substr = this.string.substr(this.pos, pattern.length); + if (cased(substr) == cased(pattern)) { + if (consume !== false) this.pos += pattern.length; + return true; + } + } else { + var match = this.string.slice(this.pos).match(pattern); + if (match && match.index > 0) return null; + if (match && consume !== false) this.pos += match[0].length; + return match; + } + }, + current: function(){return this.string.slice(this.start, this.pos);}, + hideFirstChars: function(n, inner) { + this.lineStart += n; + try { return inner(); } + finally { this.lineStart -= n; } + } + }; + + // TEXTMARKERS + + // Created with markText and setBookmark methods. A TextMarker is a + // handle that can be used to clear or find a marked position in the + // document. Line objects hold arrays (markedSpans) containing + // {from, to, marker} object pointing to such marker objects, and + // indicating that such a marker is present on that line. Multiple + // lines may point to the same marker when it spans across lines. + // The spans will have null for their from/to properties when the + // marker continues beyond the start/end of the line. Markers have + // links back to the lines they currently touch. + + var nextMarkerId = 0; + + var TextMarker = CodeMirror.TextMarker = function(doc, type) { + this.lines = []; + this.type = type; + this.doc = doc; + this.id = ++nextMarkerId; + }; + eventMixin(TextMarker); + + // Clear the marker. + TextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + var cm = this.doc.cm, withOp = cm && !cm.curOp; + if (withOp) startOperation(cm); + if (hasHandler(this, "clear")) { + var found = this.find(); + if (found) signalLater(this, "clear", found.from, found.to); + } + var min = null, max = null; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (cm && !this.collapsed) regLineChange(cm, lineNo(line), "text"); + else if (cm) { + if (span.to != null) max = lineNo(line); + if (span.from != null) min = lineNo(line); + } + line.markedSpans = removeMarkedSpan(line.markedSpans, span); + if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm) + updateLineHeight(line, textHeight(cm.display)); + } + if (cm && this.collapsed && !cm.options.lineWrapping) for (var i = 0; i < this.lines.length; ++i) { + var visual = visualLine(this.lines[i]), len = lineLength(visual); + if (len > cm.display.maxLineLength) { + cm.display.maxLine = visual; + cm.display.maxLineLength = len; + cm.display.maxLineChanged = true; + } + } + + if (min != null && cm && this.collapsed) regChange(cm, min, max + 1); + this.lines.length = 0; + this.explicitlyCleared = true; + if (this.atomic && this.doc.cantEdit) { + this.doc.cantEdit = false; + if (cm) reCheckSelection(cm.doc); + } + if (cm) signalLater(cm, "markerCleared", cm, this); + if (withOp) endOperation(cm); + if (this.parent) this.parent.clear(); + }; + + // Find the position of the marker in the document. Returns a {from, + // to} object by default. Side can be passed to get a specific side + // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the + // Pos objects returned contain a line object, rather than a line + // number (used to prevent looking up the same line twice). + TextMarker.prototype.find = function(side, lineObj) { + if (side == null && this.type == "bookmark") side = 1; + var from, to; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (span.from != null) { + from = Pos(lineObj ? line : lineNo(line), span.from); + if (side == -1) return from; + } + if (span.to != null) { + to = Pos(lineObj ? line : lineNo(line), span.to); + if (side == 1) return to; + } + } + return from && {from: from, to: to}; + }; + + // Signals that the marker's widget changed, and surrounding layout + // should be recomputed. + TextMarker.prototype.changed = function() { + var pos = this.find(-1, true), widget = this, cm = this.doc.cm; + if (!pos || !cm) return; + runInOp(cm, function() { + var line = pos.line, lineN = lineNo(pos.line); + var view = findViewForLine(cm, lineN); + if (view) { + clearLineMeasurementCacheFor(view); + cm.curOp.selectionChanged = cm.curOp.forceUpdate = true; + } + cm.curOp.updateMaxLine = true; + if (!lineIsHidden(widget.doc, line) && widget.height != null) { + var oldHeight = widget.height; + widget.height = null; + var dHeight = widgetHeight(widget) - oldHeight; + if (dHeight) + updateLineHeight(line, line.height + dHeight); + } + }); + }; + + TextMarker.prototype.attachLine = function(line) { + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) + (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this); + } + this.lines.push(line); + }; + TextMarker.prototype.detachLine = function(line) { + this.lines.splice(indexOf(this.lines, line), 1); + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + (op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this); + } + }; + + // Collapsed markers have unique ids, in order to be able to order + // them, which is needed for uniquely determining an outer marker + // when they overlap (they may nest, but not partially overlap). + var nextMarkerId = 0; + + // Create a marker, wire it up to the right lines, and + function markText(doc, from, to, options, type) { + // Shared markers (across linked documents) are handled separately + // (markTextShared will call out to this again, once per + // document). + if (options && options.shared) return markTextShared(doc, from, to, options, type); + // Ensure we are in an operation. + if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type); + + var marker = new TextMarker(doc, type), diff = cmp(from, to); + if (options) copyObj(options, marker, false); + // Don't connect empty markers unless clearWhenEmpty is false + if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false) + return marker; + if (marker.replacedWith) { + // Showing up as a widget implies collapsed (widget replaces text) + marker.collapsed = true; + marker.widgetNode = elt("span", [marker.replacedWith], "CodeMirror-widget"); + if (!options.handleMouseEvents) marker.widgetNode.setAttribute("cm-ignore-events", "true"); + if (options.insertLeft) marker.widgetNode.insertLeft = true; + } + if (marker.collapsed) { + if (conflictingCollapsedRange(doc, from.line, from, to, marker) || + from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker)) + throw new Error("Inserting collapsed marker partially overlapping an existing one"); + sawCollapsedSpans = true; + } + + if (marker.addToHistory) + addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN); + + var curLine = from.line, cm = doc.cm, updateMaxLine; + doc.iter(curLine, to.line + 1, function(line) { + if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine) + updateMaxLine = true; + if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0); + addMarkedSpan(line, new MarkedSpan(marker, + curLine == from.line ? from.ch : null, + curLine == to.line ? to.ch : null)); + ++curLine; + }); + // lineIsHidden depends on the presence of the spans, so needs a second pass + if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) { + if (lineIsHidden(doc, line)) updateLineHeight(line, 0); + }); + + if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); }); + + if (marker.readOnly) { + sawReadOnlySpans = true; + if (doc.history.done.length || doc.history.undone.length) + doc.clearHistory(); + } + if (marker.collapsed) { + marker.id = ++nextMarkerId; + marker.atomic = true; + } + if (cm) { + // Sync editor state + if (updateMaxLine) cm.curOp.updateMaxLine = true; + if (marker.collapsed) + regChange(cm, from.line, to.line + 1); + else if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.css) + for (var i = from.line; i <= to.line; i++) regLineChange(cm, i, "text"); + if (marker.atomic) reCheckSelection(cm.doc); + signalLater(cm, "markerAdded", cm, marker); + } + return marker; + } + + // SHARED TEXTMARKERS + + // A shared marker spans multiple linked documents. It is + // implemented as a meta-marker-object controlling multiple normal + // markers. + var SharedTextMarker = CodeMirror.SharedTextMarker = function(markers, primary) { + this.markers = markers; + this.primary = primary; + for (var i = 0; i < markers.length; ++i) + markers[i].parent = this; + }; + eventMixin(SharedTextMarker); + + SharedTextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + this.explicitlyCleared = true; + for (var i = 0; i < this.markers.length; ++i) + this.markers[i].clear(); + signalLater(this, "clear"); + }; + SharedTextMarker.prototype.find = function(side, lineObj) { + return this.primary.find(side, lineObj); + }; + + function markTextShared(doc, from, to, options, type) { + options = copyObj(options); + options.shared = false; + var markers = [markText(doc, from, to, options, type)], primary = markers[0]; + var widget = options.widgetNode; + linkedDocs(doc, function(doc) { + if (widget) options.widgetNode = widget.cloneNode(true); + markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)); + for (var i = 0; i < doc.linked.length; ++i) + if (doc.linked[i].isParent) return; + primary = lst(markers); + }); + return new SharedTextMarker(markers, primary); + } + + function findSharedMarkers(doc) { + return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), + function(m) { return m.parent; }); + } + + function copySharedMarkers(doc, markers) { + for (var i = 0; i < markers.length; i++) { + var marker = markers[i], pos = marker.find(); + var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to); + if (cmp(mFrom, mTo)) { + var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type); + marker.markers.push(subMark); + subMark.parent = marker; + } + } + } + + function detachSharedMarkers(markers) { + for (var i = 0; i < markers.length; i++) { + var marker = markers[i], linked = [marker.primary.doc];; + linkedDocs(marker.primary.doc, function(d) { linked.push(d); }); + for (var j = 0; j < marker.markers.length; j++) { + var subMarker = marker.markers[j]; + if (indexOf(linked, subMarker.doc) == -1) { + subMarker.parent = null; + marker.markers.splice(j--, 1); + } + } + } + } + + // TEXTMARKER SPANS + + function MarkedSpan(marker, from, to) { + this.marker = marker; + this.from = from; this.to = to; + } + + // Search an array of spans for a span matching the given marker. + function getMarkedSpanFor(spans, marker) { + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.marker == marker) return span; + } + } + // Remove a span from an array, returning undefined if no spans are + // left (we don't store arrays for lines without spans). + function removeMarkedSpan(spans, span) { + for (var r, i = 0; i < spans.length; ++i) + if (spans[i] != span) (r || (r = [])).push(spans[i]); + return r; + } + // Add a span to a line. + function addMarkedSpan(line, span) { + line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; + span.marker.attachLine(line); + } + + // Used for the algorithm that adjusts markers for a change in the + // document. These functions cut an array of spans at a given + // character position, returning an array of remaining chunks (or + // undefined if nothing remains). + function markedSpansBefore(old, startCh, isInsert) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); + if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh); + (nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)); + } + } + return nw; + } + function markedSpansAfter(old, endCh, isInsert) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); + if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh); + (nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, + span.to == null ? null : span.to - endCh)); + } + } + return nw; + } + + // Given a change object, compute the new set of marker spans that + // cover the line in which the change took place. Removes spans + // entirely within the change, reconnects spans belonging to the + // same marker that appear on both sides of the change, and cuts off + // spans partially within the change. Returns an array of span + // arrays with one element for each line in (after) the change. + function stretchSpansOverChange(doc, change) { + if (change.full) return null; + var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans; + var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans; + if (!oldFirst && !oldLast) return null; + + var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0; + // Get the spans that 'stick out' on both sides + var first = markedSpansBefore(oldFirst, startCh, isInsert); + var last = markedSpansAfter(oldLast, endCh, isInsert); + + // Next, merge those two ends + var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0); + if (first) { + // Fix up .to properties of first + for (var i = 0; i < first.length; ++i) { + var span = first[i]; + if (span.to == null) { + var found = getMarkedSpanFor(last, span.marker); + if (!found) span.to = startCh; + else if (sameLine) span.to = found.to == null ? null : found.to + offset; + } + } + } + if (last) { + // Fix up .from in last (or move them into first in case of sameLine) + for (var i = 0; i < last.length; ++i) { + var span = last[i]; + if (span.to != null) span.to += offset; + if (span.from == null) { + var found = getMarkedSpanFor(first, span.marker); + if (!found) { + span.from = offset; + if (sameLine) (first || (first = [])).push(span); + } + } else { + span.from += offset; + if (sameLine) (first || (first = [])).push(span); + } + } + } + // Make sure we didn't create any zero-length spans + if (first) first = clearEmptySpans(first); + if (last && last != first) last = clearEmptySpans(last); + + var newMarkers = [first]; + if (!sameLine) { + // Fill gap with whole-line-spans + var gap = change.text.length - 2, gapMarkers; + if (gap > 0 && first) + for (var i = 0; i < first.length; ++i) + if (first[i].to == null) + (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i].marker, null, null)); + for (var i = 0; i < gap; ++i) + newMarkers.push(gapMarkers); + newMarkers.push(last); + } + return newMarkers; + } + + // Remove spans that are empty and don't have a clearWhenEmpty + // option of false. + function clearEmptySpans(spans) { + for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false) + spans.splice(i--, 1); + } + if (!spans.length) return null; + return spans; + } + + // Used for un/re-doing changes from the history. Combines the + // result of computing the existing spans with the set of spans that + // existed in the history (so that deleting around a span and then + // undoing brings back the span). + function mergeOldSpans(doc, change) { + var old = getOldSpans(doc, change); + var stretched = stretchSpansOverChange(doc, change); + if (!old) return stretched; + if (!stretched) return old; + + for (var i = 0; i < old.length; ++i) { + var oldCur = old[i], stretchCur = stretched[i]; + if (oldCur && stretchCur) { + spans: for (var j = 0; j < stretchCur.length; ++j) { + var span = stretchCur[j]; + for (var k = 0; k < oldCur.length; ++k) + if (oldCur[k].marker == span.marker) continue spans; + oldCur.push(span); + } + } else if (stretchCur) { + old[i] = stretchCur; + } + } + return old; + } + + // Used to 'clip' out readOnly ranges when making a change. + function removeReadOnlyRanges(doc, from, to) { + var markers = null; + doc.iter(from.line, to.line + 1, function(line) { + if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) { + var mark = line.markedSpans[i].marker; + if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) + (markers || (markers = [])).push(mark); + } + }); + if (!markers) return null; + var parts = [{from: from, to: to}]; + for (var i = 0; i < markers.length; ++i) { + var mk = markers[i], m = mk.find(0); + for (var j = 0; j < parts.length; ++j) { + var p = parts[j]; + if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) continue; + var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to); + if (dfrom < 0 || !mk.inclusiveLeft && !dfrom) + newParts.push({from: p.from, to: m.from}); + if (dto > 0 || !mk.inclusiveRight && !dto) + newParts.push({from: m.to, to: p.to}); + parts.splice.apply(parts, newParts); + j += newParts.length - 1; + } + } + return parts; + } + + // Connect or disconnect spans from a line. + function detachMarkedSpans(line) { + var spans = line.markedSpans; + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.detachLine(line); + line.markedSpans = null; + } + function attachMarkedSpans(line, spans) { + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.attachLine(line); + line.markedSpans = spans; + } + + // Helpers used when computing which overlapping collapsed span + // counts as the larger one. + function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0; } + function extraRight(marker) { return marker.inclusiveRight ? 1 : 0; } + + // Returns a number indicating which of two overlapping collapsed + // spans is larger (and thus includes the other). Falls back to + // comparing ids when the spans cover exactly the same range. + function compareCollapsedMarkers(a, b) { + var lenDiff = a.lines.length - b.lines.length; + if (lenDiff != 0) return lenDiff; + var aPos = a.find(), bPos = b.find(); + var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b); + if (fromCmp) return -fromCmp; + var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b); + if (toCmp) return toCmp; + return b.id - a.id; + } + + // Find out whether a line ends or starts in a collapsed span. If + // so, return the marker for that span. + function collapsedSpanAtSide(line, start) { + var sps = sawCollapsedSpans && line.markedSpans, found; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (sp.marker.collapsed && (start ? sp.from : sp.to) == null && + (!found || compareCollapsedMarkers(found, sp.marker) < 0)) + found = sp.marker; + } + return found; + } + function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true); } + function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false); } + + // Test whether there exists a collapsed span that partially + // overlaps (covers the start or end, but not both) of a new span. + // Such overlap is not allowed. + function conflictingCollapsedRange(doc, lineNo, from, to, marker) { + var line = getLine(doc, lineNo); + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) for (var i = 0; i < sps.length; ++i) { + var sp = sps[i]; + if (!sp.marker.collapsed) continue; + var found = sp.marker.find(0); + var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker); + var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker); + if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue; + if (fromCmp <= 0 && (cmp(found.to, from) > 0 || (sp.marker.inclusiveRight && marker.inclusiveLeft)) || + fromCmp >= 0 && (cmp(found.from, to) < 0 || (sp.marker.inclusiveLeft && marker.inclusiveRight))) + return true; + } + } + + // A visual line is a line as drawn on the screen. Folding, for + // example, can cause multiple logical lines to appear on the same + // visual line. This finds the start of the visual line that the + // given line is part of (usually that is the line itself). + function visualLine(line) { + var merged; + while (merged = collapsedSpanAtStart(line)) + line = merged.find(-1, true).line; + return line; + } + + // Returns an array of logical lines that continue the visual line + // started by the argument, or undefined if there are no such lines. + function visualLineContinued(line) { + var merged, lines; + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line; + (lines || (lines = [])).push(line); + } + return lines; + } + + // Get the line number of the start of the visual line that the + // given line number is part of. + function visualLineNo(doc, lineN) { + var line = getLine(doc, lineN), vis = visualLine(line); + if (line == vis) return lineN; + return lineNo(vis); + } + // Get the line number of the start of the next visual line after + // the given line. + function visualLineEndNo(doc, lineN) { + if (lineN > doc.lastLine()) return lineN; + var line = getLine(doc, lineN), merged; + if (!lineIsHidden(doc, line)) return lineN; + while (merged = collapsedSpanAtEnd(line)) + line = merged.find(1, true).line; + return lineNo(line) + 1; + } + + // Compute whether a line is hidden. Lines count as hidden when they + // are part of a visual line that starts with another line, or when + // they are entirely covered by collapsed, non-widget span. + function lineIsHidden(doc, line) { + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (!sp.marker.collapsed) continue; + if (sp.from == null) return true; + if (sp.marker.widgetNode) continue; + if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) + return true; + } + } + function lineIsHiddenInner(doc, line, span) { + if (span.to == null) { + var end = span.marker.find(1, true); + return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker)); + } + if (span.marker.inclusiveRight && span.to == line.text.length) + return true; + for (var sp, i = 0; i < line.markedSpans.length; ++i) { + sp = line.markedSpans[i]; + if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to && + (sp.to == null || sp.to != span.from) && + (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && + lineIsHiddenInner(doc, line, sp)) return true; + } + } + + // LINE WIDGETS + + // Line widgets are block elements displayed above or below a line. + + var LineWidget = CodeMirror.LineWidget = function(doc, node, options) { + if (options) for (var opt in options) if (options.hasOwnProperty(opt)) + this[opt] = options[opt]; + this.doc = doc; + this.node = node; + }; + eventMixin(LineWidget); + + function adjustScrollWhenAboveVisible(cm, line, diff) { + if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) + addToScrollPos(cm, null, diff); + } + + LineWidget.prototype.clear = function() { + var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line); + if (no == null || !ws) return; + for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1); + if (!ws.length) line.widgets = null; + var height = widgetHeight(this); + updateLineHeight(line, Math.max(0, line.height - height)); + if (cm) runInOp(cm, function() { + adjustScrollWhenAboveVisible(cm, line, -height); + regLineChange(cm, no, "widget"); + }); + }; + LineWidget.prototype.changed = function() { + var oldH = this.height, cm = this.doc.cm, line = this.line; + this.height = null; + var diff = widgetHeight(this) - oldH; + if (!diff) return; + updateLineHeight(line, line.height + diff); + if (cm) runInOp(cm, function() { + cm.curOp.forceUpdate = true; + adjustScrollWhenAboveVisible(cm, line, diff); + }); + }; + + function widgetHeight(widget) { + if (widget.height != null) return widget.height; + var cm = widget.doc.cm; + if (!cm) return 0; + if (!contains(document.body, widget.node)) { + var parentStyle = "position: relative;"; + if (widget.coverGutter) + parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;"; + if (widget.noHScroll) + parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;"; + removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle)); + } + return widget.height = widget.node.offsetHeight; + } + + function addLineWidget(doc, handle, node, options) { + var widget = new LineWidget(doc, node, options); + var cm = doc.cm; + if (cm && widget.noHScroll) cm.display.alignWidgets = true; + changeLine(doc, handle, "widget", function(line) { + var widgets = line.widgets || (line.widgets = []); + if (widget.insertAt == null) widgets.push(widget); + else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget); + widget.line = line; + if (cm && !lineIsHidden(doc, line)) { + var aboveVisible = heightAtLine(line) < doc.scrollTop; + updateLineHeight(line, line.height + widgetHeight(widget)); + if (aboveVisible) addToScrollPos(cm, null, widget.height); + cm.curOp.forceUpdate = true; + } + return true; + }); + return widget; + } + + // LINE DATA STRUCTURE + + // Line objects. These hold state related to a line, including + // highlighting info (the styles array). + var Line = CodeMirror.Line = function(text, markedSpans, estimateHeight) { + this.text = text; + attachMarkedSpans(this, markedSpans); + this.height = estimateHeight ? estimateHeight(this) : 1; + }; + eventMixin(Line); + Line.prototype.lineNo = function() { return lineNo(this); }; + + // Change the content (text, markers) of a line. Automatically + // invalidates cached information and tries to re-estimate the + // line's height. + function updateLine(line, text, markedSpans, estimateHeight) { + line.text = text; + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + if (line.order != null) line.order = null; + detachMarkedSpans(line); + attachMarkedSpans(line, markedSpans); + var estHeight = estimateHeight ? estimateHeight(line) : 1; + if (estHeight != line.height) updateLineHeight(line, estHeight); + } + + // Detach a line from the document tree and its markers. + function cleanUpLine(line) { + line.parent = null; + detachMarkedSpans(line); + } + + function extractLineClasses(type, output) { + if (type) for (;;) { + var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/); + if (!lineClass) break; + type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length); + var prop = lineClass[1] ? "bgClass" : "textClass"; + if (output[prop] == null) + output[prop] = lineClass[2]; + else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(output[prop])) + output[prop] += " " + lineClass[2]; + } + return type; + } + + function callBlankLine(mode, state) { + if (mode.blankLine) return mode.blankLine(state); + if (!mode.innerMode) return; + var inner = CodeMirror.innerMode(mode, state); + if (inner.mode.blankLine) return inner.mode.blankLine(inner.state); + } + + function readToken(mode, stream, state, inner) { + for (var i = 0; i < 10; i++) { + if (inner) inner[0] = CodeMirror.innerMode(mode, state).mode; + var style = mode.token(stream, state); + if (stream.pos > stream.start) return style; + } + throw new Error("Mode " + mode.name + " failed to advance stream."); + } + + // Utility for getTokenAt and getLineTokens + function takeToken(cm, pos, precise, asArray) { + function getObj(copy) { + return {start: stream.start, end: stream.pos, + string: stream.current(), + type: style || null, + state: copy ? copyState(doc.mode, state) : state}; + } + + var doc = cm.doc, mode = doc.mode, style; + pos = clipPos(doc, pos); + var line = getLine(doc, pos.line), state = getStateBefore(cm, pos.line, precise); + var stream = new StringStream(line.text, cm.options.tabSize), tokens; + if (asArray) tokens = []; + while ((asArray || stream.pos < pos.ch) && !stream.eol()) { + stream.start = stream.pos; + style = readToken(mode, stream, state); + if (asArray) tokens.push(getObj(true)); + } + return asArray ? tokens : getObj(); + } + + // Run the given mode's parser over a line, calling f for each token. + function runMode(cm, text, mode, state, f, lineClasses, forceToEnd) { + var flattenSpans = mode.flattenSpans; + if (flattenSpans == null) flattenSpans = cm.options.flattenSpans; + var curStart = 0, curStyle = null; + var stream = new StringStream(text, cm.options.tabSize), style; + var inner = cm.options.addModeClass && [null]; + if (text == "") extractLineClasses(callBlankLine(mode, state), lineClasses); + while (!stream.eol()) { + if (stream.pos > cm.options.maxHighlightLength) { + flattenSpans = false; + if (forceToEnd) processLine(cm, text, state, stream.pos); + stream.pos = text.length; + style = null; + } else { + style = extractLineClasses(readToken(mode, stream, state, inner), lineClasses); + } + if (inner) { + var mName = inner[0].name; + if (mName) style = "m-" + (style ? mName + " " + style : mName); + } + if (!flattenSpans || curStyle != style) { + while (curStart < stream.start) { + curStart = Math.min(stream.start, curStart + 50000); + f(curStart, curStyle); + } + curStyle = style; + } + stream.start = stream.pos; + } + while (curStart < stream.pos) { + // Webkit seems to refuse to render text nodes longer than 57444 characters + var pos = Math.min(stream.pos, curStart + 50000); + f(pos, curStyle); + curStart = pos; + } + } + + // Compute a style array (an array starting with a mode generation + // -- for invalidation -- followed by pairs of end positions and + // style strings), which is used to highlight the tokens on the + // line. + function highlightLine(cm, line, state, forceToEnd) { + // A styles array always starts with a number identifying the + // mode/overlays that it is based on (for easy invalidation). + var st = [cm.state.modeGen], lineClasses = {}; + // Compute the base array of styles + runMode(cm, line.text, cm.doc.mode, state, function(end, style) { + st.push(end, style); + }, lineClasses, forceToEnd); + + // Run overlays, adjust style array. + for (var o = 0; o < cm.state.overlays.length; ++o) { + var overlay = cm.state.overlays[o], i = 1, at = 0; + runMode(cm, line.text, overlay.mode, true, function(end, style) { + var start = i; + // Ensure there's a token end at the current position, and that i points at it + while (at < end) { + var i_end = st[i]; + if (i_end > end) + st.splice(i, 1, end, st[i+1], i_end); + i += 2; + at = Math.min(end, i_end); + } + if (!style) return; + if (overlay.opaque) { + st.splice(start, i - start, end, "cm-overlay " + style); + i = start + 2; + } else { + for (; start < i; start += 2) { + var cur = st[start+1]; + st[start+1] = (cur ? cur + " " : "") + "cm-overlay " + style; + } + } + }, lineClasses); + } + + return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null}; + } + + function getLineStyles(cm, line, updateFrontier) { + if (!line.styles || line.styles[0] != cm.state.modeGen) { + var result = highlightLine(cm, line, line.stateAfter = getStateBefore(cm, lineNo(line))); + line.styles = result.styles; + if (result.classes) line.styleClasses = result.classes; + else if (line.styleClasses) line.styleClasses = null; + if (updateFrontier === cm.doc.frontier) cm.doc.frontier++; + } + return line.styles; + } + + // Lightweight form of highlight -- proceed over this line and + // update state, but don't save a style array. Used for lines that + // aren't currently visible. + function processLine(cm, text, state, startAt) { + var mode = cm.doc.mode; + var stream = new StringStream(text, cm.options.tabSize); + stream.start = stream.pos = startAt || 0; + if (text == "") callBlankLine(mode, state); + while (!stream.eol() && stream.pos <= cm.options.maxHighlightLength) { + readToken(mode, stream, state); + stream.start = stream.pos; + } + } + + // Convert a style as returned by a mode (either null, or a string + // containing one or more styles) to a CSS style. This is cached, + // and also looks for line-wide styles. + var styleToClassCache = {}, styleToClassCacheWithMode = {}; + function interpretTokenStyle(style, options) { + if (!style || /^\s*$/.test(style)) return null; + var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache; + return cache[style] || + (cache[style] = style.replace(/\S+/g, "cm-$&")); + } + + // Render the DOM representation of the text of a line. Also builds + // up a 'line map', which points at the DOM nodes that represent + // specific stretches of text, and is used by the measuring code. + // The returned object contains the DOM node, this map, and + // information about line-wide styles that were set by the mode. + function buildLineContent(cm, lineView) { + // The padding-right forces the element to have a 'border', which + // is needed on Webkit to be able to get line-level bounding + // rectangles for it (in measureChar). + var content = elt("span", null, null, webkit ? "padding-right: .1px" : null); + var builder = {pre: elt("pre", [content]), content: content, + col: 0, pos: 0, cm: cm, + splitSpaces: (ie || webkit) && cm.getOption("lineWrapping")}; + lineView.measure = {}; + + // Iterate over the logical lines that make up this visual line. + for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) { + var line = i ? lineView.rest[i - 1] : lineView.line, order; + builder.pos = 0; + builder.addToken = buildToken; + // Optionally wire in some hacks into the token-rendering + // algorithm, to deal with browser quirks. + if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line))) + builder.addToken = buildTokenBadBidi(builder.addToken, order); + builder.map = []; + var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line); + insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate)); + if (line.styleClasses) { + if (line.styleClasses.bgClass) + builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || ""); + if (line.styleClasses.textClass) + builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || ""); + } + + // Ensure at least a single node is present, for measuring. + if (builder.map.length == 0) + builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure))); + + // Store the map and a cache object for the current logical line + if (i == 0) { + lineView.measure.map = builder.map; + lineView.measure.cache = {}; + } else { + (lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map); + (lineView.measure.caches || (lineView.measure.caches = [])).push({}); + } + } + + // See issue #2901 + if (webkit && /\bcm-tab\b/.test(builder.content.lastChild.className)) + builder.content.className = "cm-tab-wrap-hack"; + + signal(cm, "renderLine", cm, lineView.line, builder.pre); + if (builder.pre.className) + builder.textClass = joinClasses(builder.pre.className, builder.textClass || ""); + + return builder; + } + + function defaultSpecialCharPlaceholder(ch) { + var token = elt("span", "\u2022", "cm-invalidchar"); + token.title = "\\u" + ch.charCodeAt(0).toString(16); + token.setAttribute("aria-label", token.title); + return token; + } + + // Build up the DOM representation for a single token, and add it to + // the line map. Takes care to render special characters separately. + function buildToken(builder, text, style, startStyle, endStyle, title, css) { + if (!text) return; + var displayText = builder.splitSpaces ? text.replace(/ {3,}/g, splitSpaces) : text; + var special = builder.cm.state.specialChars, mustWrap = false; + if (!special.test(text)) { + builder.col += text.length; + var content = document.createTextNode(displayText); + builder.map.push(builder.pos, builder.pos + text.length, content); + if (ie && ie_version < 9) mustWrap = true; + builder.pos += text.length; + } else { + var content = document.createDocumentFragment(), pos = 0; + while (true) { + special.lastIndex = pos; + var m = special.exec(text); + var skipped = m ? m.index - pos : text.length - pos; + if (skipped) { + var txt = document.createTextNode(displayText.slice(pos, pos + skipped)); + if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); + else content.appendChild(txt); + builder.map.push(builder.pos, builder.pos + skipped, txt); + builder.col += skipped; + builder.pos += skipped; + } + if (!m) break; + pos += skipped + 1; + if (m[0] == "\t") { + var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; + var txt = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); + txt.setAttribute("role", "presentation"); + txt.setAttribute("cm-text", "\t"); + builder.col += tabWidth; + } else { + var txt = builder.cm.options.specialCharPlaceholder(m[0]); + txt.setAttribute("cm-text", m[0]); + if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); + else content.appendChild(txt); + builder.col += 1; + } + builder.map.push(builder.pos, builder.pos + 1, txt); + builder.pos++; + } + } + if (style || startStyle || endStyle || mustWrap || css) { + var fullStyle = style || ""; + if (startStyle) fullStyle += startStyle; + if (endStyle) fullStyle += endStyle; + var token = elt("span", [content], fullStyle, css); + if (title) token.title = title; + return builder.content.appendChild(token); + } + builder.content.appendChild(content); + } + + function splitSpaces(old) { + var out = " "; + for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0"; + out += " "; + return out; + } + + // Work around nonsense dimensions being reported for stretches of + // right-to-left text. + function buildTokenBadBidi(inner, order) { + return function(builder, text, style, startStyle, endStyle, title, css) { + style = style ? style + " cm-force-border" : "cm-force-border"; + var start = builder.pos, end = start + text.length; + for (;;) { + // Find the part that overlaps with the start of this text + for (var i = 0; i < order.length; i++) { + var part = order[i]; + if (part.to > start && part.from <= start) break; + } + if (part.to >= end) return inner(builder, text, style, startStyle, endStyle, title, css); + inner(builder, text.slice(0, part.to - start), style, startStyle, null, title, css); + startStyle = null; + text = text.slice(part.to - start); + start = part.to; + } + }; + } + + function buildCollapsedSpan(builder, size, marker, ignoreWidget) { + var widget = !ignoreWidget && marker.widgetNode; + if (widget) builder.map.push(builder.pos, builder.pos + size, widget); + if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) { + if (!widget) + widget = builder.content.appendChild(document.createElement("span")); + widget.setAttribute("cm-marker", marker.id); + } + if (widget) { + builder.cm.display.input.setUneditable(widget); + builder.content.appendChild(widget); + } + builder.pos += size; + } + + // Outputs a number of spans to make up a line, taking highlighting + // and marked text into account. + function insertLineContent(line, builder, styles) { + var spans = line.markedSpans, allText = line.text, at = 0; + if (!spans) { + for (var i = 1; i < styles.length; i+=2) + builder.addToken(builder, allText.slice(at, at = styles[i]), interpretTokenStyle(styles[i+1], builder.cm.options)); + return; + } + + var len = allText.length, pos = 0, i = 1, text = "", style, css; + var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed; + for (;;) { + if (nextChange == pos) { // Update current marker set + spanStyle = spanEndStyle = spanStartStyle = title = css = ""; + collapsed = null; nextChange = Infinity; + var foundBookmarks = []; + for (var j = 0; j < spans.length; ++j) { + var sp = spans[j], m = sp.marker; + if (sp.from <= pos && (sp.to == null || sp.to > pos)) { + if (sp.to != null && nextChange > sp.to) { nextChange = sp.to; spanEndStyle = ""; } + if (m.className) spanStyle += " " + m.className; + if (m.css) css = m.css; + if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle; + if (m.endStyle && sp.to == nextChange) spanEndStyle += " " + m.endStyle; + if (m.title && !title) title = m.title; + if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0)) + collapsed = sp; + } else if (sp.from > pos && nextChange > sp.from) { + nextChange = sp.from; + } + if (m.type == "bookmark" && sp.from == pos && m.widgetNode) foundBookmarks.push(m); + } + if (collapsed && (collapsed.from || 0) == pos) { + buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos, + collapsed.marker, collapsed.from == null); + if (collapsed.to == null) return; + } + if (!collapsed && foundBookmarks.length) for (var j = 0; j < foundBookmarks.length; ++j) + buildCollapsedSpan(builder, 0, foundBookmarks[j]); + } + if (pos >= len) break; + + var upto = Math.min(len, nextChange); + while (true) { + if (text) { + var end = pos + text.length; + if (!collapsed) { + var tokenText = end > upto ? text.slice(0, upto - pos) : text; + builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle, + spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title, css); + } + if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;} + pos = end; + spanStartStyle = ""; + } + text = allText.slice(at, at = styles[i++]); + style = interpretTokenStyle(styles[i++], builder.cm.options); + } + } + } + + // DOCUMENT DATA STRUCTURE + + // By default, updates that start and end at the beginning of a line + // are treated specially, in order to make the association of line + // widgets and marker elements with the text behave more intuitive. + function isWholeLineUpdate(doc, change) { + return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" && + (!doc.cm || doc.cm.options.wholeLineUpdateBefore); + } + + // Perform a change on the document data structure. + function updateDoc(doc, change, markedSpans, estimateHeight) { + function spansFor(n) {return markedSpans ? markedSpans[n] : null;} + function update(line, text, spans) { + updateLine(line, text, spans, estimateHeight); + signalLater(line, "change", line, change); + } + function linesFor(start, end) { + for (var i = start, result = []; i < end; ++i) + result.push(new Line(text[i], spansFor(i), estimateHeight)); + return result; + } + + var from = change.from, to = change.to, text = change.text; + var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line); + var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; + + // Adjust the line structure + if (change.full) { + doc.insert(0, linesFor(0, text.length)); + doc.remove(text.length, doc.size - text.length); + } else if (isWholeLineUpdate(doc, change)) { + // This is a whole-line replace. Treated specially to make + // sure line objects move the way they are supposed to. + var added = linesFor(0, text.length - 1); + update(lastLine, lastLine.text, lastSpans); + if (nlines) doc.remove(from.line, nlines); + if (added.length) doc.insert(from.line, added); + } else if (firstLine == lastLine) { + if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans); + } else { + var added = linesFor(1, text.length - 1); + added.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight)); + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + doc.insert(from.line + 1, added); + } + } else if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)); + doc.remove(from.line + 1, nlines); + } else { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans); + var added = linesFor(1, text.length - 1); + if (nlines > 1) doc.remove(from.line + 1, nlines - 1); + doc.insert(from.line + 1, added); + } + + signalLater(doc, "change", doc, change); + } + + // The document is represented as a BTree consisting of leaves, with + // chunk of lines in them, and branches, with up to ten leaves or + // other branch nodes below them. The top node is always a branch + // node, and is the document object itself (meaning it has + // additional methods and properties). + // + // All nodes have parent links. The tree is used both to go from + // line numbers to line objects, and to go from objects to numbers. + // It also indexes by height, and is used to convert between height + // and line object, and to find the total height of the document. + // + // See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html + + function LeafChunk(lines) { + this.lines = lines; + this.parent = null; + for (var i = 0, height = 0; i < lines.length; ++i) { + lines[i].parent = this; + height += lines[i].height; + } + this.height = height; + } + + LeafChunk.prototype = { + chunkSize: function() { return this.lines.length; }, + // Remove the n lines at offset 'at'. + removeInner: function(at, n) { + for (var i = at, e = at + n; i < e; ++i) { + var line = this.lines[i]; + this.height -= line.height; + cleanUpLine(line); + signalLater(line, "delete"); + } + this.lines.splice(at, n); + }, + // Helper used to collapse a small branch into a single leaf. + collapse: function(lines) { + lines.push.apply(lines, this.lines); + }, + // Insert the given array of lines at offset 'at', count them as + // having the given height. + insertInner: function(at, lines, height) { + this.height += height; + this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); + for (var i = 0; i < lines.length; ++i) lines[i].parent = this; + }, + // Used to iterate over a part of the tree. + iterN: function(at, n, op) { + for (var e = at + n; at < e; ++at) + if (op(this.lines[at])) return true; + } + }; + + function BranchChunk(children) { + this.children = children; + var size = 0, height = 0; + for (var i = 0; i < children.length; ++i) { + var ch = children[i]; + size += ch.chunkSize(); height += ch.height; + ch.parent = this; + } + this.size = size; + this.height = height; + this.parent = null; + } + + BranchChunk.prototype = { + chunkSize: function() { return this.size; }, + removeInner: function(at, n) { + this.size -= n; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var rm = Math.min(n, sz - at), oldHeight = child.height; + child.removeInner(at, rm); + this.height -= oldHeight - child.height; + if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } + if ((n -= rm) == 0) break; + at = 0; + } else at -= sz; + } + // If the result is smaller than 25 lines, ensure that it is a + // single leaf node. + if (this.size - n < 25 && + (this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) { + var lines = []; + this.collapse(lines); + this.children = [new LeafChunk(lines)]; + this.children[0].parent = this; + } + }, + collapse: function(lines) { + for (var i = 0; i < this.children.length; ++i) this.children[i].collapse(lines); + }, + insertInner: function(at, lines, height) { + this.size += lines.length; + this.height += height; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at <= sz) { + child.insertInner(at, lines, height); + if (child.lines && child.lines.length > 50) { + while (child.lines.length > 50) { + var spilled = child.lines.splice(child.lines.length - 25, 25); + var newleaf = new LeafChunk(spilled); + child.height -= newleaf.height; + this.children.splice(i + 1, 0, newleaf); + newleaf.parent = this; + } + this.maybeSpill(); + } + break; + } + at -= sz; + } + }, + // When a node has grown, check whether it should be split. + maybeSpill: function() { + if (this.children.length <= 10) return; + var me = this; + do { + var spilled = me.children.splice(me.children.length - 5, 5); + var sibling = new BranchChunk(spilled); + if (!me.parent) { // Become the parent node + var copy = new BranchChunk(me.children); + copy.parent = me; + me.children = [copy, sibling]; + me = copy; + } else { + me.size -= sibling.size; + me.height -= sibling.height; + var myIndex = indexOf(me.parent.children, me); + me.parent.children.splice(myIndex + 1, 0, sibling); + } + sibling.parent = me.parent; + } while (me.children.length > 10); + me.parent.maybeSpill(); + }, + iterN: function(at, n, op) { + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var used = Math.min(n, sz - at); + if (child.iterN(at, used, op)) return true; + if ((n -= used) == 0) break; + at = 0; + } else at -= sz; + } + } + }; + + var nextDocId = 0; + var Doc = CodeMirror.Doc = function(text, mode, firstLine) { + if (!(this instanceof Doc)) return new Doc(text, mode, firstLine); + if (firstLine == null) firstLine = 0; + + BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); + this.first = firstLine; + this.scrollTop = this.scrollLeft = 0; + this.cantEdit = false; + this.cleanGeneration = 1; + this.frontier = firstLine; + var start = Pos(firstLine, 0); + this.sel = simpleSelection(start); + this.history = new History(null); + this.id = ++nextDocId; + this.modeOption = mode; + + if (typeof text == "string") text = splitLines(text); + updateDoc(this, {from: start, to: start, text: text}); + setSelection(this, simpleSelection(start), sel_dontScroll); + }; + + Doc.prototype = createObj(BranchChunk.prototype, { + constructor: Doc, + // Iterate over the document. Supports two forms -- with only one + // argument, it calls that for each line in the document. With + // three, it iterates over the range given by the first two (with + // the second being non-inclusive). + iter: function(from, to, op) { + if (op) this.iterN(from - this.first, to - from, op); + else this.iterN(this.first, this.first + this.size, from); + }, + + // Non-public interface for adding and removing lines. + insert: function(at, lines) { + var height = 0; + for (var i = 0; i < lines.length; ++i) height += lines[i].height; + this.insertInner(at - this.first, lines, height); + }, + remove: function(at, n) { this.removeInner(at - this.first, n); }, + + // From here, the methods are part of the public interface. Most + // are also available from CodeMirror (editor) instances. + + getValue: function(lineSep) { + var lines = getLines(this, this.first, this.first + this.size); + if (lineSep === false) return lines; + return lines.join(lineSep || "\n"); + }, + setValue: docMethodOp(function(code) { + var top = Pos(this.first, 0), last = this.first + this.size - 1; + makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), + text: splitLines(code), origin: "setValue", full: true}, true); + setSelection(this, simpleSelection(top)); + }), + replaceRange: function(code, from, to, origin) { + from = clipPos(this, from); + to = to ? clipPos(this, to) : from; + replaceRange(this, code, from, to, origin); + }, + getRange: function(from, to, lineSep) { + var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); + if (lineSep === false) return lines; + return lines.join(lineSep || "\n"); + }, + + getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;}, + + getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line);}, + getLineNumber: function(line) {return lineNo(line);}, + + getLineHandleVisualStart: function(line) { + if (typeof line == "number") line = getLine(this, line); + return visualLine(line); + }, + + lineCount: function() {return this.size;}, + firstLine: function() {return this.first;}, + lastLine: function() {return this.first + this.size - 1;}, + + clipPos: function(pos) {return clipPos(this, pos);}, + + getCursor: function(start) { + var range = this.sel.primary(), pos; + if (start == null || start == "head") pos = range.head; + else if (start == "anchor") pos = range.anchor; + else if (start == "end" || start == "to" || start === false) pos = range.to(); + else pos = range.from(); + return pos; + }, + listSelections: function() { return this.sel.ranges; }, + somethingSelected: function() {return this.sel.somethingSelected();}, + + setCursor: docMethodOp(function(line, ch, options) { + setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options); + }), + setSelection: docMethodOp(function(anchor, head, options) { + setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options); + }), + extendSelection: docMethodOp(function(head, other, options) { + extendSelection(this, clipPos(this, head), other && clipPos(this, other), options); + }), + extendSelections: docMethodOp(function(heads, options) { + extendSelections(this, clipPosArray(this, heads, options)); + }), + extendSelectionsBy: docMethodOp(function(f, options) { + extendSelections(this, map(this.sel.ranges, f), options); + }), + setSelections: docMethodOp(function(ranges, primary, options) { + if (!ranges.length) return; + for (var i = 0, out = []; i < ranges.length; i++) + out[i] = new Range(clipPos(this, ranges[i].anchor), + clipPos(this, ranges[i].head)); + if (primary == null) primary = Math.min(ranges.length - 1, this.sel.primIndex); + setSelection(this, normalizeSelection(out, primary), options); + }), + addSelection: docMethodOp(function(anchor, head, options) { + var ranges = this.sel.ranges.slice(0); + ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor))); + setSelection(this, normalizeSelection(ranges, ranges.length - 1), options); + }), + + getSelection: function(lineSep) { + var ranges = this.sel.ranges, lines; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + lines = lines ? lines.concat(sel) : sel; + } + if (lineSep === false) return lines; + else return lines.join(lineSep || "\n"); + }, + getSelections: function(lineSep) { + var parts = [], ranges = this.sel.ranges; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + if (lineSep !== false) sel = sel.join(lineSep || "\n"); + parts[i] = sel; + } + return parts; + }, + replaceSelection: function(code, collapse, origin) { + var dup = []; + for (var i = 0; i < this.sel.ranges.length; i++) + dup[i] = code; + this.replaceSelections(dup, collapse, origin || "+input"); + }, + replaceSelections: docMethodOp(function(code, collapse, origin) { + var changes = [], sel = this.sel; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + changes[i] = {from: range.from(), to: range.to(), text: splitLines(code[i]), origin: origin}; + } + var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse); + for (var i = changes.length - 1; i >= 0; i--) + makeChange(this, changes[i]); + if (newSel) setSelectionReplaceHistory(this, newSel); + else if (this.cm) ensureCursorVisible(this.cm); + }), + undo: docMethodOp(function() {makeChangeFromHistory(this, "undo");}), + redo: docMethodOp(function() {makeChangeFromHistory(this, "redo");}), + undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true);}), + redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true);}), + + setExtending: function(val) {this.extend = val;}, + getExtending: function() {return this.extend;}, + + historySize: function() { + var hist = this.history, done = 0, undone = 0; + for (var i = 0; i < hist.done.length; i++) if (!hist.done[i].ranges) ++done; + for (var i = 0; i < hist.undone.length; i++) if (!hist.undone[i].ranges) ++undone; + return {undo: done, redo: undone}; + }, + clearHistory: function() {this.history = new History(this.history.maxGeneration);}, + + markClean: function() { + this.cleanGeneration = this.changeGeneration(true); + }, + changeGeneration: function(forceSplit) { + if (forceSplit) + this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null; + return this.history.generation; + }, + isClean: function (gen) { + return this.history.generation == (gen || this.cleanGeneration); + }, + + getHistory: function() { + return {done: copyHistoryArray(this.history.done), + undone: copyHistoryArray(this.history.undone)}; + }, + setHistory: function(histData) { + var hist = this.history = new History(this.history.maxGeneration); + hist.done = copyHistoryArray(histData.done.slice(0), null, true); + hist.undone = copyHistoryArray(histData.undone.slice(0), null, true); + }, + + addLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + if (!line[prop]) line[prop] = cls; + else if (classTest(cls).test(line[prop])) return false; + else line[prop] += " " + cls; + return true; + }); + }), + removeLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function(line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + var cur = line[prop]; + if (!cur) return false; + else if (cls == null) line[prop] = null; + else { + var found = cur.match(classTest(cls)); + if (!found) return false; + var end = found.index + found[0].length; + line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null; + } + return true; + }); + }), + + addLineWidget: docMethodOp(function(handle, node, options) { + return addLineWidget(this, handle, node, options); + }), + removeLineWidget: function(widget) { widget.clear(); }, + + markText: function(from, to, options) { + return markText(this, clipPos(this, from), clipPos(this, to), options, "range"); + }, + setBookmark: function(pos, options) { + var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), + insertLeft: options && options.insertLeft, + clearWhenEmpty: false, shared: options && options.shared, + handleMouseEvents: options && options.handleMouseEvents}; + pos = clipPos(this, pos); + return markText(this, pos, pos, realOpts, "bookmark"); + }, + findMarksAt: function(pos) { + pos = clipPos(this, pos); + var markers = [], spans = getLine(this, pos.line).markedSpans; + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if ((span.from == null || span.from <= pos.ch) && + (span.to == null || span.to >= pos.ch)) + markers.push(span.marker.parent || span.marker); + } + return markers; + }, + findMarks: function(from, to, filter) { + from = clipPos(this, from); to = clipPos(this, to); + var found = [], lineNo = from.line; + this.iter(from.line, to.line + 1, function(line) { + var spans = line.markedSpans; + if (spans) for (var i = 0; i < spans.length; i++) { + var span = spans[i]; + if (!(lineNo == from.line && from.ch > span.to || + span.from == null && lineNo != from.line|| + lineNo == to.line && span.from > to.ch) && + (!filter || filter(span.marker))) + found.push(span.marker.parent || span.marker); + } + ++lineNo; + }); + return found; + }, + getAllMarks: function() { + var markers = []; + this.iter(function(line) { + var sps = line.markedSpans; + if (sps) for (var i = 0; i < sps.length; ++i) + if (sps[i].from != null) markers.push(sps[i].marker); + }); + return markers; + }, + + posFromIndex: function(off) { + var ch, lineNo = this.first; + this.iter(function(line) { + var sz = line.text.length + 1; + if (sz > off) { ch = off; return true; } + off -= sz; + ++lineNo; + }); + return clipPos(this, Pos(lineNo, ch)); + }, + indexFromPos: function (coords) { + coords = clipPos(this, coords); + var index = coords.ch; + if (coords.line < this.first || coords.ch < 0) return 0; + this.iter(this.first, coords.line, function (line) { + index += line.text.length + 1; + }); + return index; + }, + + copy: function(copyHistory) { + var doc = new Doc(getLines(this, this.first, this.first + this.size), this.modeOption, this.first); + doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; + doc.sel = this.sel; + doc.extend = false; + if (copyHistory) { + doc.history.undoDepth = this.history.undoDepth; + doc.setHistory(this.getHistory()); + } + return doc; + }, + + linkedDoc: function(options) { + if (!options) options = {}; + var from = this.first, to = this.first + this.size; + if (options.from != null && options.from > from) from = options.from; + if (options.to != null && options.to < to) to = options.to; + var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from); + if (options.sharedHist) copy.history = this.history; + (this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); + copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; + copySharedMarkers(copy, findSharedMarkers(this)); + return copy; + }, + unlinkDoc: function(other) { + if (other instanceof CodeMirror) other = other.doc; + if (this.linked) for (var i = 0; i < this.linked.length; ++i) { + var link = this.linked[i]; + if (link.doc != other) continue; + this.linked.splice(i, 1); + other.unlinkDoc(this); + detachSharedMarkers(findSharedMarkers(this)); + break; + } + // If the histories were shared, split them again + if (other.history == this.history) { + var splitIds = [other.id]; + linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true); + other.history = new History(null); + other.history.done = copyHistoryArray(this.history.done, splitIds); + other.history.undone = copyHistoryArray(this.history.undone, splitIds); + } + }, + iterLinkedDocs: function(f) {linkedDocs(this, f);}, + + getMode: function() {return this.mode;}, + getEditor: function() {return this.cm;} + }); + + // Public alias. + Doc.prototype.eachLine = Doc.prototype.iter; + + // Set up methods on CodeMirror's prototype to redirect to the editor's document. + var dontDelegate = "iter insert remove copy getEditor".split(" "); + for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) + CodeMirror.prototype[prop] = (function(method) { + return function() {return method.apply(this.doc, arguments);}; + })(Doc.prototype[prop]); + + eventMixin(Doc); + + // Call f for all linked documents. + function linkedDocs(doc, f, sharedHistOnly) { + function propagate(doc, skip, sharedHist) { + if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) { + var rel = doc.linked[i]; + if (rel.doc == skip) continue; + var shared = sharedHist && rel.sharedHist; + if (sharedHistOnly && !shared) continue; + f(rel.doc, shared); + propagate(rel.doc, doc, shared); + } + } + propagate(doc, null, true); + } + + // Attach a document to an editor. + function attachDoc(cm, doc) { + if (doc.cm) throw new Error("This document is already in use."); + cm.doc = doc; + doc.cm = cm; + estimateLineHeights(cm); + loadMode(cm); + if (!cm.options.lineWrapping) findMaxLine(cm); + cm.options.mode = doc.modeOption; + regChange(cm); + } + + // LINE UTILITIES + + // Find the line object corresponding to the given line number. + function getLine(doc, n) { + n -= doc.first; + if (n < 0 || n >= doc.size) throw new Error("There is no line " + (n + doc.first) + " in the document."); + for (var chunk = doc; !chunk.lines;) { + for (var i = 0;; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; break; } + n -= sz; + } + } + return chunk.lines[n]; + } + + // Get the part of a document between two positions, as an array of + // strings. + function getBetween(doc, start, end) { + var out = [], n = start.line; + doc.iter(start.line, end.line + 1, function(line) { + var text = line.text; + if (n == end.line) text = text.slice(0, end.ch); + if (n == start.line) text = text.slice(start.ch); + out.push(text); + ++n; + }); + return out; + } + // Get the lines between from and to, as array of strings. + function getLines(doc, from, to) { + var out = []; + doc.iter(from, to, function(line) { out.push(line.text); }); + return out; + } + + // Update the height of a line, propagating the height change + // upwards to parent nodes. + function updateLineHeight(line, height) { + var diff = height - line.height; + if (diff) for (var n = line; n; n = n.parent) n.height += diff; + } + + // Given a line object, find its line number by walking up through + // its parent links. + function lineNo(line) { + if (line.parent == null) return null; + var cur = line.parent, no = indexOf(cur.lines, line); + for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { + for (var i = 0;; ++i) { + if (chunk.children[i] == cur) break; + no += chunk.children[i].chunkSize(); + } + } + return no + cur.first; + } + + // Find the line at the given vertical position, using the height + // information in the document tree. + function lineAtHeight(chunk, h) { + var n = chunk.first; + outer: do { + for (var i = 0; i < chunk.children.length; ++i) { + var child = chunk.children[i], ch = child.height; + if (h < ch) { chunk = child; continue outer; } + h -= ch; + n += child.chunkSize(); + } + return n; + } while (!chunk.lines); + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i], lh = line.height; + if (h < lh) break; + h -= lh; + } + return n + i; + } + + + // Find the height above the given line. + function heightAtLine(lineObj) { + lineObj = visualLine(lineObj); + + var h = 0, chunk = lineObj.parent; + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i]; + if (line == lineObj) break; + else h += line.height; + } + for (var p = chunk.parent; p; chunk = p, p = chunk.parent) { + for (var i = 0; i < p.children.length; ++i) { + var cur = p.children[i]; + if (cur == chunk) break; + else h += cur.height; + } + } + return h; + } + + // Get the bidi ordering for the given line (and cache it). Returns + // false for lines that are fully left-to-right, and an array of + // BidiSpan objects otherwise. + function getOrder(line) { + var order = line.order; + if (order == null) order = line.order = bidiOrdering(line.text); + return order; + } + + // HISTORY + + function History(startGen) { + // Arrays of change events and selections. Doing something adds an + // event to done and clears undo. Undoing moves events from done + // to undone, redoing moves them in the other direction. + this.done = []; this.undone = []; + this.undoDepth = Infinity; + // Used to track when changes can be merged into a single undo + // event + this.lastModTime = this.lastSelTime = 0; + this.lastOp = this.lastSelOp = null; + this.lastOrigin = this.lastSelOrigin = null; + // Used by the isClean() method + this.generation = this.maxGeneration = startGen || 1; + } + + // Create a history change event from an updateDoc-style change + // object. + function historyChangeFromChange(doc, change) { + var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; + attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); + linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true); + return histChange; + } + + // Pop all selection events off the end of a history array. Stop at + // a change event. + function clearSelectionEvents(array) { + while (array.length) { + var last = lst(array); + if (last.ranges) array.pop(); + else break; + } + } + + // Find the top change event in the history. Pop off selection + // events that are in the way. + function lastChangeEvent(hist, force) { + if (force) { + clearSelectionEvents(hist.done); + return lst(hist.done); + } else if (hist.done.length && !lst(hist.done).ranges) { + return lst(hist.done); + } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) { + hist.done.pop(); + return lst(hist.done); + } + } + + // Register a change in the history. Merges changes that are within + // a single operation, ore are close together with an origin that + // allows merging (starting with "+") into a single event. + function addChangeToHistory(doc, change, selAfter, opId) { + var hist = doc.history; + hist.undone.length = 0; + var time = +new Date, cur; + + if ((hist.lastOp == opId || + hist.lastOrigin == change.origin && change.origin && + ((change.origin.charAt(0) == "+" && doc.cm && hist.lastModTime > time - doc.cm.options.historyEventDelay) || + change.origin.charAt(0) == "*")) && + (cur = lastChangeEvent(hist, hist.lastOp == opId))) { + // Merge this change into the last event + var last = lst(cur.changes); + if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) { + // Optimized case for simple insertion -- don't want to add + // new changesets for every character typed + last.to = changeEnd(change); + } else { + // Add new sub-event + cur.changes.push(historyChangeFromChange(doc, change)); + } + } else { + // Can not be merged, start a new event. + var before = lst(hist.done); + if (!before || !before.ranges) + pushSelectionToHistory(doc.sel, hist.done); + cur = {changes: [historyChangeFromChange(doc, change)], + generation: hist.generation}; + hist.done.push(cur); + while (hist.done.length > hist.undoDepth) { + hist.done.shift(); + if (!hist.done[0].ranges) hist.done.shift(); + } + } + hist.done.push(selAfter); + hist.generation = ++hist.maxGeneration; + hist.lastModTime = hist.lastSelTime = time; + hist.lastOp = hist.lastSelOp = opId; + hist.lastOrigin = hist.lastSelOrigin = change.origin; + + if (!last) signal(doc, "historyAdded"); + } + + function selectionEventCanBeMerged(doc, origin, prev, sel) { + var ch = origin.charAt(0); + return ch == "*" || + ch == "+" && + prev.ranges.length == sel.ranges.length && + prev.somethingSelected() == sel.somethingSelected() && + new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500); + } + + // Called whenever the selection changes, sets the new selection as + // the pending selection in the history, and pushes the old pending + // selection into the 'done' array when it was significantly + // different (in number of selected ranges, emptiness, or time). + function addSelectionToHistory(doc, sel, opId, options) { + var hist = doc.history, origin = options && options.origin; + + // A new event is started when the previous origin does not match + // the current, or the origins don't allow matching. Origins + // starting with * are always merged, those starting with + are + // merged when similar and close together in time. + if (opId == hist.lastSelOp || + (origin && hist.lastSelOrigin == origin && + (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin || + selectionEventCanBeMerged(doc, origin, lst(hist.done), sel)))) + hist.done[hist.done.length - 1] = sel; + else + pushSelectionToHistory(sel, hist.done); + + hist.lastSelTime = +new Date; + hist.lastSelOrigin = origin; + hist.lastSelOp = opId; + if (options && options.clearRedo !== false) + clearSelectionEvents(hist.undone); + } + + function pushSelectionToHistory(sel, dest) { + var top = lst(dest); + if (!(top && top.ranges && top.equals(sel))) + dest.push(sel); + } + + // Used to store marked span information in the history. + function attachLocalSpans(doc, change, from, to) { + var existing = change["spans_" + doc.id], n = 0; + doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function(line) { + if (line.markedSpans) + (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans; + ++n; + }); + } + + // When un/re-doing restores text containing marked spans, those + // that have been explicitly cleared should not be restored. + function removeClearedSpans(spans) { + if (!spans) return null; + for (var i = 0, out; i < spans.length; ++i) { + if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); } + else if (out) out.push(spans[i]); + } + return !out ? spans : out.length ? out : null; + } + + // Retrieve and filter the old marked spans stored in a change event. + function getOldSpans(doc, change) { + var found = change["spans_" + doc.id]; + if (!found) return null; + for (var i = 0, nw = []; i < change.text.length; ++i) + nw.push(removeClearedSpans(found[i])); + return nw; + } + + // Used both to provide a JSON-safe object in .getHistory, and, when + // detaching a document, to split the history in two + function copyHistoryArray(events, newGroup, instantiateSel) { + for (var i = 0, copy = []; i < events.length; ++i) { + var event = events[i]; + if (event.ranges) { + copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event); + continue; + } + var changes = event.changes, newChanges = []; + copy.push({changes: newChanges}); + for (var j = 0; j < changes.length; ++j) { + var change = changes[j], m; + newChanges.push({from: change.from, to: change.to, text: change.text}); + if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) { + if (indexOf(newGroup, Number(m[1])) > -1) { + lst(newChanges)[prop] = change[prop]; + delete change[prop]; + } + } + } + } + return copy; + } + + // Rebasing/resetting history to deal with externally-sourced changes + + function rebaseHistSelSingle(pos, from, to, diff) { + if (to < pos.line) { + pos.line += diff; + } else if (from < pos.line) { + pos.line = from; + pos.ch = 0; + } + } + + // Tries to rebase an array of history events given a change in the + // document. If the change touches the same lines as the event, the + // event, and everything 'behind' it, is discarded. If the change is + // before the event, the event's positions are updated. Uses a + // copy-on-write scheme for the positions, to avoid having to + // reallocate them all on every rebase, but also avoid problems with + // shared position objects being unsafely updated. + function rebaseHistArray(array, from, to, diff) { + for (var i = 0; i < array.length; ++i) { + var sub = array[i], ok = true; + if (sub.ranges) { + if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true; } + for (var j = 0; j < sub.ranges.length; j++) { + rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff); + rebaseHistSelSingle(sub.ranges[j].head, from, to, diff); + } + continue; + } + for (var j = 0; j < sub.changes.length; ++j) { + var cur = sub.changes[j]; + if (to < cur.from.line) { + cur.from = Pos(cur.from.line + diff, cur.from.ch); + cur.to = Pos(cur.to.line + diff, cur.to.ch); + } else if (from <= cur.to.line) { + ok = false; + break; + } + } + if (!ok) { + array.splice(0, i + 1); + i = 0; + } + } + } + + function rebaseHist(hist, change) { + var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1; + rebaseHistArray(hist.done, from, to, diff); + rebaseHistArray(hist.undone, from, to, diff); + } + + // EVENT UTILITIES + + // Due to the fact that we still support jurassic IE versions, some + // compatibility wrappers are needed. + + var e_preventDefault = CodeMirror.e_preventDefault = function(e) { + if (e.preventDefault) e.preventDefault(); + else e.returnValue = false; + }; + var e_stopPropagation = CodeMirror.e_stopPropagation = function(e) { + if (e.stopPropagation) e.stopPropagation(); + else e.cancelBubble = true; + }; + function e_defaultPrevented(e) { + return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false; + } + var e_stop = CodeMirror.e_stop = function(e) {e_preventDefault(e); e_stopPropagation(e);}; + + function e_target(e) {return e.target || e.srcElement;} + function e_button(e) { + var b = e.which; + if (b == null) { + if (e.button & 1) b = 1; + else if (e.button & 2) b = 3; + else if (e.button & 4) b = 2; + } + if (mac && e.ctrlKey && b == 1) b = 3; + return b; + } + + // EVENT HANDLING + + // Lightweight event framework. on/off also work on DOM nodes, + // registering native DOM handlers. + + var on = CodeMirror.on = function(emitter, type, f) { + if (emitter.addEventListener) + emitter.addEventListener(type, f, false); + else if (emitter.attachEvent) + emitter.attachEvent("on" + type, f); + else { + var map = emitter._handlers || (emitter._handlers = {}); + var arr = map[type] || (map[type] = []); + arr.push(f); + } + }; + + var off = CodeMirror.off = function(emitter, type, f) { + if (emitter.removeEventListener) + emitter.removeEventListener(type, f, false); + else if (emitter.detachEvent) + emitter.detachEvent("on" + type, f); + else { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + for (var i = 0; i < arr.length; ++i) + if (arr[i] == f) { arr.splice(i, 1); break; } + } + }; + + var signal = CodeMirror.signal = function(emitter, type /*, values...*/) { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + var args = Array.prototype.slice.call(arguments, 2); + for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args); + }; + + var orphanDelayedCallbacks = null; + + // Often, we want to signal events at a point where we are in the + // middle of some work, but don't want the handler to start calling + // other methods on the editor, which might be in an inconsistent + // state or simply not expect any other events to happen. + // signalLater looks whether there are any handlers, and schedules + // them to be executed when the last operation ends, or, if no + // operation is active, when a timeout fires. + function signalLater(emitter, type /*, values...*/) { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + var args = Array.prototype.slice.call(arguments, 2), list; + if (operationGroup) { + list = operationGroup.delayedCallbacks; + } else if (orphanDelayedCallbacks) { + list = orphanDelayedCallbacks; + } else { + list = orphanDelayedCallbacks = []; + setTimeout(fireOrphanDelayed, 0); + } + function bnd(f) {return function(){f.apply(null, args);};}; + for (var i = 0; i < arr.length; ++i) + list.push(bnd(arr[i])); + } + + function fireOrphanDelayed() { + var delayed = orphanDelayedCallbacks; + orphanDelayedCallbacks = null; + for (var i = 0; i < delayed.length; ++i) delayed[i](); + } + + // The DOM events that CodeMirror handles can be overridden by + // registering a (non-DOM) handler on the editor for the event name, + // and preventDefault-ing the event in that handler. + function signalDOMEvent(cm, e, override) { + if (typeof e == "string") + e = {type: e, preventDefault: function() { this.defaultPrevented = true; }}; + signal(cm, override || e.type, cm, e); + return e_defaultPrevented(e) || e.codemirrorIgnore; + } + + function signalCursorActivity(cm) { + var arr = cm._handlers && cm._handlers.cursorActivity; + if (!arr) return; + var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []); + for (var i = 0; i < arr.length; ++i) if (indexOf(set, arr[i]) == -1) + set.push(arr[i]); + } + + function hasHandler(emitter, type) { + var arr = emitter._handlers && emitter._handlers[type]; + return arr && arr.length > 0; + } + + // Add on and off methods to a constructor's prototype, to make + // registering events on such objects more convenient. + function eventMixin(ctor) { + ctor.prototype.on = function(type, f) {on(this, type, f);}; + ctor.prototype.off = function(type, f) {off(this, type, f);}; + } + + // MISC UTILITIES + + // Number of pixels added to scroller and sizer to hide scrollbar + var scrollerGap = 30; + + // Returned or thrown by various protocols to signal 'I'm not + // handling this'. + var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}}; + + // Reused option objects for setSelection & friends + var sel_dontScroll = {scroll: false}, sel_mouse = {origin: "*mouse"}, sel_move = {origin: "+move"}; + + function Delayed() {this.id = null;} + Delayed.prototype.set = function(ms, f) { + clearTimeout(this.id); + this.id = setTimeout(f, ms); + }; + + // Counts the column offset in a string, taking tabs into account. + // Used mostly to find indentation. + var countColumn = CodeMirror.countColumn = function(string, end, tabSize, startIndex, startValue) { + if (end == null) { + end = string.search(/[^\s\u00a0]/); + if (end == -1) end = string.length; + } + for (var i = startIndex || 0, n = startValue || 0;;) { + var nextTab = string.indexOf("\t", i); + if (nextTab < 0 || nextTab >= end) + return n + (end - i); + n += nextTab - i; + n += tabSize - (n % tabSize); + i = nextTab + 1; + } + }; + + // The inverse of countColumn -- find the offset that corresponds to + // a particular column. + function findColumn(string, goal, tabSize) { + for (var pos = 0, col = 0;;) { + var nextTab = string.indexOf("\t", pos); + if (nextTab == -1) nextTab = string.length; + var skipped = nextTab - pos; + if (nextTab == string.length || col + skipped >= goal) + return pos + Math.min(skipped, goal - col); + col += nextTab - pos; + col += tabSize - (col % tabSize); + pos = nextTab + 1; + if (col >= goal) return pos; + } + } + + var spaceStrs = [""]; + function spaceStr(n) { + while (spaceStrs.length <= n) + spaceStrs.push(lst(spaceStrs) + " "); + return spaceStrs[n]; + } + + function lst(arr) { return arr[arr.length-1]; } + + var selectInput = function(node) { node.select(); }; + if (ios) // Mobile Safari apparently has a bug where select() is broken. + selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length; }; + else if (ie) // Suppress mysterious IE10 errors + selectInput = function(node) { try { node.select(); } catch(_e) {} }; + + function indexOf(array, elt) { + for (var i = 0; i < array.length; ++i) + if (array[i] == elt) return i; + return -1; + } + function map(array, f) { + var out = []; + for (var i = 0; i < array.length; i++) out[i] = f(array[i], i); + return out; + } + + function nothing() {} + + function createObj(base, props) { + var inst; + if (Object.create) { + inst = Object.create(base); + } else { + nothing.prototype = base; + inst = new nothing(); + } + if (props) copyObj(props, inst); + return inst; + }; + + function copyObj(obj, target, overwrite) { + if (!target) target = {}; + for (var prop in obj) + if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop))) + target[prop] = obj[prop]; + return target; + } + + function bind(f) { + var args = Array.prototype.slice.call(arguments, 1); + return function(){return f.apply(null, args);}; + } + + var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; + var isWordCharBasic = CodeMirror.isWordChar = function(ch) { + return /\w/.test(ch) || ch > "\x80" && + (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)); + }; + function isWordChar(ch, helper) { + if (!helper) return isWordCharBasic(ch); + if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) return true; + return helper.test(ch); + } + + function isEmpty(obj) { + for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false; + return true; + } + + // Extending unicode characters. A series of a non-extending char + + // any number of extending chars is treated as a single unit as far + // as editing and measuring is concerned. This is not fully correct, + // since some scripts/fonts/browsers also treat other configurations + // of code points as a group. + var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/; + function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch); } + + // DOM UTILITIES + + function elt(tag, content, className, style) { + var e = document.createElement(tag); + if (className) e.className = className; + if (style) e.style.cssText = style; + if (typeof content == "string") e.appendChild(document.createTextNode(content)); + else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]); + return e; + } + + var range; + if (document.createRange) range = function(node, start, end, endNode) { + var r = document.createRange(); + r.setEnd(endNode || node, end); + r.setStart(node, start); + return r; + }; + else range = function(node, start, end) { + var r = document.body.createTextRange(); + try { r.moveToElementText(node.parentNode); } + catch(e) { return r; } + r.collapse(true); + r.moveEnd("character", end); + r.moveStart("character", start); + return r; + }; + + function removeChildren(e) { + for (var count = e.childNodes.length; count > 0; --count) + e.removeChild(e.firstChild); + return e; + } + + function removeChildrenAndAdd(parent, e) { + return removeChildren(parent).appendChild(e); + } + + var contains = CodeMirror.contains = function(parent, child) { + if (child.nodeType == 3) // Android browser always returns false when child is a textnode + child = child.parentNode; + if (parent.contains) + return parent.contains(child); + do { + if (child.nodeType == 11) child = child.host; + if (child == parent) return true; + } while (child = child.parentNode); + }; + + function activeElt() { return document.activeElement; } + // Older versions of IE throws unspecified error when touching + // document.activeElement in some cases (during loading, in iframe) + if (ie && ie_version < 11) activeElt = function() { + try { return document.activeElement; } + catch(e) { return document.body; } + }; + + function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*"); } + var rmClass = CodeMirror.rmClass = function(node, cls) { + var current = node.className; + var match = classTest(cls).exec(current); + if (match) { + var after = current.slice(match.index + match[0].length); + node.className = current.slice(0, match.index) + (after ? match[1] + after : ""); + } + }; + var addClass = CodeMirror.addClass = function(node, cls) { + var current = node.className; + if (!classTest(cls).test(current)) node.className += (current ? " " : "") + cls; + }; + function joinClasses(a, b) { + var as = a.split(" "); + for (var i = 0; i < as.length; i++) + if (as[i] && !classTest(as[i]).test(b)) b += " " + as[i]; + return b; + } + + // WINDOW-WIDE EVENTS + + // These must be handled carefully, because naively registering a + // handler for each editor will cause the editors to never be + // garbage collected. + + function forEachCodeMirror(f) { + if (!document.body.getElementsByClassName) return; + var byClass = document.body.getElementsByClassName("CodeMirror"); + for (var i = 0; i < byClass.length; i++) { + var cm = byClass[i].CodeMirror; + if (cm) f(cm); + } + } + + var globalsRegistered = false; + function ensureGlobalHandlers() { + if (globalsRegistered) return; + registerGlobalHandlers(); + globalsRegistered = true; + } + function registerGlobalHandlers() { + // When the window resizes, we need to refresh active editors. + var resizeTimer; + on(window, "resize", function() { + if (resizeTimer == null) resizeTimer = setTimeout(function() { + resizeTimer = null; + forEachCodeMirror(onResize); + }, 100); + }); + // When the window loses focus, we want to show the editor as blurred + on(window, "blur", function() { + forEachCodeMirror(onBlur); + }); + } + + // FEATURE DETECTION + + // Detect drag-and-drop + var dragAndDrop = function() { + // There is *some* kind of drag-and-drop support in IE6-8, but I + // couldn't get it to work yet. + if (ie && ie_version < 9) return false; + var div = elt('div'); + return "draggable" in div || "dragDrop" in div; + }(); + + var zwspSupported; + function zeroWidthElement(measure) { + if (zwspSupported == null) { + var test = elt("span", "\u200b"); + removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])); + if (measure.firstChild.offsetHeight != 0) + zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8); + } + var node = zwspSupported ? elt("span", "\u200b") : + elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); + node.setAttribute("cm-text", ""); + return node; + } + + // Feature-detect IE's crummy client rect reporting for bidi text + var badBidiRects; + function hasBadBidiRects(measure) { + if (badBidiRects != null) return badBidiRects; + var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA")); + var r0 = range(txt, 0, 1).getBoundingClientRect(); + if (!r0 || r0.left == r0.right) return false; // Safari returns null in some cases (#2780) + var r1 = range(txt, 1, 2).getBoundingClientRect(); + return badBidiRects = (r1.right - r0.right < 3); + } + + // See if "".split is the broken IE version, if so, provide an + // alternative way to split lines. + var splitLines = CodeMirror.splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { + var pos = 0, result = [], l = string.length; + while (pos <= l) { + var nl = string.indexOf("\n", pos); + if (nl == -1) nl = string.length; + var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl); + var rt = line.indexOf("\r"); + if (rt != -1) { + result.push(line.slice(0, rt)); + pos += rt + 1; + } else { + result.push(line); + pos = nl + 1; + } + } + return result; + } : function(string){return string.split(/\r\n?|\n/);}; + + var hasSelection = window.getSelection ? function(te) { + try { return te.selectionStart != te.selectionEnd; } + catch(e) { return false; } + } : function(te) { + try {var range = te.ownerDocument.selection.createRange();} + catch(e) {} + if (!range || range.parentElement() != te) return false; + return range.compareEndPoints("StartToEnd", range) != 0; + }; + + var hasCopyEvent = (function() { + var e = elt("div"); + if ("oncopy" in e) return true; + e.setAttribute("oncopy", "return;"); + return typeof e.oncopy == "function"; + })(); + + var badZoomedRects = null; + function hasBadZoomedRects(measure) { + if (badZoomedRects != null) return badZoomedRects; + var node = removeChildrenAndAdd(measure, elt("span", "x")); + var normal = node.getBoundingClientRect(); + var fromRange = range(node, 0, 1).getBoundingClientRect(); + return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1; + } + + // KEY NAMES + + var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", + 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", + 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", + 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", 107: "=", 109: "-", 127: "Delete", + 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", + 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", + 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"}; + CodeMirror.keyNames = keyNames; + (function() { + // Number keys + for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i); + // Alphabetic keys + for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i); + // Function keys + for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i; + })(); + + // BIDI HELPERS + + function iterateBidiSections(order, from, to, f) { + if (!order) return f(from, to, "ltr"); + var found = false; + for (var i = 0; i < order.length; ++i) { + var part = order[i]; + if (part.from < to && part.to > from || from == to && part.to == from) { + f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr"); + found = true; + } + } + if (!found) f(from, to, "ltr"); + } + + function bidiLeft(part) { return part.level % 2 ? part.to : part.from; } + function bidiRight(part) { return part.level % 2 ? part.from : part.to; } + + function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; } + function lineRight(line) { + var order = getOrder(line); + if (!order) return line.text.length; + return bidiRight(lst(order)); + } + + function lineStart(cm, lineN) { + var line = getLine(cm.doc, lineN); + var visual = visualLine(line); + if (visual != line) lineN = lineNo(visual); + var order = getOrder(visual); + var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual); + return Pos(lineN, ch); + } + function lineEnd(cm, lineN) { + var merged, line = getLine(cm.doc, lineN); + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line; + lineN = null; + } + var order = getOrder(line); + var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line); + return Pos(lineN == null ? lineNo(line) : lineN, ch); + } + function lineStartSmart(cm, pos) { + var start = lineStart(cm, pos.line); + var line = getLine(cm.doc, start.line); + var order = getOrder(line); + if (!order || order[0].level == 0) { + var firstNonWS = Math.max(0, line.text.search(/\S/)); + var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch; + return Pos(start.line, inWS ? 0 : firstNonWS); + } + return start; + } + + function compareBidiLevel(order, a, b) { + var linedir = order[0].level; + if (a == linedir) return true; + if (b == linedir) return false; + return a < b; + } + var bidiOther; + function getBidiPartAt(order, pos) { + bidiOther = null; + for (var i = 0, found; i < order.length; ++i) { + var cur = order[i]; + if (cur.from < pos && cur.to > pos) return i; + if ((cur.from == pos || cur.to == pos)) { + if (found == null) { + found = i; + } else if (compareBidiLevel(order, cur.level, order[found].level)) { + if (cur.from != cur.to) bidiOther = found; + return i; + } else { + if (cur.from != cur.to) bidiOther = i; + return found; + } + } + } + return found; + } + + function moveInLine(line, pos, dir, byUnit) { + if (!byUnit) return pos + dir; + do pos += dir; + while (pos > 0 && isExtendingChar(line.text.charAt(pos))); + return pos; + } + + // This is needed in order to move 'visually' through bi-directional + // text -- i.e., pressing left should make the cursor go left, even + // when in RTL text. The tricky part is the 'jumps', where RTL and + // LTR text touch each other. This often requires the cursor offset + // to move more than one unit, in order to visually move one unit. + function moveVisually(line, start, dir, byUnit) { + var bidi = getOrder(line); + if (!bidi) return moveLogically(line, start, dir, byUnit); + var pos = getBidiPartAt(bidi, start), part = bidi[pos]; + var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit); + + for (;;) { + if (target > part.from && target < part.to) return target; + if (target == part.from || target == part.to) { + if (getBidiPartAt(bidi, target) == pos) return target; + part = bidi[pos += dir]; + return (dir > 0) == part.level % 2 ? part.to : part.from; + } else { + part = bidi[pos += dir]; + if (!part) return null; + if ((dir > 0) == part.level % 2) + target = moveInLine(line, part.to, -1, byUnit); + else + target = moveInLine(line, part.from, 1, byUnit); + } + } + } + + function moveLogically(line, start, dir, byUnit) { + var target = start + dir; + if (byUnit) while (target > 0 && isExtendingChar(line.text.charAt(target))) target += dir; + return target < 0 || target > line.text.length ? null : target; + } + + // Bidirectional ordering algorithm + // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm + // that this (partially) implements. + + // One-char codes used for character types: + // L (L): Left-to-Right + // R (R): Right-to-Left + // r (AL): Right-to-Left Arabic + // 1 (EN): European Number + // + (ES): European Number Separator + // % (ET): European Number Terminator + // n (AN): Arabic Number + // , (CS): Common Number Separator + // m (NSM): Non-Spacing Mark + // b (BN): Boundary Neutral + // s (B): Paragraph Separator + // t (S): Segment Separator + // w (WS): Whitespace + // N (ON): Other Neutrals + + // Returns null if characters are ordered as they appear + // (left-to-right), or an array of sections ({from, to, level} + // objects) in the order in which they occur visually. + var bidiOrdering = (function() { + // Character types for codepoints 0 to 0xff + var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"; + // Character types for codepoints 0x600 to 0x6ff + var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmm"; + function charType(code) { + if (code <= 0xf7) return lowTypes.charAt(code); + else if (0x590 <= code && code <= 0x5f4) return "R"; + else if (0x600 <= code && code <= 0x6ed) return arabicTypes.charAt(code - 0x600); + else if (0x6ee <= code && code <= 0x8ac) return "r"; + else if (0x2000 <= code && code <= 0x200b) return "w"; + else if (code == 0x200c) return "b"; + else return "L"; + } + + var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/; + var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/; + // Browsers seem to always treat the boundaries of block elements as being L. + var outerType = "L"; + + function BidiSpan(level, from, to) { + this.level = level; + this.from = from; this.to = to; + } + + return function(str) { + if (!bidiRE.test(str)) return false; + var len = str.length, types = []; + for (var i = 0, type; i < len; ++i) + types.push(type = charType(str.charCodeAt(i))); + + // W1. Examine each non-spacing mark (NSM) in the level run, and + // change the type of the NSM to the type of the previous + // character. If the NSM is at the start of the level run, it will + // get the type of sor. + for (var i = 0, prev = outerType; i < len; ++i) { + var type = types[i]; + if (type == "m") types[i] = prev; + else prev = type; + } + + // W2. Search backwards from each instance of a European number + // until the first strong type (R, L, AL, or sor) is found. If an + // AL is found, change the type of the European number to Arabic + // number. + // W3. Change all ALs to R. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (type == "1" && cur == "r") types[i] = "n"; + else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; } + } + + // W4. A single European separator between two European numbers + // changes to a European number. A single common separator between + // two numbers of the same type changes to that type. + for (var i = 1, prev = types[0]; i < len - 1; ++i) { + var type = types[i]; + if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1"; + else if (type == "," && prev == types[i+1] && + (prev == "1" || prev == "n")) types[i] = prev; + prev = type; + } + + // W5. A sequence of European terminators adjacent to European + // numbers changes to all European numbers. + // W6. Otherwise, separators and terminators change to Other + // Neutral. + for (var i = 0; i < len; ++i) { + var type = types[i]; + if (type == ",") types[i] = "N"; + else if (type == "%") { + for (var end = i + 1; end < len && types[end] == "%"; ++end) {} + var replace = (i && types[i-1] == "!") || (end < len && types[end] == "1") ? "1" : "N"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // W7. Search backwards from each instance of a European number + // until the first strong type (R, L, or sor) is found. If an L is + // found, then change the type of the European number to L. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (cur == "L" && type == "1") types[i] = "L"; + else if (isStrong.test(type)) cur = type; + } + + // N1. A sequence of neutrals takes the direction of the + // surrounding strong text if the text on both sides has the same + // direction. European and Arabic numbers act as if they were R in + // terms of their influence on neutrals. Start-of-level-run (sor) + // and end-of-level-run (eor) are used at level run boundaries. + // N2. Any remaining neutrals take the embedding direction. + for (var i = 0; i < len; ++i) { + if (isNeutral.test(types[i])) { + for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {} + var before = (i ? types[i-1] : outerType) == "L"; + var after = (end < len ? types[end] : outerType) == "L"; + var replace = before || after ? "L" : "R"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // Here we depart from the documented algorithm, in order to avoid + // building up an actual levels array. Since there are only three + // levels (0, 1, 2) in an implementation that doesn't take + // explicit embedding into account, we can build up the order on + // the fly, without following the level-based algorithm. + var order = [], m; + for (var i = 0; i < len;) { + if (countsAsLeft.test(types[i])) { + var start = i; + for (++i; i < len && countsAsLeft.test(types[i]); ++i) {} + order.push(new BidiSpan(0, start, i)); + } else { + var pos = i, at = order.length; + for (++i; i < len && types[i] != "L"; ++i) {} + for (var j = pos; j < i;) { + if (countsAsNum.test(types[j])) { + if (pos < j) order.splice(at, 0, new BidiSpan(1, pos, j)); + var nstart = j; + for (++j; j < i && countsAsNum.test(types[j]); ++j) {} + order.splice(at, 0, new BidiSpan(2, nstart, j)); + pos = j; + } else ++j; + } + if (pos < i) order.splice(at, 0, new BidiSpan(1, pos, i)); + } + } + if (order[0].level == 1 && (m = str.match(/^\s+/))) { + order[0].from = m[0].length; + order.unshift(new BidiSpan(0, 0, m[0].length)); + } + if (lst(order).level == 1 && (m = str.match(/\s+$/))) { + lst(order).to -= m[0].length; + order.push(new BidiSpan(0, len - m[0].length, len)); + } + if (order[0].level != lst(order).level) + order.push(new BidiSpan(order[0].level, len, len)); + + return order; + }; + })(); + + // THE END + + CodeMirror.version = "5.1.1"; + + return CodeMirror; +}); \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/public/js/policy-create.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/public/js/policy-create.js new file mode 100644 index 000000000..84e988643 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/public/js/policy-create.js @@ -0,0 +1,670 @@ +/* + * 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. + */ + +var validateStep = {}; +var stepForwardFrom = {}; +var stepBackFrom = {}; +var policy = {}; +var configuredOperations = []; +var deviceTypeLabel; + +/** + * Method to update the visibility of grouped input. + * @param domElement HTML grouped-input element with class name "grouped-input" + */ +var updateGroupedInputVisibility = function (domElement) { + if ($(".parent-input:first", domElement).is(":checked")) { + if ($(".grouped-child-input:first", domElement).hasClass("disabled")) { + $(".grouped-child-input:first", domElement).removeClass("disabled"); + } + $(".child-input", domElement).each(function () { + $(this).prop('disabled', false); + }); + } else { + if (!$(".grouped-child-input:first", domElement).hasClass("disabled")) { + $(".grouped-child-input:first", domElement).addClass("disabled"); + } + $(".child-input", domElement).each(function () { + $(this).prop('disabled', true); + }); + } +}; + +/** + * Checks if provided number is valid against a range. + * + * @param numberInput Number Input + * @param min Minimum Limit + * @param max Maximum Limit + * @returns {boolean} Returns true if input is within the specified range + */ +var inputIsValidAgainstRange = function (numberInput, min, max) { + return (numberInput == min || (numberInput > min && numberInput < max) || numberInput == max); +}; + +/** + * Checks if provided input is valid against RegEx input. + * + * @param regExp Regular expression + * @param input Input string to check + * @returns {boolean} Returns true if input matches RegEx + */ +var inputIsValidAgainstRegExp = function (regExp, input) { + return regExp.test(input); +}; + +validateStep["policy-profile"] = function () { + return true; +}; + +stepForwardFrom["policy-profile"] = function () { + policy["profile"] = operationModule.generateProfile(policy["platform"], configuredOperations); + // updating next-page wizard title with selected platform + $("#policy-criteria-page-wizard-title").text("ADD " + deviceTypeLabel + " POLICY"); +}; + +stepBackFrom["policy-profile"] = function () { + // reinitialize configuredOperations + configuredOperations = []; + // clearing already-loaded platform specific hidden-operations html content from the relevant div + // so that, the wrong content would not be shown at the first glance, in case + // the user selects a different platform + $(".wr-advance-operations").html( + "
      " + + "
      " + + "     " + + "" + + "    " + + "Loading Platform Features . . ." + + "
      " + + "
      " + + "
      " + ); +}; + +stepForwardFrom["policy-criteria"] = function () { + $("input[type='radio'].select-users-radio").each(function () { + if ($(this).is(':radio')) { + if ($(this).is(":checked")) { + if ($(this).attr("id") == "users-radio-btn") { + policy["selectedUsers"] = $("#users-input").val(); + } else if ($(this).attr("id") == "user-roles-radio-btn") { + policy["selectedUserRoles"] = $("#user-roles-input").val(); + } + } + } + }); + policy["selectedNonCompliantAction"] = $("#action-input").find(":selected").data("action"); + policy["selectedOwnership"] = $("#ownership-input").val(); + // updating next-page wizard title with selected platform + $("#policy-naming-page-wizard-title").text("ADD " + deviceTypeLabel + " POLICY"); +}; + +/** + * Checks if provided input is valid against provided length range. + * + * @param input Alphanumeric or non-alphanumeric input + * @param minLength Minimum Required Length + * @param maxLength Maximum Required Length + * @returns {boolean} Returns true if input matches the provided minimum length and maximum length + */ +var inputIsValidAgainstLength = function (input, minLength, maxLength) { + var length = input.length; + return (length == minLength || (length > minLength && length < maxLength) || length == maxLength); +}; + +validateStep["policy-naming"] = function () { + var validationStatus = {}; + + // taking values of inputs to be validated + var policyName = $("input#policy-name-input").val(); + // starting validation process and updating validationStatus + if (!policyName) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = "Policy name is empty. You cannot proceed."; + } else if (!inputIsValidAgainstLength(policyName, 1, 30)) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = + "Policy name exceeds maximum allowed length. Please check."; + } else { + validationStatus["error"] = false; + } + // ending validation process + + // start taking specific actions upon validation + var wizardIsToBeContinued; + if (validationStatus["error"]) { + wizardIsToBeContinued = false; + var mainErrorMsgWrapper = "#policy-naming-main-error-msg"; + var mainErrorMsg = mainErrorMsgWrapper + " span"; + $(mainErrorMsg).text(validationStatus["mainErrorMsg"]); + $(mainErrorMsgWrapper).removeClass("hidden"); + } else { + wizardIsToBeContinued = true; + } + + return wizardIsToBeContinued; +}; + +validateStep["policy-naming-publish"] = function () { + var validationStatus = {}; + + // taking values of inputs to be validated + var policyName = $("input#policy-name-input").val(); + // starting validation process and updating validationStatus + if (!policyName) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = "Policy name is empty. You cannot proceed."; + } else if (!inputIsValidAgainstLength(policyName, 1, 30)) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = + "Policy name exceeds maximum allowed length. Please check."; + } else { + validationStatus["error"] = false; + } + // ending validation process + + // start taking specific actions upon validation + var wizardIsToBeContinued; + if (validationStatus["error"]) { + wizardIsToBeContinued = false; + var mainErrorMsgWrapper = "#policy-naming-main-error-msg"; + var mainErrorMsg = mainErrorMsgWrapper + " span"; + $(mainErrorMsg).text(validationStatus["mainErrorMsg"]); + $(mainErrorMsgWrapper).removeClass("hidden"); + } else { + wizardIsToBeContinued = true; + } + + return wizardIsToBeContinued; +}; + +stepForwardFrom["policy-naming-publish"] = function () { + policy["policyName"] = $("#policy-name-input").val(); + policy["description"] = $("#policy-description-input").val(); + //All data is collected. Policy can now be updated. + savePolicy(policy, "publish"); +}; +stepForwardFrom["policy-naming"] = function () { + policy["policyName"] = $("#policy-name-input").val(); + policy["description"] = $("#policy-description-input").val(); + //All data is collected. Policy can now be updated. + savePolicy(policy, "save"); +}; + +var savePolicy = function (policy, state) { + var profilePayloads = [{ + "featureCode": "CONFIG", + "deviceTypeId": policy["platformId"], + "content": {"policyDefinition": window.queryEditor.getValue()} + }]; + + var payload = { + "policyName": policy["policyName"], + "description": policy["description"], + "compliance": policy["selectedNonCompliantAction"], + "ownershipType": "ANY", + "profile": { + "profileName": policy["policyName"], + "deviceType": { + "id": policy["platformId"], + "name": policy["platform"] + }, + "profileFeaturesList": profilePayloads + } + }; + + if (policy["selectedUsers"]) { + payload["users"] = policy["selectedUsers"]; + } else if (policy["selectedUserRoles"]) { + payload["roles"] = policy["selectedUserRoles"]; + } else { + payload["users"] = []; + payload["roles"] = []; + } + + var serviceURL; + if (state == "save") { + serviceURL = "/devicemgt_admin/policies/inactive-policy" + } else if (state == "publish") { + serviceURL = "/devicemgt_admin/policies/active-policy" + } + invokerUtil.post( + serviceURL, + payload, + function () { + $(".add-policy").addClass("hidden"); + $(".policy-naming").addClass("hidden"); + $(".policy-message").removeClass("hidden"); + if (state == "publish") { + publishToDevice(); + } + }, + function (err) { + console.log(err); + } + ); +}; + +function publishToDevice() { + var payload = { + "policyName": policy["policyName"], + "description": policy["description"], + "compliance": policy["selectedNonCompliantAction"], + "ownershipType": "ANY", + "deviceId": getParameterByName('deviceId'), + "profile": { + "profileName": policy["policyName"], + "deviceType": { + "id": policy["platformId"], + "name": policy["platform"] + }, + "policyDefinition": window.queryEditor.getValue(), + "policyDescription": policy["description"] + } + }; + + var successCallback = function (data, status) { + console.log("Data: " + data + "\nStatus: " + status); + }; + + var data = { + url: "/devicemgt/api/policies/add", + type: "POST", + contentType: "application/json", + accept: "application/json", + success: successCallback, + data: JSON.stringify(payload) + }; + + $.ajax(data).fail(function (jqXHR) { + console.log("Error: " + jqXHR); + }); + +} + +// Start of functions related to grid-input-view + +/** + * Method to set count id to cloned elements. + * @param {object} addFormContainer + */ +var setId = function (addFormContainer) { + $(addFormContainer).find("[data-add-form-clone]").each(function (i) { + $(this).attr("id", $(this).attr("data-add-form-clone").slice(1) + "-" + (i + 1)); + if ($(this).find(".index").length > 0) { + $(this).find(".index").html(i + 1); + } + }); +}; + +/** + * Method to set count id to cloned elements. + * @param {object} addFormContainer + */ +var showHideHelpText = function (addFormContainer) { + var helpText = "[data-help-text=add-form]"; + if ($(addFormContainer).find("[data-add-form-clone]").length > 0) { + $(addFormContainer).find(helpText).hide(); + } else { + $(addFormContainer).find(helpText).show(); + } +}; + +function formatRepo(user) { + if (user.loading) { + return user.text + } + if (!user.username) { + return; + } + var markup = '
      ' + + '
      ' + + '
      ' + + '
      ' + user.username + '
      '; + if (user.firstname) { + markup += '
      ' + user.firstname + '
      '; + } + if (user.emailAddress) { + markup += '
      ' + user.emailAddress + '
      '; + } + markup += '
      '; + return markup; +} + +function formatRepoSelection(user) { + return user.username || user.text; +} + +// End of functions related to grid-input-view + + +$(document).ready(function () { + window.queryEditor = CodeMirror.fromTextArea(document.getElementById('policy-definition-input'), { + mode: MIME_TYPE_SIDDHI_QL, + indentWithTabs: true, + smartIndent: true, + lineNumbers: true, + matchBrackets: true, + autofocus: true, + extraKeys: { + "Shift-2": function (cm) { + insertStr(cm, cm.getCursor(), '@'); + CodeMirror.showHint(cm, getAnnotationHints); + }, + "Ctrl-Space": "autocomplete" + } + }); + + $("#users-input").select2({ + multiple: true, + tags: true, + ajax: { + url: window.location.origin + "/devicemgt/api/invoker/execute/", + method: "POST", + dataType: 'json', + delay: 250, + id: function (user) { + return user.username; + }, + data: function (params) { + var postData = {}; + postData.actionMethod = "GET"; + postData.actionUrl = "/devicemgt_admin/users"; + postData.actionPayload = JSON.stringify({ + q: params.term, // search term + page: params.page + }); + + return JSON.stringify(postData); + }, + processResults: function (data, page) { + var newData = []; + $.each(data.responseContent, function (index, value) { + value.id = value.username; + newData.push(value); + }); + return { + results: newData + }; + }, + cache: true + }, + escapeMarkup: function (markup) { + return markup; + }, // let our custom formatter work + minimumInputLength: 1, + templateResult: formatRepo, // omitted for brevity, see the source of this page + templateSelection: formatRepoSelection // omitted for brevity, see the source of this page + }); + + // Adding initial state of wizard-steps. + $("#policy-profile-wizard-steps").html($(".wr-steps").html()); + + policy["platform"] = $("#platform").data("platform"); + policy["platformId"] = $("#platform").data("platform-id"); + deviceTypeLabel = $("#platform").data("platform-label"); + // updating next-page wizard title with selected platform + $("#policy-profile-page-wizard-title").text("ADD " + deviceTypeLabel + " POLICY"); + + $("select.select2[multiple=multiple]").select2({ + "tags": true + }); + + $("#users-select-field").hide(); + $("#user-roles-select-field").show(); + + $("input[type='radio'].select-users-radio").change(function () { + if ($("#users-radio-btn").is(":checked")) { + $("#user-roles-select-field").hide(); + $("#users-select-field").show(); + } + if ($("#user-roles-radio-btn").is(":checked")) { + $("#users-select-field").hide(); + $("#user-roles-select-field").show(); + } + }); + + // Support for special input type "ANY" on user(s) & user-role(s) selection + $("#user-roles-input").select2({ + "tags": true + }).on("select2:select", function (e) { + if (e.params.data.id == "ANY") { + $(this).val("ANY").trigger("change"); + } else { + $("option[value=ANY]", this).prop("selected", false).parent().trigger("change"); + } + }); + + // Maintains an array of configured features of the profile + var advanceOperations = ".wr-advance-operations"; + $(advanceOperations).on("click", ".wr-input-control.switch", function (event) { + var operationCode = $(this).parents(".operation-data").data("operation-code"); + var operation = $(this).parents(".operation-data").data("operation"); + var operationDataWrapper = $(this).data("target"); + // prevents event bubbling by figuring out what element it's being called from. + if (event.target.tagName == "INPUT") { + var featureConfiguredIcon; + if ($("input[type='checkbox']", this).is(":checked")) { + configuredOperations.push(operationCode); + // when a feature is enabled, if "zero-configured-features" msg is available, hide that. + var zeroConfiguredOperationsErrorMsg = "#policy-profile-main-error-msg"; + if (!$(zeroConfiguredOperationsErrorMsg).hasClass("hidden")) { + $(zeroConfiguredOperationsErrorMsg).addClass("hidden"); + } + // add configured-state-icon to the feature + featureConfiguredIcon = "#" + operation + "-configured"; + if ($(featureConfiguredIcon).hasClass("hidden")) { + $(featureConfiguredIcon).removeClass("hidden"); + } + } else { + //splicing the array if operation is present. + var index = $.inArray(operationCode, configuredOperations); + if (index != -1) { + configuredOperations.splice(index, 1); + } + // when a feature is disabled, clearing all its current configured, error or success states + var subErrorMsgWrapper = "#" + operation + "-feature-error-msg"; + var subErrorIcon = "#" + operation + "-error"; + var subOkIcon = "#" + operation + "-ok"; + featureConfiguredIcon = "#" + operation + "-configured"; + + if (!$(subErrorMsgWrapper).hasClass("hidden")) { + $(subErrorMsgWrapper).addClass("hidden"); + } + if (!$(subErrorIcon).hasClass("hidden")) { + $(subErrorIcon).addClass("hidden"); + } + if (!$(subOkIcon).hasClass("hidden")) { + $(subOkIcon).addClass("hidden"); + } + if (!$(featureConfiguredIcon).hasClass("hidden")) { + $(featureConfiguredIcon).addClass("hidden"); + } + // reinitializing input fields into the defaults + $(operationDataWrapper + " input").each( + function () { + if ($(this).is("input:text")) { + $(this).val($(this).data("default")); + } else if ($(this).is("input:password")) { + $(this).val(""); + } else if ($(this).is("input:checkbox")) { + $(this).prop("checked", $(this).data("default")); + // if this checkbox is the parent input of a grouped-input + if ($(this).hasClass("parent-input")) { + var groupedInput = $(this).parent().parent().parent(); + updateGroupedInputVisibility(groupedInput); + } + } + } + ); + // reinitializing select fields into the defaults + $(operationDataWrapper + " select").each( + function () { + var defaultOption = $(this).data("default"); + $("option:eq(" + defaultOption + ")", this).prop("selected", "selected"); + } + ); + // collapsing expanded-panes (upon the selection of html-select-options) if any + $(operationDataWrapper + " .expanded").each( + function () { + if ($(this).hasClass("expanded")) { + $(this).removeClass("expanded"); + } + $(this).slideUp(); + } + ); + // removing all entries of grid-input elements if exist + $(operationDataWrapper + " .grouped-array-input").each( + function () { + var gridInputs = $(this).find("[data-add-form-clone]"); + if (gridInputs.length > 0) { + gridInputs.remove(); + } + var helpTexts = $(this).find("[data-help-text=add-form]"); + if (helpTexts.length > 0) { + helpTexts.show(); + } + } + ); + } + } + }); + + // adding support for cloning multiple profiles per feature with cloneable class definitions + $(advanceOperations).on("click", ".multi-view.add.enabled", function () { + // get a copy of .cloneable and create new .cloned div element + var cloned = "

      " + $(".cloneable", $(this).parent().parent()).html() + "
      "; + // append newly created .cloned div element to panel-body + $(this).parent().parent().append(cloned); + // enable remove action of newly cloned div element + $(".cloned", $(this).parent().parent()).each( + function () { + if ($(".multi-view.remove", this).hasClass("disabled")) { + $(".multi-view.remove", this).removeClass("disabled"); + } + if (!$(".multi-view.remove", this).hasClass("enabled")) { + $(".multi-view.remove", this).addClass("enabled"); + } + } + ); + }); + + $(advanceOperations).on("click", ".multi-view.remove.enabled", function () { + $(this).parent().remove(); + }); + + // enabling or disabling grouped-input based on the status of a parent check-box + $(advanceOperations).on("click", ".grouped-input", function () { + updateGroupedInputVisibility(this); + }); + + // add form entry click function for grid inputs + $(advanceOperations).on("click", "[data-click-event=add-form]", function () { + var addFormContainer = $("[data-add-form-container=" + $(this).attr("href") + "]"); + var clonedForm = $("[data-add-form=" + $(this).attr("href") + "]").clone(). + find("[data-add-form-element=clone]").attr("data-add-form-clone", $(this).attr("href")); + + // adding class .child-input to capture text-input-array-values + $("input, select", clonedForm).addClass("child-input"); + + $(addFormContainer).append(clonedForm); + setId(addFormContainer); + showHideHelpText(addFormContainer); + }); + + // remove form entry click function for grid inputs + $(advanceOperations).on("click", "[data-click-event=remove-form]", function () { + var addFormContainer = $("[data-add-form-container=" + $(this).attr("href") + "]"); + + $(this).closest("[data-add-form-element=clone]").remove(); + setId(addFormContainer); + showHideHelpText(addFormContainer); + }); + + $(".wizard-stepper").click(function () { + // button clicked here can be either a continue button or a back button. + var currentStep = $(this).data("current"); + var validationIsRequired = $(this).data("validate"); + var wizardIsToBeContinued; + + if (validationIsRequired) { + wizardIsToBeContinued = validateStep[currentStep](); + } else { + wizardIsToBeContinued = true; + } + + if (wizardIsToBeContinued) { + // When moving back and forth, following code segment will + // remove if there are any visible error-messages. + var errorMsgWrappers = ".alert.alert-danger"; + $(errorMsgWrappers).each( + function () { + if (!$(this).hasClass("hidden")) { + $(this).addClass("hidden"); + } + } + ); + + var nextStep = $(this).data("next"); + var isBackBtn = $(this).data("is-back-btn"); + + // if current button is a continuation... + if (!isBackBtn) { + // initiate stepForwardFrom[*] functions to gather form data. + if (stepForwardFrom[currentStep]) { + stepForwardFrom[currentStep](this); + } + } else { + // initiate stepBackFrom[*] functions to rollback. + if (stepBackFrom[currentStep]) { + stepBackFrom[currentStep](); + } + } + + // following step occurs only at the last stage of the wizard. + if (!nextStep) { + window.location.href = $(this).data("direct"); + } + + // updating next wizard step as current. + $(".itm-wiz").each(function () { + var step = $(this).data("step"); + if (step == nextStep) { + $(this).addClass("itm-wiz-current"); + } else { + $(this).removeClass("itm-wiz-current"); + } + }); + + // adding next update of wizard-steps. + $("#" + nextStep + "-wizard-steps").html($(".wr-steps").html()); + + // hiding current section of the wizard and showing next section. + $("." + currentStep).addClass("hidden"); + $("." + nextStep).removeClass("hidden"); + } + }); +}); + +function getParameterByName(name) { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), + results = regex.exec(location.search); + return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/public/js/sql.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/public/js/sql.js new file mode 100644 index 000000000..9f92fbc93 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/public/js/sql.js @@ -0,0 +1,310 @@ +/* + * 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. + */ + +/*Annotations, Annotation Names and relevant tokens*/ +var ANNOTATION_IMPORT = "Import"; +var ANNOTATION_EXPORT = "Export"; + +var ANNOTATION_TOKEN_AT = "@"; +var ANNOTATION_TOKEN_OPENING_BRACKET = "("; +var ANNOTATION_TOKEN_CLOSING_BRACKET = ")"; + +var REGEX_LINE_STARTING_WITH_PLAN = /^@Plan.*/g; +var REGEX_LINE_STARTING_WITH_SINGLE_LINE_COMMENT = /^--.*/g; +var REGEX_LINE_STARTING_WITH_MULTI_LINE_COMMENT = /^\/\*.*\*\//g; +var REGEX_LINE_STARTING_WITH_IMPORT_STATEMENT = /^@Import.*/g; + +var SIDDHI_STATEMENT_DELIMETER = ";"; +var SIDDHI_LINE_BREAK = "\n"; +var SIDDHI_LINE_BREAK_CHARACTER = '\n'; +var SIDDHI_SINGLE_QUOTE = "'"; +var SIDDHI_SPACE_LITERAL = " "; + +var SIDDHI_LITERAL_DEFINE_STREAM = "define stream"; + +var MIME_TYPE_SIDDHI_QL = "text/siddhi-ql"; + + +CodeMirror.defineMode("sql", function (config, parserConfig) { + "use strict"; + + var client = parserConfig.client || {}, + atoms = parserConfig.atoms || {"false":true, "true":true, "null":true}, + builtin = parserConfig.builtin || {}, + keywords = parserConfig.keywords || {}, + operatorChars = parserConfig.operatorChars || /^[*+\-%<>!=&|~^]/, + support = parserConfig.support || {}, + hooks = parserConfig.hooks || {}, + dateSQL = parserConfig.dateSQL || {"date":true, "time":true, "timestamp":true}; + + function tokenBase(stream, state) { + var ch = stream.next(); + + // call hooks from the mime type + if (hooks[ch]) { + var result = hooks[ch](stream, state); + if (result !== false) return result; + } + + if (ch.charCodeAt(0) > 47 && ch.charCodeAt(0) < 58) { + // numbers + // ref: http://dev.mysql.com/doc/refman/5.5/en/number-literals.html + stream.match(/^[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?/); + support.decimallessFloat == true && stream.eat('.'); + return "number"; + } else if (ch == "'" || (ch == '"' && support.doubleQuote)) { + // strings + // ref: http://dev.mysql.com/doc/refman/5.5/en/string-literals.html + state.tokenize = tokenLiteral(ch); + return state.tokenize(stream, state); + } else if (/^[\(\),\;\[\]]/.test(ch)) { + // no highlightning + return null; + } else if ((ch == "-" && stream.eat("-") && (!support.commentSpaceRequired || stream.eat(" ")))) { + // 1-line comments + // ref: https://kb.askmonty.org/en/comment-syntax/ + stream.skipToEnd(); + return "comment"; + } else if (ch == "/" && stream.eat("*")) { + // multi-line comments + // ref: https://kb.askmonty.org/en/comment-syntax/ + state.tokenize = tokenComment; + return state.tokenize(stream, state); + } else if (ch == ".") { + // .1 for 0.1 + if (support.zerolessFloat == true && stream.match(/^(?:\d+(?:e[+-]?\d+)?)/i)) { + return "number"; + } + } else { + stream.eatWhile(/^[_\-\w\d]/); /* Character '-' will also be eaten, to prevent the highlight happening in keywords being embedded in non-keyword strings. For example, 'all' in 'all-nonkeyword' */ + var word = stream.current().toLowerCase(); // Added toLowerCase() to highlight keywords in a case insensitive manner. + // dates (standard SQL syntax) + // ref: http://dev.mysql.com/doc/refman/5.5/en/date-and-time-literals.html + if (dateSQL.hasOwnProperty(word) && (stream.match(/^( )+'[^']*'/) || stream.match(/^( )+"[^"]*"/))) + return "number"; + if (atoms.hasOwnProperty(word)) return "atom"; + if (builtin.hasOwnProperty(word)) return "builtin"; + if (keywords.hasOwnProperty(word)) return "keyword"; + if (client.hasOwnProperty(word)) return "string-2"; + return null; + } + } + + // 'string', with char specified in quote escaped by '\' + function tokenLiteral(quote) { + return function (stream, state) { + var escaped = false, ch; + while ((ch = stream.next()) != null) { + if (ch == quote && !escaped) { + state.tokenize = tokenBase; + break; + } + escaped = !escaped && ch == "\\"; + } + return "string"; + }; + } + + function tokenComment(stream, state) { + while (true) { + if (stream.skipTo("*")) { + stream.next(); + if (stream.eat("/")) { + state.tokenize = tokenBase; + break; + } + } else { + stream.skipToEnd(); + break; + } + } + return "comment"; + } + + function pushContext(stream, state, type) { + state.context = { + prev:state.context, + indent:stream.indentation(), + col:stream.column(), + type:type + }; + } + + function popContext(state) { + state.indent = state.context.indent; + state.context = state.context.prev; + } + + return { + startState:function () { + return {tokenize:tokenBase, context:null}; + }, + + token:function (stream, state) { + if (stream.sol()) { + if (state.context && state.context.align == null) + state.context.align = false; + } + if (stream.eatSpace()) return null; + + var style = state.tokenize(stream, state); + if (style == "comment") return style; + + if (state.context && state.context.align == null) + state.context.align = true; + + var tok = stream.current(); + if (tok == "(") + pushContext(stream, state, ")"); + else if (tok == "[") + pushContext(stream, state, "]"); + else if (state.context && state.context.type == tok) + popContext(state); + return style; + }, + + indent:function (state, textAfter) { + var cx = state.context; + if (!cx) return CodeMirror.Pass; + var closing = textAfter.charAt(0) == cx.type; + if (cx.align) return cx.col + (closing ? 0 : 1); + else return cx.indent + (closing ? 0 : config.indentUnit); + }, + + blockCommentStart: "/*", + blockCommentEnd: "*/", + lineComment: "--" + }; +}); + +(function () { + "use strict"; + + // `identifier` + function hookIdentifier(stream) { + // MySQL/MariaDB identifiers + // ref: http://dev.mysql.com/doc/refman/5.6/en/identifier-qualifiers.html + var ch; + while ((ch = stream.next()) != null) { + if (ch == "`" && !stream.eat("`")) return "variable-2"; + } + stream.backUp(stream.current().length - 1); + return stream.eatWhile(/\w/) ? "variable-2" : null; + } + + // variable token + function hookVar(stream) { + // variables + // @@prefix.varName @varName + // varName can be quoted with ` or ' or " + // ref: http://dev.mysql.com/doc/refman/5.5/en/user-variables.html + if (stream.eat("@")) { + stream.match(/^session\./); + stream.match(/^local\./); + stream.match(/^global\./); + } + + if (stream.eat("'")) { + stream.match(/^.*'/); + return "variable-2"; + } else if (stream.eat('"')) { + stream.match(/^.*"/); + return "variable-2"; + } else if (stream.eat("`")) { + stream.match(/^.*`/); + return "variable-2"; + } else if (stream.match(/^[0-9a-zA-Z$\.\_]+/)) { + return "variable-2"; + } + return null; + } + + ; + + // short client keyword token + function hookClient(stream) { + // \N means NULL + // ref: http://dev.mysql.com/doc/refman/5.5/en/null-values.html + if (stream.eat("N")) { + return "atom"; + } + // \g, etc + // ref: http://dev.mysql.com/doc/refman/5.5/en/mysql-commands.html + return stream.match(/^[a-zA-Z.#!?]/) ? "variable-2" : null; + } + + // these keywords are used by all SQL dialects (however, a mode can still overwrite it) + var sqlKeywordsWithoutSymbols = "all and as begin by contains define delete end events " + + "every first for from full group having inner insert into join last " + + "left not of on or outer output partition raw return right select snapshot stream table "; + var sqlKeywords = ", : ? # ( ) " + sqlKeywordsWithoutSymbols; + var builtIn = "bool double float int long object string "; + var atoms = "false true null "; + var dateSQL = "days hours milliseconds minutes months seconds "; + var allSqlSuggestions = sqlKeywordsWithoutSymbols + builtIn + atoms + dateSQL; + + // turn a space-separated list into an array + function set(str) { + var obj = {}, words = str.split(" "); + for (var i = 0; i < words.length; ++i) obj[words[i]] = true; + return obj; + } + + // A generic SQL Mode. It's not a standard, it just try to support what is generally supported + CodeMirror.defineMIME(MIME_TYPE_SIDDHI_QL, { + name:"sql", + keywords:set(sqlKeywords), + builtin:set(builtIn), + atoms:set(atoms), + operatorChars:/^[*+%<>!=/]/, + dateSQL:set(dateSQL), + support:set("doubleQuote "), + allSqlSuggestions:set(allSqlSuggestions) + }); +}()); + +/* + How Properties of Mime Types are used by SQL Mode + ================================================= + + keywords: + A list of keywords you want to be highlighted. + functions: + A list of function names you want to be highlighted. + builtin: + A list of builtin types you want to be highlighted (if you want types to be of class "builtin" instead of "keyword"). + operatorChars: + All characters that must be handled as operators. + client: + Commands parsed and executed by the client (not the server). + support: + A list of supported syntaxes which are not common, but are supported by more than 1 DBMS. + * ODBCdotTable: .tableName + * zerolessFloat: .1 + * doubleQuote + * nCharCast: N'string' + * charsetCast: _utf8'string' + * commentHash: use # char for comments + * commentSlashSlash: use // for comments + * commentSpaceRequired: require a space after -- for comments + atoms: + Keywords that must be highlighted as atoms,. Some DBMS's support more atoms than others: + UNKNOWN, INFINITY, UNDERFLOW, NaN... + dateSQL: + Used for date/time SQL standard syntax, because not all DBMS's support same temporal types. + */ \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/wizard.hbs b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/wizard.hbs new file mode 100644 index 000000000..4cc6fa272 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/wizard.hbs @@ -0,0 +1,255 @@ +{{#zone "topCss"}} + {{css "css/codemirror.css"}} +{{/zone}} + +
      +
      + + + + + + + + +
      +
      +

      ADD POLICY

      +
      +
      +
      +
      +
      +

      Step 2: Configure profile

      +
      + + +
      +
      +
      + +
      +
      +
      +
      + +
      +
      +
      +
      + + +
      +
      +{{#zone "bottomJs"}} + {{js "js/codemirror.js"}} + {{js "js/sql.js"}} + {{js "js/policy-create.js"}} +{{/zone}} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/wizard.js b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/wizard.js new file mode 100644 index 000000000..925a97ea4 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/wizard.js @@ -0,0 +1,47 @@ +/* + * 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. + */ + +function onRequest(context) { + //var log = new Log("wizard.js"); + var DTYPE_CONF_DEVICE_TYPE_KEY = "deviceType"; + var DTYPE_CONF_DEVICE_TYPE_LABEL_KEY = "label"; + + var userModule = require("/app/modules/user.js")["userModule"]; + var utility = require('/app/modules/utility.js').utility; + var response = userModule.getRoles(); + var wizardPage = {}; + if (response["status"] == "success") { + wizardPage["roles"] = response["content"]; + } + var deviceType = context.uriParams.deviceType; + var typesListResponse = userModule.getPlatforms(); + if (typesListResponse["status"] == "success") { + for (var type in typesListResponse["content"]) { + if (deviceType == typesListResponse["content"][type]["name"]) { + wizardPage["type"] = typesListResponse["content"][type]; + var deviceTypeLabel = deviceType; + var configs = utility.getDeviceTypeConfig(deviceType); + if (configs && configs[DTYPE_CONF_DEVICE_TYPE_KEY][DTYPE_CONF_DEVICE_TYPE_LABEL_KEY]) { + deviceTypeLabel = configs[DTYPE_CONF_DEVICE_TYPE_KEY][DTYPE_CONF_DEVICE_TYPE_LABEL_KEY]; + } + wizardPage["type"]["label"] = deviceTypeLabel; + } + } + } + return wizardPage; +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/wizard.json b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/wizard.json new file mode 100644 index 000000000..fd2590129 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.policy.wizard/wizard.json @@ -0,0 +1,3 @@ +{ + "version" : "1.0.0" +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.ui.header.logo/logo.hbs b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.ui.header.logo/logo.hbs new file mode 100644 index 000000000..b1841e186 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.ui.header.logo/logo.hbs @@ -0,0 +1 @@ +{{#zone "productName"}}{{@app.conf.appName}}{{/zone}} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.ui.header.logo/logo.json b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.ui.header.logo/logo.json new file mode 100644 index 000000000..4e30bc074 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot.ui/src/main/resources/jaggeryapps/devicemgt/app/units/iot.unit.ui.header.logo/logo.json @@ -0,0 +1,5 @@ +{ + "version": "1.0.0", + "index": 29, + "extends": "mdm.unit.ui.header.logo" +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/pom.xml b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/pom.xml new file mode 100644 index 000000000..cfdc32dbc --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/pom.xml @@ -0,0 +1,213 @@ + + + + + + + iot-base-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot + bundle + WSO2 Carbon - IoT Device Management Common Impl + WSO2 Carbon - IoT Device Management and Control Implementation + http://wso2.org + + + + + org.apache.felix + maven-scr-plugin + + + maven-compiler-plugin + + 1.7 + 1.7 + + 2.3.2 + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + ${project.artifactId} + ${project.artifactId} + ${carbon.devicemgt.plugins.version} + IoT Server Impl Bundle + org.wso2.carbon.device.mgt.iot.internal + + org.jivesoftware.smack.*, + javax.xml.namespace;resolution:=optional, + javax.xml.validation;resolution:=optional, + org.apache.commons.codec.binary, + org.apache.commons.collections.map, + org.apache.http.*, + org.apache.commons.io.*, + org.apache.commons.logging.*, + org.json;version="${commons-json.version}", + org.wso2.carbon.base.*, + org.wso2.carbon.databridge.*, + org.wso2.carbon.user.api, + org.wso2.carbon.user.core.service, + org.osgi.framework, + org.osgi.service.component, + javax.xml.bind.*;resolution:=optional, + javax.naming;resolution:=optional, + javax.sql;resolution:=optional, + javax.xml.bind.annotation.*;resolution:=optional, + javax.xml.parsers.*;resolution:=optional, + javax.net;resolution:=optional, + javax.net.ssl;resolution:=optional, + javax.crypto, + org.apache.tomcat.util.codec.binary, + org.w3c.dom;resolution:=optional, + org.wso2.carbon.core;version="${carbon.kernel.version.range}", + org.wso2.carbon.utils.*;version="${carbon.kernel.version.range}", + org.wso2.carbon.device.mgt.common.*, + org.wso2.carbon.device.mgt.core.*, + org.wso2.carbon.context.*;version="${carbon.kernel.version.range}", + org.wso2.carbon.ndatasource.core;version="${carbon.kernel.version.range}", + org.eclipse.paho.client.mqttv3.*;version="${eclipse.paho.version}" + + + !org.wso2.carbon.device.mgt.iot.internal, + org.wso2.carbon.device.mgt.iot.*;version="${project.version}" + + + + + + + + + + + + + + + + + + + + + + + + + org.eclipse.osgi + org.eclipse.osgi + + + org.eclipse.osgi + org.eclipse.osgi.services + + + org.wso2.carbon + org.wso2.carbon.logging + + + org.wso2.carbon + org.wso2.carbon.utils + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.core + + + org.wso2.carbon + org.wso2.carbon.ndatasource.core + + + org.wso2.carbon.devicemgt + org.wso2.carbon.policy.mgt.core + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.policy.mgt.common + provided + + + + org.json.wso2 + json + + + org.wso2.carbon + org.wso2.carbon.core + + + commons-collections + commons-collections + + + commons-configuration + commons-configuration + + + org.wso2.carbon.analytics-common + org.wso2.carbon.databridge.agent + + + org.wso2.carbon.analytics-common + org.wso2.carbon.databridge.core + + + org.wso2.carbon.commons + org.wso2.carbon.databridge.commons + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.analytics + + + + org.igniterealtime.smack.wso2 + smack + + + org.igniterealtime.smack.wso2 + smackx + + + commons-codec.wso2 + commons-codec + + + diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/DeviceManagementConfigurationManager.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/DeviceManagementConfigurationManager.java new file mode 100644 index 000000000..00774a3cf --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/DeviceManagementConfigurationManager.java @@ -0,0 +1,118 @@ +/* + * 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.device.mgt.iot.config.server; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.w3c.dom.Document; +import org.wso2.carbon.device.mgt.iot.config.server.datasource.ControlQueue; +import org.wso2.carbon.device.mgt.iot.config.server.datasource.DeviceManagementConfiguration; +import org.wso2.carbon.device.mgt.iot.exception.DeviceControllerException; +import org.wso2.carbon.device.mgt.iot.util.IotDeviceManagementUtil; +import org.wso2.carbon.utils.CarbonUtils; +import javax.xml.XMLConstants; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.ValidationEvent; +import javax.xml.bind.ValidationEventHandler; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import java.io.File; +import java.util.List; + +/** + * Class responsible for the iot device manager configuration initialization. + */ +public class DeviceManagementConfigurationManager { + + private static final Log log = LogFactory.getLog(DeviceManagementConfigurationManager.class); + private static final String DEVICE_MGT_CONFIG_XML_NAME = "devicemgt-config.xml"; + private static final String DEVICE_MGT_ROOT_DIRECTORY = "iot"; + private final String XMLCONFIGS_FILE_LOCATION = + CarbonUtils.getCarbonConfigDirPath() + File.separator + + DEVICE_MGT_ROOT_DIRECTORY + File.separator + DEVICE_MGT_CONFIG_XML_NAME; + private static final String IOT_DEVICE_CONFIG_XSD_NAME = "devicemgt-config.xsd"; + private final String XSDCONFIGS_FILE_LOCATION = + CarbonUtils.getCarbonConfigDirPath() + File.separator + + DEVICE_MGT_ROOT_DIRECTORY + File.separator + IOT_DEVICE_CONFIG_XSD_NAME; + private DeviceManagementConfiguration currentDeviceManagementConfiguration; + private static DeviceManagementConfigurationManager deviceConfigurationManager = + new DeviceManagementConfigurationManager(); + + private DeviceManagementConfigurationManager() { + } + + public static DeviceManagementConfigurationManager getInstance() { + return deviceConfigurationManager; + } + + public void initConfig() throws DeviceControllerException { + try { + SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + Schema schema = sf.newSchema(new File(XSDCONFIGS_FILE_LOCATION)); + + File deviceCloudMgtConfig = new File(XMLCONFIGS_FILE_LOCATION); + Document doc = IotDeviceManagementUtil.convertToDocument(deviceCloudMgtConfig); + JAXBContext deviceCloudContext = JAXBContext.newInstance(DeviceManagementConfiguration.class); + Unmarshaller unmarshaller = deviceCloudContext.createUnmarshaller(); + unmarshaller.setSchema(schema); + unmarshaller.setEventHandler(new IotConfigValidationEventHandler()); + this.currentDeviceManagementConfiguration = (DeviceManagementConfiguration) unmarshaller.unmarshal(doc); + } catch (Exception e) { + String error = "Error occurred while initializing DeviceController configurations"; + log.error(error); + throw new DeviceControllerException(error, e); + } + } + + public DeviceManagementConfiguration getDeviceCloudMgtConfig() { + return currentDeviceManagementConfiguration; + } + + public ControlQueue getControlQueue(String name) { + List controlQueues = currentDeviceManagementConfiguration.getControlQueues().getControlQueue(); + if (controlQueues != null) { + for (ControlQueue controlQueue : controlQueues) { + if (controlQueue.getName().equals(name)) { + return controlQueue; + } + } + } + return null; + } + + private class IotConfigValidationEventHandler implements ValidationEventHandler { + @Override + public boolean handleEvent(ValidationEvent event) { + String error = "\nEVENT" + "\nSEVERITY: " + event.getSeverity() + + "\n MESSAGE: " + event.getMessage() + + "\n LINKED EXCEPTION: " + event.getLinkedException() + + "\n LOCATOR" + + "\n LINE NUMBER: " + event.getLocator().getLineNumber() + + "\n COLUMN NUMBER: " + event.getLocator().getColumnNumber() + + "\n OFFSET: " + event.getLocator().getOffset() + + "\n OBJECT: " + event.getLocator().getObject() + + "\n NODE: " + event.getLocator().getNode() + + "\n URL: " + event.getLocator().getURL(); + log.error(error); + return true; + } + } + +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/datasource/ControlQueue.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/datasource/ControlQueue.java new file mode 100644 index 000000000..145763bfa --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/datasource/ControlQueue.java @@ -0,0 +1,229 @@ + +/* + * 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.device.mgt.iot.config.server.datasource; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + *

      Java class for ControlQueue complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="ControlQueue">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="Name" type="{http://www.w3.org/2001/XMLSchema}string"/>
      + *         <element name="Enabled" type="{http://www.w3.org/2001/XMLSchema}boolean"/>
      + *         <element name="ControlClass" type="{http://www.w3.org/2001/XMLSchema}string"/>
      + *         <element name="Protocol" type="{http://www.w3.org/2001/XMLSchema}string"/>
      + *         <element name="ServerURL" type="{http://www.w3.org/2001/XMLSchema}string"/>
      + *         <element name="Port" type="{http://www.w3.org/2001/XMLSchema}short"/>
      + *         <element name="Username" type="{http://www.w3.org/2001/XMLSchema}string"/>
      + *         <element name="Password" type="{http://www.w3.org/2001/XMLSchema}string"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "ControlQueue", propOrder = { + "name", + "enabled", + "controlClass", + "protocol", + "serverURL", + "port", + "username", + "password" +}) +public class ControlQueue { + + @XmlElement(name = "Name", required = true) + protected String name; + @XmlElement(name = "Enabled") + protected boolean enabled; + @XmlElement(name = "ControlClass", required = true) + protected String controlClass; + @XmlElement(name = "Protocol", required = true) + protected String protocol; + @XmlElement(name = "ServerURL", required = true) + protected String serverURL; + @XmlElement(name = "Port") + protected short port; + @XmlElement(name = "Username", required = true) + protected String username; + @XmlElement(name = "Password", required = true) + protected String password; + + /** + * Gets the value of the name property. + * + * @return possible object is + * {@link String } + */ + public String getName() { + return name; + } + + /** + * Sets the value of the name property. + * + * @param value allowed object is + * {@link String } + */ + public void setName(String value) { + this.name = value; + } + + /** + * Gets the value of the enabled property. + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Sets the value of the enabled property. + */ + public void setEnabled(boolean value) { + this.enabled = value; + } + + /** + * Gets the value of the controlClass property. + * + * @return possible object is + * {@link String } + */ + public String getControlClass() { + return controlClass; + } + + /** + * Sets the value of the controlClass property. + * + * @param value allowed object is + * {@link String } + */ + public void setControlClass(String value) { + this.controlClass = value; + } + + /** + * Gets the value of the protocol property. + * + * @return possible object is + * {@link String } + */ + public String getProtocol() { + return protocol; + } + + /** + * Sets the value of the protocol property. + * + * @param value allowed object is + * {@link String } + */ + public void setProtocol(String value) { + this.protocol = value; + } + + /** + * Gets the value of the serverURL property. + * + * @return possible object is + * {@link String } + */ + public String getServerURL() { + return serverURL; + } + + /** + * Sets the value of the serverURL property. + * + * @param value allowed object is + * {@link String } + */ + public void setServerURL(String value) { + this.serverURL = value; + } + + /** + * Gets the value of the port property. + */ + public short getPort() { + return port; + } + + /** + * Sets the value of the port property. + */ + public void setPort(short value) { + this.port = value; + } + + /** + * Gets the value of the username property. + * + * @return possible object is + * {@link String } + */ + public String getUsername() { + return username; + } + + /** + * Sets the value of the username property. + * + * @param value allowed object is + * {@link String } + */ + public void setUsername(String value) { + this.username = value; + } + + /** + * Gets the value of the password property. + * + * @return possible object is + * {@link String } + */ + public String getPassword() { + return password; + } + + /** + * Sets the value of the password property. + * + * @param value allowed object is + * {@link String } + */ + public void setPassword(String value) { + this.password = value; + } + +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/datasource/ControlQueuesConfig.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/datasource/ControlQueuesConfig.java new file mode 100644 index 000000000..e7b82ce0b --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/datasource/ControlQueuesConfig.java @@ -0,0 +1,82 @@ + +/* + * 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.device.mgt.iot.config.server.datasource; + +import java.util.ArrayList; +import java.util.List; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + *

      Java class for ControlQueuesConfig complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="ControlQueuesConfig">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="ControlQueue" type="{}ControlQueue" maxOccurs="unbounded" minOccurs="0"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "ControlQueuesConfig", propOrder = { + "controlQueue" +}) +public class ControlQueuesConfig { + + @XmlElement(name = "ControlQueue") + protected List controlQueue; + + /** + * Gets the value of the controlQueue property. + *

      + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the controlQueue property. + *

      + *

      + * For example, to add a new item, do as follows: + *

      +     *    getControlQueue().add(newItem);
      +     * 
      + *

      + *

      + *

      + * Objects of the following type(s) are allowed in the list + * {@link ControlQueue } + */ + public List getControlQueue() { + if (controlQueue == null) { + controlQueue = new ArrayList(); + } + return this.controlQueue; + } + +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/datasource/DeviceManagementConfiguration.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/datasource/DeviceManagementConfiguration.java new file mode 100644 index 000000000..6194049c4 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/datasource/DeviceManagementConfiguration.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. + */ + +package org.wso2.carbon.device.mgt.iot.config.server.datasource; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + +/** + *

      Java class for DeviceCloudConfig complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="DeviceManagementConfigurations">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="ControlQueues" type="{}ControlQueuesConfig"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "DeviceManagementConfigurations", propOrder = { + "controlQueues" +}) + +@XmlRootElement(name = "DeviceManagementConfigurations") +public class DeviceManagementConfiguration { + @XmlElement(name = "ControlQueues", required = true) + protected ControlQueuesConfig controlQueues; + + /** + * Gets the value of the controlQueues property. + * + * @return possible object is + * {@link ControlQueuesConfig } + */ + public ControlQueuesConfig getControlQueues() { + return controlQueues; + } + + /** + * Sets the value of the controlQueues property. + * + * @param value allowed object is + * {@link ControlQueuesConfig } + */ + public void setControlQueues(ControlQueuesConfig value) { + this.controlQueues = value; + } +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/datasource/ObjectFactory.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/datasource/ObjectFactory.java new file mode 100644 index 000000000..9bc313579 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/config/server/datasource/ObjectFactory.java @@ -0,0 +1,69 @@ + +/* + * 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.device.mgt.iot.config.server.datasource; + +import javax.xml.bind.JAXBElement; +import javax.xml.bind.annotation.XmlElementDecl; +import javax.xml.bind.annotation.XmlRegistry; +import javax.xml.namespace.QName; + +/** + * This object contains factory methods for each + * Java content interface and Java element interface + * generated in the org.wso2.carbon.device.mgt.iot.common.config.server.configs package. + *

      An ObjectFactory allows you to programatically + * construct new instances of the Java representation + * for XML content. The Java representation of XML + * content can consist of schema derived interfaces + * and classes representing the binding of schema + * type definitions, element declarations and model + * groups. Factory methods for each of these are + * provided in this class. + */ +@XmlRegistry +public class ObjectFactory { + + private final static QName _DeviceCloudConfiguration_QNAME = new QName("", "DeviceManagementConfigurations"); + + /** + * Create a new ObjectFactory that can be used to create new instances of schema derived classes for package: + * org.wso2.carbon.device.mgt.iot.common.config.server.configs + */ + public ObjectFactory() { + } + + /** + * Create an instance of {@link DeviceManagementConfiguration } + */ + public DeviceManagementConfiguration createDeviceCloudConfig() { + return new DeviceManagementConfiguration(); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link DeviceManagementConfiguration }{@code >}} + */ + @XmlElementDecl(namespace = "", name = "DeviceManagementConfigurations") + public JAXBElement createDeviceCloudConfiguration( + DeviceManagementConfiguration value) { + return new JAXBElement(_DeviceCloudConfiguration_QNAME, + DeviceManagementConfiguration.class, null, value); + } + +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/controlqueue/mqtt/MqttConfig.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/controlqueue/mqtt/MqttConfig.java new file mode 100644 index 000000000..26abefe49 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/controlqueue/mqtt/MqttConfig.java @@ -0,0 +1,81 @@ +/* + * 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.device.mgt.iot.controlqueue.mqtt; + +import org.wso2.carbon.device.mgt.iot.config.server.DeviceManagementConfigurationManager; +import org.wso2.carbon.device.mgt.iot.config.server.datasource.ControlQueue; + +public class MqttConfig { + private String mqttQueueEndpoint; + private String mqttQueueUsername; + private String mqttQueuePassword; + private boolean isEnabled; + private static final String MQTT_QUEUE_CONFIG_NAME = "MQTT"; + private static final String LOCALHOST = "localhost"; + private static final String PORT_OFFSET_PROPERTY = "portOffset"; + private ControlQueue mqttControlQueue; + private static MqttConfig mqttConfig = new MqttConfig(); + + public String getMqttQueueEndpoint() { + return mqttQueueEndpoint; + } + + public String getMqttQueueUsername() { + return mqttQueueUsername; + } + + public String getMqttQueuePassword() { + return mqttQueuePassword; + } + + public ControlQueue getMqttControlQueue() { + return mqttControlQueue; + } + + public boolean isEnabled() { + return isEnabled; + } + + public static String getMqttQueueConfigName() { + return MQTT_QUEUE_CONFIG_NAME; + } + + private MqttConfig() { + + mqttControlQueue = DeviceManagementConfigurationManager.getInstance().getControlQueue(MQTT_QUEUE_CONFIG_NAME); + int portOffset = Integer.parseInt(System.getProperty(PORT_OFFSET_PROPERTY)); + String brokerURL = mqttControlQueue.getServerURL(); + + if (portOffset != 0 && brokerURL.contains(LOCALHOST)) { + // if using the internal MB (meaning URL is localhost and there is a portOffset) + // then increment port accordingly + int mqttPort = mqttControlQueue.getPort(); + mqttPort = mqttPort + portOffset; + mqttQueueEndpoint = mqttControlQueue.getServerURL() + ":" + mqttPort; + } else { + mqttQueueEndpoint = mqttControlQueue.getServerURL() + ":" + mqttControlQueue.getPort(); + } + mqttQueueUsername = mqttControlQueue.getUsername(); + mqttQueuePassword = mqttControlQueue.getPassword(); + isEnabled = mqttControlQueue.isEnabled(); + } + public static MqttConfig getInstance() { + return mqttConfig; + } +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/controlqueue/xmpp/XmppAccount.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/controlqueue/xmpp/XmppAccount.java new file mode 100644 index 000000000..94c6dd6d5 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/controlqueue/xmpp/XmppAccount.java @@ -0,0 +1,60 @@ +/* + * 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.device.mgt.iot.controlqueue.xmpp; + +public class XmppAccount { + + private String username; + private String password; + private String accountName; + private String email; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getAccountName() { + return accountName; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/controlqueue/xmpp/XmppConfig.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/controlqueue/xmpp/XmppConfig.java new file mode 100644 index 000000000..e06ee66f0 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/controlqueue/xmpp/XmppConfig.java @@ -0,0 +1,93 @@ +/* + * 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.device.mgt.iot.controlqueue.xmpp; + +import org.wso2.carbon.device.mgt.iot.config.server.DeviceManagementConfigurationManager; +import org.wso2.carbon.device.mgt.iot.config.server.datasource.ControlQueue; + +public class XmppConfig { + + private String xmppServerIP; + private int xmppServerPort; + private String xmppEndpoint; + private String xmppUsername; + private String xmppPassword; + private boolean isEnabled; + private static final String XMPP_QUEUE_CONFIG_NAME = "XMPP"; + private final int SERVER_CONNECTION_PORT = 5222; + private ControlQueue xmppControlQueue; + private static XmppConfig xmppConfig = new XmppConfig(); + + public String getXmppServerIP() { + return xmppServerIP; + } + + public int getXmppServerPort() { + return xmppServerPort; + } + + public String getXmppEndpoint() { + return xmppEndpoint; + } + + public String getXmppUsername() { + return xmppUsername; + } + + public String getXmppPassword() { + return xmppPassword; + } + + public ControlQueue getXmppControlQueue() { + return xmppControlQueue; + } + + public boolean isEnabled() { + return isEnabled; + } + + public static String getXmppQueueConfigName() { + return XMPP_QUEUE_CONFIG_NAME; + } + + private XmppConfig() { + xmppControlQueue = DeviceManagementConfigurationManager.getInstance().getControlQueue( + XMPP_QUEUE_CONFIG_NAME); + xmppServerIP = xmppControlQueue.getServerURL(); + int indexOfChar = xmppServerIP.lastIndexOf('/'); + + if (indexOfChar != -1) { + xmppServerIP = xmppServerIP.substring((indexOfChar + 1), xmppServerIP.length()); + } + + xmppServerPort = xmppControlQueue.getPort(); + xmppEndpoint = xmppControlQueue.getServerURL() + ":" + xmppServerPort; + xmppUsername = xmppControlQueue.getUsername(); + xmppPassword = xmppControlQueue.getPassword(); + isEnabled = xmppControlQueue.isEnabled(); + } + + public static XmppConfig getInstance() { + return xmppConfig; + } + + public int getSERVER_CONNECTION_PORT() { + return SERVER_CONNECTION_PORT; + } +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/controlqueue/xmpp/XmppServerClient.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/controlqueue/xmpp/XmppServerClient.java new file mode 100644 index 000000000..55c86707d --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/controlqueue/xmpp/XmppServerClient.java @@ -0,0 +1,352 @@ +/* + * 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.device.mgt.iot.controlqueue.xmpp; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.util.EntityUtils; +import org.json.JSONArray; +import org.json.JSONObject; +import org.wso2.carbon.device.mgt.iot.exception.DeviceControllerException; +import org.wso2.carbon.device.mgt.iot.exception.IoTException; +import org.wso2.carbon.device.mgt.iot.util.IoTUtil; + +import javax.ws.rs.core.MediaType; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +public class XmppServerClient { + + private static final Log log = LogFactory.getLog(XmppServerClient.class); + private static final String XMPP_SERVER_API_CONTEXT = "/plugins/restapi/v1"; + private static final String XMPP_USERS_API = "/users"; + private static final String XMPP_SESSIONS_API = "/sessions"; + @SuppressWarnings("unused") + private static final String XMPP_GROUPS_API = "/groups"; + @SuppressWarnings("unused") + private static final String APPLICATION_JSON_MT = "application/json"; + private static final String DEVICEMGT_CONFIG_FILE = "devicemgt-config.xml"; + private String xmppEndpoint; + private String xmppUsername; + private String xmppPassword; + private boolean xmppEnabled = false; + + public XmppServerClient() { + } + + public void initControlQueue() { + xmppEndpoint = XmppConfig.getInstance().getXmppEndpoint(); + xmppUsername = XmppConfig.getInstance().getXmppUsername(); + xmppPassword = XmppConfig.getInstance().getXmppPassword(); + xmppEnabled = XmppConfig.getInstance().isEnabled(); + } + + public boolean createXMPPAccount(XmppAccount newUserAccount) throws DeviceControllerException { + if (xmppEnabled) { + String xmppUsersAPIEndpoint = xmppEndpoint + XMPP_SERVER_API_CONTEXT + XMPP_USERS_API; + if (log.isDebugEnabled()) { + log.debug("The Create-UserAccount Endpoint URL of the XMPP Server is set to: " + xmppUsersAPIEndpoint); + } + + String encodedString = xmppUsername + ":" + xmppPassword; + encodedString = new String(Base64.encodeBase64(encodedString.getBytes(StandardCharsets.UTF_8))); + String authorizationHeader = "Basic " + encodedString; + String jsonRequest = "{\n" + + " \"username\": \"" + newUserAccount.getUsername() + "\"," + + " \"password\": \"" + newUserAccount.getPassword() + "\"," + + " \"name\": \"" + newUserAccount.getAccountName() + "\"," + + " \"email\": \"" + newUserAccount.getEmail() + "\"," + + " \"properties\": {" + + " \"property\": [" + + " {" + + " \"@key\": \"console.rows_per_page\"," + + " \"@value\": \"user-summary=8\"" + + " }," + + " {" + + " \"@key\": \"console.order\"," + + " \"@value\": \"session-summary=1\"" + + " }" + + " ]" + + " }" + + "}"; + + StringEntity requestEntity; + try { + requestEntity = new StringEntity(jsonRequest, MediaType.APPLICATION_JSON, + StandardCharsets.UTF_8.toString()); + } catch (UnsupportedEncodingException e) { + return false; + } + + URL xmppUserApiUrl; + try { + xmppUserApiUrl = new URL(xmppUsersAPIEndpoint); + } catch (MalformedURLException e) { + String errMsg = "Malformed XMPP URL + " + xmppUsersAPIEndpoint; + log.error(errMsg); + throw new DeviceControllerException(errMsg); + } + HttpClient httpClient; + try { + httpClient = IoTUtil.getHttpClient(xmppUserApiUrl.getPort(), xmppUserApiUrl.getProtocol()); + } catch (Exception e) { + log.error("Error on getting a http client for port :" + xmppUserApiUrl.getPort() + " protocol :" + + xmppUserApiUrl.getProtocol()); + return false; + } + + HttpPost httpPost = new HttpPost(xmppUsersAPIEndpoint); + httpPost.addHeader(HttpHeaders.AUTHORIZATION, authorizationHeader); + httpPost.setEntity(requestEntity); + + try { + HttpResponse httpResponse = httpClient.execute(httpPost); + + if (httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_CREATED) { + String response = IoTUtil.getResponseString(httpResponse); + String errorMsg = "XMPP Server returned status: '" + httpResponse.getStatusLine().getStatusCode() + + "' for account creation with error:\n" + response; + log.error(errorMsg); + throw new DeviceControllerException(errorMsg); + } else { + EntityUtils.consume(httpResponse.getEntity()); + return true; + } + } catch (IOException | IoTException e) { + String errorMsg = "Error occured whilst trying a 'POST' at : " + xmppUsersAPIEndpoint; + log.error(errorMsg); + throw new DeviceControllerException(errorMsg, e); + } + + } else { + log.warn(String.format("XMPP set to false in [%s]", DEVICEMGT_CONFIG_FILE)); + return false; + } + } + + public boolean doesXMPPUserAccountExist(String username) throws DeviceControllerException { + if (xmppEnabled) { + String xmppCheckUserAPIEndpoint = xmppEndpoint + XMPP_SERVER_API_CONTEXT + XMPP_USERS_API + "/" + username; + if (log.isDebugEnabled()) { + log.debug("The Check-User-Account Endpoint URL of the XMPP Server is set to: " + + xmppCheckUserAPIEndpoint); + } + + String encodedString = xmppUsername + ":" + xmppPassword; + encodedString = new String(Base64.encodeBase64(encodedString.getBytes(StandardCharsets.UTF_8))); + String authorizationHeader = "Basic " + encodedString; + + URL xmppUserApiUrl; + try { + xmppUserApiUrl = new URL(xmppCheckUserAPIEndpoint); + } catch (MalformedURLException e) { + String errMsg = "Malformed XMPP URL + " + xmppCheckUserAPIEndpoint; + log.error(errMsg); + throw new DeviceControllerException(errMsg, e); + } + + HttpClient httpClient; + try { + httpClient = IoTUtil.getHttpClient(xmppUserApiUrl.getPort(), xmppUserApiUrl.getProtocol()); + } catch (Exception e) { + String errorMsg = "Error on getting a http client for port :" + xmppUserApiUrl.getPort() + + " protocol :" + xmppUserApiUrl.getProtocol(); + log.error(errorMsg); + throw new DeviceControllerException(errorMsg, e); + } + + HttpGet httpGet = new HttpGet(xmppCheckUserAPIEndpoint); + httpGet.addHeader(HttpHeaders.AUTHORIZATION, authorizationHeader); + + try { + HttpResponse httpResponse = httpClient.execute(httpGet); + + if (httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { + String response = IoTUtil.getResponseString(httpResponse); + if (log.isDebugEnabled()) { + log.debug("XMPP Server returned status: '" + httpResponse.getStatusLine().getStatusCode() + + "' for checking existence of account [" + username + "] with message:\n" + + response + "\nProbably, an account with this username does not exist."); + } + return false; + } + + } catch (IOException | IoTException e) { + String errorMsg = "Error occured whilst trying a 'GET' at : " + xmppCheckUserAPIEndpoint; + log.error(errorMsg); + throw new DeviceControllerException(errorMsg, e); + } + + if (log.isDebugEnabled()) { + log.debug("XMPP Server already has an account for the username - [" + username + "]."); + } + return true; + } else { + String warnMsg = String.format("XMPP set to false in [%s]", DEVICEMGT_CONFIG_FILE); + log.warn(warnMsg); + throw new DeviceControllerException(warnMsg); + } + } + + public JSONArray getAllCurrentUserSessions() throws DeviceControllerException { + if (xmppEnabled) { + JSONArray xmppSessions; + String xmppSessionsAPIEndpoint = xmppEndpoint + XMPP_SERVER_API_CONTEXT + XMPP_SESSIONS_API; + + if (log.isDebugEnabled()) { + log.debug("The Get-Sessions Endpoint URL of the XMPP Server is set to: " + xmppSessionsAPIEndpoint); + } + + String encodedString = xmppUsername + ":" + xmppPassword; + encodedString = new String(Base64.encodeBase64(encodedString.getBytes(StandardCharsets.UTF_8))); + String authorizationHeader = "Basic " + encodedString; + + URL xmppUserApiUrl; + try { + xmppUserApiUrl = new URL(xmppSessionsAPIEndpoint); + } catch (MalformedURLException e) { + String errMsg = "Malformed XMPP URL + " + xmppSessionsAPIEndpoint; + log.error(errMsg); + throw new DeviceControllerException(errMsg, e); + } + + HttpClient httpClient; + try { + httpClient = IoTUtil.getHttpClient(xmppUserApiUrl.getPort(), xmppUserApiUrl.getProtocol()); + } catch (Exception e) { + String errorMsg = "Error on getting a http client for port :" + xmppUserApiUrl.getPort() + + " protocol :" + xmppUserApiUrl.getProtocol(); + log.error(errorMsg); + throw new DeviceControllerException(errorMsg, e); + } + + HttpGet httpGet = new HttpGet(xmppSessionsAPIEndpoint); + httpGet.addHeader(HttpHeaders.AUTHORIZATION, authorizationHeader); + httpGet.addHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON); + + try { + HttpResponse httpResponse = httpClient.execute(httpGet); + + if (httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { + String errorMsg = "XMPP Server returned status: '" + httpResponse.getStatusLine().getStatusCode() + + "' for checking current XMPP Sessions."; + log.error(errorMsg); + throw new DeviceControllerException(errorMsg); + } + + String response = IoTUtil.getResponseString(httpResponse); + xmppSessions = new JSONObject(response).getJSONArray("session"); + return xmppSessions; + + } catch (IOException | IoTException e) { + String errorMsg = "Error occured whilst trying a 'GET' at : " + xmppSessionsAPIEndpoint; + log.error(errorMsg); + throw new DeviceControllerException(errorMsg, e); + } + + } else { + String warnMsg = String.format("XMPP set to false in [%s]", DEVICEMGT_CONFIG_FILE); + log.warn(warnMsg); + throw new DeviceControllerException(warnMsg); + } + } + + public void deleteCurrentXmppSessions() throws DeviceControllerException { + JSONArray xmppSessionsArray; + try { + xmppSessionsArray = getAllCurrentUserSessions(); + } catch (DeviceControllerException e) { + if (e.getMessage().contains(DEVICEMGT_CONFIG_FILE)) { + log.warn(String.format("XMPP set to false in [%s]", DEVICEMGT_CONFIG_FILE)); + return; + } else { + throw e; + } + } + + if (xmppSessionsArray.length() != 0) { + String xmppSessionsAPIEndpoint = xmppEndpoint + XMPP_SERVER_API_CONTEXT + XMPP_SESSIONS_API; + String encodedString = xmppUsername + ":" + xmppPassword; + encodedString = new String(Base64.encodeBase64(encodedString.getBytes(StandardCharsets.UTF_8))); + String authorizationHeader = "Basic " + encodedString; + + if (log.isDebugEnabled()) { + log.debug("The Get-Sessions Endpoint URL of the XMPP Server is set to: " + xmppSessionsAPIEndpoint); + } + + URL xmppUserApiUrl; + try { + xmppUserApiUrl = new URL(xmppSessionsAPIEndpoint); + } catch (MalformedURLException e) { + String errMsg = "Malformed XMPP URL + " + xmppSessionsAPIEndpoint; + log.error(errMsg); + throw new DeviceControllerException(errMsg, e); + } + + HttpClient httpClient; + try { + httpClient = IoTUtil.getHttpClient(xmppUserApiUrl.getPort(), xmppUserApiUrl.getProtocol()); + } catch (Exception e) { + String errorMsg = "Error on getting a http client for port :" + xmppUserApiUrl.getPort() + + " protocol :" + xmppUserApiUrl.getProtocol(); + log.error(errorMsg); + throw new DeviceControllerException(errorMsg, e); + } + + for (int i = 0; i < xmppSessionsArray.length(); i++) { + + String sessionName = xmppSessionsArray.getJSONObject(i).getString("username"); + String xmppUserSessionsAPIEndpoint = xmppSessionsAPIEndpoint + "/" + sessionName; + + HttpDelete httpDelete = new HttpDelete(xmppUserSessionsAPIEndpoint); + httpDelete.addHeader(HttpHeaders.AUTHORIZATION, authorizationHeader); + + try { + HttpResponse httpResponse = httpClient.execute(httpDelete); + + if (httpResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { + String errorMsg = + "XMPP Server returned status: '" + httpResponse.getStatusLine().getStatusCode() + + "' for checking current XMPP Sessions."; + log.error(errorMsg); + throw new DeviceControllerException(errorMsg); + } + + } catch (IOException e) { + String errorMsg = "Error occured whilst trying a 'DELETE' user-session [" + sessionName + "] " + + "at : " + xmppUserSessionsAPIEndpoint; + log.error(errorMsg); + throw new DeviceControllerException(errorMsg, e); + } + } + } + } +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/exception/DeviceControllerException.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/exception/DeviceControllerException.java new file mode 100644 index 000000000..c3bce4848 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/exception/DeviceControllerException.java @@ -0,0 +1,42 @@ +/* + * 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.device.mgt.iot.exception; + +public class DeviceControllerException extends Exception { + + public DeviceControllerException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public DeviceControllerException(Throwable cause) { + super(cause); + } + + public DeviceControllerException(String message, Throwable cause) { + super(message, cause); + } + + public DeviceControllerException(String message) { + super(message); + } + + public DeviceControllerException() { + } +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/exception/IoTException.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/exception/IoTException.java new file mode 100644 index 000000000..85639cb63 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/exception/IoTException.java @@ -0,0 +1,43 @@ +/* + * 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.device.mgt.iot.exception; + +public class IoTException extends Exception { + + public IoTException() { + super(); + } + + public IoTException(String message) { + super(message); + } + + public IoTException(String message, Throwable cause) { + super(message, cause); + } + + public IoTException(Throwable cause) { + super(cause); + } + + protected IoTException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/exception/NotImplementedException.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/exception/NotImplementedException.java new file mode 100644 index 000000000..7ce9b3f80 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/exception/NotImplementedException.java @@ -0,0 +1,43 @@ +/* + * 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.device.mgt.iot.exception; + +public class NotImplementedException extends RuntimeException { + + public NotImplementedException() { + super(); + } + + public NotImplementedException(String message) { + super(message); + } + + public NotImplementedException(String message, Throwable cause) { + super(message, cause); + } + + public NotImplementedException(Throwable cause) { + super(cause); + } + + protected NotImplementedException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/exception/UnauthorizedException.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/exception/UnauthorizedException.java new file mode 100644 index 000000000..1af199b17 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/exception/UnauthorizedException.java @@ -0,0 +1,39 @@ +/* + * 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.device.mgt.iot.exception; + +public class UnauthorizedException extends Exception { + + public UnauthorizedException(String message) { + super(message); + } + + public UnauthorizedException(String message, Throwable cause) { + super(message, cause); + } + + public UnauthorizedException(Throwable cause) { + super(cause); + } + + public UnauthorizedException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/internal/IoTCommonDataHolder.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/internal/IoTCommonDataHolder.java new file mode 100644 index 000000000..6ed374350 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/internal/IoTCommonDataHolder.java @@ -0,0 +1,54 @@ +/* + * 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.device.mgt.iot.internal; + +import org.wso2.carbon.base.ServerConfiguration; +import org.wso2.carbon.databridge.core.DataBridgeReceiverService; + +public class IoTCommonDataHolder { + + private static IoTCommonDataHolder thisInstance = new IoTCommonDataHolder(); + String trustStoreLocaiton; + String trustStorePassword; + private IoTCommonDataHolder() { + + } + + public void initialize(){ + setTrustStore(); + } + + public static IoTCommonDataHolder getInstance() { + return thisInstance; + } + + private void setTrustStore(){ + this.trustStoreLocaiton = ServerConfiguration.getInstance().getFirstProperty("Security.TrustStore.Location"); + this.trustStorePassword = ServerConfiguration.getInstance().getFirstProperty("Security.TrustStore.Password"); + } + + public String getTrustStoreLocation(){ + return trustStoreLocaiton; + } + + public String getTrustStorePassword(){ + return trustStorePassword; + } + +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/internal/IotDeviceManagementServiceComponent.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/internal/IotDeviceManagementServiceComponent.java new file mode 100644 index 000000000..dadc99f97 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/internal/IotDeviceManagementServiceComponent.java @@ -0,0 +1,100 @@ +/* + * 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.device.mgt.iot.internal; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; +import org.wso2.carbon.core.ServerStartupObserver; +import org.wso2.carbon.device.mgt.iot.config.server.DeviceManagementConfigurationManager; +import org.wso2.carbon.device.mgt.iot.controlqueue.xmpp.XmppConfig; +import org.wso2.carbon.device.mgt.iot.controlqueue.xmpp.XmppServerClient; +import org.wso2.carbon.device.mgt.iot.exception.DeviceControllerException; +import org.wso2.carbon.device.mgt.iot.service.IoTServerStartupListener; +import org.wso2.carbon.utils.ConfigurationContextService; + +/** + * @scr.component name="org.wso2.carbon.device.mgt.iot.internal.IotDeviceManagementServiceComponent" + * immediate="true" + * @scr.reference name="config.context.service" + * interface="org.wso2.carbon.utils.ConfigurationContextService" + * cardinality="0..1" + * policy="dynamic" + * bind="setConfigurationContextService" + * unbind="unsetConfigurationContextService" + */ +public class IotDeviceManagementServiceComponent { + + private static final Log log = LogFactory.getLog(IotDeviceManagementServiceComponent.class); + public static ConfigurationContextService configurationContextService; + + protected void activate(ComponentContext ctx) { + if (log.isDebugEnabled()) { + log.debug("Activating Iot Device Management Service Component"); + } + try { + BundleContext bundleContext = ctx.getBundleContext(); + /* Initialize the data source configuration */ + DeviceManagementConfigurationManager.getInstance().initConfig(); + bundleContext.registerService(ServerStartupObserver.class.getName(), new IoTServerStartupListener(), null); + IoTCommonDataHolder.getInstance().initialize(); + if (log.isDebugEnabled()) { + log.debug("Iot Device Management Service Component has been successfully activated"); + } + } catch (Throwable e) { + log.error("Error occurred while activating Iot Device Management Service Component", e); + } + } + + protected void deactivate(ComponentContext ctx) { + XmppConfig xmppConfig = XmppConfig.getInstance(); + try { + if (xmppConfig.isEnabled()) { + XmppServerClient xmppServerClient = new XmppServerClient(); + xmppServerClient.initControlQueue(); + xmppServerClient.deleteCurrentXmppSessions(); + } + } catch (DeviceControllerException e) { + String errorMsg = "An error occurred whilst trying to delete all existing XMPP login sessions at " + + "[" + xmppConfig.getXmppEndpoint() + "]."; + log.error(errorMsg, e); + } + + if (log.isDebugEnabled()) { + log.debug("De-activating Iot Device Management Service Component"); + } + } + + protected void setConfigurationContextService(ConfigurationContextService configurationContextService) { + if (log.isDebugEnabled()) { + log.debug("Setting ConfigurationContextService"); + } + + IotDeviceManagementServiceComponent.configurationContextService = configurationContextService; + + } + + protected void unsetConfigurationContextService(ConfigurationContextService configurationContextService) { + if (log.isDebugEnabled()) { + log.debug("Un-setting ConfigurationContextService"); + } + IotDeviceManagementServiceComponent.configurationContextService = null; + } +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/mqtt/PolicyPush.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/mqtt/PolicyPush.java new file mode 100644 index 000000000..9fc4fddd9 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/mqtt/PolicyPush.java @@ -0,0 +1,66 @@ +/* + * 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.device.mgt.iot.mqtt; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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.eclipse.paho.client.mqttv3.persist.MemoryPersistence; + +public class PolicyPush { + + private static final Log log = LogFactory.getLog(PolicyPush.class); + + public boolean pushToMQTT(String topic, String content, String broker, String clientId) { + + byte qos = 2; + MemoryPersistence persistence = new MemoryPersistence(); + + try { + MqttClient me = new MqttClient(broker, clientId, persistence); + MqttConnectOptions connOpts = new MqttConnectOptions(); + connOpts.setCleanSession(true); + if (log.isDebugEnabled()) { + log.debug("Connecting to broker: " + broker); + } + me.connect(connOpts); + if (log.isDebugEnabled()) { + log.debug("Connected"); + log.debug("Publishing message: " + content); + } + MqttMessage message = new MqttMessage(content.getBytes()); + message.setQos(qos); + me.publish(topic, message); + if (log.isDebugEnabled()) { + log.debug("Message published"); + } + me.disconnect(); + if (log.isDebugEnabled()) { + log.debug("Disconnected"); + } + return true; + } catch (MqttException ex) { + log.error("Error occurred when trying to publish to MQTT Queue", ex); + return false; + } + } +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/sensormgt/DeviceRecord.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/sensormgt/DeviceRecord.java new file mode 100644 index 000000000..5f4d6eb90 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/sensormgt/DeviceRecord.java @@ -0,0 +1,43 @@ +/* + * 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.device.mgt.iot.sensormgt; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +@XmlRootElement +public class DeviceRecord implements Serializable { + private Map sensorDataList = new HashMap<>(); + + public DeviceRecord(String sensorName, String sensorValue, long time) { + sensorDataList.put(sensorName, new SensorRecord(sensorValue, time)); + } + + @XmlElement + public Map getSensorDataList() { + return sensorDataList; + } + + public void addDeviceRecord(String sensorName, String sensorValue, long time) { + sensorDataList.put(sensorName, new SensorRecord(sensorValue, time)); + } +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/sensormgt/SensorDataManager.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/sensormgt/SensorDataManager.java new file mode 100644 index 000000000..fa1cc7c15 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/sensormgt/SensorDataManager.java @@ -0,0 +1,137 @@ +/* + * 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.device.mgt.iot.sensormgt; + +import org.wso2.carbon.device.mgt.iot.exception.DeviceControllerException; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * This class is used to store latest sensor value readings against a device id in an in-memory map. + */ +public class SensorDataManager { + + private static final SensorDataManager instance = new SensorDataManager(); + private Map deviceMap = new HashMap<>(); + + private SensorDataManager() { + } + + public static SensorDataManager getInstance() { + return instance; + } + + /** + * Store sensor record in a map. + * + * @param deviceId + * @param sensorName + * @param sensorValue + * @param time + * @return if success returns true + */ + public boolean setSensorRecord(String deviceId, String sensorName, String sensorValue, long time) { + DeviceRecord deviceRecord = deviceMap.get(deviceId); + if (deviceRecord == null) { + deviceRecord = new DeviceRecord(sensorName, sensorValue, time); + } else { + deviceRecord.addDeviceRecord(sensorName, sensorValue, time); + } + deviceMap.put(deviceId, deviceRecord); + return true; + } + + /** + * Returns last updated sensor records list for a device + * + * @param deviceId + * @return list of sensor records + */ + public SensorRecord[] getSensorRecords(String deviceId) throws DeviceControllerException { + DeviceRecord deviceRecord = deviceMap.get(deviceId); + if (deviceRecord != null) { + Collection list = deviceRecord.getSensorDataList().values(); + return list.toArray(new SensorRecord[list.size()]); + } + throw new DeviceControllerException("Error: No records found for the device ID: " + deviceId); + } + + /** + * Returns last updated sensor record for a device's sensor + * + * @param deviceId + * @param sensorName + * @return sensor record + */ + public SensorRecord getSensorRecord(String deviceId, String sensorName) throws + DeviceControllerException { + DeviceRecord deviceRecord = deviceMap.get(deviceId); + if (deviceRecord != null) { + SensorRecord sensorRecord = deviceRecord.getSensorDataList().get(sensorName); + if (sensorRecord != null) { + return sensorRecord; + } + throw new DeviceControllerException("Error: No records found for the Device ID: " + deviceId + + " Sensor Name: " + sensorName); + } + throw new DeviceControllerException("Error: No records found for the device ID: " + deviceId); + } + + /** + * Returns last updated sensor value for a device's sensor + * + * @param deviceId + * @param sensorName + * @return sensor reading + */ + public String getSensorRecordValue(String deviceId, String sensorName) throws DeviceControllerException { + DeviceRecord deviceRecord = deviceMap.get(deviceId); + if (deviceRecord != null) { + SensorRecord sensorRecord = deviceRecord.getSensorDataList().get(sensorName); + if (sensorRecord != null) { + return sensorRecord.getSensorValue(); + } + throw new DeviceControllerException("Error: No records found for the Device ID: " + deviceId + + " Sensor Name: " + sensorName); + } + throw new DeviceControllerException("Error: No records found for the device ID: " + deviceId); + } + + /** + * Returns last updated sensor value reading time for a device's sensor + * + * @param deviceId + * @param sensorName + * @return time in millis + */ + public long getSensorRecordTime(String deviceId, String sensorName) throws DeviceControllerException { + DeviceRecord deviceRecord = deviceMap.get(deviceId); + if (deviceRecord != null) { + SensorRecord sensorRecord = deviceRecord.getSensorDataList().get(sensorName); + if (sensorRecord != null) { + return sensorRecord.getTime(); + } + throw new DeviceControllerException("Error: No records found for the Device ID: " + deviceId + + " Sensor Name: " + sensorName); + } + throw new DeviceControllerException("Error: No records found for the device ID: " + deviceId); + } +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/sensormgt/SensorRecord.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/sensormgt/SensorRecord.java new file mode 100644 index 000000000..88b9916fd --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/sensormgt/SensorRecord.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.device.mgt.iot.sensormgt; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; + +@XmlRootElement +public class SensorRecord implements Serializable { + //sensor value float, int, boolean all should be converted into string + private String sensorValue; + private long time; + + public SensorRecord(String sensorValue, long time) { + this.sensorValue = sensorValue; + this.time = time; + } + + @XmlElement + public String getSensorValue() { + return sensorValue; + } + + @XmlElement + public long getTime() { + return time; + } + +} \ No newline at end of file diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/service/IoTServerStartupListener.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/service/IoTServerStartupListener.java new file mode 100644 index 000000000..22fd657cc --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/service/IoTServerStartupListener.java @@ -0,0 +1,41 @@ +/* + * 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.device.mgt.iot.service; + +import org.wso2.carbon.core.ServerStartupObserver; + +public class IoTServerStartupListener implements ServerStartupObserver { + private static volatile boolean serverReady = false; + @Override + public void completingServerStartup() { + } + + @Override + public void completedServerStartup() { + IoTServerStartupListener.setServerReady(true); + } + + public static boolean isServerReady() { + return IoTServerStartupListener.serverReady; + } + + public static void setServerReady(boolean serverReady) { + IoTServerStartupListener.serverReady = serverReady; + } +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/CommunicationUtils.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/CommunicationUtils.java new file mode 100644 index 000000000..59cd7813d --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/CommunicationUtils.java @@ -0,0 +1,219 @@ +/* + * 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.device.mgt.iot.transport; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.tomcat.util.codec.binary.Base64; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; + +/** + * This is a utility class which contains methods common to the communication process of a client and the server. The + * methods include encryption/decryption of payloads and signing/verification of payloads received and to be sent. + */ +public class CommunicationUtils { + private static final Log log = LogFactory.getLog(TransportUtils.class); + // The Signature Algorithm used. + private static final String SIGNATURE_ALG = "SHA1withRSA"; + // The Encryption Algorithm and the Padding used. + private static final String CIPHER_PADDING = "RSA/ECB/PKCS1Padding"; + + /** + * Encrypts the message with the key that's passed in. + * + * @param message the message to be encrypted. + * @param encryptionKey the key to use for the encryption of the message. + * @return the encrypted message in String format. + * @throws TransportHandlerException if an error occurs with the encryption flow which can be due to Padding + * issues, encryption key being invalid or the algorithm used is unrecognizable. + */ + public static String encryptMessage(String message, Key encryptionKey) throws TransportHandlerException { + Cipher encrypter; + byte[] cipherData; + + try { + encrypter = Cipher.getInstance(CIPHER_PADDING); + encrypter.init(Cipher.ENCRYPT_MODE, encryptionKey); + cipherData = encrypter.doFinal(message.getBytes(StandardCharsets.UTF_8)); + + } catch (NoSuchAlgorithmException e) { + String errorMsg = "Algorithm not found exception occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (NoSuchPaddingException e) { + String errorMsg = "No Padding error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (InvalidKeyException e) { + String errorMsg = "InvalidKey exception occurred for encryptionKey \n[\n" + encryptionKey + "\n]\n"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (BadPaddingException e) { + String errorMsg = "Bad Padding error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (IllegalBlockSizeException e) { + String errorMsg = "Illegal blockSize error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } + + return Base64.encodeBase64String(cipherData); + } + +///TODO:: Exception needs to change according to the common package + + /** + * Signed a given message using the PrivateKey that's passes in. + * + * @param message the message to be signed. Ideally some encrypted payload. + * @param signatureKey the PrivateKey with which the message is to be signed. + * @return the Base64Encoded String of the signed payload. + * @throws TransportHandlerException if some error occurs with the signing process which may be related to the + * signature algorithm used or the key used for signing. + */ + public static String signMessage(String message, PrivateKey signatureKey) throws TransportHandlerException { + + Signature signature; + String signedEncodedString; + + try { + signature = Signature.getInstance(SIGNATURE_ALG); + signature.initSign(signatureKey); + signature.update(Base64.decodeBase64(message)); + + byte[] signatureBytes = signature.sign(); + signedEncodedString = Base64.encodeBase64String(signatureBytes); + + } catch (NoSuchAlgorithmException e) { + String errorMsg = + "Algorithm not found exception occurred for Signature instance of [" + SIGNATURE_ALG + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (SignatureException e) { + String errorMsg = "Signature exception occurred for Signature instance of [" + SIGNATURE_ALG + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (InvalidKeyException e) { + String errorMsg = "InvalidKey exception occurred for signatureKey \n[\n" + signatureKey + "\n]\n"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } + return signedEncodedString; + } + + /** + * Verifies some signed-data against the a Public-Key to ensure that it was produced by the holder of the + * corresponding Private Key. + * + * @param data the actual payoad which was signed by some Private Key. + * @param signedData the signed data produced by signing the payload using a Private Key. + * @param verificationKey the corresponding Public Key which is an exact pair of the Private-Key with we expect + * the data to be signed by. + * @return true if the signed data verifies to be signed by the corresponding Private Key. + * @throws TransportHandlerException if some error occurs with the verification process which may be related to + * the signature algorithm used or the key used for signing. + */ + public static boolean verifySignature(String data, String signedData, PublicKey verificationKey) + throws TransportHandlerException { + + Signature signature; + boolean verified; + + try { + signature = Signature.getInstance(SIGNATURE_ALG); + signature.initVerify(verificationKey); + signature.update(Base64.decodeBase64(data)); + + verified = signature.verify(Base64.decodeBase64(signedData)); + + } catch (NoSuchAlgorithmException e) { + String errorMsg = + "Algorithm not found exception occurred for Signature instance of [" + SIGNATURE_ALG + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (SignatureException e) { + String errorMsg = "Signature exception occurred for Signature instance of [" + SIGNATURE_ALG + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (InvalidKeyException e) { + String errorMsg = "InvalidKey exception occurred for signatureKey \n[\n" + verificationKey + "\n]\n"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } + return verified; + } + + /** + * Encrypts the message with the key that's passed in. + * + * @param encryptedMessage the encrypted message that is supposed to be decrypted. + * @param decryptKey the key to use in the decryption process. + * @return the decrypted message in String format. + * @throws TransportHandlerException if an error occurs with the encryption flow which can be due to Padding + * issues, encryption key being invalid or the algorithm used is unrecognizable. + */ + public static String decryptMessage(String encryptedMessage, Key decryptKey) throws TransportHandlerException { + + Cipher decrypter; + String decryptedMessage; + + try { + + decrypter = Cipher.getInstance(CIPHER_PADDING); + decrypter.init(Cipher.DECRYPT_MODE, decryptKey); + decryptedMessage = new String(decrypter.doFinal(Base64.decodeBase64(encryptedMessage)), + StandardCharsets.UTF_8); + + } catch (NoSuchAlgorithmException e) { + String errorMsg = "Algorithm not found exception occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (NoSuchPaddingException e) { + String errorMsg = "No Padding error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (InvalidKeyException e) { + String errorMsg = "InvalidKey exception occurred for encryptionKey \n[\n" + decryptKey + "\n]\n"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (BadPaddingException e) { + String errorMsg = "Bad Padding error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (IllegalBlockSizeException e) { + String errorMsg = "Illegal blockSize error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } + return decryptedMessage; + } +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/TransportHandler.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/TransportHandler.java new file mode 100644 index 000000000..b81bdba4f --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/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.device.mgt.iot.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/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/TransportHandlerException.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/TransportHandlerException.java new file mode 100644 index 000000000..c862e56d8 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/TransportHandlerException.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.device.mgt.iot.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/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/TransportUtils.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/TransportUtils.java new file mode 100644 index 000000000..b6f32778b --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/TransportUtils.java @@ -0,0 +1,296 @@ +/* + * 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.device.mgt.iot.transport; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.DatagramSocket; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.NetworkInterface; +import java.net.ServerSocket; +import java.net.SocketException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +public class TransportUtils { + private static final Log log = LogFactory.getLog(TransportUtils.class); + + public static final int MIN_PORT_NUMBER = 9000; + public static final int MAX_PORT_NUMBER = 11000; + + /** + * Given a server endpoint as a String, this method splits it into Protocol, Host and Port + * + * @param ipString a network endpoint in the format - '://:' + * @return a map with keys "Protocol", "Host" & "Port" for the related values from the ipString + * @throws TransportHandlerException + */ + public static Map getHostAndPort(String ipString) + throws TransportHandlerException { + Map ipPortMap = new HashMap(); + String[] ipPortArray = ipString.split(":"); + + if (ipPortArray.length != 3) { + String errorMsg = + "The IP String - '" + ipString + + "' is invalid. It needs to be in format '://:'."; + log.info(errorMsg); + throw new TransportHandlerException(errorMsg); + } + + ipPortMap.put("Protocol", ipPortArray[0]); + ipPortMap.put("Host", ipPortArray[1].replace(File.separator, "")); + ipPortMap.put("Port", ipPortArray[2]); + return ipPortMap; + } + + /** + * This method validates whether a specific IP Address is of IPv4 type + * + * @param ipAddress the IP Address which needs to be validated + * @return true if it is of IPv4 type and false otherwise + */ + public static boolean validateIPv4(String ipAddress) { + try { + if (ipAddress == null || ipAddress.isEmpty()) { + return false; + } + + String[] parts = ipAddress.split("\\."); + if (parts.length != 4) { + return false; + } + + for (String s : parts) { + int i = Integer.parseInt(s); + if ((i < 0) || (i > 255)) { + return false; + } + } + return !ipAddress.endsWith("."); + + } catch (NumberFormatException nfe) { + log.warn("The IP Address: " + ipAddress + " could not " + + "be validated against IPv4-style"); + return false; + } + } + + public static Map getInterfaceIPMap() throws TransportHandlerException { + + Map interfaceToIPMap = new HashMap(); + Enumeration networkInterfaces; + String networkInterfaceName = ""; + String ipAddress; + + try { + networkInterfaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException exception) { + String errorMsg = + "Error encountered whilst trying to get the list of network-interfaces"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, exception); + } + + try { + for (; networkInterfaces.hasMoreElements(); ) { + networkInterfaceName = networkInterfaces.nextElement().getName(); + + if (log.isDebugEnabled()) { + log.debug("Network Interface: " + networkInterfaceName); + log.debug("------------------------------------------"); + } + + Enumeration interfaceIPAddresses = NetworkInterface.getByName( + networkInterfaceName).getInetAddresses(); + + for (; interfaceIPAddresses.hasMoreElements(); ) { + ipAddress = interfaceIPAddresses.nextElement().getHostAddress(); + + if (log.isDebugEnabled()) { + log.debug("IP Address: " + ipAddress); + } + + if (TransportUtils.validateIPv4(ipAddress)) { + interfaceToIPMap.put(networkInterfaceName, ipAddress); + } + } + + if (log.isDebugEnabled()) { + log.debug("------------------------------------------"); + } + } + } catch (SocketException exception) { + String errorMsg = + "Error encountered whilst trying to get the IP Addresses of the network " + + "interface: " + networkInterfaceName; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, exception); + } + + return interfaceToIPMap; + } + + /** + * Attempts to find a free port between the MIN_PORT_NUMBER(9000) and MAX_PORT_NUMBER(11000). + * Tries 'RANDOMLY picked' port numbers between this range up-until "randomAttempts" number of + * times. If still fails, then tries each port in descending order from the MAX_PORT_NUMBER + * whilst skipping already attempted ones via random selection. + * + * @param randomAttempts no of times to TEST port numbers picked randomly over the given range + * @return an available/free port + */ + public static synchronized int getAvailablePort(int randomAttempts) { + ArrayList failedPorts = new ArrayList(randomAttempts); + + Random randomNum = new Random(); + int randomPort = MAX_PORT_NUMBER; + + while (randomAttempts > 0) { + randomPort = randomNum.nextInt(MAX_PORT_NUMBER - MIN_PORT_NUMBER) + MIN_PORT_NUMBER; + + if (checkIfPortAvailable(randomPort)) { + return randomPort; + } + failedPorts.add(randomPort); + randomAttempts--; + } + + randomPort = MAX_PORT_NUMBER; + + while (true) { + if (!failedPorts.contains(randomPort) && checkIfPortAvailable(randomPort)) { + return randomPort; + } + randomPort--; + } + } + + private static boolean checkIfPortAvailable(int port) { + ServerSocket tcpSocket = null; + DatagramSocket udpSocket = null; + + try { + tcpSocket = new ServerSocket(port); + tcpSocket.setReuseAddress(true); + + udpSocket = new DatagramSocket(port); + udpSocket.setReuseAddress(true); + return true; + } catch (IOException ex) { + // denotes the port is in use + } finally { + if (tcpSocket != null) { + try { + tcpSocket.close(); + } catch (IOException e) { + /* not to be thrown */ + } + } + + if (udpSocket != null) { + udpSocket.close(); + } + } + + return false; + } + + /** + * This is a utility method that creates and returns a HTTP connection object. + * + * @param urlString the URL pattern to which the connection needs to be created + * @return an HTTPConnection object which cn be used to send HTTP requests + * @throws TransportHandlerException if errors occur when creating the HTTP connection with + * the given URL string + */ + public static HttpURLConnection getHttpConnection(String urlString) throws + TransportHandlerException { + URL connectionUrl; + HttpURLConnection httpConnection; + + try { + connectionUrl = new URL(urlString); + httpConnection = (HttpURLConnection) connectionUrl.openConnection(); + } catch (MalformedURLException e) { + String errorMsg = "Error occured whilst trying to form HTTP-URL from string: " + urlString; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (IOException exception) { + String errorMsg = "Error occured whilst trying to open a connection to: " + urlString; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, exception); + } + return httpConnection; + } + + /** + * This is a utility method that reads and returns the response from a HTTP connection + * + * @param httpConnection the connection from which a response is expected + * @return the response (as a string) from the given HTTP connection + * @throws TransportHandlerException if any errors occur whilst reading the response from + * the connection stream + */ + public static String readResponseFromHttpRequest(HttpURLConnection httpConnection) + throws TransportHandlerException { + BufferedReader bufferedReader; + try { + bufferedReader = new BufferedReader(new InputStreamReader( + httpConnection.getInputStream(), StandardCharsets.UTF_8)); + } catch (IOException exception) { + String errorMsg = "There is an issue with connecting the reader to the input stream at: " + + httpConnection.getURL(); + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, exception); + } + + String responseLine; + StringBuilder completeResponse = new StringBuilder(); + + try { + while ((responseLine = bufferedReader.readLine()) != null) { + completeResponse.append(responseLine); + } + } catch (IOException exception) { + String errorMsg = "Error occured whilst trying read from the connection stream at: " + + httpConnection.getURL(); + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, exception); + } + try { + bufferedReader.close(); + } catch (IOException exception) { + log.error("Could not succesfully close the bufferedReader to the connection at: " + httpConnection.getURL()); + } + return completeResponse.toString(); + } +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/mqtt/MQTTTransportHandler.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/mqtt/MQTTTransportHandler.java new file mode 100644 index 000000000..a413bbf9c --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/mqtt/MQTTTransportHandler.java @@ -0,0 +1,402 @@ +/* + * 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.device.mgt.iot.transport.mqtt; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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.device.mgt.iot.transport.TransportHandler; +import org.wso2.carbon.device.mgt.iot.transport.TransportHandlerException; + +import java.io.File; +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 Log log = LogFactory.getLog(MQTTTransportHandler.class); + 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 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. + */ + protected MQTTTransportHandler(String deviceOwner, String deviceType, + String mqttBrokerEndPoint, String subscribeTopic) { + this.clientId = deviceOwner + ":" + deviceType; + this.subscribeTopic = subscribeTopic; + this.clientWillTopic = DISCONNECTION_WILL_TOPIC_PREFIX + deviceType; + this.mqttBrokerEndPoint = mqttBrokerEndPoint; + this.timeoutInterval = DEFAULT_TIMEOUT_INTERVAL; + 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.info("MQTT client was created with ClientID : " + clientId); + } catch (MqttException ex) { + String errorMsg = "Initializing the MQTT Client failed."; + log.error(errorMsg, ex); + //TODO:: Throw the error out + } + 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); + if (log.isDebugEnabled()) { + log.debug("MQTT Client connected to queue at: " + this.mqttBrokerEndPoint); + } + } catch (MqttException ex) { + String errorMsg = "MQTT Exception occured whilst connecting to queue at [" + this.mqttBrokerEndPoint + "]"; + log.error(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); + if (log.isDebugEnabled()) { + log.debug("Client [" + clientId + "] subscribed to topic: " + subscribeTopic); + } + } catch (MqttException ex) { + String errorMsg = "MQTT Exception occurred whilst client [" + clientId + "] tried to subscribe to " + + "topic: [" + subscribeTopic + "]"; + log.error(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); + if (log.isDebugEnabled()) { + log.debug("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.info(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); + if (log.isDebugEnabled()) { + log.debug("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.info(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) { + if (log.isDebugEnabled()) { + log.warn("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) { + if (log.isDebugEnabled()) { + log.debug("Got an MQTT message '" + mqttMessage.toString() + "' for topic '" + topic + "'."); + } + + Thread messageProcessorThread = new Thread() { + public void run() { + try { + processIncomingMessage(mqttMessage, topic); + } catch (TransportHandlerException e) { + log.error("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 (log.isDebugEnabled()) { + if (iMqttDeliveryToken.getMessage() != null) { + String message = iMqttDeliveryToken.getMessage().toString(); + log.debug("Message to client [" + client + "] under topic (" + topic + + ") was delivered successfully with the delivery message: '" + message + "'"); + } else { + log.debug("Message to client [" + client + "] under topic (" + topic + + ") was delivered successfully."); + } + } + } else { + log.warn("FAILED: Delivery of MQTT message to [" + client + "] under topic [" + topic + "] failed."); + } + } catch (MqttException e) { + log.warn("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; + } +} + diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/xmpp/XMPPTransportHandler.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/xmpp/XMPPTransportHandler.java new file mode 100644 index 000000000..1c57897f8 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/transport/xmpp/XMPPTransportHandler.java @@ -0,0 +1,376 @@ +/* + * 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.device.mgt.iot.transport.xmpp; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.FromContainsFilter; +import org.jivesoftware.smack.filter.OrFilter; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.filter.PacketTypeFilter; +import org.jivesoftware.smack.filter.ToContainsFilter; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.wso2.carbon.device.mgt.iot.transport.TransportHandler; +import org.wso2.carbon.device.mgt.iot.transport.TransportHandlerException; + +/** + * 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 HTTP-Transport specific implementations. The class implements utility methods for the + * case of a HTTP 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 HTTP specific functionality (ideally a device API writer who would like to communicate to the device + * via HTTP Protocol). + *

      + * This class contains the IoT-Server specific implementation for all the XMPP functionality. This includes + * connecting to a XMPP Server & Login-In using the device's/server's XMPP-Account, Setting listeners and filters on + * incoming XMPP messages and Sending XMPP replies for messages received. Makes use of the 'Smack-XMPP' library + * provided by jivesoftware/igniterealtime. + */ +public abstract class XMPPTransportHandler implements TransportHandler { + + private static final Log log = LogFactory.getLog(XMPPTransportHandler.class); + protected String server; + protected int timeoutInterval; // millis + //TODO:: Shouldnt be hard-coded. Need to be read from configs + private static final int DEFAULT_XMPP_PORT = 5222; + private XMPPConnection connection; + private int port; + private PacketFilter filter; + private PacketListener listener; + + /** + * Constructor for XMPPTransportHandler passing only the server-IP. + * + * @param server the IP of the XMPP server. + */ + @SuppressWarnings("unused") + protected XMPPTransportHandler(String server) { + this.server = server; + this.port = DEFAULT_XMPP_PORT; + this.timeoutInterval = DEFAULT_TIMEOUT_INTERVAL; + initXMPPClient(); + } + + /** + * Constructor for XMPPTransportHandler passing server-IP and the XMPP-port. + * + * @param server the IP of the XMPP server. + * @param port the XMPP server's port to connect to. (default - 5222) + */ + protected XMPPTransportHandler(String server, int port) { + this.server = server; + this.port = port; + this.timeoutInterval = DEFAULT_TIMEOUT_INTERVAL; + initXMPPClient(); + } + + /** + * Constructor for XMPPTransportHandler passing server-IP, the XMPP-port and the + * timeoutInterval used by listeners to the server and for reconnection schedules. + * + * @param server the IP of the XMPP server. + * @param port the XMPP server's port to connect to. (default - 5222) + * @param timeoutInterval the timeout interval to use for the connection and reconnection + */ + @SuppressWarnings("unused") + protected XMPPTransportHandler(String server, int port, int timeoutInterval) { + this.server = server; + this.port = port; + this.timeoutInterval = timeoutInterval; + initXMPPClient(); + } + + /** + * Sets the client's time-out-limit whilst waiting for XMPP-replies from server. + * + * @param millis the time in millis to be set as the time-out-limit whilst waiting for a + * XMPP-reply. + */ + @SuppressWarnings("unused") + public void setTimeoutInterval(int millis) { + this.timeoutInterval = millis; + } + + /** + * Checks whether the connection to the XMPP-Server persists. + * + * @return true if the client is connected to the XMPP-Server, else false. + */ + @Override + public boolean isConnected() { + return connection.isConnected(); + } + + /** + * Initializes the XMPP Client. Sets the time-out-limit whilst waiting for XMPP-replies from + * server. Sets the XMPP configurations to connect to the server and creates the + * XMPPConnection object used for connecting and Logging-In. + */ + private void initXMPPClient() { + log.info(String.format("Initializing connection to XMPP Server at %1$s via port " + + "%2$d.", server, port)); + SmackConfiguration.setPacketReplyTimeout(timeoutInterval); + ConnectionConfiguration config = new ConnectionConfiguration(server, port); +// TODO:: Need to enable SASL-Authentication appropriately + config.setSASLAuthenticationEnabled(false); + config.setSecurityMode(ConnectionConfiguration.SecurityMode.disabled); + connection = new XMPPConnection(config); + } + +//TODO:: Re-check all exception handling + + /** + * Connects to the XMPP-Server and if attempt unsuccessful, then throws exception. + * + * @throws TransportHandlerException in the event of 'Connecting to' the XMPP server fails. + */ + protected void connectToServer() throws TransportHandlerException { + try { + connection.connect(); + log.info(String.format( + "Connection to XMPP Server at %1$s established successfully......", server)); + + } catch (XMPPException xmppExcepion) { + String errorMsg = + "Connection attempt to the XMPP Server at " + server + " via port " + port + + " failed."; + log.info(errorMsg); + throw new TransportHandlerException(errorMsg, xmppExcepion); + } + } + + /** + * If successfully established connection, then tries to Log in using the device's XMPP + * Account credentials. + * + * @param username the username of the device's XMPP-Account. + * @param password the password of the device's XMPP-Account. + * @param resource the resource the resource, specific to the XMPP-Account to which the login + * is made to + * @throws TransportHandlerException in the event of 'Logging into' the XMPP server fails. + */ + protected void loginToServer(String username, String password, String resource) + throws TransportHandlerException { + if (isConnected()) { + try { + if (resource == null) { + connection.login(username, password); + log.info(String.format("Logged into XMPP Server at %1$s as user %2$s......", + server, username)); + } else { + connection.login(username, password, resource); + log.info(String.format( + "Logged into XMPP Server at %1$s as user %2$s on resource %3$s......", + server, username, resource)); + } + } catch (XMPPException xmppException) { + String errorMsg = + "Login attempt to the XMPP Server at " + server + " with username - " + + username + " failed."; + log.info(errorMsg); + throw new TransportHandlerException(errorMsg, xmppException); + } + } else {//TODO:: Log not required + String errorMsg = + "Not connected to XMPP-Server to attempt Login. Please 'connectToServer' " + + "before Login"; + if (log.isDebugEnabled()) { + log.debug(errorMsg); + } + throw new TransportHandlerException(errorMsg); + } + } + + /** + * Sets a filter for all the incoming XMPP-Messages on the Sender's JID (XMPP-Account ID). + * Also creates a listener for the incoming messages and connects the listener to the + * XMPPConnection alongside the set filter. + * + * @param senderJID the JID (XMPP-Account ID of the sender) to which the filter is to be set. + */ + @SuppressWarnings("unused") + protected void setFilterOnSender(String senderJID) { + filter = new AndFilter(new PacketTypeFilter(Message.class), new FromContainsFilter( + senderJID)); + listener = new PacketListener() { + @Override + public void processPacket(Packet packet) { + if (packet instanceof Message) { + final Message xmppMessage = (Message) packet; + Thread msgProcessThread = new Thread() { + public void run() { + try { + processIncomingMessage(xmppMessage); + } catch (TransportHandlerException e) { + log.error("An error occurred when trying to process received XMPP message " + + "[" + xmppMessage.getBody() + "].", e); + } + } + }; + msgProcessThread.setDaemon(true); + msgProcessThread.start(); + } + } + }; + connection.addPacketListener(listener, filter); + } + + /** + * Sets a filter for all the incoming XMPP-Messages on the Receiver's JID (XMPP-Account ID). + * Also creates a listener for the incoming messages and connects the listener to the + * XMPPConnection alongside the set filter. + * + * @param receiverJID the JID (XMPP-Account ID of the receiver) to which the filter is to be + * set. + */ + protected void setFilterOnReceiver(String receiverJID) { + filter = new AndFilter(new PacketTypeFilter(Message.class), new ToContainsFilter( + receiverJID)); + listener = new PacketListener() { + @Override + public void processPacket(Packet packet) { + if (packet instanceof Message) { + final Message xmppMessage = (Message) packet; + Thread msgProcessThread = new Thread() { + public void run() { + try { + processIncomingMessage(xmppMessage); + } catch (TransportHandlerException e) { + log.error("An error occurred when trying to process received XMPP message " + + "[" + xmppMessage.getBody() + "].", e); + } + } + }; + msgProcessThread.setDaemon(true); + msgProcessThread.start(); + } + } + }; + connection.addPacketListener(listener, filter); + } + + /** + * Sets a filter for all the incoming XMPP-Messages on the From-JID & To-JID (XMPP-Account IDs) + * passed in. Also creates a listener for the incoming messages and connects the listener to + * the XMPPConnection alongside the set filter. + * + * @param senderJID the From-JID (XMPP-Account ID) to which the filter is to be set. + * @param receiverJID the To-JID (XMPP-Account ID) to which the filter is to be set. + * @param andCondition if true: then filter is set with 'AND' operator (senderJID && + * receiverJID), + * if false: then the filter is set with 'OR' operator (senderJID | + * receiverJID) + */ + @SuppressWarnings("unused") + protected void setMessageFilterAndListener(String senderJID, String receiverJID, boolean + andCondition) { + PacketFilter jidFilter; + + if (andCondition) { + jidFilter = new AndFilter(new FromContainsFilter(senderJID), new ToContainsFilter( + receiverJID)); + } else { + jidFilter = new OrFilter(new FromContainsFilter(senderJID), new ToContainsFilter( + receiverJID)); + } + + filter = new AndFilter(new PacketTypeFilter(Message.class), jidFilter); + listener = new PacketListener() { + @Override + public void processPacket(Packet packet) { + if (packet instanceof Message) { + final Message xmppMessage = (Message) packet; + Thread msgProcessThread = new Thread() { + public void run() { + try { + processIncomingMessage(xmppMessage); + } catch (TransportHandlerException e) { + log.error("An error occurred when trying to process received XMPP message " + + "[" + xmppMessage.getBody() + "].", e); + } + } + }; + msgProcessThread.setDaemon(true); + msgProcessThread.start(); + } + } + }; + connection.addPacketListener(listener, filter); + } + + /** + * Sends an XMPP message. Calls the overloaded method with Subject set to "Reply-From-Device" + * + * @param JID the JID (XMPP Account ID) to which the message is to be sent to. + * @param message the XMPP-Message that is to be sent. + */ + @SuppressWarnings("unused") + protected void sendXMPPMessage(String JID, String message) { + sendXMPPMessage(JID, message, "XMPP-Message"); + } + + /** + * Overloaded method to send an XMPP message. Includes the subject to be mentioned in the + * message that is sent. + * + * @param JID the JID (XMPP Account ID) to which the message is to be sent to. + * @param message the XMPP-Message that is to be sent. + * @param subject the subject that the XMPP-Message would carry. + */ + protected void sendXMPPMessage(String JID, String message, String subject) { + Message xmppMessage = new Message(); + xmppMessage.setTo(JID); + xmppMessage.setSubject(subject); + xmppMessage.setBody(message); + xmppMessage.setType(Message.Type.chat); + sendXMPPMessage(JID, xmppMessage); + } + + /** + * Sends an XMPP message. + * + * @param JID the JID (XMPP Account ID) to which the message is to be sent to. + * @param xmppMessage the XMPP-Message that is to be sent. + */ + protected void sendXMPPMessage(String JID, Message xmppMessage) { + connection.sendPacket(xmppMessage); + if (log.isDebugEnabled()) { + log.debug("Message: '" + xmppMessage.getBody() + "' sent to XMPP JID [" + JID + "] sent successfully."); + } + } + + /** + * Closes the connection to the XMPP Server. + */ + public void closeConnection() { + if (connection != null && isConnected()) { + connection.disconnect(); + } + } + +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/util/IoTUtil.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/util/IoTUtil.java new file mode 100644 index 000000000..76e375e27 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/util/IoTUtil.java @@ -0,0 +1,127 @@ +/* + * 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.device.mgt.iot.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.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.PoolingClientConnectionManager; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpParams; +import org.apache.http.util.EntityUtils; +import org.wso2.carbon.base.ServerConfiguration; +import org.wso2.carbon.device.mgt.iot.exception.IoTException; +import org.wso2.carbon.device.mgt.iot.internal.IoTCommonDataHolder; +import org.wso2.carbon.utils.NetworkUtils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.SocketException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; + +public class IoTUtil { + + public static final String HOST_NAME = "HostName"; + private static final Log log = LogFactory.getLog(IoTUtil.class); + + /** + * Return a http client instance + * + * @param port - server port + * @param protocol- service endpoint protocol http/https + * @return + */ + public static HttpClient getHttpClient(int port, String protocol) + throws UnrecoverableKeyException, NoSuchAlgorithmException, KeyStoreException, + KeyManagementException { + SchemeRegistry registry = new SchemeRegistry(); + + if ("https".equals(protocol)) { + System.setProperty("javax.net.ssl.trustStrore", IoTCommonDataHolder.getInstance().getTrustStoreLocation()); + System.setProperty("javax.net.ssl.trustStorePassword", + IoTCommonDataHolder.getInstance().getTrustStorePassword()); + + if (port >= 0) { + registry.register(new Scheme("https", port, SSLSocketFactory.getSocketFactory())); + } else { + registry.register(new Scheme("https", 443, SSLSocketFactory.getSocketFactory())); + } + } else if ("http".equals(protocol)) { + if (port >= 0) { + registry.register(new Scheme("http", port, PlainSocketFactory.getSocketFactory())); + } else { + registry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory())); + } + } + HttpParams params = new BasicHttpParams(); + PoolingClientConnectionManager tcm = new PoolingClientConnectionManager(registry); + HttpClient client = new DefaultHttpClient(tcm, params); + return client; + } + + public static String getResponseString(HttpResponse httpResponse) throws IoTException { + 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; + } catch (IOException e) { + throw new IoTException("Error while reading the response from the remote. " + + e.getMessage(), e); + } 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 String getHostName() throws IoTException { + String hostName = ServerConfiguration.getInstance().getFirstProperty(HOST_NAME); + + try { + if (hostName == null) { + hostName = NetworkUtils.getLocalHostname(); + } + } catch (SocketException e) { + throw new IoTException("Error while trying to read hostname.", e); + } + + return hostName; + } + +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/util/IotDeviceManagementUtil.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/util/IotDeviceManagementUtil.java new file mode 100644 index 000000000..f1d14ff76 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/util/IotDeviceManagementUtil.java @@ -0,0 +1,346 @@ +/* + * 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.device.mgt.iot.util; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.w3c.dom.Document; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.utils.CarbonUtils; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * Provides utility methods required by the iot device management bundle. + */ +public class IotDeviceManagementUtil { + + private static final Log log = LogFactory.getLog(IotDeviceManagementUtil.class.getName()); + + public static Document convertToDocument(File file) throws DeviceManagementException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + try { + DocumentBuilder docBuilder = factory.newDocumentBuilder(); + return docBuilder.parse(file); + } catch (Exception e) { + throw new DeviceManagementException("Error occurred while parsing file, while converting " + + "to a org.w3c.dom.Document : " + e.getMessage(), e); + } + } + + private static Device.Property getProperty(String property, String value) { + if (property != null) { + Device.Property prop = new Device.Property(); + prop.setName(property); + prop.setValue(value); + return prop; + } + return null; + } + + public static ZipArchive getSketchArchive(String archivesPath, String templateSketchPath, Map contextParams) + throws DeviceManagementException, IOException { + + String sep = File.separator; + String sketchPath = CarbonUtils.getCarbonHome() + sep + templateSketchPath; + + FileUtils.deleteDirectory(new File(archivesPath));//clear directory + FileUtils.deleteDirectory(new File(archivesPath + ".zip"));//clear zip + if (!new File(archivesPath).mkdirs()) { //new dir + String message = "Could not create directory at path: " + archivesPath; + log.error(message); + throw new DeviceManagementException(message); + } + + String zipFileName = "zipFile.zip"; + + try { + Map> properties = getProperties(sketchPath + sep + "sketch" + ".properties"); + List templateFiles = properties.get("templates"); + +// zipFileName = properties.get("zipfilename").get(0); + zipFileName = contextParams.get("DEVICE_NAME") + ".zip"; + + for (String templateFile : templateFiles) { + parseTemplate(templateSketchPath + sep + templateFile, archivesPath + sep + templateFile, + contextParams); + } + + templateFiles.add("sketch.properties"); // ommit copying the props file + copyFolder(new File(sketchPath), new File(archivesPath), templateFiles); + + } catch (IOException ex) { + throw new DeviceManagementException( + "Error occurred when trying to read property " + "file sketch.properties", ex); + } + + try { + createZipArchive(archivesPath); + } catch (IOException e) { + String message = "Zip file for the specific device agent not found at path: " + archivesPath; + log.error(message); + log.error(e); + throw new DeviceManagementException(message, e); + } + FileUtils.deleteDirectory(new File(archivesPath));//clear folder + + /* now get the zip file */ + File zip = new File(archivesPath + ".zip"); + return new ZipArchive(zipFileName, zip); + } + + private static Map> getProperties(String propertyFilePath) throws IOException { + Properties prop = new Properties(); + InputStream input = null; + + try { + + input = new FileInputStream(propertyFilePath); + + // load a properties file + prop.load(input); + Map> properties = new HashMap>(); + + String templates = prop.getProperty("templates"); + List list = new ArrayList(Arrays.asList(templates.split(","))); + properties.put("templates", list); + + final String filename = prop.getProperty("zipfilename"); + list = new ArrayList() {{ + add(filename); + }}; + properties.put("zipfilename", list); + return properties; + + } finally { + if (input != null) { + try { + input.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + private static void parseTemplate(String srcFile, String dstFile, Map contextParams) throws IOException { + //TODO add velocity 1.7, currently commented + //TODO conflicting when calling in CXF environment with the opensaml orbit + + // /* create a context and add data */ + // VelocityContext context = new VelocityContext(contextParams); + // + // /* first, get and initialize an engine */ + // VelocityEngine ve = new VelocityEngine(); + // ve.setProperty( RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, + // "org.apache.velocity.runtime.log.Log4JLogChute" ); + // ve.setProperty("runtime.log.logsystem.log4j.logger", IotDeviceManagementUtil.class.getName()); + // ve.init(); + // + // String sep = File.separator; + // Template t = ve.getTemplate(srcFile); + // FileWriter writer = null; + // try { + // writer = new FileWriter(dstFile); + // t.merge(context, writer); + // } finally { + // if (writer != null) { + // writer.flush(); + // writer.close(); + // } + // } + + //read from file + FileInputStream inputStream = new FileInputStream(srcFile); + String content = IOUtils.toString(inputStream, StandardCharsets.UTF_8.toString()); + Iterator iterator = contextParams.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry mapEntry = (Map.Entry) iterator.next(); + content = content.replaceAll("\\$\\{" + mapEntry.getKey() + "\\}", mapEntry.getValue().toString()); + } + if (inputStream != null) { + inputStream.close(); + } + //write to file + FileOutputStream outputStream = new FileOutputStream(dstFile); + IOUtils.write(content, outputStream, StandardCharsets.UTF_8.toString()); + if (outputStream != null) { + outputStream.close(); + } + } + + private static void copyFolder(File src, File dest, List excludeFileNames) throws IOException { + + if (src.isDirectory()) { + + //if directory not exists, create it + if (!dest.exists() && !dest.mkdirs()) { + String message = "Could not create directory at path: " + dest; + log.error(message); + throw new IOException(message); + } + + //list all the directory contents + String files[] = src.list(); + + if (files == null) { + log.warn("There are no files insides the directory " + src.getAbsolutePath()); + return; + } + + for (String file : files) { + //construct the src and dest file structure + File srcFile = new File(src, file); + File destFile = new File(dest, file); + //recursive copy + copyFolder(srcFile, destFile, excludeFileNames); + } + + } else { + for (String fileName : excludeFileNames) { + if (src.getName().equals(fileName)) { + return; + } + } + //if file, then copy it + //Use bytes stream to support all file types + InputStream in = null; + OutputStream out = null; + + try { + in = new FileInputStream(src); + out = new FileOutputStream(dest); + + byte[] buffer = new byte[1024]; + + int length; + //copy the file content in bytes + while ((length = in.read(buffer)) > 0) { + out.write(buffer, 0, length); + } + } finally { + silentClose(in); + silentClose(out); + } + } + } + + private static void silentClose(InputStream is) { + if (is == null) { + return; + } + + try { + is.close(); + } catch (IOException e) { + // do nothing + } + + } + + private static void silentClose(OutputStream os) { + if (os == null) { + return; + } + + try { + + os.close(); + } catch (IOException e) { + // do nothing + } + } + + private static boolean createZipArchive(String srcFolder) throws IOException { + BufferedInputStream origin = null; + ZipOutputStream out = null; + + try { + final int BUFFER = 2048; + + FileOutputStream dest = new FileOutputStream(new File(srcFolder + ".zip")); + + out = new ZipOutputStream(new BufferedOutputStream(dest)); + byte data[] = new byte[BUFFER]; + + File subDir = new File(srcFolder); + String subdirList[] = subDir.list(); + + if (subdirList == null) { + log.warn("The sub directory " + subDir.getAbsolutePath() + " is empty"); + return false; + } + + for (String sd : subdirList) { + // get a list of files from current directory + File f = new File(srcFolder + "/" + sd); + if (f.isDirectory()) { + String files[] = f.list(); + + if (files == null) { + log.warn("The current directory " + f.getAbsolutePath() + " is empty. Has no files"); + return false; + } + + for (int i = 0; i < files.length; i++) { + FileInputStream fi = new FileInputStream(srcFolder + "/" + sd + "/" + files[i]); + origin = new BufferedInputStream(fi, BUFFER); + ZipEntry entry = new ZipEntry(sd + "/" + files[i]); + out.putNextEntry(entry); + int count; + while ((count = origin.read(data, 0, BUFFER)) != -1) { + out.write(data, 0, count); + out.flush(); + } + + } + } else //it is just a file + { + FileInputStream fi = new FileInputStream(f); + origin = new BufferedInputStream(fi, BUFFER); + ZipEntry entry = new ZipEntry(sd); + out.putNextEntry(entry); + int count; + while ((count = origin.read(data, 0, BUFFER)) != -1) { + out.write(data, 0, count); + out.flush(); + } + + } + } + + out.flush(); + } finally { + silentClose(origin); + silentClose(out); + } + return true; + } + +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/util/ZipArchive.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/util/ZipArchive.java new file mode 100644 index 000000000..b9720d112 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/util/ZipArchive.java @@ -0,0 +1,49 @@ +/* + * 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.device.mgt.iot.util; + +import java.io.File; + +public class ZipArchive { + + private File zipFile = null; + private String fileName = null; + private String deviceId = null; + + public ZipArchive(String fileName, File zipFile) { + this.fileName = fileName; + this.zipFile = zipFile; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public File getZipFile() { + return zipFile; + } + + public String getFileName() { + return fileName; + } +} diff --git a/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/util/ZipUtil.java b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/util/ZipUtil.java new file mode 100644 index 000000000..8ea3396d6 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/org.wso2.carbon.device.mgt.iot/src/main/java/org/wso2/carbon/device/mgt/iot/util/ZipUtil.java @@ -0,0 +1,100 @@ +/* + * 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.device.mgt.iot.util; + +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.iot.config.server.DeviceManagementConfigurationManager; +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.IoTException; +import org.wso2.carbon.utils.CarbonUtils; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class ZipUtil { + + private static final String HTTPS_PORT_PROPERTY = "httpsPort"; + private static final String HTTP_PORT_PROPERTY = "httpPort"; + + private static final String LOCALHOST = "localhost"; + private static final String HTTPS_PROTOCOL_APPENDER = "https://"; + private static final String HTTP_PROTOCOL_APPENDER = "http://"; + + public ZipArchive createZipFile(String owner, String tenantDomain, String deviceType, + String deviceId, String deviceName, String token, + String refreshToken) + throws DeviceManagementException { + + String sep = File.separator; + String sketchFolder = "repository" + sep + "resources" + sep + "sketches"; + String archivesPath = CarbonUtils.getCarbonHome() + sep + sketchFolder + sep + "archives" + sep + deviceId; + String templateSketchPath = sketchFolder + sep + deviceType; + String iotServerIP; + + try { + iotServerIP = IoTUtil.getHostName(); + } catch (IoTException e) { + throw new DeviceManagementException(e.getMessage()); + } + String httpsServerPort = System.getProperty(HTTPS_PORT_PROPERTY); + String httpServerPort = System.getProperty(HTTP_PORT_PROPERTY); + + String httpsServerEP = HTTPS_PROTOCOL_APPENDER + iotServerIP + ":" + httpsServerPort; + String httpServerEP = HTTP_PROTOCOL_APPENDER + iotServerIP + ":" + httpServerPort; + String apimEndpoint = httpsServerEP; + String mqttEndpoint = MqttConfig.getInstance().getMqttQueueEndpoint(); + if (mqttEndpoint.contains(LOCALHOST)) { + mqttEndpoint = mqttEndpoint.replace(LOCALHOST, iotServerIP); + } + + String xmppEndpoint = XmppConfig.getInstance().getXmppEndpoint(); + + int indexOfChar = xmppEndpoint.lastIndexOf(":"); + if (indexOfChar != -1) { + xmppEndpoint = xmppEndpoint.substring(0, indexOfChar); + } + + xmppEndpoint = xmppEndpoint + ":" + XmppConfig.getInstance().getSERVER_CONNECTION_PORT(); + + Map contextParams = new HashMap<>(); + //TODO:refactor remove and move to device type impl + contextParams.put("SERVER_NAME", "wso2"); + contextParams.put("DEVICE_OWNER", owner); + contextParams.put("DEVICE_ID", deviceId); + contextParams.put("DEVICE_NAME", deviceName); + contextParams.put("HTTPS_EP", httpsServerEP); + contextParams.put("HTTP_EP", httpServerEP); + contextParams.put("APIM_EP", apimEndpoint); + contextParams.put("MQTT_EP", mqttEndpoint); + contextParams.put("XMPP_EP", xmppEndpoint); + contextParams.put("DEVICE_TOKEN", token); + contextParams.put("DEVICE_REFRESH_TOKEN", refreshToken); + + ZipArchive zipFile; + try { + zipFile = IotDeviceManagementUtil.getSketchArchive(archivesPath, templateSketchPath, contextParams); + } catch (IOException e) { + throw new DeviceManagementException("Zip File Creation Failed", e); + } + + return zipFile; + } +} diff --git a/components/iot-plugins/iot-base-plugin/pom.xml b/components/iot-plugins/iot-base-plugin/pom.xml new file mode 100644 index 000000000..759eb4077 --- /dev/null +++ b/components/iot-plugins/iot-base-plugin/pom.xml @@ -0,0 +1,60 @@ + + + + + + + org.wso2.carbon.devicemgt-plugins + iot-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + iot-base-plugin + pom + WSO2 Carbon - Arduino Plugin + http://wso2.org + + + org.wso2.carbon.device.mgt.iot + org.wso2.carbon.device.mgt.iot.api + org.wso2.carbon.device.mgt.iot.ui + + + + + + + org.apache.felix + maven-scr-plugin + 1.7.2 + + + generate-scr-scrdescriptor + + scr + + + + + + + + diff --git a/components/iot-plugins/pom.xml b/components/iot-plugins/pom.xml new file mode 100644 index 000000000..0020ee7f1 --- /dev/null +++ b/components/iot-plugins/pom.xml @@ -0,0 +1,67 @@ + + + + + + + org.wso2.carbon.devicemgt-plugins + carbon-device-mgt-plugins-parent + 2.1.0-SNAPSHOT + ../../pom.xml + + + 4.0.0 + org.wso2.carbon.devicemgt-plugins + iot-plugins + pom + WSO2 Carbon - IoT Plugins + http://wso2.org + + + androidsense-plugin + arduino-plugin + camera-plugin + digital-display-plugin + drone-analyzer-plugin + raspberrypi-plugin + virtual-fire-alarm-plugin + iot-base-plugin + + + + + + + org.apache.felix + maven-scr-plugin + 1.7.2 + + + generate-scr-scrdescriptor + + scr + + + + + + + + + diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/build.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/build.xml new file mode 100644 index 000000000..a877b1141 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/build.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/pom.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/pom.xml new file mode 100644 index 000000000..cfe0fb2a2 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/pom.xml @@ -0,0 +1,92 @@ + + + + + + + + raspberrypi-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.raspberrypi.analytics + WSO2 Carbon - IoT Server RaspberryPi Analytics capp + pom + + + + + maven-clean-plugin + 2.4.1 + + + auto-clean + initialize + + clean + + + + + + maven-antrun-plugin + 1.7 + + + process-resources + + + + + + + run + + + + + + maven-assembly-plugin + 2.5.5 + + ${project.artifactId}-${carbon.device.mgt.version} + false + + src/assembly/src.xml + + + + + create-archive + package + + single + + + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/assembly/src.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/assembly/src.xml new file mode 100644 index 000000000..a5a375010 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/assembly/src.xml @@ -0,0 +1,36 @@ + + + + src + + zip + + false + ${basedir}/src + + + ${basedir}/target/carbonapps + / + true + + + \ No newline at end of file diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/EventReceiver_temperature.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/EventReceiver_temperature.xml new file mode 100644 index 000000000..28b710c27 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/EventReceiver_temperature.xml @@ -0,0 +1,26 @@ + + + + + + false + + + + diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/artifact.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/artifact.xml new file mode 100644 index 000000000..25df56734 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + EventReceiver_temperature.xml + diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/artifact.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/artifact.xml new file mode 100644 index 000000000..ccfb3b314 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + org_wso2_iot_devices_temperature.xml + diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/org_wso2_iot_devices_temperature.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/org_wso2_iot_devices_temperature.xml new file mode 100644 index 000000000..d06f73b14 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/org_wso2_iot_devices_temperature.xml @@ -0,0 +1,62 @@ + + + + + + org.wso2.iot.devices.temperature:1.0.0 + + EVENT_STORE + + + meta_owner + true + true + false + STRING + + + meta_deviceType + true + true + false + STRING + + + meta_deviceId + true + true + false + STRING + + + meta_time + true + true + false + LONG + + + temperature + false + false + false + FLOAT + + + \ No newline at end of file diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/artifact.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/artifact.xml new file mode 100644 index 000000000..27ec69702 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/artifact.xml @@ -0,0 +1,23 @@ + + + + + org.wso2.iot.devices.temperature_1.0.0.json + + diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/org.wso2.iot.devices.temperature_1.0.0.json b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/org.wso2.iot.devices.temperature_1.0.0.json new file mode 100644 index 000000000..5d94b9821 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/org.wso2.iot.devices.temperature_1.0.0.json @@ -0,0 +1,20 @@ +{ + "name": "org.wso2.iot.devices.temperature", + "version": "1.0.0", + "nickName": "Temperature Data", + "description": "Temperature data received from the Device", + "metaData": [ + {"name":"owner","type":"STRING"}, + {"name":"deviceType","type":"STRING"}, + {"name":"deviceId","type":"STRING"}, + {"name":"time","type":"LONG"} + ], + "payloadData": [ + { + "name": "temperature","type": "FLOAT" + } + ] +} + + + diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/Temperature_Sensor_Script.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/Temperature_Sensor_Script.xml new file mode 100644 index 000000000..41938dd4f --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/Temperature_Sensor_Script.xml @@ -0,0 +1,31 @@ + + + + + IoTServer_Sensor_Script + + 0 * * * * ? + diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/artifact.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/artifact.xml new file mode 100644 index 000000000..9b4228e30 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + Temperature_Sensor_Script.xml + diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/artifacts.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/artifacts.xml new file mode 100644 index 000000000..c4580f909 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.analytics/src/main/resources/carbonapps/Temperature_Sensor/artifacts.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/pom.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/pom.xml new file mode 100644 index 000000000..4173e23e1 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/pom.xml @@ -0,0 +1,158 @@ + + + + + + + raspberrypi-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl + war + WSO2 Carbon - IoT Server RaspberryPi Controller API + WSO2 Carbon - RaspberryPi Controller Service API Implementation + http://wso2.org + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.core + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + provided + + + org.apache.cxf + 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 + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot + provided + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl + provided + + + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-jaxrs + + + javax + javaee-web-api + provided + + + javax.ws.rs + jsr311-api + provided + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.analytics + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.annotations + provided + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.extensions + provided + + + + + + + + + + maven-compiler-plugin + + UTF-8 + ${wso2.maven.compiler.source} + ${wso2.maven.compiler.target} + + + + maven-war-plugin + + raspberrypi + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/RaspberryPiControllerService.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/RaspberryPiControllerService.java new file mode 100644 index 000000000..1af824749 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/RaspberryPiControllerService.java @@ -0,0 +1,295 @@ +/* + * 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.device.mgt.iot.raspberrypi.controller.service.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.apimgt.annotations.api.API; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.device.mgt.analytics.common.AnalyticsDataRecord; +import org.wso2.carbon.device.mgt.analytics.exception.DeviceManagementAnalyticsException; +import org.wso2.carbon.device.mgt.analytics.service.DeviceAnalyticsService; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.extensions.feature.mgt.annotations.DeviceType; +import org.wso2.carbon.device.mgt.extensions.feature.mgt.annotations.Feature; +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.controller.service.impl.dto.DeviceData; +import org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl.dto.SensorData; +import org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl.transport.RaspberryPiMQTTConnector; +import org.wso2.carbon.device.mgt.iot.raspberrypi.controller.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; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +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; + +@API(name = "raspberrypi", version = "1.0.0", context = "/raspberrypi", tags = {"raspberrypi"}) +@DeviceType(value = "raspberrypi") +public class RaspberryPiControllerService { + + private static Log log = LogFactory.getLog(RaspberryPiControllerService.class); + @Context //injected response proxy supporting multiple thread + private HttpServletResponse response; + private ConcurrentHashMap deviceToIpMap = new ConcurrentHashMap<>(); + private RaspberryPiMQTTConnector raspberryPiMQTTConnector; + + private boolean waitForServerStartup() { + while (!IoTServerStartupListener.isServerReady()) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + return true; + } + } + return false; + } + + public RaspberryPiMQTTConnector getRaspberryPiMQTTConnector() { + return raspberryPiMQTTConnector; + } + + public void setRaspberryPiMQTTConnector( + final RaspberryPiMQTTConnector raspberryPiMQTTConnector) { + Runnable connector = new Runnable() { + public void run() { + if (waitForServerStartup()) { + return; + } + RaspberryPiControllerService.this.raspberryPiMQTTConnector = raspberryPiMQTTConnector; + if (MqttConfig.getInstance().isEnabled()) { + raspberryPiMQTTConnector.connect(); + } else { + log.warn("MQTT disabled in 'devicemgt-config.xml'. Hence, RaspberryPiMQTTConnector not started."); + } + } + }; + Thread connectorThread = new Thread(connector); + connectorThread.setDaemon(true); + connectorThread.start(); + } + + @Path("controller/register/{deviceId}/{ip}/{port}") + @POST + public String registerDeviceIP(@PathParam("deviceId") String deviceId, @PathParam("ip") String deviceIP, + @PathParam("port") String devicePort, @Context HttpServletResponse response, + @Context HttpServletRequest request) { + try { + String result; + if (log.isDebugEnabled()) { + log.debug("Got register call from IP: " + deviceIP + " for Device ID: " + deviceId); + } + String deviceHttpEndpoint = deviceIP + ":" + devicePort; + deviceToIpMap.put(deviceId, deviceHttpEndpoint); + result = "Device-IP Registered"; + response.setStatus(Response.Status.OK.getStatusCode()); + if (log.isDebugEnabled()) { + log.debug(result); + } + return result; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + + @Path("controller/device/{deviceId}/bulb") + @POST + @Feature(code = "bulb", name = "Bulb On / Off", type = "operation", + description = "Switch on/off Raspberry Pi agent's bulb. (On / Off)") + public void switchBulb(@PathParam("deviceId") String deviceId, @FormParam("state") String state, + @Context HttpServletResponse response) { + try { + String switchToState = state.toUpperCase(); + if (!switchToState.equals(RaspberrypiConstants.STATE_ON) && !switchToState.equals( + RaspberrypiConstants.STATE_OFF)) { + log.error("The requested state change shoud be either - 'ON' or 'OFF'"); + response.setStatus(Response.Status.BAD_REQUEST.getStatusCode()); + return; + } + String callUrlPattern = RaspberrypiConstants.BULB_CONTEXT + switchToState; + try { + String deviceHTTPEndpoint = deviceToIpMap.get(deviceId); + if (deviceHTTPEndpoint == null) { + response.setStatus(Response.Status.PRECONDITION_FAILED.getStatusCode()); + return; + } + RaspberrypiServiceUtils.sendCommandViaHTTP(deviceHTTPEndpoint, callUrlPattern, true); + } catch (DeviceManagementException e) { + log.error("Failed to send switch-bulb request to device [" + deviceId + "] via "); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return; + } + response.setStatus(Response.Status.OK.getStatusCode()); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("controller/device/{deviceId}/readtemperature") + @GET + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Feature(code = "readtemperature", name = "Temperature", type = "monitor", + description = "Request temperature reading from Raspberry Pi agent") + public SensorRecord requestTemperature(@PathParam("deviceId") String deviceId, + @Context HttpServletResponse response) { + try { + SensorRecord sensorRecord = null; + if (log.isDebugEnabled()) { + log.debug("Sending request to read raspberrypi-temperature of device [" + deviceId + "] via "); + } + try { + String deviceHTTPEndpoint = deviceToIpMap.get(deviceId); + if (deviceHTTPEndpoint == null) { + response.setStatus(Response.Status.PRECONDITION_FAILED.getStatusCode()); + } + String temperatureValue = RaspberrypiServiceUtils.sendCommandViaHTTP(deviceHTTPEndpoint, + RaspberrypiConstants + .TEMPERATURE_CONTEXT, + + false); + SensorDataManager.getInstance().setSensorRecord(deviceId, RaspberrypiConstants.SENSOR_TEMPERATURE, + temperatureValue, Calendar.getInstance() + .getTimeInMillis + ()); + sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, + RaspberrypiConstants + .SENSOR_TEMPERATURE); + } catch (DeviceManagementException | DeviceControllerException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } + response.setStatus(Response.Status.OK.getStatusCode()); + return sensorRecord; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("controller/push_temperature") + @POST + @Consumes(MediaType.APPLICATION_JSON) + public void pushTemperatureData(final DeviceData dataMsg, @Context HttpServletResponse response, + @Context HttpServletRequest request) { + try { + String owner = dataMsg.owner; + String deviceId = dataMsg.deviceId; + String deviceIp = dataMsg.reply; + float temperature = dataMsg.value; + String registeredIp = deviceToIpMap.get(deviceId); + if (registeredIp == null) { + log.warn("Unregistered IP: Temperature Data Received from an un-registered IP " + deviceIp + + " for device ID - " + deviceId); + response.setStatus(Response.Status.PRECONDITION_FAILED.getStatusCode()); + return; + } else if (!registeredIp.equals(deviceIp)) { + log.warn("Conflicting IP: Received IP is " + deviceIp + ". Device with ID " + deviceId + + " is already registered under some other IP. Re-registration required"); + response.setStatus(Response.Status.CONFLICT.getStatusCode()); + return; + } + if (log.isDebugEnabled()) { + log.debug("Received Pin Data Value: " + temperature + " degrees C"); + } + SensorDataManager.getInstance().setSensorRecord(deviceId, RaspberrypiConstants.SENSOR_TEMPERATURE, + String.valueOf(temperature), + Calendar.getInstance().getTimeInMillis()); + if (!RaspberrypiServiceUtils.publishToDAS(dataMsg.deviceId, dataMsg.value)) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + log.warn("An error occured whilst trying to publish temperature data of raspberrypi with ID [" + + deviceId + "] of owner [" + owner + "]"); + } + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + /** + * Retreive Sensor data for the device type + */ + @Path("controller/stats/device/{deviceId}/sensors/temperature") + @GET + @Consumes("application/json") + @Produces("application/json") + public SensorData[] getArduinoTemperatureStats(@PathParam("deviceId") String deviceId, + @QueryParam("username") String user, + @QueryParam("from") long from, + @QueryParam("to") 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); + } + return sensorDatas.toArray(new SensorData[sensorDatas.size()]); + } catch (DeviceManagementAnalyticsException e) { + String errorMsg = "Error on retrieving stats on table " + sensorTableName + " with query " + query; + log.error(errorMsg); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return sensorDatas.toArray(new SensorData[sensorDatas.size()]); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } +} diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/dto/DeviceData.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/dto/DeviceData.java new file mode 100644 index 000000000..6482d1e01 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/dto/DeviceData.java @@ -0,0 +1,36 @@ +/* + * 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.device.mgt.iot.raspberrypi.controller.service.impl.dto; + +import org.codehaus.jackson.annotate.JsonIgnoreProperties; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement + +@JsonIgnoreProperties(ignoreUnknown = true) +public class DeviceData { + @XmlElement(required = true) public String owner; + @XmlElement(required = true) public String deviceId; + @XmlElement(required = true) public String reply; + @XmlElement public Long time; + @XmlElement public String key; + @XmlElement public float value; +} diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/dto/SensorData.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/dto/SensorData.java new file mode 100644 index 000000000..4709b54f6 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/dto/SensorData.java @@ -0,0 +1,44 @@ +package org.wso2.carbon.device.mgt.iot.raspberrypi.controller.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/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/exception/RaspberrypiException.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/exception/RaspberrypiException.java new file mode 100644 index 000000000..26fa4d538 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/exception/RaspberrypiException.java @@ -0,0 +1,31 @@ +/* + * 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.device.mgt.iot.raspberrypi.controller.service.impl.exception; + +public class RaspberrypiException extends Exception { + private static final long serialVersionUID = 118512086957330189L; + + public RaspberrypiException(String errorMessage) { + super(errorMessage); + } + + public RaspberrypiException(String errorMessage, Throwable throwable) { + super(errorMessage, throwable); + } +} diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/transport/RaspberryPiMQTTConnector.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/transport/RaspberryPiMQTTConnector.java new file mode 100644 index 000000000..d00da115b --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/transport/RaspberryPiMQTTConnector.java @@ -0,0 +1,195 @@ +/* + * 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.device.mgt.iot.raspberrypi.controller.service.impl.transport; + +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.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.config.server.DeviceManagementConfigurationManager; +import org.wso2.carbon.device.mgt.iot.controlqueue.mqtt.MqttConfig; +import org.wso2.carbon.device.mgt.iot.raspberrypi.controller.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.utils.multitenancy.MultitenantUtils; + +import java.io.File; +import java.util.Calendar; +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 iotServerSubscriber = UUID.randomUUID().toString().substring(0, 5); + + private RaspberryPiMQTTConnector() { + super(iotServerSubscriber, RaspberrypiConstants.DEVICE_TYPE, + MqttConfig.getInstance().getMqttQueueEndpoint(), subscribeTopic); + } + + @Override + public void connect() { + Runnable connector = new Runnable() { + public void run() { + while (!isConnected()) { + try { + String brokerUsername = MqttConfig.getInstance().getMqttQueueUsername(); + String brokerPassword = MqttConfig.getInstance().getMqttQueuePassword(); + setUsernameAndPassword(brokerUsername, brokerPassword); + 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); + } + } + } + }; + + Thread connectorThread = new Thread(connector); + connectorThread.setDaemon(true); + connectorThread.start(); + } + + @Override + public void processIncomingMessage(MqttMessage message, String... messageParams) throws TransportHandlerException { + if(messageParams.length != 0) { + // owner and the deviceId are extracted from the MQTT topic to which the message was received. + // = [ServerName/Owner/DeviceType/DeviceId/"publisher"] + String topic = messageParams[0]; + String[] topicParams = topic.split("/"); + String deviceId = topicParams[2]; + String receivedMessage = message.toString(); + + if (log.isDebugEnabled()) { + log.debug("Received MQTT message for: [DEVICE.ID-" + deviceId + "]"); + log.debug("Message [" + receivedMessage + "] topic: [" + topic + "]"); + } + + if (receivedMessage.contains("PUBLISHER")) { + float temperature = Float.parseFloat(receivedMessage.split(":")[2]); + + try { + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + DeviceManagementProviderService deviceManagementProviderService = + (DeviceManagementProviderService) ctx.getOSGiService(DeviceManagementProviderService.class, null); + if (deviceManagementProviderService != null) { + DeviceIdentifier identifier = new DeviceIdentifier(deviceId, RaspberrypiConstants.DEVICE_TYPE); + Device device = deviceManagementProviderService.getDevice(identifier); + if (device != null) { + String owner = device.getEnrolmentInfo().getOwner(); + ctx.setTenantDomain(MultitenantUtils.getTenantDomain(owner), true); + ctx.setUsername(owner); + if (!RaspberrypiServiceUtils.publishToDAS(deviceId, temperature)) { + log.error("MQTT Subscriber: Publishing data to DAS failed."); + } + } + } + } catch (DeviceManagementException e) { + log.error("Failed to retreive the device managment service for device type " + + RaspberrypiConstants.DEVICE_TYPE, e); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + + if (log.isDebugEnabled()) { + log.debug("MQTT Subscriber: Published data to DAS successfully."); + } + + } else if (receivedMessage.contains("TEMPERATURE")) { + String temperatureValue = receivedMessage.split(":")[1]; + SensorDataManager.getInstance().setSensorRecord(deviceId, RaspberrypiConstants.SENSOR_TEMPERATURE, + temperatureValue, Calendar.getInstance().getTimeInMillis()); + } + } + } + + @Override + public void disconnect() { + Runnable stopConnection = new Runnable() { + public void run() { + while (isConnected()) { + try { + closeConnection(); + } catch (MqttException e) { + if (log.isDebugEnabled()) { + log.warn("Unable to 'STOP' MQTT connection at broker at: " + mqttBrokerEndPoint + + " for device-type - " + RaspberrypiConstants.DEVICE_TYPE, e); + } + + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException e1) { + log.error("MQTT-Terminator: Thread Sleep Interrupt Exception at device-type - " + + RaspberrypiConstants.DEVICE_TYPE, e1); + } + } + } + } + }; + + Thread terminatorThread = new Thread(stopConnection); + terminatorThread.setDaemon(true); + terminatorThread.start(); + } + + @Override + public void processIncomingMessage() throws TransportHandlerException { + + } + + @Override + public void processIncomingMessage(MqttMessage message) throws TransportHandlerException { + + } + + @Override + public void publishDeviceData() throws TransportHandlerException { + + } + + @Override + public void publishDeviceData(MqttMessage publishData) throws TransportHandlerException { + + } + + @Override + public void publishDeviceData(String... publishData) throws TransportHandlerException { + + } +} + diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/util/RaspberrypiServiceUtils.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/util/RaspberrypiServiceUtils.java new file mode 100644 index 000000000..a641ab09d --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/controller/service/impl/util/RaspberrypiServiceUtils.java @@ -0,0 +1,228 @@ +/* + * 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.device.mgt.iot.raspberrypi.controller.service.impl.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.concurrent.FutureCallback; +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.exception.DataPublisherConfigurationException; +import org.wso2.carbon.device.mgt.analytics.service.DeviceAnalyticsService; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.constants.RaspberrypiConstants; + +import javax.ws.rs.HttpMethod; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.URL; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; + +public class RaspberrypiServiceUtils { + private static final Log log = LogFactory.getLog(RaspberrypiServiceUtils.class); + + //TODO; replace this tenant domain + private static final String SUPER_TENANT = "carbon.super"; + private static final String TEMPERATURE_STREAM_DEFINITION = "org.wso2.iot.devices.temperature"; + + public static String sendCommandViaHTTP(final String deviceHTTPEndpoint, String urlContext, + boolean fireAndForgot) throws DeviceManagementException { + + String responseMsg = ""; + String urlString = RaspberrypiConstants.URL_PREFIX + deviceHTTPEndpoint + urlContext; + + if (log.isDebugEnabled()) { + log.debug(urlString); + } + + if (!fireAndForgot) { + HttpURLConnection httpConnection = getHttpConnection(urlString); + + try { + httpConnection.setRequestMethod(HttpMethod.GET); + } catch (ProtocolException e) { + String errorMsg = + "Protocol specific error occurred when trying to set method to GET" + + " for:" + urlString; + log.error(errorMsg); + throw new DeviceManagementException(errorMsg, e); + } + + responseMsg = readResponseFromGetRequest(httpConnection); + + } else { + CloseableHttpAsyncClient httpclient = null; + try { + + httpclient = HttpAsyncClients.createDefault(); + httpclient.start(); + HttpGet request = new HttpGet(urlString); + final CountDownLatch latch = new CountDownLatch(1); + Future future = httpclient.execute( + request, new FutureCallback() { + @Override + public void completed(HttpResponse httpResponse) { + latch.countDown(); + } + + @Override + public void failed(Exception e) { + latch.countDown(); + } + + @Override + public void cancelled() { + latch.countDown(); + } + }); + + latch.await(); + + } catch (InterruptedException e) { + if (log.isDebugEnabled()) { + log.debug("Sync Interrupted"); + } + } finally { + try { + if (httpclient != null) { + httpclient.close(); + + } + } catch (IOException e) { + if (log.isDebugEnabled()) { + log.debug("Failed on close"); + } + } + } + } + + return responseMsg; + } + + + /*public static boolean sendCommandViaMQTT(String deviceOwner, String deviceId, String resource, + String state) throws DeviceManagementException { + + boolean result; + DeviceController deviceController = new DeviceController(); + + try { + result = deviceController.publishMqttControl(deviceOwner, RaspberrypiConstants.DEVICE_TYPE, deviceId, resource, state); + } catch (DeviceControllerException e) { + String errorMsg = "Error whilst trying to publish to MQTT Queue"; + log.error(errorMsg); + throw new DeviceManagementException(errorMsg, e); + } + return result; + }*/ + + /* --------------------------------------------------------------------------------------- + Utility methods relevant to creating and sending http requests + --------------------------------------------------------------------------------------- */ + + /* This methods creates and returns a http connection object */ + + public static HttpURLConnection getHttpConnection(String urlString) throws + DeviceManagementException { + + URL connectionUrl = null; + HttpURLConnection httpConnection; + + try { + connectionUrl = new URL(urlString); + httpConnection = (HttpURLConnection) connectionUrl.openConnection(); + } catch (MalformedURLException e) { + String errorMsg = + "Error occured whilst trying to form HTTP-URL from string: " + urlString; + log.error(errorMsg); + throw new DeviceManagementException(errorMsg, e); + } catch (IOException e) { + String errorMsg = "Error occured whilst trying to open a connection to: " + + connectionUrl.toString(); + log.error(errorMsg); + throw new DeviceManagementException(errorMsg, e); + } + + return httpConnection; + } + + /* This methods reads and returns the response from the connection */ + + public static String readResponseFromGetRequest(HttpURLConnection httpConnection) + throws DeviceManagementException { + BufferedReader bufferedReader; + try { + bufferedReader = new BufferedReader(new InputStreamReader( + httpConnection.getInputStream())); + } catch (IOException e) { + String errorMsg = + "There is an issue with connecting the reader to the input stream at: " + + httpConnection.getURL(); + log.error(errorMsg); + throw new DeviceManagementException(errorMsg, e); + } + + String responseLine; + StringBuilder completeResponse = new StringBuilder(); + + try { + while ((responseLine = bufferedReader.readLine()) != null) { + completeResponse.append(responseLine); + } + } catch (IOException e) { + String errorMsg = + "Error occured whilst trying read from the connection stream at: " + + httpConnection.getURL(); + log.error(errorMsg); + throw new DeviceManagementException(errorMsg, e); + } + try { + bufferedReader.close(); + } catch (IOException e) { + log.error( + "Could not succesfully close the bufferedReader to the connection at: " + + httpConnection.getURL()); + } + + return completeResponse.toString(); + } + + public static boolean publishToDAS(String deviceId, float temperature) { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + DeviceAnalyticsService deviceAnalyticsService = (DeviceAnalyticsService) ctx.getOSGiService( + DeviceAnalyticsService.class, null); + String owner = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUsername(); + Object metdaData[] = {owner, RaspberrypiConstants.DEVICE_TYPE, deviceId, System.currentTimeMillis()}; + Object payloadData[] = {temperature}; + try { + deviceAnalyticsService.publishEvent(TEMPERATURE_STREAM_DEFINITION, "1.0.0", metdaData, new Object[0], payloadData); + } catch (DataPublisherConfigurationException e) { + return false; + } + return true; + } +} diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/webapp/META-INF/webapp-classloading.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/webapp/META-INF/webapp-classloading.xml new file mode 100644 index 000000000..fa4461919 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/webapp/META-INF/webapp-classloading.xml @@ -0,0 +1,33 @@ + + + + + + + + + false + + + CXF,Carbon + diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml new file mode 100644 index 000000000..0ccfb9745 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/webapp/WEB-INF/web.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..f2fe934b8 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,54 @@ + + + RaspberryPi + RaspberryPi + + + CXFServlet + org.apache.cxf.transport.servlet.CXFServlet + 1 + + + + + CXFServlet + /* + + + + isAdminService + false + + + doAuthentication + true + + + + + managed-api-enabled + true + + + managed-api-owner + admin + + + managed-api-context-template + /raspberrypi/{version} + + + managed-api-application + raspberrypi + + + managed-api-isSecured + true + + + + diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/pom.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/pom.xml new file mode 100644 index 000000000..9a2750ba3 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/pom.xml @@ -0,0 +1,166 @@ + + + + + + + raspberrypi-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl + war + WSO2 Carbon - IoT Server RaspberryPi Manager API + WSO2 Carbon - RaspberryPi Manager Service API Implementation + http://wso2.org + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.core + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + provided + + + org.apache.cxf + 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 + provided + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl + provided + + + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-jaxrs + + + javax + javaee-web-api + provided + + + javax.ws.rs + jsr311-api + provided + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.analytics + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.annotations + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.webapp.publisher + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.jwt.client.extension + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.application.extension + provided + + + + + + + + maven-compiler-plugin + + UTF-8 + ${wso2.maven.compiler.source} + ${wso2.maven.compiler.target} + + + + maven-war-plugin + + raspberrypi_mgt + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/manager/service/impl/RaspberryPiManagerService.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/manager/service/impl/RaspberryPiManagerService.java new file mode 100644 index 000000000..abc970e89 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/manager/service/impl/RaspberryPiManagerService.java @@ -0,0 +1,323 @@ +/* + * 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.device.mgt.iot.raspberrypi.manager.service.impl; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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.common.Device; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.EnrolmentInfo; +import org.wso2.carbon.device.mgt.iot.controlqueue.xmpp.XmppAccount; +import org.wso2.carbon.device.mgt.iot.controlqueue.xmpp.XmppConfig; +import org.wso2.carbon.device.mgt.iot.controlqueue.xmpp.XmppServerClient; +import org.wso2.carbon.device.mgt.iot.exception.DeviceControllerException; +import org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl.util.APIUtil; +import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.constants.RaspberrypiConstants; +import org.wso2.carbon.device.mgt.iot.util.ZipArchive; +import org.wso2.carbon.device.mgt.iot.util.ZipUtil; +import org.wso2.carbon.device.mgt.jwt.client.extension.JWTClient; +import org.wso2.carbon.device.mgt.jwt.client.extension.JWTClientManager; +import org.wso2.carbon.device.mgt.jwt.client.extension.dto.AccessTokenInfo; +import org.wso2.carbon.device.mgt.jwt.client.extension.exception.JWTClientException; +import org.wso2.carbon.user.api.UserStoreException; + +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +public class RaspberryPiManagerService { + + private static Log log = LogFactory.getLog(RaspberryPiManagerService.class); + + @Context //injected response proxy supporting multiple thread + private HttpServletResponse response; + private static final String KEY_TYPE = "PRODUCTION"; + private static ApiApplicationKey apiApplicationKey; + + @Path("manager/device/register") + @POST + public boolean register(@QueryParam("deviceId") String deviceId, + @QueryParam("name") String name) { + try { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(RaspberrypiConstants.DEVICE_TYPE); + if (APIUtil.getDeviceManagementService().isEnrolled(deviceIdentifier)) { + response.setStatus(Response.Status.CONFLICT.getStatusCode()); + return false; + } + Device device = new Device(); + device.setDeviceIdentifier(deviceId); + EnrolmentInfo enrolmentInfo = new EnrolmentInfo(); + enrolmentInfo.setDateOfEnrolment(new Date().getTime()); + enrolmentInfo.setDateOfLastUpdate(new Date().getTime()); + enrolmentInfo.setStatus(EnrolmentInfo.Status.ACTIVE); + device.setName(name); + device.setType(RaspberrypiConstants.DEVICE_TYPE); + enrolmentInfo.setOwner(APIUtil.getAuthenticatedUser()); + device.setEnrolmentInfo(enrolmentInfo); + boolean added = APIUtil.getDeviceManagementService().enrollDevice(device); + if (added) { + response.setStatus(Response.Status.OK.getStatusCode()); + } else { + response.setStatus(Response.Status.NOT_ACCEPTABLE.getStatusCode()); + } + return added; + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/device/{device_id}") + @DELETE + public void removeDevice(@PathParam("device_id") String deviceId, + @Context HttpServletResponse response) { + try { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(RaspberrypiConstants.DEVICE_TYPE); + boolean removed = APIUtil.getDeviceManagementService().disenrollDevice( + deviceIdentifier); + if (removed) { + response.setStatus(Response.Status.OK.getStatusCode()); + } else { + response.setStatus(Response.Status.NOT_ACCEPTABLE.getStatusCode()); + } + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/device/{device_id}") + @PUT + public boolean updateDevice(@PathParam("device_id") String deviceId, + @QueryParam("name") String name, + @Context HttpServletResponse response) { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(RaspberrypiConstants.DEVICE_TYPE); + try { + Device device = APIUtil.getDeviceManagementService().getDevice(deviceIdentifier); + device.setDeviceIdentifier(deviceId); + device.getEnrolmentInfo().setDateOfLastUpdate(new Date().getTime()); + device.setName(name); + device.setType(RaspberrypiConstants.DEVICE_TYPE); + boolean updated = APIUtil.getDeviceManagementService().modifyEnrollment(device); + if (updated) { + response.setStatus(Response.Status.OK.getStatusCode()); + } else { + response.setStatus(Response.Status.NOT_ACCEPTABLE.getStatusCode()); + } + return updated; + } catch (DeviceManagementException e) { + log.error(e.getErrorMessage()); + return false; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/device/{device_id}") + @GET + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Device getDevice(@PathParam("device_id") String deviceId) { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(RaspberrypiConstants.DEVICE_TYPE); + try { + return APIUtil.getDeviceManagementService().getDevice(deviceIdentifier); + } catch (DeviceManagementException ex) { + log.error("Error occurred while retrieving device with Id " + deviceId + "\n" + ex); + return null; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/devices") + @GET + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Device[] getRaspberrypiDevices() { + try { + List userDevices = APIUtil.getDeviceManagementService().getDevicesOfUser( + APIUtil.getAuthenticatedUser()); + ArrayList usersRaspberrypiDevices = new ArrayList<>(); + for (Device device : userDevices) { + if (device.getType().equals(RaspberrypiConstants.DEVICE_TYPE) && + device.getEnrolmentInfo().getStatus().equals(EnrolmentInfo.Status.ACTIVE)) { + usersRaspberrypiDevices.add(device); + } + } + return usersRaspberrypiDevices.toArray(new Device[]{}); + + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/device/{sketch_type}/download") + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response downloadSketch(@QueryParam("deviceName") String deviceName, @PathParam("sketch_type") String + sketchType) { + try { + ZipArchive zipFile = createDownloadFile(APIUtil.getAuthenticatedUser(), deviceName, sketchType); + Response.ResponseBuilder response = Response.ok(FileUtils.readFileToByteArray(zipFile.getZipFile())); + response.type("application/zip"); + response.header("Content-Disposition", "attachment; filename=\"" + zipFile.getFileName() + "\""); + return response.build(); + } catch (IllegalArgumentException ex) { + return Response.status(400).entity(ex.getMessage()).build();//bad request + } catch (DeviceManagementException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (JWTClientException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (DeviceControllerException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (IOException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (APIManagerException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (UserStoreException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + + @Path("manager/device/{sketch_type}/generate_link") + @GET + public Response generateSketchLink(@QueryParam("deviceName") String deviceName, + @PathParam("sketch_type") String sketchType) { + + try { + ZipArchive zipFile = createDownloadFile(APIUtil.getAuthenticatedUser(), deviceName, sketchType); + Response.ResponseBuilder rb = Response.ok(zipFile.getDeviceId()); + return rb.build(); + } catch (IllegalArgumentException ex) { + return Response.status(400).entity(ex.getMessage()).build();//bad request + } catch (DeviceManagementException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (JWTClientException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (DeviceControllerException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (APIManagerException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (UserStoreException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + + private ZipArchive createDownloadFile(String owner, String deviceName, String sketchType) + throws DeviceManagementException, JWTClientException, APIManagerException, DeviceControllerException, + UserStoreException { + //create new device id + String deviceId = shortUUID(); + if (apiApplicationKey == null) { + String applicationUsername = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUserRealm().getRealmConfiguration() + .getAdminUserName(); + APIManagementProviderService apiManagementProviderService = APIUtil.getAPIManagementProviderService(); + String[] tags = {RaspberrypiConstants.DEVICE_TYPE}; + apiApplicationKey = apiManagementProviderService.generateAndRetrieveApplicationKeys( + RaspberrypiConstants.DEVICE_TYPE, tags, KEY_TYPE, applicationUsername, true); + } + JWTClient jwtClient = JWTClientManager.getInstance().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(); + //adding registering data + XmppAccount newXmppAccount = new XmppAccount(); + newXmppAccount.setAccountName(owner + "_" + deviceId); + newXmppAccount.setUsername(deviceId); + newXmppAccount.setPassword(accessToken); + newXmppAccount.setEmail(deviceId + "@wso2.com"); + XmppServerClient xmppServerClient = new XmppServerClient(); + xmppServerClient.initControlQueue(); + boolean status; + if (XmppConfig.getInstance().isEnabled()) { + status = xmppServerClient.createXMPPAccount(newXmppAccount); + if (!status) { + String msg = "XMPP Account was not created for device - " + deviceId + " of owner - " + owner + + ".XMPP might have been disabled in org.wso2.carbon.device.mgt.iot.common.config" + + ".server.configs"; + log.warn(msg); + throw new DeviceManagementException(msg); + } + } + //Register the device with CDMF + status = register(deviceId, deviceName); + if (!status) { + String msg = "Error occurred while registering the device with " + "id: " + deviceId + " owner:" + owner; + throw new DeviceManagementException(msg); + } + ZipUtil ziputil = new ZipUtil(); + ZipArchive zipFile = ziputil.createZipFile(owner, APIUtil.getTenantDomainOftheUser(), sketchType, + deviceId, deviceName, accessToken, refreshToken); + zipFile.setDeviceId(deviceId); + return zipFile; + } + + private static String shortUUID() { + UUID uuid = UUID.randomUUID(); + long l = ByteBuffer.wrap(uuid.toString().getBytes(StandardCharsets.UTF_8)).getLong(); + return Long.toString(l, Character.MAX_RADIX); + } + +} diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/manager/service/impl/util/APIUtil.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/manager/service/impl/util/APIUtil.java new file mode 100644 index 000000000..412629443 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/manager/service/impl/util/APIUtil.java @@ -0,0 +1,55 @@ +package org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl.util; + +import org.apache.commons.logging.Log; +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; + +/** + * This class provides utility functions used by REST-API. + */ +public class APIUtil { + + private static Log log = LogFactory.getLog(APIUtil.class); + + public static String getAuthenticatedUser() { + PrivilegedCarbonContext threadLocalCarbonContext = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + String username = threadLocalCarbonContext.getUsername(); + String tenantDomain = threadLocalCarbonContext.getTenantDomain(); + if (username.endsWith(tenantDomain)) { + return username.substring(0, username.lastIndexOf("@")); + } + 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 = + (DeviceManagementProviderService) ctx.getOSGiService(DeviceManagementProviderService.class, null); + if (deviceManagementProviderService == null) { + String msg = "Device Management service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return deviceManagementProviderService; + } + + public static APIManagementProviderService getAPIManagementProviderService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + APIManagementProviderService apiManagementProviderService = + (APIManagementProviderService) ctx.getOSGiService(DeviceManagementProviderService.class, null); + if (apiManagementProviderService == null) { + String msg = "API management provider service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return apiManagementProviderService; + } +} diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/webapp/META-INF/webapp-classloading.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/webapp/META-INF/webapp-classloading.xml new file mode 100644 index 000000000..fa4461919 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/webapp/META-INF/webapp-classloading.xml @@ -0,0 +1,33 @@ + + + + + + + + + false + + + CXF,Carbon + diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml new file mode 100644 index 000000000..48216c9b8 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/webapp/WEB-INF/web.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..abda0a6a4 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,50 @@ + + + RaspberryPi + RaspberryPi + + + CXFServlet + org.apache.cxf.transport.servlet.CXFServlet + 1 + + + + CXFServlet + /* + + + isAdminService + false + + + doAuthentication + true + + + + + managed-api-enabled + false + + + managed-api-owner + admin + + + managed-api-context-template + /raspberrypi/{version} + + + managed-api-application + raspberrypi + + + managed-api-isSecured + true + + diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/pom.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/pom.xml new file mode 100644 index 000000000..8d96e651a --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/pom.xml @@ -0,0 +1,124 @@ + + + + + + + + raspberrypi-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl + bundle + WSO2 Carbon - IoT Server RaspberryPi Management Plugin + WSO2 Carbon - RaspberryPi Management/Control Plugin Implementation + http://wso2.org + + + + + org.apache.felix + maven-scr-plugin + + + maven-compiler-plugin + + 1.7 + 1.7 + + 2.3.2 + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + ${project.artifactId} + ${project.artifactId} + ${carbon.devicemgt.plugins.version} + IoT Server Impl Bundle + org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.internal + + org.osgi.framework, + org.osgi.service.component, + org.apache.commons.logging, + javax.xml.bind.*, + javax.naming;resolution:=optional,, + javax.sql;resolution:=optional, + javax.xml.bind.annotation.*;resolution:=optional, + javax.xml.parsers;resolution:=optional, + javax.net;resolution:=optional, + javax.net.ssl;resolution:=optional, + org.w3c.dom;resolution:=optional, + org.wso2.carbon.device.mgt.common.*, + org.wso2.carbon.device.mgt.common, + org.wso2.carbon.context.*, + org.wso2.carbon.ndatasource.core, + org.wso2.carbon.device.mgt.iot.*, + org.wso2.carbon.device.mgt.extensions.feature.mgt.*, + org.wso2.carbon.utils.* + + + !org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.internal, + org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.* + + + + + + + + + + org.eclipse.osgi + org.eclipse.osgi + + + org.eclipse.osgi + org.eclipse.osgi.services + + + org.wso2.carbon + org.wso2.carbon.logging + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + + + org.wso2.carbon + org.wso2.carbon.ndatasource.core + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.extensions + + + org.wso2.carbon + org.wso2.carbon.utils + + + \ No newline at end of file diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/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.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/constants/RaspberrypiConstants.java new file mode 100644 index 000000000..c6534e46f --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/constants/RaspberrypiConstants.java @@ -0,0 +1,39 @@ +/* + * 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.device.mgt.iot.raspberrypi.plugin.constants; + +public class RaspberrypiConstants { + + public final static String DEVICE_TYPE = "raspberrypi"; + public final static String DEVICE_PLUGIN_DEVICE_NAME = "DEVICE_NAME"; + public final static String DEVICE_PLUGIN_DEVICE_ID = "RASPBERRYPI_DEVICE_ID"; + public final static String STATE_ON = "ON"; + public final static String STATE_OFF = "OFF"; + + public static final String URL_PREFIX = "http://"; + public static final String BULB_CONTEXT = "/BULB/"; + public static final String TEMPERATURE_CONTEXT = "/TEMPERATURE/"; + + //type of the sensor + public static final String SENSOR_TEMPERATURE = "temperature"; + //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"; + +} diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/exception/RaspberrypiDeviceMgtPluginException.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/exception/RaspberrypiDeviceMgtPluginException.java new file mode 100644 index 000000000..805775195 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/exception/RaspberrypiDeviceMgtPluginException.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.device.mgt.iot.raspberrypi.plugin.exception; + + +public class RaspberrypiDeviceMgtPluginException extends Exception{ + + private String errorMessage; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public RaspberrypiDeviceMgtPluginException(String msg, Exception nestedEx) { + super(msg, nestedEx); + setErrorMessage(msg); + } + + public RaspberrypiDeviceMgtPluginException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public RaspberrypiDeviceMgtPluginException(String msg) { + super(msg); + setErrorMessage(msg); + } + + public RaspberrypiDeviceMgtPluginException() { + super(); + } + + public RaspberrypiDeviceMgtPluginException(Throwable cause) { + super(cause); + } + +} diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/RaspberrypiManager.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/RaspberrypiManager.java new file mode 100644 index 000000000..098438d46 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/RaspberrypiManager.java @@ -0,0 +1,252 @@ +/* + * 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.device.mgt.iot.raspberrypi.plugin.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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.common.DeviceManager; +import org.wso2.carbon.device.mgt.common.EnrolmentInfo; +import org.wso2.carbon.device.mgt.common.FeatureManager; +import org.wso2.carbon.device.mgt.common.configuration.mgt.TenantConfiguration; +import org.wso2.carbon.device.mgt.common.license.mgt.License; +import org.wso2.carbon.device.mgt.common.license.mgt.LicenseManagementException; +import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.exception.RaspberrypiDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl.dao.RaspberrypiDAO; +import java.util.List; + +/** + * This represents the Raspberrypi implementation of DeviceManagerService. + */ +public class RaspberrypiManager implements DeviceManager { + + private static final RaspberrypiDAO raspberrypiDAO = new RaspberrypiDAO(); + private static final Log log = LogFactory.getLog(RaspberrypiManager.class); + + @Override + public FeatureManager getFeatureManager() { + return null; + } + + @Override + public boolean saveConfiguration(TenantConfiguration tenantConfiguration) + throws DeviceManagementException { + return false; + } + + @Override + public TenantConfiguration getConfiguration() throws DeviceManagementException { + return null; + } + + @Override + public boolean enrollDevice(Device device) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Enrolling a new Raspberrypi device : " + device.getDeviceIdentifier()); + } + RaspberrypiDAO.beginTransaction(); + status = raspberrypiDAO.getDeviceDAO().addDevice(device); + RaspberrypiDAO.commitTransaction(); + } catch (RaspberrypiDeviceMgtPluginException e) { + try { + RaspberrypiDAO.rollbackTransaction(); + } catch (RaspberrypiDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the device enrol transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while enrolling the Raspberrypi device : " + device.getDeviceIdentifier(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean modifyEnrollment(Device device) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Modifying the Raspberrypi device enrollment data"); + } + RaspberrypiDAO.beginTransaction(); + status = raspberrypiDAO.getDeviceDAO().updateDevice(device); + RaspberrypiDAO.commitTransaction(); + } catch (RaspberrypiDeviceMgtPluginException e) { + try { + RaspberrypiDAO.rollbackTransaction(); + } catch (RaspberrypiDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the update device transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while updating the enrollment of the Raspberrypi device : " + + device.getDeviceIdentifier(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean disenrollDevice(DeviceIdentifier deviceId) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Dis-enrolling Raspberrypi device : " + deviceId); + } + RaspberrypiDAO.beginTransaction(); + status = raspberrypiDAO.getDeviceDAO().deleteDevice(deviceId.getId()); + RaspberrypiDAO.commitTransaction(); + } catch (RaspberrypiDeviceMgtPluginException e) { + try { + RaspberrypiDAO.rollbackTransaction(); + } catch (RaspberrypiDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the device dis enrol transaction :" + deviceId.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while removing the Raspberrypi device : " + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean isEnrolled(DeviceIdentifier deviceId) throws DeviceManagementException { + boolean isEnrolled = false; + try { + if (log.isDebugEnabled()) { + log.debug("Checking the enrollment of Raspberrypi device : " + deviceId.getId()); + } + Device iotDevice = raspberrypiDAO.getDeviceDAO().getDevice(deviceId.getId()); + if (iotDevice != null) { + isEnrolled = true; + } + } catch (RaspberrypiDeviceMgtPluginException e) { + String msg = "Error while checking the enrollment status of Raspberrypi device : " + + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return isEnrolled; + } + + @Override + public boolean isActive(DeviceIdentifier deviceId) throws DeviceManagementException { + return true; + } + + @Override + public boolean setActive(DeviceIdentifier deviceId, boolean status) + throws DeviceManagementException { + return true; + } + + @Override + public Device getDevice(DeviceIdentifier deviceId) throws DeviceManagementException { + Device device; + try { + if (log.isDebugEnabled()) { + log.debug("Getting the details of Raspberrypi device : " + deviceId.getId()); + } + device = raspberrypiDAO.getDeviceDAO().getDevice(deviceId.getId()); + } catch (RaspberrypiDeviceMgtPluginException e) { + String msg = "Error while fetching the Raspberrypi device : " + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return device; + } + + @Override + public boolean setOwnership(DeviceIdentifier deviceId, String ownershipType) + throws DeviceManagementException { + return true; + } + + public boolean isClaimable(DeviceIdentifier deviceIdentifier) throws DeviceManagementException { + return false; + } + + @Override + public boolean setStatus(DeviceIdentifier deviceId, String currentOwner, + EnrolmentInfo.Status status) throws DeviceManagementException { + return false; + } + + @Override + public License getLicense(String s) throws LicenseManagementException { + return null; + } + + @Override + public void addLicense(License license) throws LicenseManagementException { + + } + + @Override + public boolean requireDeviceAuthorization() { + return false; + } + + @Override + public boolean updateDeviceInfo(DeviceIdentifier deviceIdentifier, Device device) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug( + "updating the details of Raspberrypi device : " + deviceIdentifier); + } + RaspberrypiDAO.beginTransaction(); + status = raspberrypiDAO.getDeviceDAO().updateDevice(device); + RaspberrypiDAO.commitTransaction(); + } catch (RaspberrypiDeviceMgtPluginException e) { + try { + RaspberrypiDAO.rollbackTransaction(); + } catch (RaspberrypiDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the update device info transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = + "Error while updating the Raspberrypi device : " + deviceIdentifier; + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public List getAllDevices() throws DeviceManagementException { + List devices = null; + try { + if (log.isDebugEnabled()) { + log.debug("Fetching the details of all Raspberrypi devices"); + } + List iotDevices = raspberrypiDAO.getDeviceDAO().getAllDevices(); + } catch (RaspberrypiDeviceMgtPluginException e) { + String msg = "Error while fetching all Raspberrypi devices."; + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return devices; + } +} \ No newline at end of file diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/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.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/RaspberrypiManagerService.java new file mode 100644 index 000000000..96ce170ef --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/RaspberrypiManagerService.java @@ -0,0 +1,108 @@ +/* + * 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.device.mgt.iot.raspberrypi.plugin.impl; + +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.DeviceManager; +import org.wso2.carbon.device.mgt.common.app.mgt.Application; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManagementException; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManager; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; +import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.constants.RaspberrypiConstants; + +import java.util.List; + +public class RaspberrypiManagerService implements DeviceManagementService { + + private DeviceManager deviceManager; + + @Override + public String getType() { + return RaspberrypiConstants.DEVICE_TYPE; + } + + @Override + public String getProviderTenantDomain() { + return "carbon.super"; + } + + @Override + public boolean isSharedWithAllTenants() { + return false; + } + + @Override + public void init() throws DeviceManagementException { + deviceManager = new RaspberrypiManager(); + } + + @Override + public DeviceManager getDeviceManager() { + return deviceManager; + } + + @Override + public ApplicationManager getApplicationManager() { + return null; + } + + @Override + public void notifyOperationToDevices(Operation operation, List deviceIds) + throws DeviceManagementException { + + } + + @Override + public Application[] getApplications(String domain, int pageNumber, int size) + throws ApplicationManagementException { + return new Application[0]; + } + + @Override + public void updateApplicationStatus(DeviceIdentifier deviceId, Application application, + String status) throws ApplicationManagementException { + + } + + @Override + public String getApplicationStatus(DeviceIdentifier deviceId, Application application) + throws ApplicationManagementException { + return null; + } + + @Override + public void installApplicationForDevices(Operation operation, List deviceIdentifiers) + throws ApplicationManagementException { + + } + + @Override + public void installApplicationForUsers(Operation operation, List userNameList) + throws ApplicationManagementException { + + } + + @Override + public void installApplicationForUserRoles(Operation operation, List userRoleList) + throws ApplicationManagementException { + + } +} diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/dao/RaspberrypiDAO.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/dao/RaspberrypiDAO.java new file mode 100644 index 000000000..cdd317743 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/dao/RaspberrypiDAO.java @@ -0,0 +1,124 @@ +/* + * 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.device.mgt.iot.raspberrypi.plugin.impl.dao; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.exception.RaspberrypiDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.constants.RaspberrypiConstants; +import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl.dao.impl.RaspberrypiDeviceDAOImpl; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +public class RaspberrypiDAO { + + private static final Log log = LogFactory.getLog(RaspberrypiDAO.class); + static DataSource dataSource; + private static ThreadLocal currentConnection = new ThreadLocal(); + + public RaspberrypiDAO() { + initRaspberrypiDAO(); + } + + public RaspberrypiDeviceDAOImpl getDeviceDAO() { + return new RaspberrypiDeviceDAOImpl(); + } + + public static void initRaspberrypiDAO() { + try { + Context ctx = new InitialContext(); + dataSource = (DataSource) ctx.lookup(RaspberrypiConstants.DATA_SOURCE_NAME); + } catch (NamingException e) { + log.error("Error while looking up the data source: " + RaspberrypiConstants.DATA_SOURCE_NAME); + } + } + + public static void beginTransaction() throws RaspberrypiDeviceMgtPluginException { + try { + Connection conn = dataSource.getConnection(); + conn.setAutoCommit(false); + currentConnection.set(conn); + } catch (SQLException e) { + throw new RaspberrypiDeviceMgtPluginException("Error occurred while retrieving datasource connection", e); + } + } + + public static Connection getConnection() throws RaspberrypiDeviceMgtPluginException { + if (currentConnection.get() == null) { + try { + currentConnection.set(dataSource.getConnection()); + } catch (SQLException e) { + throw new RaspberrypiDeviceMgtPluginException("Error occurred while retrieving data source connection", e); + } + } + return currentConnection.get(); + } + + public static void commitTransaction() throws RaspberrypiDeviceMgtPluginException { + try { + Connection conn = currentConnection.get(); + if (conn != null) { + conn.commit(); + } else { + if (log.isDebugEnabled()) { + log.debug("Datasource connection associated with the current thread is null, hence commit " + + "has not been attempted"); + } + } + } catch (SQLException e) { + throw new RaspberrypiDeviceMgtPluginException("Error occurred while committing the transaction", e); + } finally { + closeConnection(); + } + } + + public static void closeConnection() throws RaspberrypiDeviceMgtPluginException { + Connection con = currentConnection.get(); + if (con != null) { + try { + con.close(); + } catch (SQLException e) { + log.error("Error occurred while close the connection"); + } + } + currentConnection.remove(); + } + + public static void rollbackTransaction() throws RaspberrypiDeviceMgtPluginException { + try { + Connection conn = currentConnection.get(); + if (conn != null) { + conn.rollback(); + } else { + if (log.isDebugEnabled()) { + log.debug("Datasource connection associated with the current thread is null, hence rollback " + + "has not been attempted"); + } + } + } catch (SQLException e) { + throw new RaspberrypiDeviceMgtPluginException("Error occurred while rollback the transaction", e); + } finally { + closeConnection(); + } + } +} \ No newline at end of file diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/dao/impl/RaspberrypiDeviceDAOImpl.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/dao/impl/RaspberrypiDeviceDAOImpl.java new file mode 100644 index 000000000..ae9c22620 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/dao/impl/RaspberrypiDeviceDAOImpl.java @@ -0,0 +1,191 @@ +/* + * 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.device.mgt.iot.raspberrypi.plugin.impl.dao.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.exception.RaspberrypiDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl.util.RaspberrypiUtils; +import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl.dao.RaspberrypiDAO; +import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.constants.RaspberrypiConstants; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * Implements CRUD for Raspberrypi Devices. + */ +public class RaspberrypiDeviceDAOImpl { + + private static final Log log = LogFactory.getLog(RaspberrypiDeviceDAOImpl.class); + + public Device getDevice(String iotDeviceId) throws RaspberrypiDeviceMgtPluginException { + Connection conn; + PreparedStatement stmt = null; + Device device = null; + ResultSet resultSet = null; + try { + conn = RaspberrypiDAO.getConnection(); + String selectDBQuery = + "SELECT RASPBERRYPI_DEVICE_ID, DEVICE_NAME FROM RASPBERRYPI_DEVICE WHERE RASPBERRYPI_DEVICE_ID = ?"; + stmt = conn.prepareStatement(selectDBQuery); + stmt.setString(1, iotDeviceId); + resultSet = stmt.executeQuery(); + + if (resultSet.next()) { + device = new Device(); + device.setName(resultSet.getString(RaspberrypiConstants.DEVICE_PLUGIN_DEVICE_NAME)); + if (log.isDebugEnabled()) { + log.debug("Raspberrypi device " + iotDeviceId + " data has been fetched from " + + "Raspberrypi database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while fetching Raspberrypi device : '" + iotDeviceId + "'"; + log.error(msg, e); + throw new RaspberrypiDeviceMgtPluginException(msg, e); + } finally { + RaspberrypiUtils.cleanupResources(stmt, resultSet); + RaspberrypiDAO.closeConnection(); + } + return device; + } + + public boolean addDevice(Device device) throws RaspberrypiDeviceMgtPluginException { + boolean status = false; + Connection conn; + PreparedStatement stmt = null; + try { + conn = RaspberrypiDAO.getConnection(); + String createDBQuery = + "INSERT INTO RASPBERRYPI_DEVICE(RASPBERRYPI_DEVICE_ID, DEVICE_NAME) VALUES (?, ?)"; + + stmt = conn.prepareStatement(createDBQuery); + stmt.setString(1, device.getDeviceIdentifier()); + stmt.setString(2, device.getName()); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Raspberrypi device " + device.getDeviceIdentifier() + " data has been" + + " added to the Raspberrypi database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while adding the Raspberrypi device '" + + device.getDeviceIdentifier() + "' to the Raspberrypi db."; + log.error(msg, e); + throw new RaspberrypiDeviceMgtPluginException(msg, e); + } finally { + RaspberrypiUtils.cleanupResources(stmt, null); + } + return status; + } + + public boolean updateDevice(Device device) throws RaspberrypiDeviceMgtPluginException { + boolean status = false; + Connection conn; + PreparedStatement stmt = null; + try { + conn = RaspberrypiDAO.getConnection(); + String updateDBQuery = "UPDATE RASPBERRYPI_DEVICE SET DEVICE_NAME = ? WHERE RASPBERRYPI_DEVICE_ID = ?"; + stmt = conn.prepareStatement(updateDBQuery); + stmt.setString(1, device.getName()); + stmt.setString(2, device.getDeviceIdentifier()); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Raspberrypi device " + device.getDeviceIdentifier() + " data has been" + + " modified."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while modifying the Raspberrypi device '" + device.getDeviceIdentifier() + + "' data."; + log.error(msg, e); + throw new RaspberrypiDeviceMgtPluginException(msg, e); + } finally { + RaspberrypiUtils.cleanupResources(stmt, null); + } + return status; + } + + public boolean deleteDevice(String iotDeviceId) throws RaspberrypiDeviceMgtPluginException { + boolean status = false; + Connection conn; + PreparedStatement stmt = null; + try { + conn = RaspberrypiDAO.getConnection(); + String deleteDBQuery = "DELETE FROM RASPBERRYPI_DEVICE WHERE RASPBERRYPI_DEVICE_ID = ?"; + stmt = conn.prepareStatement(deleteDBQuery); + stmt.setString(1, iotDeviceId); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Raspberrypi device " + iotDeviceId + " data has deleted" + + " from the Raspberrypi database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while deleting Raspberrypi device " + iotDeviceId; + log.error(msg, e); + throw new RaspberrypiDeviceMgtPluginException(msg, e); + } finally { + RaspberrypiUtils.cleanupResources(stmt, null); + } + return status; + } + + public List getAllDevices() throws RaspberrypiDeviceMgtPluginException { + Connection conn; + PreparedStatement stmt = null; + ResultSet resultSet = null; + Device device; + List devices = new ArrayList<>(); + try { + conn = RaspberrypiDAO.getConnection(); + String selectDBQuery = "SELECT RASPBERRYPI_DEVICE_ID, DEVICE_NAME FROM RASPBERRYPI_DEVICE"; + stmt = conn.prepareStatement(selectDBQuery); + resultSet = stmt.executeQuery(); + while (resultSet.next()) { + device = new Device(); + device.setDeviceIdentifier(resultSet.getString(RaspberrypiConstants.DEVICE_PLUGIN_DEVICE_ID)); + device.setName(resultSet.getString(RaspberrypiConstants.DEVICE_PLUGIN_DEVICE_NAME)); + devices.add(device); + } + if (log.isDebugEnabled()) { + log.debug("All Raspberrypi device details have fetched from Raspberrypi database."); + } + return devices; + } catch (SQLException e) { + String msg = "Error occurred while fetching all Raspberrypi device data'"; + log.error(msg, e); + throw new RaspberrypiDeviceMgtPluginException(msg, e); + } finally { + RaspberrypiUtils.cleanupResources(stmt, resultSet); + RaspberrypiDAO.closeConnection(); + } + } +} \ No newline at end of file diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/feature/RaspberrypiFeatureManager.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/feature/RaspberrypiFeatureManager.java new file mode 100644 index 000000000..755b7ae0d --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/feature/RaspberrypiFeatureManager.java @@ -0,0 +1,58 @@ +/* + * 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.device.mgt.iot.raspberrypi.plugin.impl.feature; + +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.Feature; +import org.wso2.carbon.device.mgt.common.FeatureManager; +import org.wso2.carbon.device.mgt.extensions.feature.mgt.GenericFeatureManager; +import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.constants.RaspberrypiConstants; + +import java.util.List; + +public class RaspberrypiFeatureManager implements FeatureManager { + @Override + public boolean addFeature(Feature feature) throws DeviceManagementException { + return false; + } + + @Override + public boolean addFeatures(List features) throws DeviceManagementException { + return false; + } + + @Override + public Feature getFeature(String name) throws DeviceManagementException { + GenericFeatureManager genericFeatureManager = GenericFeatureManager.getInstance(); + return genericFeatureManager.getFeature(RaspberrypiConstants.DEVICE_TYPE, name); + } + + @Override + public List getFeatures() throws DeviceManagementException { + GenericFeatureManager genericFeatureManager = GenericFeatureManager.getInstance(); + return genericFeatureManager.getFeatures(RaspberrypiConstants.DEVICE_TYPE); + } + + @Override + public boolean removeFeature(String name) throws DeviceManagementException { + return false; + } + + @Override + public boolean addSupportedFeaturesToDB() throws DeviceManagementException { + return false; + } +} diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/util/DeviceSchemaInitializer.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/util/DeviceSchemaInitializer.java new file mode 100644 index 000000000..7919c4d76 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/util/DeviceSchemaInitializer.java @@ -0,0 +1,50 @@ +/* + * 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.device.mgt.iot.raspberrypi.plugin.impl.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.utils.CarbonUtils; +import org.wso2.carbon.utils.dbcreator.DatabaseCreator; + +import javax.sql.DataSource; +import java.io.File; + +/** + * Provides methods for initializing the database script. + */ +public class DeviceSchemaInitializer extends DatabaseCreator{ + + private static final Log log = LogFactory.getLog(DeviceSchemaInitializer.class); + private static final String setupSQLScriptBaseLocation = CarbonUtils.getCarbonHome() + File.separator + "dbscripts" + + File.separator + "cdm" + File.separator + "plugins" + File.separator; + + public DeviceSchemaInitializer(DataSource dataSource) { + super(dataSource); + } + + @Override + protected String getDbScriptLocation(String databaseType) { + String scriptName = databaseType + ".sql"; + if (log.isDebugEnabled()) { + log.debug("Loading database script from :" + scriptName); + } + return setupSQLScriptBaseLocation.replaceFirst("DBTYPE", databaseType) + scriptName; + } +} diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/util/RaspberrypiUtils.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/util/RaspberrypiUtils.java new file mode 100644 index 000000000..bd0858141 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/impl/util/RaspberrypiUtils.java @@ -0,0 +1,111 @@ +/* + * 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.device.mgt.iot.raspberrypi.plugin.impl.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.constants.RaspberrypiConstants; +import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.exception.RaspberrypiDeviceMgtPluginException; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; + +/** + * Contains utility methods used by Raspberrypi plugin. + */ +public class RaspberrypiUtils { + + private static Log log = LogFactory.getLog(RaspberrypiUtils.class); + + public static String getDeviceProperty(List deviceProperties, String propertyKey) { + String deviceProperty = ""; + for(Device.Property property :deviceProperties){ + if(propertyKey.equals(property.getName())){ + deviceProperty = property.getValue(); + } + } + return deviceProperty; + } + + public static Device.Property getProperty(String property, String value) { + if (property != null) { + Device.Property prop = new Device.Property(); + prop.setName(property); + prop.setValue(value); + return prop; + } + return null; + } + + public static void cleanupResources(Connection conn, PreparedStatement stmt, ResultSet rs) { + if (rs != null) { + try { + rs.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing result set", e); + } + } + if (stmt != null) { + try { + stmt.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing prepared statement", e); + } + } + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing database connection", e); + } + } + } + + public static void cleanupResources(PreparedStatement stmt, ResultSet rs) { + cleanupResources(null, stmt, rs); + } + + /** + * Creates the device management schema. + */ + public static void setupDeviceManagementSchema() throws RaspberrypiDeviceMgtPluginException { + try { + Context ctx = new InitialContext(); + DataSource dataSource = (DataSource) ctx.lookup(RaspberrypiConstants.DATA_SOURCE_NAME); + DeviceSchemaInitializer initializer = + new DeviceSchemaInitializer(dataSource); + log.info("Initializing device management repository database schema"); + initializer.createRegistryDatabase(); + } catch (NamingException e) { + log.error("Error while looking up the data source: " + RaspberrypiConstants.DATA_SOURCE_NAME); + } catch (Exception e) { + throw new RaspberrypiDeviceMgtPluginException("Error occurred while initializing Iot Device " + + "Management database schema", e); + } + } +} diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/internal/RaspberrypiManagementServiceComponent.java b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/internal/RaspberrypiManagementServiceComponent.java new file mode 100644 index 000000000..0b78f2d85 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/raspberrypi/plugin/internal/RaspberrypiManagementServiceComponent.java @@ -0,0 +1,85 @@ +/* + * 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.device.mgt.iot.raspberrypi.plugin.internal; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.ComponentContext; +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; +import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.exception.RaspberrypiDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl.RaspberrypiManagerService; +import org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl.util.RaspberrypiUtils; + +/** + * @scr.component name="org.wso2.carbon.device.mgt.iot.raspberrypi.internal.RaspberrypiManagementServiceComponent" + * immediate="true" + */ +public class RaspberrypiManagementServiceComponent { + + private ServiceRegistration raspberrypiServiceRegRef; + private static final Log log = LogFactory.getLog(RaspberrypiManagementServiceComponent.class); + + protected void activate(ComponentContext ctx) { + if (log.isDebugEnabled()) { + log.debug("Activating Raspberrypi Device Management Service Component"); + } + try { + BundleContext bundleContext = ctx.getBundleContext(); + raspberrypiServiceRegRef = + bundleContext.registerService(DeviceManagementService.class.getName(), + new RaspberrypiManagerService(), null); + String setupOption = System.getProperty("setup"); + if (setupOption != null) { + if (log.isDebugEnabled()) { + log.debug("-Dsetup is enabled. Iot Device management repository schema initialization is about " + + "to begin"); + } + try { + RaspberrypiUtils.setupDeviceManagementSchema(); + } catch (RaspberrypiDeviceMgtPluginException e) { + log.error("Exception occurred while initializing device management database schema", e); + } + } + if (log.isDebugEnabled()) { + log.debug("Raspberrypi Device Management Service Component has been successfully activated"); + } + } catch (Throwable e) { + log.error( + "Error occurred while activating Raspberrypi Device Management Service Component", e); + } + } + + protected void deactivate(ComponentContext ctx) { + if (log.isDebugEnabled()) { + log.debug("De-activating Raspberrypi Device Management Service Component"); + } + try { + if (raspberrypiServiceRegRef != null) { + raspberrypiServiceRegRef.unregister(); + } + if (log.isDebugEnabled()) { + log.debug("Raspberrypi Device Management Service Component has been successfully de-activated"); + } + } catch (Throwable e) { + log.error("Error occurred while de-activating Raspberrypi Device Management bundle", e); + } + } +} diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/pom.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/pom.xml new file mode 100644 index 000000000..29e8c9e80 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/pom.xml @@ -0,0 +1,62 @@ + + + + + + + + raspberrypi-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.raspberrypi.ui + WSO2 Carbon - IoT Server RaspberryPi UI + pom + + + + + maven-assembly-plugin + 2.5.5 + + ${project.artifactId}-${carbon.device.mgt.version} + false + + src/assembly/src.xml + + + + + create-archive + package + + single + + + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/assembly/src.xml b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/assembly/src.xml new file mode 100644 index 000000000..2797034e0 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/assembly/src.xml @@ -0,0 +1,36 @@ + + + + src + + zip + + false + ${basedir}/src + + + ${basedir}/src/main/resources/jaggeryapps/devicemgt + / + true + + + \ No newline at end of file diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.device-view/device-view.hbs b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.device-view/device-view.hbs new file mode 100644 index 000000000..d3c6c39b2 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.device-view/device-view.hbs @@ -0,0 +1,68 @@ +{{#zone "topCss"}} + +{{/zone}} + +{{#zone "device-thumbnail"}} + +{{/zone}} + +{{#zone "device-opetations"}} +

      + Operations +
      +
      + {{unit "iot.unit.device.operation-bar" device=device}} +
      +{{/zone}} + +{{#zone "device-detail-properties"}} +
      + +
      +
      + +
      +
      Device Statistics
      + {{unit "iot.unit.device.stats" device=device}} +
      +
      +
      Operations Log
      +
      + +
      +
      + Not available yet +
      +
      +
      +
      +
      +
      +
      +
      +{{/zone}} \ No newline at end of file diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.device-view/device-view.js b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.device-view/device-view.js new file mode 100644 index 000000000..ea9439d4e --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.device-view/device-view.js @@ -0,0 +1,35 @@ +/* + * 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. + */ + +function onRequest(context) { + var log = new Log("detail.js"); + var deviceType = context.uriParams.deviceType; + var deviceId = request.getParameter("id"); + + if (deviceType != null && deviceType != undefined && deviceId != null && deviceId != undefined) { + var deviceModule = require("/app/modules/device.js").deviceModule; + var device = deviceModule.viewDevice(deviceType, deviceId); + + if (device && device.status != "error") { + return {"device": device}; + } else { + response.sendError(404, "Device Id " + deviceId + "of type " + deviceType + " cannot be found!"); + exit(); + } + } +} \ No newline at end of file diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.device-view/device-view.json b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.device-view/device-view.json new file mode 100644 index 000000000..9eecd8f5b --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.device-view/device-view.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0" +} \ No newline at end of file diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.device-view/public/images/respberry-icon.png b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.device-view/public/images/respberry-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..fe7065142f65a256763557df31e1a2ef1cf8eb99 GIT binary patch literal 40558 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
    • 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

      R~8;NHV0F- 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 II*xb#4;5{2p#T5? literal 0 HcmV?d00001 diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/public/images/myDevices_analytics.png b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/public/images/myDevices_analytics.png new file mode 100644 index 0000000000000000000000000000000000000000..51d63966a4852e048c865a93d782c1299bed5d5c GIT binary patch literal 220158 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�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

    • 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≷KL?pb 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%0AP6P$HqAFSL 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 Twnn#+U;)I;#0p6^c8~iHx6;YM literal 0 HcmV?d00001 diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/public/images/thumb.png b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/public/images/thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..510859d86cab2dbac49a30b96f213183593d0ea4 GIT binary patch literal 44816 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

      R~8;NHV0F- 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 II*xb#4;5{2p#T5? literal 0 HcmV?d00001 diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/public/js/download.js b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/public/js/download.js new file mode 100644 index 000000000..c8901643b --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/public/js/download.js @@ -0,0 +1,183 @@ +/* + * 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. + */ + +var modalPopup = ".wr-modalpopup"; +var modalPopupContainer = modalPopup + " .modalpopup-container"; +var modalPopupContent = modalPopup + " .modalpopup-content"; +var body = "body"; + +/* + * set popup maximum height function. + */ +function setPopupMaxHeight() { + $(modalPopupContent).css('max-height', ($(body).height() - ($(body).height() / 100 * 30))); + $(modalPopupContainer).css('margin-top', (-($(modalPopupContainer).height() / 2))); +} + +/* + * show popup function. + */ +function showPopup() { + $(modalPopup).show(); + setPopupMaxHeight(); + $('#downloadForm').validate({ + rules: { + deviceName: { + minlength: 4, + required: true + } + }, + highlight: function (element) { + $(element).closest('.control-group').removeClass('success').addClass('error'); + }, + success: function (element) { + $(element).closest('.control-group').removeClass('error').addClass('success'); + $('label[for=deviceName]').remove(); + } + }); + var deviceType = ""; + $('.deviceType').each(function () { + if (this.value != "") { + deviceType = this.value; + } + }); +} + +/* + * hide popup function. + */ +function hidePopup() { + $('label[for=deviceName]').remove(); + $('.control-group').removeClass('success').removeClass('error'); + $(modalPopupContent).html(''); + $(modalPopup).hide(); +} + +/* + * DOM ready functions. + */ +$(document).ready(function () { + attachEvents(); +}); + +function attachEvents() { + /** + * Following click function would execute + * when a user clicks on "Download" link + * on Device Management page in WSO2 DC Console. + */ + $("a.download-link").click(function () { + var sketchType = $(this).data("sketchtype"); + var deviceType = $(this).data("devicetype"); + var downloadDeviceAPI = "/devicemgt/api/devices/sketch/generate_link"; + var payload = {"sketchType": sketchType, "deviceType": deviceType}; + $(modalPopupContent).html($('#download-device-modal-content').html()); + showPopup(); + var deviceName; + $("a#download-device-download-link").click(function () { + $('.new-device-name').each(function () { + if (this.value != "") { + deviceName = this.value; + } + }); + $('label[for=deviceName]').remove(); + if (deviceName && deviceName.length >= 4) { + payload.deviceName = deviceName; + invokerUtil.post( + downloadDeviceAPI, + payload, + function (data, textStatus, jqxhr) { + doAction(data); + }, + function (data) { + doAction(data); + } + ); + }else if(deviceName){ + $('.controls').append(''); + $('.control-group').removeClass('success').addClass('error'); + } else { + $('.controls').append(''); + $('.control-group').removeClass('success').addClass('error'); + } + }); + + $("a#download-device-cancel-link").click(function () { + hidePopup(); + }); + }); +} + +function downloadAgent() { + var deviceName; + $('.new-device-name').each(function () { + if (this.value != "") { + deviceName = this.value; + } + }); + var deviceNameFormat = /^[^~?!#$:;%^*`+={}\[\]\\()|<>,'"]{1,30}$/; + if (deviceName && deviceNameFormat.test(deviceName)) { + $('#downloadForm').submit(); + hidePopup(); + $(modalPopupContent).html($('#device-agent-downloading-content').html()); + showPopup(); + setTimeout(function () { + hidePopup(); + }, 1000); + }else { + $("#invalid-username-error-msg span").text("Invalid device name"); + $("#invalid-username-error-msg").removeClass("hidden"); + } +} + +function doAction(data) { + //if it is saml redirection response + if (data.status == null) { + document.write(data); + } + + if (data.status == "200") { + $(modalPopupContent).html($('#download-device-modal-content-links').html()); + $("input#download-device-url").val(data.responseText); + $("input#download-device-url").focus(function () { + $(this).select(); + }); + showPopup(); + } else if (data.status == "401") { + $(modalPopupContent).html($('#device-401-content').html()); + $("#device-401-link").click(function () { + window.location = "/devicemgt/login"; + }); + showPopup(); + } else if (data == "403") { + $(modalPopupContent).html($('#device-403-content').html()); + $("#device-403-link").click(function () { + window.location = "/devicemgt/login"; + }); + showPopup(); + } else { + $(modalPopupContent).html($('#device-unexpected-error-content').html()); + $("a#device-unexpected-error-link").click(function () { + hidePopup(); + }); + } +} \ No newline at end of file diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/public/js/jquery.validate.js b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/public/js/jquery.validate.js new file mode 100644 index 000000000..fe7ecf07a --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/public/js/jquery.validate.js @@ -0,0 +1,1227 @@ +/* + * 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. + */ + +(function($) { + +$.extend($.fn, { + // http://docs.jquery.com/Plugins/Validation/validate + validate: function( options ) { + + // if nothing is selected, return nothing; can't chain anyway + if ( !this.length ) { + if ( options && options.debug && window.console ) { + console.warn( "Nothing selected, can't validate, returning nothing." ); + } + return; + } + + // check if a validator for this form was already created + var validator = $.data( this[0], "validator" ); + if ( validator ) { + return validator; + } + + // Add novalidate tag if HTML5. + this.attr( "novalidate", "novalidate" ); + + validator = new $.validator( options, this[0] ); + $.data( this[0], "validator", validator ); + + if ( validator.settings.onsubmit ) { + + this.validateDelegate( ":submit", "click", function( event ) { + if ( validator.settings.submitHandler ) { + validator.submitButton = event.target; + } + // allow suppressing validation by adding a cancel class to the submit button + if ( $(event.target).hasClass("cancel") ) { + validator.cancelSubmit = true; + } + }); + + // validate the form on submit + this.submit( function( event ) { + if ( validator.settings.debug ) { + // prevent form submit to be able to see console output + event.preventDefault(); + } + function handle() { + var hidden; + if ( validator.settings.submitHandler ) { + if ( validator.submitButton ) { + // insert a hidden input as a replacement for the missing submit button + hidden = $("").attr("name", validator.submitButton.name).val(validator.submitButton.value).appendTo(validator.currentForm); + } + validator.settings.submitHandler.call( validator, validator.currentForm, event ); + if ( validator.submitButton ) { + // and clean up afterwards; thanks to no-block-scope, hidden can be referenced + hidden.remove(); + } + return false; + } + return true; + } + + // prevent submit for invalid forms or custom submit handlers + if ( validator.cancelSubmit ) { + validator.cancelSubmit = false; + return handle(); + } + if ( validator.form() ) { + if ( validator.pendingRequest ) { + validator.formSubmitted = true; + return false; + } + return handle(); + } else { + validator.focusInvalid(); + return false; + } + }); + } + + return validator; + }, + // http://docs.jquery.com/Plugins/Validation/valid + valid: function() { + if ( $(this[0]).is("form")) { + return this.validate().form(); + } else { + var valid = true; + var validator = $(this[0].form).validate(); + this.each(function() { + valid &= validator.element(this); + }); + return valid; + } + }, + // attributes: space seperated list of attributes to retrieve and remove + removeAttrs: function( attributes ) { + var result = {}, + $element = this; + $.each(attributes.split(/\s/), function( index, value ) { + result[value] = $element.attr(value); + $element.removeAttr(value); + }); + return result; + }, + // http://docs.jquery.com/Plugins/Validation/rules + rules: function( command, argument ) { + var element = this[0]; + + if ( command ) { + var settings = $.data(element.form, "validator").settings; + var staticRules = settings.rules; + var existingRules = $.validator.staticRules(element); + switch(command) { + case "add": + $.extend(existingRules, $.validator.normalizeRule(argument)); + staticRules[element.name] = existingRules; + if ( argument.messages ) { + settings.messages[element.name] = $.extend( settings.messages[element.name], argument.messages ); + } + break; + case "remove": + if ( !argument ) { + delete staticRules[element.name]; + return existingRules; + } + var filtered = {}; + $.each(argument.split(/\s/), function( index, method ) { + filtered[method] = existingRules[method]; + delete existingRules[method]; + }); + return filtered; + } + } + + var data = $.validator.normalizeRules( + $.extend( + {}, + $.validator.classRules(element), + $.validator.attributeRules(element), + $.validator.dataRules(element), + $.validator.staticRules(element) + ), element); + + // make sure required is at front + if ( data.required ) { + var param = data.required; + delete data.required; + data = $.extend({required: param}, data); + } + + return data; + } +}); + +// Custom selectors +$.extend($.expr[":"], { + // http://docs.jquery.com/Plugins/Validation/blank + blank: function( a ) { return !$.trim("" + a.value); }, + // http://docs.jquery.com/Plugins/Validation/filled + filled: function( a ) { return !!$.trim("" + a.value); }, + // http://docs.jquery.com/Plugins/Validation/unchecked + unchecked: function( a ) { return !a.checked; } +}); + +// constructor for validator +$.validator = function( options, form ) { + this.settings = $.extend( true, {}, $.validator.defaults, options ); + this.currentForm = form; + this.init(); +}; + +$.validator.format = function( source, params ) { + if ( arguments.length === 1 ) { + return function() { + var args = $.makeArray(arguments); + args.unshift(source); + return $.validator.format.apply( this, args ); + }; + } + if ( arguments.length > 2 && params.constructor !== Array ) { + params = $.makeArray(arguments).slice(1); + } + if ( params.constructor !== Array ) { + params = [ params ]; + } + $.each(params, function( i, n ) { + source = source.replace( new RegExp("\\{" + i + "\\}", "g"), function() { + return n; + }); + }); + return source; +}; + +$.extend($.validator, { + + defaults: { + messages: {}, + groups: {}, + rules: {}, + errorClass: "error", + validClass: "valid", + errorElement: "label", + focusInvalid: true, + errorContainer: $([]), + errorLabelContainer: $([]), + onsubmit: true, + ignore: ":hidden", + ignoreTitle: false, + onfocusin: function( element, event ) { + this.lastActive = element; + + // hide error label and remove error class on focus if enabled + if ( this.settings.focusCleanup && !this.blockFocusCleanup ) { + if ( this.settings.unhighlight ) { + this.settings.unhighlight.call( this, element, this.settings.errorClass, this.settings.validClass ); + } + this.addWrapper(this.errorsFor(element)).hide(); + } + }, + onfocusout: function( element, event ) { + if ( !this.checkable(element) && (element.name in this.submitted || !this.optional(element)) ) { + this.element(element); + } + }, + onkeyup: function( element, event ) { + if ( event.which === 9 && this.elementValue(element) === "" ) { + return; + } else if ( element.name in this.submitted || element === this.lastElement ) { + this.element(element); + } + }, + onclick: function( element, event ) { + // click on selects, radiobuttons and checkboxes + if ( element.name in this.submitted ) { + this.element(element); + } + // or option elements, check parent select in that case + else if ( element.parentNode.name in this.submitted ) { + this.element(element.parentNode); + } + }, + highlight: function( element, errorClass, validClass ) { + if ( element.type === "radio" ) { + this.findByName(element.name).addClass(errorClass).removeClass(validClass); + } else { + $(element).addClass(errorClass).removeClass(validClass); + } + }, + unhighlight: function( element, errorClass, validClass ) { + if ( element.type === "radio" ) { + this.findByName(element.name).removeClass(errorClass).addClass(validClass); + } else { + $(element).removeClass(errorClass).addClass(validClass); + } + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/setDefaults + setDefaults: function( settings ) { + $.extend( $.validator.defaults, settings ); + }, + + messages: { + required: "This field is required.", + remote: "Please fix this field.", + email: "Please enter a valid email address.", + url: "Please enter a valid URL.", + date: "Please enter a valid date.", + dateISO: "Please enter a valid date (ISO).", + number: "Please enter a valid number.", + digits: "Please enter only digits.", + creditcard: "Please enter a valid credit card number.", + equalTo: "Please enter the same value again.", + maxlength: $.validator.format("Please enter no more than {0} characters."), + minlength: $.validator.format("Please enter at least {0} characters."), + rangelength: $.validator.format("Please enter a value between {0} and {1} characters long."), + range: $.validator.format("Please enter a value between {0} and {1}."), + max: $.validator.format("Please enter a value less than or equal to {0}."), + min: $.validator.format("Please enter a value greater than or equal to {0}.") + }, + + autoCreateRanges: false, + + prototype: { + + init: function() { + this.labelContainer = $(this.settings.errorLabelContainer); + this.errorContext = this.labelContainer.length && this.labelContainer || $(this.currentForm); + this.containers = $(this.settings.errorContainer).add( this.settings.errorLabelContainer ); + this.submitted = {}; + this.valueCache = {}; + this.pendingRequest = 0; + this.pending = {}; + this.invalid = {}; + this.reset(); + + var groups = (this.groups = {}); + $.each(this.settings.groups, function( key, value ) { + if ( typeof value === "string" ) { + value = value.split(/\s/); + } + $.each(value, function( index, name ) { + groups[name] = key; + }); + }); + var rules = this.settings.rules; + $.each(rules, function( key, value ) { + rules[key] = $.validator.normalizeRule(value); + }); + + function delegate(event) { + var validator = $.data(this[0].form, "validator"), + eventType = "on" + event.type.replace(/^validate/, ""); + if ( validator.settings[eventType] ) { + validator.settings[eventType].call(validator, this[0], event); + } + } + $(this.currentForm) + .validateDelegate(":text, [type='password'], [type='file'], select, textarea, " + + "[type='number'], [type='search'] ,[type='tel'], [type='url'], " + + "[type='email'], [type='datetime'], [type='date'], [type='month'], " + + "[type='week'], [type='time'], [type='datetime-local'], " + + "[type='range'], [type='color'] ", + "focusin focusout keyup", delegate) + .validateDelegate("[type='radio'], [type='checkbox'], select, option", "click", delegate); + + if ( this.settings.invalidHandler ) { + $(this.currentForm).bind("invalid-form.validate", this.settings.invalidHandler); + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/form + form: function() { + this.checkForm(); + $.extend(this.submitted, this.errorMap); + this.invalid = $.extend({}, this.errorMap); + if ( !this.valid() ) { + $(this.currentForm).triggerHandler("invalid-form", [this]); + } + this.showErrors(); + return this.valid(); + }, + + checkForm: function() { + this.prepareForm(); + for ( var i = 0, elements = (this.currentElements = this.elements()); elements[i]; i++ ) { + this.check( elements[i] ); + } + return this.valid(); + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/element + element: function( element ) { + element = this.validationTargetFor( this.clean( element ) ); + this.lastElement = element; + this.prepareElement( element ); + this.currentElements = $(element); + var result = this.check( element ) !== false; + if ( result ) { + delete this.invalid[element.name]; + } else { + this.invalid[element.name] = true; + } + if ( !this.numberOfInvalids() ) { + // Hide error containers on last error + this.toHide = this.toHide.add( this.containers ); + } + this.showErrors(); + return result; + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/showErrors + showErrors: function( errors ) { + if ( errors ) { + // add items to error list and map + $.extend( this.errorMap, errors ); + this.errorList = []; + for ( var name in errors ) { + this.errorList.push({ + message: errors[name], + element: this.findByName(name)[0] + }); + } + // remove items from success list + this.successList = $.grep( this.successList, function( element ) { + return !(element.name in errors); + }); + } + if ( this.settings.showErrors ) { + this.settings.showErrors.call( this, this.errorMap, this.errorList ); + } else { + this.defaultShowErrors(); + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/resetForm + resetForm: function() { + if ( $.fn.resetForm ) { + $(this.currentForm).resetForm(); + } + this.submitted = {}; + this.lastElement = null; + this.prepareForm(); + this.hideErrors(); + this.elements().removeClass( this.settings.errorClass ).removeData( "previousValue" ); + }, + + numberOfInvalids: function() { + return this.objectLength(this.invalid); + }, + + objectLength: function( obj ) { + var count = 0; + for ( var i in obj ) { + count++; + } + return count; + }, + + hideErrors: function() { + this.addWrapper( this.toHide ).hide(); + }, + + valid: function() { + return this.size() === 0; + }, + + size: function() { + return this.errorList.length; + }, + + focusInvalid: function() { + if ( this.settings.focusInvalid ) { + try { + $(this.findLastActive() || this.errorList.length && this.errorList[0].element || []) + .filter(":visible") + .focus() + // manually trigger focusin event; without it, focusin handler isn't called, findLastActive won't have anything to find + .trigger("focusin"); + } catch(e) { + // ignore IE throwing errors when focusing hidden elements + } + } + }, + + findLastActive: function() { + var lastActive = this.lastActive; + return lastActive && $.grep(this.errorList, function( n ) { + return n.element.name === lastActive.name; + }).length === 1 && lastActive; + }, + + elements: function() { + var validator = this, + rulesCache = {}; + + // select all valid inputs inside the form (no submit or reset buttons) + return $(this.currentForm) + .find("input, select, textarea") + .not(":submit, :reset, :image, [disabled]") + .not( this.settings.ignore ) + .filter(function() { + if ( !this.name ) { + if ( window.console ) { + console.error( "%o has no name assigned", this ); + } + throw new Error( "Failed to validate, found an element with no name assigned. See console for element reference." ); + } + + // select only the first element for each name, and only those with rules specified + if ( this.name in rulesCache || !validator.objectLength($(this).rules()) ) { + return false; + } + + rulesCache[this.name] = true; + return true; + }); + }, + + clean: function( selector ) { + return $(selector)[0]; + }, + + errors: function() { + var errorClass = this.settings.errorClass.replace(" ", "."); + return $(this.settings.errorElement + "." + errorClass, this.errorContext); + }, + + reset: function() { + this.successList = []; + this.errorList = []; + this.errorMap = {}; + this.toShow = $([]); + this.toHide = $([]); + this.currentElements = $([]); + }, + + prepareForm: function() { + this.reset(); + this.toHide = this.errors().add( this.containers ); + }, + + prepareElement: function( element ) { + this.reset(); + this.toHide = this.errorsFor(element); + }, + + elementValue: function( element ) { + var type = $(element).attr("type"), + val = $(element).val(); + + if ( type === "radio" || type === "checkbox" ) { + return $("input[name='" + $(element).attr("name") + "']:checked").val(); + } + + if ( typeof val === "string" ) { + return val.replace(/\r/g, ""); + } + return val; + }, + + check: function( element ) { + element = this.validationTargetFor( this.clean( element ) ); + + var rules = $(element).rules(); + var dependencyMismatch = false; + var val = this.elementValue(element); + var result; + + for (var method in rules ) { + var rule = { method: method, parameters: rules[method] }; + try { + + result = $.validator.methods[method].call( this, val, element, rule.parameters ); + + // if a method indicates that the field is optional and therefore valid, + // don't mark it as valid when there are no other rules + if ( result === "dependency-mismatch" ) { + dependencyMismatch = true; + continue; + } + dependencyMismatch = false; + + if ( result === "pending" ) { + this.toHide = this.toHide.not( this.errorsFor(element) ); + return; + } + + if ( !result ) { + this.formatAndAdd( element, rule ); + return false; + } + } catch(e) { + if ( this.settings.debug && window.console ) { + console.log( "Exception occured when checking element " + element.id + ", check the '" + rule.method + "' method.", e ); + } + throw e; + } + } + if ( dependencyMismatch ) { + return; + } + if ( this.objectLength(rules) ) { + this.successList.push(element); + } + return true; + }, + + // return the custom message for the given element and validation method + // specified in the element's HTML5 data attribute + customDataMessage: function( element, method ) { + return $(element).data("msg-" + method.toLowerCase()) || (element.attributes && $(element).attr("data-msg-" + method.toLowerCase())); + }, + + // return the custom message for the given element name and validation method + customMessage: function( name, method ) { + var m = this.settings.messages[name]; + return m && (m.constructor === String ? m : m[method]); + }, + + // return the first defined argument, allowing empty strings + findDefined: function() { + for(var i = 0; i < arguments.length; i++) { + if ( arguments[i] !== undefined ) { + return arguments[i]; + } + } + return undefined; + }, + + defaultMessage: function( element, method ) { + return this.findDefined( + this.customMessage( element.name, method ), + this.customDataMessage( element, method ), + // title is never undefined, so handle empty string as undefined + !this.settings.ignoreTitle && element.title || undefined, + $.validator.messages[method], + "Warning: No message defined for " + element.name + "" + ); + }, + + formatAndAdd: function( element, rule ) { + var message = this.defaultMessage( element, rule.method ), + theregex = /\$?\{(\d+)\}/g; + if ( typeof message === "function" ) { + message = message.call(this, rule.parameters, element); + } else if (theregex.test(message)) { + message = $.validator.format(message.replace(theregex, "{$1}"), rule.parameters); + } + this.errorList.push({ + message: message, + element: element + }); + + this.errorMap[element.name] = message; + this.submitted[element.name] = message; + }, + + addWrapper: function( toToggle ) { + if ( this.settings.wrapper ) { + toToggle = toToggle.add( toToggle.parent( this.settings.wrapper ) ); + } + return toToggle; + }, + + defaultShowErrors: function() { + var i, elements; + for ( i = 0; this.errorList[i]; i++ ) { + var error = this.errorList[i]; + if ( this.settings.highlight ) { + this.settings.highlight.call( this, error.element, this.settings.errorClass, this.settings.validClass ); + } + this.showLabel( error.element, error.message ); + } + if ( this.errorList.length ) { + this.toShow = this.toShow.add( this.containers ); + } + if ( this.settings.success ) { + for ( i = 0; this.successList[i]; i++ ) { + this.showLabel( this.successList[i] ); + } + } + if ( this.settings.unhighlight ) { + for ( i = 0, elements = this.validElements(); elements[i]; i++ ) { + this.settings.unhighlight.call( this, elements[i], this.settings.errorClass, this.settings.validClass ); + } + } + this.toHide = this.toHide.not( this.toShow ); + this.hideErrors(); + this.addWrapper( this.toShow ).show(); + }, + + validElements: function() { + return this.currentElements.not(this.invalidElements()); + }, + + invalidElements: function() { + return $(this.errorList).map(function() { + return this.element; + }); + }, + + showLabel: function( element, message ) { + var label = this.errorsFor( element ); + if ( label.length ) { + // refresh error/success class + label.removeClass( this.settings.validClass ).addClass( this.settings.errorClass ); + + // check if we have a generated label, replace the message then + if ( label.attr("generated") ) { + label.html(message); + } + } else { + // create label + label = $("<" + this.settings.errorElement + "/>") + .attr({"for": this.idOrName(element), generated: true}) + .addClass(this.settings.errorClass) + .html(message || ""); + if ( this.settings.wrapper ) { + // make sure the element is visible, even in IE + // actually showing the wrapped element is handled elsewhere + label = label.hide().show().wrap("<" + this.settings.wrapper + "/>").parent(); + } + if ( !this.labelContainer.append(label).length ) { + if ( this.settings.errorPlacement ) { + this.settings.errorPlacement(label, $(element) ); + } else { + label.insertAfter(element); + } + } + } + if ( !message && this.settings.success ) { + label.text(""); + if ( typeof this.settings.success === "string" ) { + label.addClass( this.settings.success ); + } else { + this.settings.success( label, element ); + } + } + this.toShow = this.toShow.add(label); + }, + + errorsFor: function( element ) { + var name = this.idOrName(element); + return this.errors().filter(function() { + return $(this).attr("for") === name; + }); + }, + + idOrName: function( element ) { + return this.groups[element.name] || (this.checkable(element) ? element.name : element.id || element.name); + }, + + validationTargetFor: function( element ) { + // if radio/checkbox, validate first element in group instead + if ( this.checkable(element) ) { + element = this.findByName( element.name ).not(this.settings.ignore)[0]; + } + return element; + }, + + checkable: function( element ) { + return (/radio|checkbox/i).test(element.type); + }, + + findByName: function( name ) { + return $(this.currentForm).find("[name='" + name + "']"); + }, + + getLength: function( value, element ) { + switch( element.nodeName.toLowerCase() ) { + case "select": + return $("option:selected", element).length; + case "input": + if ( this.checkable( element) ) { + return this.findByName(element.name).filter(":checked").length; + } + } + return value.length; + }, + + depend: function( param, element ) { + return this.dependTypes[typeof param] ? this.dependTypes[typeof param](param, element) : true; + }, + + dependTypes: { + "boolean": function( param, element ) { + return param; + }, + "string": function( param, element ) { + return !!$(param, element.form).length; + }, + "function": function( param, element ) { + return param(element); + } + }, + + optional: function( element ) { + var val = this.elementValue(element); + return !$.validator.methods.required.call(this, val, element) && "dependency-mismatch"; + }, + + startRequest: function( element ) { + if ( !this.pending[element.name] ) { + this.pendingRequest++; + this.pending[element.name] = true; + } + }, + + stopRequest: function( element, valid ) { + this.pendingRequest--; + // sometimes synchronization fails, make sure pendingRequest is never < 0 + if ( this.pendingRequest < 0 ) { + this.pendingRequest = 0; + } + delete this.pending[element.name]; + if ( valid && this.pendingRequest === 0 && this.formSubmitted && this.form() ) { + $(this.currentForm).submit(); + this.formSubmitted = false; + } else if (!valid && this.pendingRequest === 0 && this.formSubmitted) { + $(this.currentForm).triggerHandler("invalid-form", [this]); + this.formSubmitted = false; + } + }, + + previousValue: function( element ) { + return $.data(element, "previousValue") || $.data(element, "previousValue", { + old: null, + valid: true, + message: this.defaultMessage( element, "remote" ) + }); + } + + }, + + classRuleSettings: { + required: {required: true}, + email: {email: true}, + url: {url: true}, + date: {date: true}, + dateISO: {dateISO: true}, + number: {number: true}, + digits: {digits: true}, + creditcard: {creditcard: true} + }, + + addClassRules: function( className, rules ) { + if ( className.constructor === String ) { + this.classRuleSettings[className] = rules; + } else { + $.extend(this.classRuleSettings, className); + } + }, + + classRules: function( element ) { + var rules = {}; + var classes = $(element).attr("class"); + if ( classes ) { + $.each(classes.split(" "), function() { + if ( this in $.validator.classRuleSettings ) { + $.extend(rules, $.validator.classRuleSettings[this]); + } + }); + } + return rules; + }, + + attributeRules: function( element ) { + var rules = {}; + var $element = $(element); + + for (var method in $.validator.methods) { + var value; + + // support for in both html5 and older browsers + if ( method === "required" ) { + value = $element.get(0).getAttribute(method); + // Some browsers return an empty string for the required attribute + // and non-HTML5 browsers might have required="" markup + if ( value === "" ) { + value = true; + } + // force non-HTML5 browsers to return bool + value = !!value; + } else { + value = $element.attr(method); + } + + if ( value ) { + rules[method] = value; + } else if ( $element[0].getAttribute("type") === method ) { + rules[method] = true; + } + } + + // maxlength may be returned as -1, 2147483647 (IE) and 524288 (safari) for text inputs + if ( rules.maxlength && /-1|2147483647|524288/.test(rules.maxlength) ) { + delete rules.maxlength; + } + + return rules; + }, + + dataRules: function( element ) { + var method, value, + rules = {}, $element = $(element); + for (method in $.validator.methods) { + value = $element.data("rule-" + method.toLowerCase()); + if ( value !== undefined ) { + rules[method] = value; + } + } + return rules; + }, + + staticRules: function( element ) { + var rules = {}; + var validator = $.data(element.form, "validator"); + if ( validator.settings.rules ) { + rules = $.validator.normalizeRule(validator.settings.rules[element.name]) || {}; + } + return rules; + }, + + normalizeRules: function( rules, element ) { + // handle dependency check + $.each(rules, function( prop, val ) { + // ignore rule when param is explicitly false, eg. required:false + if ( val === false ) { + delete rules[prop]; + return; + } + if ( val.param || val.depends ) { + var keepRule = true; + switch (typeof val.depends) { + case "string": + keepRule = !!$(val.depends, element.form).length; + break; + case "function": + keepRule = val.depends.call(element, element); + break; + } + if ( keepRule ) { + rules[prop] = val.param !== undefined ? val.param : true; + } else { + delete rules[prop]; + } + } + }); + + // evaluate parameters + $.each(rules, function( rule, parameter ) { + rules[rule] = $.isFunction(parameter) ? parameter(element) : parameter; + }); + + // clean number parameters + $.each(["minlength", "maxlength", "min", "max"], function() { + if ( rules[this] ) { + rules[this] = Number(rules[this]); + } + }); + $.each(["rangelength", "range"], function() { + var parts; + if ( rules[this] ) { + if ( $.isArray(rules[this]) ) { + rules[this] = [Number(rules[this][0]), Number(rules[this][1])]; + } else if ( typeof rules[this] === "string" ) { + parts = rules[this].split(/[\s,]+/); + rules[this] = [Number(parts[0]), Number(parts[1])]; + } + } + }); + + if ( $.validator.autoCreateRanges ) { + // auto-create ranges + if ( rules.min && rules.max ) { + rules.range = [rules.min, rules.max]; + delete rules.min; + delete rules.max; + } + if ( rules.minlength && rules.maxlength ) { + rules.rangelength = [rules.minlength, rules.maxlength]; + delete rules.minlength; + delete rules.maxlength; + } + } + + return rules; + }, + + // Converts a simple string to a {string: true} rule, e.g., "required" to {required:true} + normalizeRule: function( data ) { + if ( typeof data === "string" ) { + var transformed = {}; + $.each(data.split(/\s/), function() { + transformed[this] = true; + }); + data = transformed; + } + return data; + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/addMethod + addMethod: function( name, method, message ) { + $.validator.methods[name] = method; + $.validator.messages[name] = message !== undefined ? message : $.validator.messages[name]; + if ( method.length < 3 ) { + $.validator.addClassRules(name, $.validator.normalizeRule(name)); + } + }, + + methods: { + + // http://docs.jquery.com/Plugins/Validation/Methods/required + required: function( value, element, param ) { + // check if dependency is met + if ( !this.depend(param, element) ) { + return "dependency-mismatch"; + } + if ( element.nodeName.toLowerCase() === "select" ) { + // could be an array for select-multiple or a string, both are fine this way + var val = $(element).val(); + return val && val.length > 0; + } + if ( this.checkable(element) ) { + return this.getLength(value, element) > 0; + } + return $.trim(value).length > 0; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/remote + remote: function( value, element, param ) { + if ( this.optional(element) ) { + return "dependency-mismatch"; + } + + var previous = this.previousValue(element); + if (!this.settings.messages[element.name] ) { + this.settings.messages[element.name] = {}; + } + previous.originalMessage = this.settings.messages[element.name].remote; + this.settings.messages[element.name].remote = previous.message; + + param = typeof param === "string" && {url:param} || param; + + if ( previous.old === value ) { + return previous.valid; + } + + previous.old = value; + var validator = this; + this.startRequest(element); + var data = {}; + data[element.name] = value; + $.ajax($.extend(true, { + url: param, + mode: "abort", + port: "validate" + element.name, + dataType: "json", + data: data, + success: function( response ) { + validator.settings.messages[element.name].remote = previous.originalMessage; + var valid = response === true || response === "true"; + if ( valid ) { + var submitted = validator.formSubmitted; + validator.prepareElement(element); + validator.formSubmitted = submitted; + validator.successList.push(element); + delete validator.invalid[element.name]; + validator.showErrors(); + } else { + var errors = {}; + var message = response || validator.defaultMessage( element, "remote" ); + errors[element.name] = previous.message = $.isFunction(message) ? message(value) : message; + validator.invalid[element.name] = true; + validator.showErrors(errors); + } + previous.valid = valid; + validator.stopRequest(element, valid); + } + }, param)); + return "pending"; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/minlength + minlength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || length >= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/maxlength + maxlength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || length <= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/rangelength + rangelength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || ( length >= param[0] && length <= param[1] ); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/min + min: function( value, element, param ) { + return this.optional(element) || value >= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/max + max: function( value, element, param ) { + return this.optional(element) || value <= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/range + range: function( value, element, param ) { + return this.optional(element) || ( value >= param[0] && value <= param[1] ); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/email + email: function( value, element ) { + // contributed by Scott Gonzalez: http://projects.scottsplayground.com/email_address_validation/ + return this.optional(element) || /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/url + url: function( value, element ) { + // contributed by Scott Gonzalez: http://projects.scottsplayground.com/iri/ + return this.optional(element) || /^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/date + date: function( value, element ) { + return this.optional(element) || !/Invalid|NaN/.test(new Date(value).toString()); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/dateISO + dateISO: function( value, element ) { + return this.optional(element) || /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/number + number: function( value, element ) { + return this.optional(element) || /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/digits + digits: function( value, element ) { + return this.optional(element) || /^\d+$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/creditcard + // based on http://en.wikipedia.org/wiki/Luhn + creditcard: function( value, element ) { + if ( this.optional(element) ) { + return "dependency-mismatch"; + } + // accept only spaces, digits and dashes + if ( /[^0-9 \-]+/.test(value) ) { + return false; + } + var nCheck = 0, + nDigit = 0, + bEven = false; + + value = value.replace(/\D/g, ""); + + for (var n = value.length - 1; n >= 0; n--) { + var cDigit = value.charAt(n); + nDigit = parseInt(cDigit, 10); + if ( bEven ) { + if ( (nDigit *= 2) > 9 ) { + nDigit -= 9; + } + } + nCheck += nDigit; + bEven = !bEven; + } + + return (nCheck % 10) === 0; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/equalTo + equalTo: function( value, element, param ) { + // bind to the blur event of the target in order to revalidate whenever the target field is updated + // TODO find a way to bind the event just once, avoiding the unbind-rebind overhead + var target = $(param); + if ( this.settings.onfocusout ) { + target.unbind(".validate-equalTo").bind("blur.validate-equalTo", function() { + $(element).valid(); + }); + } + return value === target.val(); + } + + } + +}); + +// deprecated, use $.validator.format instead +$.format = $.validator.format; + +}(jQuery)); + +// ajax mode: abort +// usage: $.ajax({ mode: "abort"[, port: "uniqueport"]}); +// if mode:"abort" is used, the previous request on that port (port can be undefined) is aborted via XMLHttpRequest.abort() +(function($) { + var pendingRequests = {}; + // Use a prefilter if available (1.5+) + if ( $.ajaxPrefilter ) { + $.ajaxPrefilter(function( settings, _, xhr ) { + var port = settings.port; + if ( settings.mode === "abort" ) { + if ( pendingRequests[port] ) { + pendingRequests[port].abort(); + } + pendingRequests[port] = xhr; + } + }); + } else { + // Proxy ajax + var ajax = $.ajax; + $.ajax = function( settings ) { + var mode = ( "mode" in settings ? settings : $.ajaxSettings ).mode, + port = ( "port" in settings ? settings : $.ajaxSettings ).port; + if ( mode === "abort" ) { + if ( pendingRequests[port] ) { + pendingRequests[port].abort(); + } + return (pendingRequests[port] = ajax.apply(this, arguments)); + } + return ajax.apply(this, arguments); + }; + } +}(jQuery)); + +// provides delegate(type: String, delegate: Selector, handler: Callback) plugin for easier event delegation +// handler is only called when $(event.target).is(delegate), in the scope of the jquery-object for event.target +(function($) { + $.extend($.fn, { + validateDelegate: function( delegate, type, handler ) { + return this.bind(type, function( event ) { + var target = $(event.target); + if ( target.is(delegate) ) { + return handler.apply(target, arguments); + } + }); + } + }); +}(jQuery)); diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/type-view.hbs b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/type-view.hbs new file mode 100644 index 000000000..67e4d0225 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/type-view.hbs @@ -0,0 +1,324 @@ +

      +

      Raspberry Pi

      +
      +
      + +
      + +
      + +
      +

      What it Does

      +
      +

      Connect a RaspberryPi to WSO2 IoT Server and manage it.

      +
      +

      What You Need

      +
      +
        +
      • + ITEM 01 +    Raspberry Pi with SD Card(Internet Enabled [Wifi or Ethernet]). +
      • +
      • + ITEM 02 +    Adafruit DHT11 Temperature Sensor. +
      • +
      • + ITEM 03 +    LED Bulb. +
      • +
      • + ITEM 04 +    Resister (eg : 470 ohm). +
      • +
      • + STEP 05 +    Proceed to [Prepare] section. +
      +
      + View API
        + + + Download Agent + +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +

      +
      +
      +

      Prepare

      +
      +
        +
      • + 01 +    Connect RaspberryPi, DHT11 Temperature sensor and LED as per the + schematic below. +
      • +
      • + 02 +    Ensure your RaspberryPi Board can connect to Internet. +
      • +
      • + 03 +    Download RaspberryPi Agent via [Download Agent] button above. + (Alternatively you can use the "curl" command to directly download the Agent to your + RaspberryPi.) +
      • +
      • + 04 +    Copy downloaded Agent into your RaspberryPi. +
      • +
      • + 05 +    Unzip the downloaded Agent and start terminal to run below commands. +
      • +
      • +                + + + + +    [sudo ./testAgent.sh]    -    Script to test agent + functionality. +
      • +
      • +                + + + + +    [sudo ./startService.sh]    -    Use this to start + agent as Daemon. +
      • +
      +
      +
      +
      +

      Schematic Diagram

      +
      +

      Click on the image to zoom

      +
      + + + +
      +
      +
      +
      +

      Try Out

      +
      +
        +
      • + 01 +    You can view all your connected devices at + [Device Management] page. +
      • +
      • + 02 +    Select one of connected devices and check for available control + operations and monitor Real-Time data. +
      • +
      • + 03 +    You can also view analytics of the data published to IoT-Server by + navigating to Device Analytics page. +
      • +
      +
      +

      Click on the image to zoom

      +
      + + + +
      +
      + +{{#zone "topCss"}} + +{{/zone}} + +{{#zone "bottomJs"}} + {{js "/js/download.js"}} + {{js "/js/jquery.validate.js"}} +{{/zone}} \ No newline at end of file diff --git a/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/type-view.json b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/type-view.json new file mode 100644 index 000000000..9eecd8f5b --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/org.wso2.carbon.device.mgt.iot.raspberrypi.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.raspberrypi.type-view/type-view.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0" +} \ No newline at end of file diff --git a/components/iot-plugins/raspberrypi-plugin/pom.xml b/components/iot-plugins/raspberrypi-plugin/pom.xml new file mode 100644 index 000000000..3b85d06d0 --- /dev/null +++ b/components/iot-plugins/raspberrypi-plugin/pom.xml @@ -0,0 +1,62 @@ + + + + + + + org.wso2.carbon.devicemgt-plugins + iot-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + raspberrypi-plugin + pom + WSO2 Carbon - Arduino Plugin + http://wso2.org + + + org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl + org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl + org.wso2.carbon.device.mgt.iot.raspberrypi.ui + org.wso2.carbon.device.mgt.iot.raspberrypi.analytics + org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl + + + + + + + org.apache.felix + maven-scr-plugin + 1.7.2 + + + generate-scr-scrdescriptor + + scr + + + + + + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/pom.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/pom.xml new file mode 100644 index 000000000..99bbf77db --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/pom.xml @@ -0,0 +1,255 @@ + + + + + 4.0.0 + + + virtual-fire-alarm-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl + WSO2 Carbon - IoT Server VirtualFireAlarm Advanced Agent + WSO2 Carbon - VirtualFireAlarm Device Advanced Agent Implementation + http://wso2.org + + + + + org.apache.maven.plugins + maven-compiler-plugin + + UTF-8 + ${wso2.maven.compiler.source} + ${wso2.maven.compiler.target} + + 2.3.2 + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.Bootstrap + + + + + jar-with-dependencies + + wso2-firealarm-virtual-agent-advanced + false + + + + make-assembly + + package + + + single + + + + + + + + + + + + org.eclipse.jetty + jetty-server + ${jetty.version} + + + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + ${paho.mqtt.version} + + + + + org.igniterealtime.smack.wso2 + smack + ${smack.wso2.version} + + + org.igniterealtime.smack.wso2 + smackx + ${smackx.wso2.version} + + + + + org.bouncycastle.wso2 + bcprov-jdk15on + ${bcprov.wso2.version} + + + org.bouncycastle.wso2 + bcpkix-jdk15on + ${bcpkix.wso2.version} + + + + + com.google.code.jscep.wso2 + jscep + ${jscep.version} + + + + commons-codec.wso2 + commons-codec + + + + commons-lang + commons-lang + ${commons-lang.version} + + + + commons-logging + commons-logging + ${common-logging.version} + + + + commons-io + commons-io + ${commons.io} + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-simple + ${slf4j.version} + + + + org.json.wso2 + json + + + + + org.wso2.siddhi + siddhi-query + ${sidhdhi.version} + + + org.wso2.siddhi + siddhi-core + ${sidhdhi.version} + + + + org.apache.httpcomponents + httpclient + ${apache-httpclient.version} + + + + com.google.code.gson + gson + ${gson.version} + + + + + + + + + wso2-nexus + WSO2 internal Repository + http://maven.wso2.org/nexus/content/groups/wso2-public/ + + true + daily + ignore + + + + wso2-maven2-repository + http://dist.wso2.org/maven2 + + + + + + 1.7 + 1.7 + + + 8.1.3.v20120416 + + + 1.0.2 + + + 3.0.4.wso2v1 + 3.0.4.wso2v1 + + + 1.49.wso2v1 + 1.49.wso2v1 + + + 2.0.2.wso2v2 + + + 1.2.17 + 1.2 + 2.4 + 1.7 + 2.6 + + + 4.5 + + + 1.7.13 + + + 2.1.0-wso2v1 + + 2.3.1 + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/Bootstrap.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/Bootstrap.java new file mode 100644 index 000000000..663a207f9 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/Bootstrap.java @@ -0,0 +1,36 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.advanced; + +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.core.AgentManager; + +public class Bootstrap { + + /** + * @param args the command line arguments + */ + public static void main(String[] args) { + System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog"); + System.setProperty("org.apache.commons.logging.simplelog.defaultlog", "info"); + System.setProperty("org.apache.commons.logging.simplelog.showdatetime", "true"); + System.setProperty("org.apache.commons.logging.simplelog.dateTimeFormat", "HH:mm:ss"); + AgentManager.getInstance().init(); + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/communication/http/FireAlarmHTTPCommunicator.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/communication/http/FireAlarmHTTPCommunicator.java new file mode 100644 index 000000000..bc4a7bfb7 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/communication/http/FireAlarmHTTPCommunicator.java @@ -0,0 +1,495 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.advanced.communication.http; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.core.AgentConstants; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.core.AgentManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.exception.AgentCoreOperationException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.transport.TransportHandlerException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.transport.TransportUtils; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.transport.http.HTTPTransportHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.ProtocolException; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Enumeration; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class FireAlarmHTTPCommunicator extends HTTPTransportHandler { + private static final Log log = LogFactory.getLog(FireAlarmHTTPCommunicator.class); + + private ScheduledExecutorService service = Executors.newScheduledThreadPool(2); + private ScheduledFuture dataPushServiceHandler; + private ScheduledFuture connectorServiceHandler; + + public FireAlarmHTTPCommunicator() { + super(); + } + + public FireAlarmHTTPCommunicator(int port) { + super(port); + } + + public FireAlarmHTTPCommunicator(int port, int reconnectionInterval) { + super(port, reconnectionInterval); + } + + public ScheduledFuture getDataPushServiceHandler() { + return dataPushServiceHandler; + } + + public void connect() { + Runnable connect = new Runnable() { + public void run() { + if (!isConnected()) { + try { + processIncomingMessage(); + server.start(); + registerThisDevice(); + publishDeviceData(); + log.info("HTTP Server started at port: " + port); + + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.warn("Unable to 'START' HTTP server. Will retry after " + + timeoutInterval / 1000 + " seconds."); + } + } + } + } + }; + + connectorServiceHandler = service.scheduleAtFixedRate(connect, 0, timeoutInterval, + TimeUnit.MILLISECONDS); + } + + + @Override + public void processIncomingMessage() { + server.setHandler(new AbstractHandler() { + public void handle(String s, Request request, HttpServletRequest + httpServletRequest, + HttpServletResponse httpServletResponse) + throws IOException, ServletException { + httpServletResponse.setContentType("text/html;charset=utf-8"); + httpServletResponse.setStatus(HttpServletResponse.SC_OK); + request.setHandled(true); + + AgentManager agentManager = AgentManager.getInstance(); + String pathContext = request.getPathInfo(); + String separator = (File.separatorChar == '\\') ? ("\\\\") : (File.separator); + + if (pathContext.toUpperCase().contains( + separator + AgentConstants.TEMPERATURE_CONTROL)) { + httpServletResponse.getWriter().println( + agentManager.getTemperature()); + + } else if (pathContext.toUpperCase().contains( + separator + AgentConstants.HUMIDITY_CONTROL)) { + httpServletResponse.getWriter().println( + agentManager.getHumidity()); + + } else if (pathContext.toUpperCase().contains( + separator + AgentConstants.BULB_CONTROL)) { + String[] pathVariables = pathContext.split(separator); + + if (pathVariables.length != 3) { + httpServletResponse.getWriter().println( + "Invalid BULB-control received by the device. Need to be in " + + "'{host}:{port}/BULB/{ON|OFF}' format."); + return; + } + + String switchState = pathVariables[2]; + + if (switchState == null) { + httpServletResponse.getWriter().println( + "Please specify switch-status of the BULB."); + } else { + boolean status = switchState.toUpperCase().equals( + AgentConstants.CONTROL_ON); + agentManager.changeAlarmStatus(status); + httpServletResponse.getWriter().println("Bulb is " + (status ? + AgentConstants.CONTROL_ON : AgentConstants.CONTROL_OFF)); + } + } else { + httpServletResponse.getWriter().println( + "Invalid control command received by the device."); + } + } + }); + } + + @Override + public void publishDeviceData() { + final AgentManager agentManager = AgentManager.getInstance(); + int publishInterval = agentManager.getPushInterval(); + final String deviceOwner = agentManager.getAgentConfigs().getDeviceOwner(); + final String deviceID = agentManager.getAgentConfigs().getDeviceId(); + boolean simulationMode = false; + int duration = 2 * 60; + int frequency = 5; + + Runnable pushDataRunnable = new Runnable() { + @Override + public void run() { + + String pushDataPayload = String.format(AgentConstants.PUSH_DATA_PAYLOAD, deviceOwner, + deviceID, (agentManager.getDeviceIP() + ":" + port), + agentManager.getTemperature()); + executeDataPush(pushDataPayload); + } + }; + + if (!simulationMode) { + dataPushServiceHandler = service.scheduleAtFixedRate(pushDataRunnable, publishInterval, + publishInterval, + TimeUnit.SECONDS); + } else { + String pushDataPayload = String.format(AgentConstants.PUSH_SIMULATION_DATA_PAYLOAD, deviceOwner, + deviceID, (agentManager.getDeviceIP() + ":" + port), + agentManager.getTemperature(), true, duration, frequency); + executeDataPush(pushDataPayload); + + } + } + + + private void executeDataPush(String pushDataPayload) { + AgentManager agentManager = AgentManager.getInstance(); + int responseCode = -1; + String pushDataEndPointURL = agentManager.getPushDataAPIEP(); + HttpURLConnection httpConnection = null; + + try { + httpConnection = TransportUtils.getHttpConnection(agentManager.getPushDataAPIEP()); + httpConnection.setRequestMethod(AgentConstants.HTTP_POST); + httpConnection.setRequestProperty("Authorization", "Bearer " + + agentManager.getAgentConfigs().getAuthToken()); + httpConnection.setRequestProperty("Content-Type", + AgentConstants.APPLICATION_JSON_TYPE); + + httpConnection.setDoOutput(true); + DataOutputStream dataOutPutWriter = new DataOutputStream( + httpConnection.getOutputStream()); + dataOutPutWriter.writeBytes(pushDataPayload); + dataOutPutWriter.flush(); + dataOutPutWriter.close(); + + responseCode = httpConnection.getResponseCode(); + httpConnection.disconnect(); + + log.info(AgentConstants.LOG_APPENDER + "Message - '" + pushDataPayload + + "' was published to server at: " + httpConnection.getURL()); + + } catch (ProtocolException exception) { + String errorMsg = + "Protocol specific error occurred when trying to set method to " + + AgentConstants.HTTP_POST + " for:" + pushDataEndPointURL; + log.error(AgentConstants.LOG_APPENDER + errorMsg); + + } catch (IOException exception) { + String errorMsg = + "An IO error occurred whilst trying to get the response code from: " + + pushDataEndPointURL + " for a " + AgentConstants.HTTP_POST + + " " + "method."; + log.error(AgentConstants.LOG_APPENDER + errorMsg); + + } catch (TransportHandlerException exception) { + log.error(AgentConstants.LOG_APPENDER + + "Error encountered whilst trying to create HTTP-Connection " + + "to IoT-Server EP at: " + + pushDataEndPointURL); + } + + if (responseCode == HttpStatus.CONFLICT_409 || + responseCode == HttpStatus.PRECONDITION_FAILED_412) { + log.warn(AgentConstants.LOG_APPENDER + + "DeviceIP is being Re-Registered due to Push-Data failure " + + "with response code: " + + responseCode); + registerThisDevice(); + + } else if (responseCode != HttpStatus.NO_CONTENT_204) { + if (log.isDebugEnabled()) { + log.error(AgentConstants.LOG_APPENDER + "Status Code: " + responseCode + + " encountered whilst trying to Push-Device-Data to IoT " + + "Server at: " + + agentManager.getPushDataAPIEP()); + } + agentManager.updateAgentStatus(AgentConstants.SERVER_NOT_RESPONDING); + } + + if (log.isDebugEnabled()) { + log.debug(AgentConstants.LOG_APPENDER + "Push-Data call with payload - " + + pushDataPayload + ", to IoT Server returned status " + + responseCode); + } + } + + @Override + public void disconnect() { + Runnable stopConnection = new Runnable() { + public void run() { + while (isConnected()) { + try { + dataPushServiceHandler.cancel(true); + connectorServiceHandler.cancel(true); + closeConnection(); + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.warn(AgentConstants.LOG_APPENDER + + "Unable to 'STOP' HTTP server at port: " + port); + } + + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException e1) { + log.error(AgentConstants.LOG_APPENDER + + "HTTP-Termination: Thread Sleep Interrupt " + + "Exception"); + } + } + } + } + }; + + Thread terminatorThread = new Thread(stopConnection); + terminatorThread.setDaemon(true); + terminatorThread.start(); + } + + @Override + public void processIncomingMessage(Object message, String... messageParams) { + + } + + @Override + public void publishDeviceData(String... publishData) { + + } + + public void registerThisDevice() { + final AgentManager agentManager = AgentManager.getInstance(); + agentManager.updateAgentStatus("Registering..."); + + final Runnable ipRegistration = new Runnable() { + @Override + public void run() { + while (isConnected()) { + try { + int responseCode = registerDeviceIP( + agentManager.getAgentConfigs().getDeviceOwner(), + agentManager.getAgentConfigs().getDeviceId()); + + if (responseCode == HttpStatus.OK_200) { + agentManager.updateAgentStatus(AgentConstants.REGISTERED); + break; + } else { + log.error(AgentConstants.LOG_APPENDER + + "Device Registration with IoT Server at:" + " " + + agentManager.getIpRegistrationEP() + + " failed with response - '" + responseCode + ":" + + HttpStatus.getMessage(responseCode) + "'"); + agentManager.updateAgentStatus(AgentConstants.RETRYING_TO_REGISTER); + } + } catch (AgentCoreOperationException exception) { + log.error(AgentConstants.LOG_APPENDER + + "Error encountered whilst trying to register the " + + "Device's IP at: " + + agentManager.getIpRegistrationEP() + + ".\nCheck whether the network-interface provided is " + + "accurate"); + agentManager.updateAgentStatus(AgentConstants.REGISTRATION_FAILED); + } + + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException e1) { + log.error(AgentConstants.LOG_APPENDER + + "Device Registration: Thread Sleep Interrupt Exception"); + } + } + } + }; + + Thread ipRegisterThread = new Thread(ipRegistration); + ipRegisterThread.setDaemon(true); + ipRegisterThread.start(); + } + + + /** + * This method calls the "Register-API" of the IoT Server in order to register the device's IP + * against its ID. + * + * @param deviceOwner the owner of the device by whose name the agent was downloaded. + * (Read from configuration file) + * @param deviceID the deviceId that is auto-generated whilst downloading the agent. + * (Read from configuration file) + * @return the status code of the HTTP-Post call to the Register-API of the IoT-Server + * @throws AgentCoreOperationException if any errors occur when an HTTPConnection session is + * created + */ + private int registerDeviceIP(String deviceOwner, String deviceID) + throws AgentCoreOperationException { + int responseCode = -1; + final AgentManager agentManager = AgentManager.getInstance(); + + String networkInterface = agentManager.getNetworkInterface(); + String deviceIPAddress = getDeviceIP(networkInterface); + + if (deviceIPAddress == null) { + throw new AgentCoreOperationException( + "An IP address could not be retrieved for the selected network interface - '" + + networkInterface + "."); + } + + agentManager.setDeviceIP(deviceIPAddress); + log.info(AgentConstants.LOG_APPENDER + "Device IP Address: " + deviceIPAddress); + + String deviceIPRegistrationEP = agentManager.getIpRegistrationEP(); + String registerEndpointURLString = + deviceIPRegistrationEP + File.separator + deviceOwner + File.separator + deviceID + + File.separator + deviceIPAddress + File.separator + port; + + if (log.isDebugEnabled()) { + log.debug(AgentConstants.LOG_APPENDER + "DeviceIP Registration EndPoint: " + + registerEndpointURLString); + } + + HttpURLConnection httpConnection; + try { + httpConnection = TransportUtils.getHttpConnection(registerEndpointURLString); + } catch (TransportHandlerException e) { + String errorMsg = + "Protocol specific error occurred when trying to fetch an HTTPConnection to:" + + " " + + registerEndpointURLString; + log.error(AgentConstants.LOG_APPENDER + errorMsg); + throw new AgentCoreOperationException(); + } + + try { + httpConnection.setRequestMethod(AgentConstants.HTTP_POST); + httpConnection.setRequestProperty("Authorization", "Bearer " + + agentManager.getAgentConfigs().getAuthToken()); + httpConnection.setDoOutput(true); + responseCode = httpConnection.getResponseCode(); + + } catch (ProtocolException exception) { + String errorMsg = "Protocol specific error occurred when trying to set method to " + + AgentConstants.HTTP_POST + " for:" + registerEndpointURLString; + log.error(AgentConstants.LOG_APPENDER + errorMsg); + throw new AgentCoreOperationException(errorMsg, exception); + + } catch (IOException exception) { + String errorMsg = "An IO error occurred whilst trying to get the response code from:" + + " " + + registerEndpointURLString + " for a " + AgentConstants.HTTP_POST + " method."; + log.error(AgentConstants.LOG_APPENDER + errorMsg); + throw new AgentCoreOperationException(errorMsg, exception); + } + + log.info(AgentConstants.LOG_APPENDER + "DeviceIP - " + deviceIPAddress + + ", registration with IoT Server at : " + + agentManager.getAgentConfigs().getHTTPS_ServerEndpoint() + + " returned status " + + responseCode); + + return responseCode; + } + + /*------------------------------------------------------------------------------------------*/ + /* Utility methods relevant to creating and sending HTTP requests to the Iot-Server */ + /*------------------------------------------------------------------------------------------*/ + + /** + * This method is used to get the IP of the device in which the agent is run on. + * + * @return the IP Address of the device + * @throws AgentCoreOperationException if any errors occur whilst trying to get the IP address + */ + private String getDeviceIP() throws AgentCoreOperationException { + try { + return Inet4Address.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + String errorMsg = "Error encountered whilst trying to get the device IP address."; + log.error(AgentConstants.LOG_APPENDER + errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + } + + /** + * This is an overloaded method that fetches the public IPv4 address of the given network + * interface + * + * @param networkInterfaceName the network-interface of whose IPv4 address is to be retrieved + * @return the IP Address iof the device + * @throws AgentCoreOperationException if any errors occur whilst trying to get details of the + * given network interface + */ + private String getDeviceIP(String networkInterfaceName) throws + AgentCoreOperationException { + String ipAddress = null; + try { + Enumeration interfaceIPAddresses = NetworkInterface.getByName( + networkInterfaceName).getInetAddresses(); + for (; interfaceIPAddresses.hasMoreElements(); ) { + InetAddress ip = interfaceIPAddresses.nextElement(); + ipAddress = ip.getHostAddress(); + if (log.isDebugEnabled()) { + log.debug(AgentConstants.LOG_APPENDER + "IP Address: " + ipAddress); + } + + if (TransportUtils.validateIPv4(ipAddress)) { + return ipAddress; + } + } + } catch (SocketException | NullPointerException exception) { + String errorMsg = + "Error encountered whilst trying to get IP Addresses of the network interface: " + + networkInterfaceName + + ".\nPlease check whether the name of the network interface used is correct"; + log.error(AgentConstants.LOG_APPENDER + errorMsg); + throw new AgentCoreOperationException(errorMsg, exception); + } + + return ipAddress; + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/communication/mqtt/FireAlarmMQTTCommunicator.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/communication/mqtt/FireAlarmMQTTCommunicator.java new file mode 100644 index 000000000..50e7f384e --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/communication/mqtt/FireAlarmMQTTCommunicator.java @@ -0,0 +1,294 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.advanced.communication.mqtt; + +import com.google.gson.Gson; +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.device.mgt.iot.virtualfirealarm.agent.advanced.core.AgentConstants; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.core.AgentManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.core.AgentUtilOperations; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.exception.AgentCoreOperationException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.transport.TransportHandlerException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.transport.mqtt.MQTTTransportHandler; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class FireAlarmMQTTCommunicator extends MQTTTransportHandler { + + private static final Log log = LogFactory.getLog(FireAlarmMQTTCommunicator.class); + private static final Gson gson = new Gson(); + + private ScheduledExecutorService service = Executors.newScheduledThreadPool(2); + private ScheduledFuture dataPushServiceHandler; + + public FireAlarmMQTTCommunicator(String deviceOwner, String deviceType, + String mqttBrokerEndPoint, String subscribeTopic) { + super(deviceOwner, deviceType, mqttBrokerEndPoint, subscribeTopic); + } + + @SuppressWarnings("unused") + public FireAlarmMQTTCommunicator(String deviceOwner, String deviceType, + String mqttBrokerEndPoint, String subscribeTopic, + int intervalInMillis) { + super(deviceOwner, deviceType, mqttBrokerEndPoint, subscribeTopic, intervalInMillis); + } + + public ScheduledFuture getDataPushServiceHandler() { + return dataPushServiceHandler; + } + + //TODO:: Terminate logs with a period + //TODO: Need to print exceptions + @Override + public void connect() { + final AgentManager agentManager = AgentManager.getInstance(); + Runnable connector = new Runnable() { + public void run() { + while (!isConnected()) { + try { + connectToQueue(); + agentManager.updateAgentStatus("Connected to MQTT Queue"); + } catch (TransportHandlerException e) { + log.warn(AgentConstants.LOG_APPENDER + "Connection to MQTT Broker at: " + mqttBrokerEndPoint + + " failed.\n Will retry in " + timeoutInterval + " milli-seconds."); + } + + try { + subscribeToQueue(); + agentManager.updateAgentStatus("Subscribed to MQTT Queue"); + publishDeviceData(); + + } catch (TransportHandlerException e) { + log.warn(AgentConstants.LOG_APPENDER + "Subscription to MQTT Broker at: " + + mqttBrokerEndPoint + " failed"); + agentManager.updateAgentStatus("Subscription to broker failed."); + } + + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException ex) { + log.error(AgentConstants.LOG_APPENDER + "MQTT: Connect-Thread Sleep Interrupt Exception."); + } + } + } + }; + + Thread connectorThread = new Thread(connector); + connectorThread.setDaemon(true); + connectorThread.start(); + } + + + @Override + public void processIncomingMessage(MqttMessage message, String... messageParams) { + final AgentManager agentManager = AgentManager.getInstance(); + String serverName = agentManager.getAgentConfigs().getServerName(); + String deviceOwner = agentManager.getAgentConfigs().getDeviceOwner(); + String deviceID = agentManager.getAgentConfigs().getDeviceId(); + String receivedMessage; + String replyMessage; + String securePayLoad; + + try { + receivedMessage = message.toString(); + if(!receivedMessage.contains("POLICY")){ + receivedMessage = AgentUtilOperations.extractMessageFromPayload(receivedMessage); + } + log.info(AgentConstants.LOG_APPENDER + "Message [" + receivedMessage + "] was received"); + } catch (AgentCoreOperationException e) { + log.warn(AgentConstants.LOG_APPENDER + "Could not extract message from payload.", e); + return; + } + + + String[] controlSignal = receivedMessage.split(":"); + // message- ":" format.(ex: "BULB:ON", "TEMPERATURE", "HUMIDITY") + + try { + switch (controlSignal[0].toUpperCase()) { + case AgentConstants.BULB_CONTROL: + boolean stateToSwitch = controlSignal[1].equals(AgentConstants.CONTROL_ON); + agentManager.changeAlarmStatus(stateToSwitch); + log.info( + AgentConstants.LOG_APPENDER + "Bulb was switched to state: '" + controlSignal[1] + "'"); + break; + + case AgentConstants.TEMPERATURE_CONTROL: + int currentTemperature = agentManager.getTemperature(); + + String replyTemperature = "Current temperature was read as: '" + currentTemperature + "C'"; + log.info(AgentConstants.LOG_APPENDER + replyTemperature); + + String tempPublishTopic = String.format(AgentConstants.MQTT_PUBLISH_TOPIC, + serverName, deviceOwner, deviceID); + + replyMessage = AgentConstants.TEMPERATURE_CONTROL + ":" + currentTemperature; + securePayLoad = AgentUtilOperations.prepareSecurePayLoad(replyMessage); + publishToQueue(tempPublishTopic, securePayLoad); + break; + + case AgentConstants.HUMIDITY_CONTROL: + int currentHumidity = agentManager.getHumidity(); + + String replyHumidity = "Current humidity was read as: '" + currentHumidity + "%'"; + log.info(AgentConstants.LOG_APPENDER + replyHumidity); + + String humidPublishTopic = String.format( + AgentConstants.MQTT_PUBLISH_TOPIC, serverName, deviceOwner, deviceID); + + replyMessage = AgentConstants.HUMIDITY_CONTROL + ":" + currentHumidity; + securePayLoad = AgentUtilOperations.prepareSecurePayLoad(replyMessage); + publishToQueue(humidPublishTopic, securePayLoad); + break; + + case AgentConstants.POLICY_SIGNAL: + String policy = controlSignal[1]; + updateCEPPolicy(policy); + + default: + log.warn(AgentConstants.LOG_APPENDER + "'" + controlSignal[0] + + "' is invalid and not-supported for this device-type"); + break; + } + } catch (AgentCoreOperationException e) { + log.warn(AgentConstants.LOG_APPENDER + "Preparing Secure payload failed", e); + } catch (TransportHandlerException e) { + log.error(AgentConstants.LOG_APPENDER + + "MQTT - Publishing, reply message to the MQTT Queue at: " + + agentManager.getAgentConfigs().getMqttBrokerEndpoint() + " failed"); + } + + } + + @Override + public void publishDeviceData() { + final AgentManager agentManager = AgentManager.getInstance(); + int publishInterval = agentManager.getPushInterval(); + Runnable pushDataRunnable = new Runnable() { + @Override + public void run() { + int currentTemperature = agentManager.getTemperature(); + String message = "PUBLISHER:" + AgentConstants.TEMPERATURE_CONTROL + ":" + currentTemperature; + + try { + String payLoad = AgentUtilOperations.prepareSecurePayLoad(message); + + MqttMessage pushMessage = new MqttMessage(); + pushMessage.setPayload(payLoad.getBytes(StandardCharsets.UTF_8)); + pushMessage.setQos(DEFAULT_MQTT_QUALITY_OF_SERVICE); + pushMessage.setRetained(false); + + String topic = String.format(AgentConstants.MQTT_PUBLISH_TOPIC, + agentManager.getAgentConfigs().getServerName(), + agentManager.getAgentConfigs().getDeviceOwner(), + agentManager.getAgentConfigs().getDeviceId()); + + publishToQueue(topic, pushMessage); + log.info(AgentConstants.LOG_APPENDER + "Message: '" + message + "' published to MQTT Queue at [" + + agentManager.getAgentConfigs().getMqttBrokerEndpoint() + "] under topic [" + + topic + "]"); + + } catch (TransportHandlerException e) { + log.warn(AgentConstants.LOG_APPENDER + "Data Publish attempt to topic - [" + + AgentConstants.MQTT_PUBLISH_TOPIC + "] failed for payload [" + message + "]"); + } catch (AgentCoreOperationException e) { + log.warn(AgentConstants.LOG_APPENDER + "Preparing Secure payload failed", e); + } + } + }; + + dataPushServiceHandler = service.scheduleAtFixedRate(pushDataRunnable, publishInterval, publishInterval, + TimeUnit.SECONDS); + } + + + @Override + public void disconnect() { + Runnable stopConnection = new Runnable() { + public void run() { + while (isConnected()) { + + if (dataPushServiceHandler != null) { + dataPushServiceHandler.cancel(true); + } + + try { + closeConnection(); + + } catch (MqttException e) { + if (log.isDebugEnabled()) { + log.warn(AgentConstants.LOG_APPENDER + + "Unable to 'STOP' MQTT connection at broker at: " + + mqttBrokerEndPoint); + } + + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException e1) { + log.error(AgentConstants.LOG_APPENDER + + "MQTT-Terminator: Thread Sleep Interrupt Exception"); + } + } + } + } + }; + + Thread terminatorThread = new Thread(stopConnection); + terminatorThread.setDaemon(true); + terminatorThread.start(); + } + + @Override + public void processIncomingMessage() { + + } + + @Override + public void publishDeviceData(String... publishData) { + + } + + private boolean isJSONValid(String JSON_STRING) { + try { + gson.fromJson(JSON_STRING, Object.class); + return true; + } catch (com.google.gson.JsonSyntaxException ex) { + return false; + } + } + + + private void updateCEPPolicy(String message) { + AgentManager agentManager = AgentManager.getInstance(); + System.out.println(" Message : " + message); + String fileLocation = agentManager.getRootPath() + AgentConstants.CEP_FILE_NAME; + message = AgentUtilOperations.formatMessage(message); + AgentUtilOperations.writeToFile(message, fileLocation); + agentManager.addToPolicyLog(message); + } + + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/communication/xmpp/FireAlarmXMPPCommunicator.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/communication/xmpp/FireAlarmXMPPCommunicator.java new file mode 100644 index 000000000..55c3cf1d0 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/communication/xmpp/FireAlarmXMPPCommunicator.java @@ -0,0 +1,265 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.advanced.communication.xmpp; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jivesoftware.smack.packet.Message; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.core.AgentConstants; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.core.AgentManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.core.AgentUtilOperations; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.exception.AgentCoreOperationException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.transport.TransportHandlerException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.transport.xmpp.XMPPTransportHandler; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class FireAlarmXMPPCommunicator extends XMPPTransportHandler { + + private static final Log log = LogFactory.getLog(FireAlarmXMPPCommunicator.class); + + private ScheduledExecutorService service = Executors.newScheduledThreadPool(2); + private ScheduledFuture dataPushServiceHandler; + private ScheduledFuture connectorServiceHandler; + + private String username; + private String password; + private String resource; + private String xmppAdminJID; + private String xmppDeviceJID; + + public FireAlarmXMPPCommunicator(String server) { + super(server); + } + + public FireAlarmXMPPCommunicator(String server, int port) { + super(server, port); + } + + public FireAlarmXMPPCommunicator(String server, int port, int timeout) { + super(server, port, timeout); + } + + public ScheduledFuture getDataPushServiceHandler() { + return dataPushServiceHandler; + } + + @Override + public void connect() { + final AgentManager agentManager = AgentManager.getInstance(); + username = agentManager.getAgentConfigs().getDeviceId(); + password = agentManager.getAgentConfigs().getAuthToken(); + resource = agentManager.getAgentConfigs().getDeviceOwner(); + + xmppDeviceJID = username + "@" + server; + xmppAdminJID = agentManager.getAgentConfigs().getServerName() + "_" + AgentConstants.DEVICE_TYPE + "@" + server; + + + Runnable connect = new Runnable() { + public void run() { + if (!isConnected()) { + try { + connectToServer(); + } catch (TransportHandlerException e) { + log.warn(AgentConstants.LOG_APPENDER + "Connection to XMPP server at: " + server + " failed"); + } + + try { + loginToServer(username, password, resource); + agentManager.updateAgentStatus("Connected to XMPP Server"); + setMessageFilterAndListener(xmppAdminJID, xmppDeviceJID, true); + publishDeviceData(); + } catch (TransportHandlerException e) { + log.warn(AgentConstants.LOG_APPENDER + "Login to XMPP server at: " + server + " failed"); + agentManager.updateAgentStatus("No XMPP Account for Device"); + } + } + } + }; + + connectorServiceHandler = service.scheduleAtFixedRate(connect, 0, timeoutInterval, TimeUnit.MILLISECONDS); + } + + /** + * This is an abstract method used for post processing the received XMPP-message. This + * method will be implemented as per requirement at the time of creating an object of this + * class. + * + * @param xmppMessage the xmpp message received by the listener. + */ + @Override + public void processIncomingMessage(Message xmppMessage, String... messageParams) { + final AgentManager agentManager = AgentManager.getInstance(); + String from = xmppMessage.getFrom(); + String message = xmppMessage.getBody(); + String receivedMessage; + String replyMessage; + String securePayLoad; + + try { + receivedMessage = AgentUtilOperations.extractMessageFromPayload(message); + log.info(AgentConstants.LOG_APPENDER + "Message [" + receivedMessage + "] was received"); + } catch (AgentCoreOperationException e) { + log.warn(AgentConstants.LOG_APPENDER + "Could not extract message from payload.", e); + return; + } + + String[] controlSignal = receivedMessage.split(":"); + //message- ":" format. (ex: "BULB:ON", "TEMPERATURE", "HUMIDITY") + + try { + switch (controlSignal[0].toUpperCase()) { + case AgentConstants.BULB_CONTROL: + if (controlSignal.length != 2) { + replyMessage = "BULB controls need to be in the form - 'BULB:{ON|OFF}'"; + log.warn(replyMessage); + securePayLoad = AgentUtilOperations.prepareSecurePayLoad(replyMessage); + sendXMPPMessage(xmppAdminJID, securePayLoad, "CONTROL-REPLY"); + break; + } + + agentManager.changeAlarmStatus(controlSignal[1].equals(AgentConstants.CONTROL_ON)); + log.info(AgentConstants.LOG_APPENDER + "Bulb was switched to state: '" + controlSignal[1] + "'"); + break; + + case AgentConstants.TEMPERATURE_CONTROL: + int currentTemperature = agentManager.getTemperature(); + + String replyTemperature = + "The current temperature was read to be: '" + currentTemperature + + "C'"; + log.info(AgentConstants.LOG_APPENDER + replyTemperature); + + replyMessage = AgentConstants.TEMPERATURE_CONTROL + ":" + currentTemperature; + securePayLoad = AgentUtilOperations.prepareSecurePayLoad(replyMessage); + sendXMPPMessage(xmppAdminJID, securePayLoad, "CONTROL-REPLY"); + break; + + case AgentConstants.HUMIDITY_CONTROL: + int currentHumidity = agentManager.getHumidity(); + + String replyHumidity = "The current humidity was read to be: '" + currentHumidity + "%'"; + log.info(AgentConstants.LOG_APPENDER + replyHumidity); + + replyMessage = AgentConstants.HUMIDITY_CONTROL + ":" + currentHumidity; + securePayLoad = AgentUtilOperations.prepareSecurePayLoad(replyMessage); + sendXMPPMessage(xmppAdminJID, securePayLoad, "CONTROL-REPLY"); + break; + + default: + replyMessage = "'" + controlSignal[0] + "' is invalid and not-supported for this device-type"; + log.warn(replyMessage); + securePayLoad = AgentUtilOperations.prepareSecurePayLoad(replyMessage); + sendXMPPMessage(xmppAdminJID, securePayLoad, "CONTROL-ERROR"); + break; + } + } catch (AgentCoreOperationException e) { + log.warn(AgentConstants.LOG_APPENDER + "Preparing Secure payload failed", e); + } + + } + + + @Override + public void publishDeviceData() { + final AgentManager agentManager = AgentManager.getInstance(); + int publishInterval = agentManager.getPushInterval(); + + Runnable pushDataRunnable = new Runnable() { + @Override + public void run() { + Message xmppMessage = new Message(); + + try { + int currentTemperature = agentManager.getTemperature(); + + String message = AgentConstants.TEMPERATURE_CONTROL + ":" + currentTemperature; + String payLoad = AgentUtilOperations.prepareSecurePayLoad(message); + + xmppMessage.setTo(xmppAdminJID); + xmppMessage.setSubject("PUBLISHER"); + xmppMessage.setBody(payLoad); + xmppMessage.setType(Message.Type.chat); + + sendXMPPMessage(xmppAdminJID, xmppMessage); + log.info(AgentConstants.LOG_APPENDER + "Message: '" + message + "' sent to XMPP JID - " + + "[" + xmppAdminJID + "] under subject [" + xmppMessage.getSubject() + "]."); + } catch (AgentCoreOperationException e) { + log.warn(AgentConstants.LOG_APPENDER + "Preparing Secure payload failed for XMPP JID - " + + "[" + xmppAdminJID + "] with subject - [" + xmppMessage.getSubject() + "]."); + } + } + }; + + dataPushServiceHandler = service.scheduleAtFixedRate(pushDataRunnable, publishInterval, + publishInterval, TimeUnit.SECONDS); + } + + + @Override + public void disconnect() { + Runnable stopConnection = new Runnable() { + public void run() { + + if (dataPushServiceHandler != null) { + dataPushServiceHandler.cancel(true); + } + + if (connectorServiceHandler != null) { + connectorServiceHandler.cancel(true); + } + + while (isConnected()) { + closeConnection(); + + if (log.isDebugEnabled()) { + log.warn(AgentConstants.LOG_APPENDER + + "Unable to 'STOP' connection to XMPP server at: " + server); + } + + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException e1) { + log.error(AgentConstants.LOG_APPENDER + "XMPP-Terminator: Thread Sleep Interrupt Exception"); + } + + } + } + }; + + Thread terminatorThread = new Thread(stopConnection); + terminatorThread.setDaemon(true); + terminatorThread.start(); + } + + + @Override + public void processIncomingMessage() { + + } + + @Override + public void publishDeviceData(String... publishData) { + + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/core/AgentConfiguration.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/core/AgentConfiguration.java new file mode 100644 index 000000000..49e437cc1 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/core/AgentConfiguration.java @@ -0,0 +1,155 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.advanced.core; + +/** + * A Configuration class that holds all the Agent specific details that are read from the + * 'deviceConfig.properties' file. This file is generated by the IoT-Server at the time of + * downloading the device agent from the IoT-Server. + */ +public class AgentConfiguration { + private String serverName; + private String deviceOwner; + private String deviceId; + private String deviceName; + private String controllerContext; + private String HTTPS_ServerEndpoint; + private String HTTP_ServerEndpoint; + private String apimGatewayEndpoint; + private String mqttBrokerEndpoint; + private String xmppServerEndpoint; + private String authMethod; + private String authToken; + private String refreshToken; + private int dataPushInterval; + + public String getServerName() { + return serverName; + } + + public void setServerName(String serverName) { + this.serverName = serverName; + } + + public String getDeviceOwner() { + return deviceOwner; + } + + public void setDeviceOwner(String deviceOwner) { + this.deviceOwner = deviceOwner; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getDeviceName() { + return deviceName; + } + + public void setDeviceName(String deviceName) { + this.deviceName = deviceName; + } + + public String getControllerContext() { + return controllerContext; + } + + public void setControllerContext(String controllerContext) { + this.controllerContext = controllerContext; + } + + public String getHTTPS_ServerEndpoint() { + return HTTPS_ServerEndpoint; + } + + public void setHTTPS_ServerEndpoint(String HTTPS_ServerEndpoint) { + this.HTTPS_ServerEndpoint = HTTPS_ServerEndpoint; + } + + public String getHTTP_ServerEndpoint() { + return HTTP_ServerEndpoint; + } + + public void setHTTP_ServerEndpoint(String HTTP_ServerEndpoint) { + this.HTTP_ServerEndpoint = HTTP_ServerEndpoint; + } + + public String getApimGatewayEndpoint() { + return apimGatewayEndpoint; + } + + public void setApimGatewayEndpoint(String apimGatewayEndpoint) { + this.apimGatewayEndpoint = apimGatewayEndpoint; + } + + public String getMqttBrokerEndpoint() { + return mqttBrokerEndpoint; + } + + public void setMqttBrokerEndpoint(String mqttBrokerEndpoint) { + this.mqttBrokerEndpoint = mqttBrokerEndpoint; + } + + public String getXmppServerEndpoint() { + return xmppServerEndpoint; + } + + public void setXmppServerEndpoint(String xmppServerEndpoint) { + this.xmppServerEndpoint = xmppServerEndpoint; + } + + public String getAuthMethod() { + return authMethod; + } + + public void setAuthMethod(String authMethod) { + this.authMethod = authMethod; + } + + public String getAuthToken() { + return authToken; + } + + public void setAuthToken(String authToken) { + this.authToken = authToken; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public int getDataPushInterval() { + return dataPushInterval; + } + + public void setDataPushInterval(int dataPushInterval) { + this.dataPushInterval = dataPushInterval; + } +} + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/core/AgentConstants.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/core/AgentConstants.java new file mode 100644 index 000000000..038a52240 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/core/AgentConstants.java @@ -0,0 +1,135 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.advanced.core; + +public class AgentConstants { + public static final String DEVICE_TYPE = "virtual_firealarm"; + public static final String LOG_APPENDER = "AGENT_LOG:: "; + public static final String PROPERTIES_FILE_PATH = ""; + public static final int DEFAULT_RETRY_THREAD_INTERVAL = 5000; // time in millis + /* --------------------------------------------------------------------------------------- + IoT-Server specific information + --------------------------------------------------------------------------------------- */ + public static final String DEVICE_CONTROLLER_API_EP = "/virtual_firealarm/controller"; + public static final String DEVICE_ENROLLMENT_API_EP = "/scep"; + public static final String DEVICE_REGISTER_API_EP = "/register"; + public static final String DEVICE_PUSH_TEMPERATURE_API_EP = "/push_temperature"; + public static final String PUSH_DATA_PAYLOAD = + "{\"owner\":\"%s\",\"deviceId\":\"%s\",\"reply\":\"%s\",\"value\":\"%s\"}"; + + public static final String PUSH_SIMULATION_DATA_PAYLOAD = + "{\"owner\":\"%s\",\"deviceId\":\"%s\",\"reply\":\"%s\",\"value\":\"%s\",\"isSimulated\":\"%s\",\"duration\":\"%s\",\"frequency\":\"%s\"}"; + + public static final String AGENT_CONTROL_APP_EP = "/devicemgt/device/%s?id=%s"; + public static final String DEVICE_DETAILS_PAGE_EP = "/devicemgt/device/%s?id=%s"; + public static final String DEVICE_ANALYTICS_PAGE_URL = "/devicemgt/analytics?deviceId=%s&deviceType=%s&deviceName=%s"; + /* --------------------------------------------------------------------------------------- + HTTP Connection specific information for communicating with IoT-Server + --------------------------------------------------------------------------------------- */ + public static final String HTTP_POST = "POST"; + public static final String HTTP_GET = "GET"; + public static final String APPLICATION_JSON_TYPE = "application/json"; + public static final String REGISTERED = "Registered"; + public static final String NOT_REGISTERED = "Not-Registered"; + public static final String REGISTRATION_FAILED = "Registration Failed"; + public static final String RETRYING_TO_REGISTER = "Registration Failed. Re-trying.."; + public static final String SERVER_NOT_RESPONDING = "Server not responding.."; + + /* --------------------------------------------------------------------------------------- + MQTT Connection specific information + --------------------------------------------------------------------------------------- */ + public static final int DEFAULT_MQTT_RECONNECTION_INTERVAL = 2; // time in seconds + public static final int DEFAULT_MQTT_QUALITY_OF_SERVICE = 0; + public static final String MQTT_SUBSCRIBE_TOPIC = "%s/%s/" + DEVICE_TYPE + "/%s"; + public static final String MQTT_PUBLISH_TOPIC = "%s/%s/" + DEVICE_TYPE + "/%s/publisher"; + /* --------------------------------------------------------------------------------------- + XMPP Connection specific information + --------------------------------------------------------------------------------------- */ + public static final String XMPP_ADMIN_ACCOUNT_UNAME = "admin"; + /* --------------------------------------------------------------------------------------- + Device/Agent specific properties to be read from the 'deviceConfig.properties' file + --------------------------------------------------------------------------------------- */ + public static final String AGENT_PROPERTIES_FILE_NAME = "deviceConfig.properties"; + public static final String SERVER_NAME_PROPERTY = "server-name"; + public static final String DEVICE_OWNER_PROPERTY = "owner"; + public static final String DEVICE_ID_PROPERTY = "deviceId"; + public static final String DEVICE_NAME_PROPERTY = "device-name"; + public static final String DEVICE_CONTROLLER_CONTEXT_PROPERTY = "controller-context"; + public static final String SERVER_HTTPS_EP_PROPERTY = "https-ep"; + public static final String SERVER_HTTP_EP_PROPERTY = "http-ep"; + public static final String APIM_GATEWAY_EP_PROPERTY = "apim-ep"; + public static final String MQTT_BROKER_EP_PROPERTY = "mqtt-ep"; + public static final String XMPP_SERVER_EP_PROPERTY = "xmpp-ep"; + public static final String AUTH_METHOD_PROPERTY = "auth-method"; + public static final String AUTH_TOKEN_PROPERTY = "auth-token"; + public static final String REFRESH_TOKEN_PROPERTY = "refresh-token"; + public static final String NETWORK_INTERFACE_PROPERTY = "network-interface"; + public static final String PUSH_INTERVAL_PROPERTY = "push-interval"; + /* --------------------------------------------------------------------------------------- + Default values for the Device/Agent specific configurations listed above + --------------------------------------------------------------------------------------- */ + public static final String DEFAULT_SERVER_NAME = "WSO2IoTServer"; + public static final String DEFAULT_DEVICE_OWNER = "admin"; + public static final String DEFAULT_DEVICE_ID = "1234567890"; + public static final String DEFAULT_DEVICE_NAME = "admin_1234567890"; + public static final String DEFAULT_HTTPS_SERVER_EP = "https://localhost:9443"; + public static final String DEFAULT_HTTP_SERVER_EP = "http://localhost:9763"; + public static final String DEFAULT_APIM_GATEWAY_EP = "http://127.0.0.1:8281"; + public static final String DEFAULT_MQTT_BROKER_EP = "tcp://127.0.0.1:1883"; + public static final String DEFAULT_XMPP_SERVER_EP = "http://127.0.0.1:9061"; + public static final String DEFAULT_AUTH_METHOD = "token"; + public static final String DEFAULT_AUTH_TOKEN = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0987654321"; + public static final String DEFAULT_REFRESH_TOKEN = "1234567890ZYXWVUTSRQPONMKLJIHGFEDCBA"; + public static final String DEFAULT_NETWORK_INTERFACE = "en0"; + public static final int DEFAULT_DATA_PUBLISH_INTERVAL = 15; // seconds + public static final String DEFAULT_PROTOCOL = "MQTT"; + /* --------------------------------------------------------------------------------------- + Control Signal specific constants to match the request context + --------------------------------------------------------------------------------------- */ + public static final String BULB_CONTROL = "BULB"; + public static final String TEMPERATURE_CONTROL = "TEMPERATURE"; + public static final String POLICY_SIGNAL = "POLICY"; + public static final String HUMIDITY_CONTROL = "HUMIDITY"; + public static final String CONTROL_ON = "ON"; + public static final String CONTROL_OFF = "OFF"; + public static final String AUDIO_FILE_NAME = "fireAlarmSound.mid"; + /* --------------------------------------------------------------------------------------- + Communication protocol specific Strings + --------------------------------------------------------------------------------------- */ + public static final String TCP_PREFIX = "tcp://"; + public static final String HTTP_PREFIX = "http://"; + public static final String HTTPS_PREFIX = "https://"; + public static final String HTTP_PROTOCOL = "HTTP"; + public static final String MQTT_PROTOCOL = "MQTT"; + public static final String XMPP_PROTOCOL = "XMPP"; + + public static final String CEP_FILE_NAME = "cep_query.txt"; + public static final String CEP_QUERY = "define stream fireAlarmEventStream (deviceID string, temp int);\n" + + "from fireAlarmEventStream#window.time(30 sec)\n" + + "select deviceID, max(temp) as maxValue\n" + + "group by deviceID\n" + + "insert into analyzeStream for expired-events;\n" + + "from analyzeStream[maxValue > 50]\n" + + "select maxValue\n" + + "insert into bulbOnStream;\n" + + "from fireAlarmEventStream[temp < 50]\n" + + "select deviceID, temp\n" + + "insert into bulbOffStream;"; + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/core/AgentManager.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/core/AgentManager.java new file mode 100644 index 000000000..aba3809c3 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/core/AgentManager.java @@ -0,0 +1,377 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.advanced.core; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.communication.http.FireAlarmHTTPCommunicator; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.communication.mqtt.FireAlarmMQTTCommunicator; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.communication.xmpp.FireAlarmXMPPCommunicator; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.enrollment.EnrollmentManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.exception.AgentCoreOperationException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.sidhdhi.SidhdhiQuery; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.transport.TransportHandler; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.transport.TransportHandlerException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.transport.TransportUtils; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.virtual.VirtualHardwareManager; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AgentManager { + + private static final Log log = LogFactory.getLog(AgentManager.class); + private static final Object lock = new Object(); + private static AgentManager agentManager; + private static Boolean policyUpdated = false; + private String rootPath = ""; + private boolean deviceReady = false; + private boolean isAlarmOn = false; + private String initialPolicy; + + private String deviceMgtControlUrl, deviceMgtAnalyticUrl; + private String deviceName, agentStatus; + + private int pushInterval; // seconds + private String prevProtocol, protocol; + + private String networkInterface; + private List interfaceList, protocolList; + private Map agentCommunicator; + + private AgentConfiguration agentConfigs; + + private String deviceIP; + private String enrollmentEP; + private String ipRegistrationEP; + private String pushDataAPIEP; + + private AgentManager() { + } + + public static synchronized AgentManager getInstance() { + if (agentManager == null) { + agentManager = new AgentManager(); + } + return agentManager; + } + + public static void setUpdated(Boolean isUpdated) { + synchronized (lock) { + policyUpdated = isUpdated; + } + } + + public static Boolean isUpdated() { + synchronized (lock) { + Boolean temp = policyUpdated; + policyUpdated = false; + return temp; + } + } + + public void init() { + + agentCommunicator = new HashMap<>(); + + // Read IoT-Server specific configurations from the 'deviceConfig.properties' file + this.agentConfigs = AgentUtilOperations.readIoTServerConfigs(); + + // Initialise IoT-Server URL endpoints from the configuration read from file + AgentUtilOperations.initializeServerEndPoints(); + + String analyticsPageContext = String.format(AgentConstants.DEVICE_ANALYTICS_PAGE_URL, + agentConfigs.getDeviceId(), + AgentConstants.DEVICE_TYPE, + agentConfigs.getDeviceName()); + + String controlPageContext = String.format(AgentConstants.DEVICE_DETAILS_PAGE_EP, + AgentConstants.DEVICE_TYPE, + agentConfigs.getDeviceId()); + + this.deviceMgtAnalyticUrl = agentConfigs.getHTTPS_ServerEndpoint() + analyticsPageContext; + this.deviceMgtControlUrl = agentConfigs.getHTTPS_ServerEndpoint() + controlPageContext; + + this.agentStatus = AgentConstants.NOT_REGISTERED; + this.deviceName = this.agentConfigs.getDeviceName(); + + this.pushInterval = this.agentConfigs.getDataPushInterval(); + this.networkInterface = AgentConstants.DEFAULT_NETWORK_INTERFACE; + + this.protocol = AgentConstants.DEFAULT_PROTOCOL; + this.prevProtocol = protocol; + + Map xmppIPPortMap; + try { + xmppIPPortMap = TransportUtils.getHostAndPort(agentConfigs.getXmppServerEndpoint()); + String xmppServer = xmppIPPortMap.get("Host"); + int xmppPort = Integer.parseInt(xmppIPPortMap.get("Port")); + + TransportHandler xmppCommunicator = new FireAlarmXMPPCommunicator(xmppServer, xmppPort); + agentCommunicator.put(AgentConstants.XMPP_PROTOCOL, xmppCommunicator); + + } catch (TransportHandlerException e) { + log.error("XMPP Endpoint String - " + agentConfigs.getXmppServerEndpoint() + + ", provided in the configuration file is invalid."); + } + String mqttTopic = String.format(AgentConstants.MQTT_SUBSCRIBE_TOPIC, + agentConfigs.getServerName(), + agentConfigs.getDeviceOwner(), + agentConfigs.getDeviceId()); + +// TransportHandler httpCommunicator = new FireAlarmHTTPCommunicator(); + TransportHandler mqttCommunicator = new FireAlarmMQTTCommunicator(agentConfigs.getDeviceOwner(), + agentConfigs.getDeviceId(), + agentConfigs.getMqttBrokerEndpoint(), + mqttTopic); + +// agentCommunicator.put(AgentConstants.HTTP_PROTOCOL, httpCommunicator); + agentCommunicator.put(AgentConstants.MQTT_PROTOCOL, mqttCommunicator); + + try { + interfaceList = new ArrayList<>(TransportUtils.getInterfaceIPMap().keySet()); + protocolList = new ArrayList<>(agentCommunicator.keySet()); + } catch (TransportHandlerException e) { + log.error("An error occurred whilst retrieving all NetworkInterface-IP mappings"); + } + + String siddhiQueryFilePath = rootPath + AgentConstants.CEP_FILE_NAME; + (new Thread(new SidhdhiQuery())).start(); + initialPolicy = SidhdhiQuery.readFile(siddhiQueryFilePath, StandardCharsets.UTF_8); + + //Initializing hardware at that point + //AgentManger.setDeviceReady() method should invoked from hardware after initialization + VirtualHardwareManager.getInstance().init(); + + //Wait till hardware get ready + while (!deviceReady) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + log.info(AgentConstants.LOG_APPENDER + "Sleep error in 'device ready-flag' checking thread"); + } + } + + try { + EnrollmentManager.getInstance().beginEnrollmentFlow(); + } catch (AgentCoreOperationException e) { + log.error("Device Enrollment Failed:\n"); + e.printStackTrace(); + System.exit(0); + } + + //Start agent communication + agentCommunicator.get(protocol).connect(); + } + + private void switchCommunicator(String stopProtocol, String startProtocol) { + agentCommunicator.get(stopProtocol).disconnect(); + + while (agentCommunicator.get(stopProtocol).isConnected()) { + try { + Thread.sleep(250); + } catch (InterruptedException e) { + log.info(AgentConstants.LOG_APPENDER + + "Sleep error in 'Switch-Communicator' Thread's shutdown wait."); + } + } + + agentCommunicator.get(startProtocol).connect(); + } + + public void setInterface(int interfaceId) { + if (interfaceId != -1) { + String newInterface = interfaceList.get(interfaceId); + + if (!newInterface.equals(networkInterface)) { + networkInterface = newInterface; + + if (protocol.equals(AgentConstants.HTTP_PROTOCOL) && !protocol.equals( + prevProtocol)) { + switchCommunicator(protocol, protocol); + } + } + } + } + + public void setProtocol(int protocolId) { + if (protocolId != -1) { + String newProtocol = protocolList.get(protocolId); + + if (!protocol.equals(newProtocol)) { + prevProtocol = protocol; + protocol = newProtocol; + switchCommunicator(prevProtocol, protocol); + } + } + } + + public void changeAlarmStatus(boolean isOn) { + VirtualHardwareManager.getInstance().changeAlarmStatus(isOn); + isAlarmOn = isOn; + } + + public void updateAgentStatus(String status) { + this.agentStatus = status; + } + + public void addToPolicyLog(String policy) { + VirtualHardwareManager.getInstance().addToPolicyLog(policy); + } + + public String getRootPath() { + return rootPath; + } + + /*------------------------------------------------------------------------------------------*/ + /* Getter and Setter Methods for the private variables */ + /*------------------------------------------------------------------------------------------*/ + + public void setRootPath(String rootPath) { + this.rootPath = rootPath; + } + + public void setDeviceReady(boolean deviceReady) { + this.deviceReady = deviceReady; + } + + public String getInitialPolicy() { + return initialPolicy; + } + + public String getDeviceMgtControlUrl() { + return deviceMgtControlUrl; + } + + public String getDeviceMgtAnalyticUrl() { + return deviceMgtAnalyticUrl; + } + + public AgentConfiguration getAgentConfigs() { + return agentConfigs; + } + + public String getDeviceIP() { + return deviceIP; + } + + public void setDeviceIP(String deviceIP) { + this.deviceIP = deviceIP; + } + + public String getEnrollmentEP() { + return enrollmentEP; + } + + public void setEnrollmentEP(String enrollmentEP) { + this.enrollmentEP = enrollmentEP; + } + + public String getIpRegistrationEP() { + return ipRegistrationEP; + } + + public void setIpRegistrationEP(String ipRegistrationEP) { + this.ipRegistrationEP = ipRegistrationEP; + } + + public String getPushDataAPIEP() { + return pushDataAPIEP; + } + + public void setPushDataAPIEP(String pushDataAPIEP) { + this.pushDataAPIEP = pushDataAPIEP; + } + + public String getDeviceName() { + return deviceName; + } + + public String getNetworkInterface() { + return networkInterface; + } + + public String getAgentStatus() { + return agentStatus; + } + + public int getPushInterval() { + return pushInterval; + } + + public void setPushInterval(int pushInterval) { + this.pushInterval = pushInterval; + TransportHandler transportHandler = agentCommunicator.get(protocol); + + switch (protocol) { + case AgentConstants.HTTP_PROTOCOL: + ((FireAlarmHTTPCommunicator) transportHandler).getDataPushServiceHandler() + .cancel(true); + break; + case AgentConstants.MQTT_PROTOCOL: + ((FireAlarmMQTTCommunicator) transportHandler).getDataPushServiceHandler() + .cancel(true); + break; + case AgentConstants.XMPP_PROTOCOL: + ((FireAlarmXMPPCommunicator) transportHandler).getDataPushServiceHandler() + .cancel(true); + break; + default: + log.warn("Unknown protocol " + protocol); + } + transportHandler.publishDeviceData(); + + if (log.isDebugEnabled()) { + log.debug("The Data Publish Interval was changed to: " + pushInterval); + } + } + + public List getInterfaceList() { + return interfaceList; + } + + public List getProtocolList() { + return protocolList; + } + + /** + * Get temperature reading from device + * + * @return Temperature + */ + public int getTemperature() { + return VirtualHardwareManager.getInstance().getTemperature(); + } + + /** + * Get humidity reading from device + * + * @return Humidity + */ + public int getHumidity() { + return VirtualHardwareManager.getInstance().getHumidity(); + } + + public boolean isAlarmOn() { + return isAlarmOn; + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/core/AgentUtilOperations.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/core/AgentUtilOperations.java new file mode 100644 index 000000000..cb2dbf4d1 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/core/AgentUtilOperations.java @@ -0,0 +1,371 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.advanced.core; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONObject; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.enrollment.EnrollmentManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.exception.AgentCoreOperationException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.transport.CommunicationUtils; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.transport.TransportHandlerException; + +import java.io.*; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Properties; + +/** + * This class contains all the core operations of the FireAlarm agent that are common to both + * Virtual and Real Scenarios. These operations include, connecting to and subscribing to an MQTT + * queue and to a XMPP Server. Pushing temperature data to the IoT-Server at timely intervals. + * Reading device specific configuration from a configs file etc.... + */ +public class AgentUtilOperations { + + private static final Log log = LogFactory.getLog(AgentUtilOperations.class); + private static final String JSON_MESSAGE_KEY = "Msg"; + private static final String JSON_SIGNATURE_KEY = "Sig"; + + /** + * This method reads the agent specific configurations for the device from the + * "deviceConfigs.properties" file found at /repository/conf folder. + * If the properties file is not found in the specified path, then the configuration values + * are set to the default ones in the 'AgentConstants' class. + * + * @return an object of type 'AgentConfiguration' which contains all the necessary + * configuration attributes + */ + public static AgentConfiguration readIoTServerConfigs() { + AgentManager agentManager = AgentManager.getInstance(); + AgentConfiguration iotServerConfigs = new AgentConfiguration(); + Properties properties = new Properties(); + InputStream propertiesInputStream = null; + String propertiesFileName = AgentConstants.AGENT_PROPERTIES_FILE_NAME; + + try { + ClassLoader loader = AgentUtilOperations.class.getClassLoader(); + URL path = loader.getResource(propertiesFileName); + System.out.println(path); + String root = path.getPath().replace( + "wso2-firealarm-virtual-agent-advanced.jar!/deviceConfig.properties", + "").replace("jar:", "").replace("file:", ""); + + agentManager.setRootPath(root); + + propertiesInputStream = new FileInputStream( + root + AgentConstants.AGENT_PROPERTIES_FILE_NAME); + + //load a properties file from class path, inside static method + properties.load(propertiesInputStream); + + iotServerConfigs.setServerName(properties.getProperty( + AgentConstants.SERVER_NAME_PROPERTY)); + iotServerConfigs.setDeviceOwner(properties.getProperty( + AgentConstants.DEVICE_OWNER_PROPERTY)); + iotServerConfigs.setDeviceId(properties.getProperty( + AgentConstants.DEVICE_ID_PROPERTY)); + iotServerConfigs.setDeviceName(properties.getProperty( + AgentConstants.DEVICE_NAME_PROPERTY)); + iotServerConfigs.setControllerContext(properties.getProperty( + AgentConstants.DEVICE_CONTROLLER_CONTEXT_PROPERTY)); + iotServerConfigs.setHTTPS_ServerEndpoint(properties.getProperty( + AgentConstants.SERVER_HTTPS_EP_PROPERTY)); + iotServerConfigs.setHTTP_ServerEndpoint(properties.getProperty( + AgentConstants.SERVER_HTTP_EP_PROPERTY)); + iotServerConfigs.setApimGatewayEndpoint(properties.getProperty( + AgentConstants.APIM_GATEWAY_EP_PROPERTY)); + iotServerConfigs.setMqttBrokerEndpoint(properties.getProperty( + AgentConstants.MQTT_BROKER_EP_PROPERTY)); + iotServerConfigs.setXmppServerEndpoint(properties.getProperty( + AgentConstants.XMPP_SERVER_EP_PROPERTY)); + iotServerConfigs.setAuthMethod(properties.getProperty( + AgentConstants.AUTH_METHOD_PROPERTY)); + iotServerConfigs.setAuthToken(properties.getProperty( + AgentConstants.AUTH_TOKEN_PROPERTY)); + iotServerConfigs.setRefreshToken(properties.getProperty( + AgentConstants.REFRESH_TOKEN_PROPERTY)); + iotServerConfigs.setDataPushInterval(Integer.parseInt(properties.getProperty( + AgentConstants.PUSH_INTERVAL_PROPERTY))); + + log.info(AgentConstants.LOG_APPENDER + "Server name: " + + iotServerConfigs.getServerName()); + log.info(AgentConstants.LOG_APPENDER + "Device Owner: " + + iotServerConfigs.getDeviceOwner()); + log.info(AgentConstants.LOG_APPENDER + "Device ID: " + iotServerConfigs.getDeviceId()); + log.info(AgentConstants.LOG_APPENDER + "Device Name: " + + iotServerConfigs.getDeviceName()); + log.info(AgentConstants.LOG_APPENDER + "Device Controller Context: " + + iotServerConfigs.getControllerContext()); + log.info(AgentConstants.LOG_APPENDER + "IoT Server HTTPS EndPoint: " + + iotServerConfigs.getHTTPS_ServerEndpoint()); + log.info(AgentConstants.LOG_APPENDER + "IoT Server HTTP EndPoint: " + + iotServerConfigs.getHTTP_ServerEndpoint()); + log.info(AgentConstants.LOG_APPENDER + "API-Manager Gateway EndPoint: " + + iotServerConfigs.getApimGatewayEndpoint()); + log.info(AgentConstants.LOG_APPENDER + "MQTT Broker EndPoint: " + + iotServerConfigs.getMqttBrokerEndpoint()); + log.info(AgentConstants.LOG_APPENDER + "XMPP Server EndPoint: " + + iotServerConfigs.getXmppServerEndpoint()); + log.info(AgentConstants.LOG_APPENDER + "Authentication Method: " + + iotServerConfigs.getAuthMethod()); + log.info(AgentConstants.LOG_APPENDER + "Authentication Token: " + + iotServerConfigs.getAuthToken()); + log.info(AgentConstants.LOG_APPENDER + "Refresh Token: " + + iotServerConfigs.getRefreshToken()); + log.info(AgentConstants.LOG_APPENDER + "Data Push Interval: " + + iotServerConfigs.getDataPushInterval()); + + } catch (FileNotFoundException ex) { + log.error(AgentConstants.LOG_APPENDER + "Unable to find " + propertiesFileName + + " file at: " + AgentConstants.PROPERTIES_FILE_PATH); + iotServerConfigs = setDefaultDeviceConfigs(); + + } catch (IOException ex) { + log.error(AgentConstants.LOG_APPENDER + "Error occurred whilst trying to fetch '" + + propertiesFileName + "' from: " + + AgentConstants.PROPERTIES_FILE_PATH); + iotServerConfigs = setDefaultDeviceConfigs(); + + } finally { + if (propertiesInputStream != null) { + try { + propertiesInputStream.close(); + } catch (IOException e) { + log.error(AgentConstants.LOG_APPENDER + + "Error occurred whilst trying to close InputStream resource used to read the '" + + propertiesFileName + "' file"); + } + } + } + return iotServerConfigs; + } + + /** + * Sets the default Device specific configurations listed in the 'AgentConstants' class. + * + * @return an object of AgentConfiguration class including all default device specific configs. + */ + private static AgentConfiguration setDefaultDeviceConfigs() { + log.warn(AgentConstants.LOG_APPENDER + + "Default Values are being set to all Agent specific configurations"); + + AgentConfiguration iotServerConfigs = new AgentConfiguration(); + + iotServerConfigs.setDeviceOwner(AgentConstants.DEFAULT_SERVER_NAME); + iotServerConfigs.setDeviceOwner(AgentConstants.DEFAULT_DEVICE_OWNER); + iotServerConfigs.setDeviceId(AgentConstants.DEFAULT_DEVICE_ID); + iotServerConfigs.setDeviceName(AgentConstants.DEFAULT_DEVICE_NAME); + iotServerConfigs.setControllerContext(AgentConstants.DEVICE_CONTROLLER_API_EP); + iotServerConfigs.setHTTPS_ServerEndpoint(AgentConstants.DEFAULT_HTTPS_SERVER_EP); + iotServerConfigs.setHTTP_ServerEndpoint(AgentConstants.DEFAULT_HTTP_SERVER_EP); + iotServerConfigs.setApimGatewayEndpoint(AgentConstants.DEFAULT_APIM_GATEWAY_EP); + iotServerConfigs.setMqttBrokerEndpoint(AgentConstants.DEFAULT_MQTT_BROKER_EP); + iotServerConfigs.setXmppServerEndpoint(AgentConstants.DEFAULT_XMPP_SERVER_EP); + iotServerConfigs.setAuthMethod(AgentConstants.DEFAULT_AUTH_METHOD); + iotServerConfigs.setAuthToken(AgentConstants.DEFAULT_AUTH_TOKEN); + iotServerConfigs.setRefreshToken(AgentConstants.DEFAULT_REFRESH_TOKEN); + iotServerConfigs.setDataPushInterval(AgentConstants.DEFAULT_DATA_PUBLISH_INTERVAL); + + return iotServerConfigs; + } + + + /** + * This method constructs the URLs for each of the API Endpoints called by the device agent + * Ex: Register API, Push-Data API + * + * @throws AgentCoreOperationException if any error occurs at socket level whilst trying to + * retrieve the deviceIP of the network-interface read + * from the configs file + */ + public static void initializeServerEndPoints() { + AgentManager agentManager = AgentManager.getInstance(); + String serverSecureEndpoint = agentManager.getAgentConfigs().getHTTPS_ServerEndpoint(); + String serverUnSecureEndpoint = agentManager.getAgentConfigs().getHTTP_ServerEndpoint(); + String backEndContext = agentManager.getAgentConfigs().getControllerContext(); + + String deviceControllerAPIEndpoint = serverSecureEndpoint + backEndContext; + + String deviceEnrollmentEndpoint = + serverUnSecureEndpoint + backEndContext + AgentConstants.DEVICE_ENROLLMENT_API_EP; + agentManager.setEnrollmentEP(deviceEnrollmentEndpoint); + + String registerEndpointURL = + deviceControllerAPIEndpoint + AgentConstants.DEVICE_REGISTER_API_EP; + agentManager.setIpRegistrationEP(registerEndpointURL); + + String pushDataEndPointURL = + deviceControllerAPIEndpoint + AgentConstants.DEVICE_PUSH_TEMPERATURE_API_EP; + agentManager.setPushDataAPIEP(pushDataEndPointURL); + + log.info(AgentConstants.LOG_APPENDER + "IoT Server's Device Controller API Endpoint: " + + deviceControllerAPIEndpoint); + log.info(AgentConstants.LOG_APPENDER + "Device Enrollment EndPoint: " + + registerEndpointURL); + log.info(AgentConstants.LOG_APPENDER + "DeviceIP Registration EndPoint: " + + registerEndpointURL); + log.info(AgentConstants.LOG_APPENDER + "Push-Data API EndPoint: " + pushDataEndPointURL); + } + + + public static String prepareSecurePayLoad(String message) throws AgentCoreOperationException { + PublicKey serverPublicKey = EnrollmentManager.getInstance().getServerPublicKey(); + PrivateKey devicePrivateKey = EnrollmentManager.getInstance().getPrivateKey(); + + String encryptedMsg; + try { + encryptedMsg = CommunicationUtils.encryptMessage(message, serverPublicKey); + } catch (TransportHandlerException e) { + String errorMsg = "Error occurred whilst trying to encryptMessage: [" + message + "]"; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + + String signedPayload; + try { + signedPayload = CommunicationUtils.signMessage(encryptedMsg, devicePrivateKey); + } catch (TransportHandlerException e) { + String errorMsg = "Error occurred whilst trying to sign encrypted message of: [" + message + "]"; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + + JSONObject jsonPayload = new JSONObject(); + jsonPayload.put(JSON_MESSAGE_KEY, encryptedMsg); + jsonPayload.put(JSON_SIGNATURE_KEY, signedPayload); + + return jsonPayload.toString(); + } + + + public static String extractMessageFromPayload(String message) throws AgentCoreOperationException { + String actualMessage; + + PublicKey serverPublicKey = EnrollmentManager.getInstance().getServerPublicKey(); + PrivateKey devicePrivateKey = EnrollmentManager.getInstance().getPrivateKey(); + + JSONObject jsonPayload = new JSONObject(message); + Object encryptedMessage = jsonPayload.get(JSON_MESSAGE_KEY); + Object signedPayload = jsonPayload.get(JSON_SIGNATURE_KEY); + boolean verification; + + if (encryptedMessage != null && signedPayload != null) { + try { + verification = CommunicationUtils.verifySignature( + encryptedMessage.toString(), signedPayload.toString(), serverPublicKey); + } catch (TransportHandlerException e) { + String errorMsg = + "Error occurred whilst trying to verify signature on received message: [" + message + "]"; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + } else { + String errorMsg = "The received message is in an INVALID format. " + + "Need to be JSON - {\"Msg\":\"\", \"Sig\":\"\"}."; + throw new AgentCoreOperationException(errorMsg); + } + + try { + if (verification) { + actualMessage = CommunicationUtils.decryptMessage(encryptedMessage.toString(), devicePrivateKey); + } else { + String errorMsg = "Could not verify payload signature. The message was not signed by a valid client"; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg); + } + } catch (TransportHandlerException e) { + String errorMsg = "Error occurred whilst trying to decrypt received message: [" + encryptedMessage + "]"; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + + return actualMessage; + } + + public static String formatMessage(String message) { + StringBuilder formattedMsg = new StringBuilder(message); + + ArrayList keyWordList = new ArrayList(); + keyWordList.add("define"); + keyWordList.add("from"); + keyWordList.add("select"); + keyWordList.add("group"); + keyWordList.add("insert"); + keyWordList.add(";"); + + + for (String keyWord : keyWordList) { + int startIndex = 0; + + while (true) { + int keyWordIndex = formattedMsg.indexOf(keyWord, startIndex); + + if (keyWordIndex == -1) { + break; + } + + if (keyWord.equals(";")) { + if (keyWordIndex != 0 && (keyWordIndex + 1) != formattedMsg.length() && + formattedMsg.charAt(keyWordIndex + 1) == ' ') { + formattedMsg.setCharAt((keyWordIndex + 1), '\n'); + } + } else { + if (keyWordIndex != 0 && formattedMsg.charAt(keyWordIndex - 1) == ' ') { + formattedMsg.setCharAt((keyWordIndex - 1), '\n'); + } + } + startIndex = keyWordIndex + 1; + } + } + return formattedMsg.toString(); + } + + public static boolean writeToFile(String content, String fileLocation) { + File file = new File(fileLocation); + + try (FileOutputStream fop = new FileOutputStream(file)) { + + // if file doesn't exists, then create it + if (!file.exists()) { + file.createNewFile(); + } + + // get the content in bytes + byte[] contentInBytes = content.getBytes(StandardCharsets.UTF_8); + + fop.write(contentInBytes); + fop.flush(); + fop.close(); + + System.out.println("Done"); + AgentManager.setUpdated(true); + return true; + + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + +} + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/enrollment/EnrollmentManager.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/enrollment/EnrollmentManager.java new file mode 100644 index 000000000..b6f26ad05 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/enrollment/EnrollmentManager.java @@ -0,0 +1,440 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.advanced.enrollment; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.X500NameBuilder; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; +import org.jscep.client.Client; +import org.jscep.client.ClientException; +import org.jscep.client.EnrollmentResponse; +import org.jscep.client.verification.CertificateVerifier; +import org.jscep.client.verification.OptimisticCertificateVerifier; +import org.jscep.transaction.TransactionException; +import org.jscep.transport.response.Capabilities; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.core.AgentConstants; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.core.AgentManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.exception.AgentCoreOperationException; +import sun.security.x509.X509CertImpl; + +import java.math.BigInteger; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.CertStore; +import java.security.cert.CertStoreException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Date; + +/** + * This class controls the entire SCEP enrolment process of the client. It is a singleton for any single client which + * has the agent code running in it. The main functionality of this class includes generating a Private-Public Key + * Pair for the enrollment flow, creating the Certificate-Sign-Request using the generated Public-Key to send to the + * SEP server, Contacting the SCEP server to receive the Signed Certificate and requesting for the server's public + * key for encrypting the payloads. + * The provider for all Cryptographic functions used in this class are "BouncyCastle" and the Asymmetric-Key pair + * algorithm used is "RSA" with a key size of 2048. The signature algorithm used is "SHA1withRSA". + * This class also holds the "SCEPUrl" (Server Url read from the configs file), the Private-Public Keys of the + * client, Signed SCEP certificate and the server's public certificate. + */ + +//TODO: Need to save cert and keys to file after initial enrollment... +public class EnrollmentManager { + private static final Log log = LogFactory.getLog(EnrollmentManager.class); + private static EnrollmentManager enrollmentManager; + + private static final String KEY_PAIR_ALGORITHM = "RSA"; + private static final String PROVIDER = "BC"; + private static final String SIGNATURE_ALG = "SHA1withRSA"; + private static final int KEY_SIZE = 2048; + + // Seed to our PRNG. Make sure this is initialised randomly, NOT LIKE THIS + private static final byte[] SEED = ")(*&^%$#@!".getBytes(); + private static final int CERT_VALIDITY = 730; + + // URL of our SCEP server + private String SCEPUrl; + private PrivateKey privateKey; + private PublicKey publicKey; + private PublicKey serverPublicKey; + private X509Certificate SCEPCertificate; + + + /** + * Constructor of the EnrollmentManager. Initializes the SCEPUrl as read from the configuration file by the + * AgentManager. + */ + private EnrollmentManager() { + this.SCEPUrl = AgentManager.getInstance().getEnrollmentEP(); + } + + /** + * Method to return the current singleton instance of the EnrollmentManager. + * + * @return the current singleton instance if available and if not initializes a new instance and returns it. + */ + public static EnrollmentManager getInstance() { + if (enrollmentManager == null) { + enrollmentManager = new EnrollmentManager(); + } + return enrollmentManager; + } + + + /** + * Method to control the entire enrollment flow. This method calls the method to create the Private-Public Key + * Pair, calls the specific method to generate the Certificate-Sign-Request, creates a one time self signed + * certificate to present to the SCEP server with the initial CSR, calls the specific method to connect to the + * SCEP Server and to get the SCEP Certificate and also calls the method that requests the SCEP Server for its + * PublicKey for future payload encryption. + * + * @throws AgentCoreOperationException if the private method generateCertSignRequest() fails with an error or if + * there is an error creating a self-sign certificate to present to the + * server (whilst trying to get the CSR signed) + */ + public void beginEnrollmentFlow() throws AgentCoreOperationException { + Security.addProvider(new BouncyCastleProvider()); + + KeyPair keyPair = generateKeyPair(); + this.privateKey = keyPair.getPrivate(); + this.publicKey = keyPair.getPublic(); + + if (log.isDebugEnabled()) { + log.info(AgentConstants.LOG_APPENDER + "DevicePrivateKey:\n[\n" + privateKey + "\n]\n"); + log.info(AgentConstants.LOG_APPENDER + "DevicePublicKey:\n[\n" + publicKey + "\n]\n"); + } + + PKCS10CertificationRequest certSignRequest = generateCertSignRequest(); + + /** + * ----------------------------------------------------------------------------------------------- + * Generate an ephemeral self-signed certificate. This is needed to present to the CA in the SCEP request. + * In the future, add proper EKU and attributes in the request. The CA does NOT have to honour any of this. + * ----------------------------------------------------------------------------------------------- + */ + X500Name issuer = new X500Name("CN=Temporary Issuer"); + BigInteger serial = new BigInteger(32, new SecureRandom()); + Date fromDate = new Date(); + Date toDate = new Date(System.currentTimeMillis() + (CERT_VALIDITY * 86400000L)); + + // Build the self-signed cert using BC, sign it with our private key (self-signed) + X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(issuer, serial, fromDate, toDate, + certSignRequest.getSubject(), + certSignRequest.getSubjectPublicKeyInfo()); + ContentSigner sigGen; + X509Certificate tmpCert; + + try { + sigGen = new JcaContentSignerBuilder(SIGNATURE_ALG).setProvider(PROVIDER).build(keyPair.getPrivate()); + tmpCert = new JcaX509CertificateConverter().setProvider(PROVIDER).getCertificate(certBuilder.build(sigGen)); + } catch (OperatorCreationException e) { + String errorMsg = "Error occurred whilst creating a ContentSigner for the Temp-Self-Signed Certificate."; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } catch (CertificateException e) { + String errorMsg = "Error occurred whilst trying to create Temp-Self-Signed Certificate."; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + /** + * ----------------------------------------------------------------------------------------------- + */ + + this.SCEPCertificate = getSignedCertificateFromServer(tmpCert, certSignRequest); + this.serverPublicKey = initPublicKeyOfServer(); + + if (log.isDebugEnabled()) { + log.info(AgentConstants.LOG_APPENDER + "TemporaryCertPublicKey:\n[\n" + tmpCert.getPublicKey() + "\n]\n"); + log.info(AgentConstants.LOG_APPENDER + "ServerPublicKey:\n[\n" + serverPublicKey + "\n]\n"); + } + + } + + + /** + * This method creates the Public-Private Key pair for the current client. + * + * @return the generated KeyPair object + * @throws AgentCoreOperationException when the given Security Provider does not exist or the Algorithmn used to + * generate the key pair is invalid. + */ + private KeyPair generateKeyPair() throws AgentCoreOperationException { + + // Generate key pair + KeyPairGenerator keyPairGenerator; + try { + keyPairGenerator = KeyPairGenerator.getInstance(KEY_PAIR_ALGORITHM, PROVIDER); + keyPairGenerator.initialize(KEY_SIZE, new SecureRandom(SEED)); + } catch (NoSuchAlgorithmException e) { + String errorMsg = "Algorithm [" + KEY_PAIR_ALGORITHM + "] provided for KeyPairGenerator is invalid."; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } catch (NoSuchProviderException e) { + String errorMsg = "Provider [" + PROVIDER + "] provided for KeyPairGenerator does not exist."; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + + return keyPairGenerator.genKeyPair(); + } + + + /** + * This method creates the PKCS10 Certificate Sign Request which is to be sent to the SCEP Server using the + * generated PublicKey of the client. The certificate parameters used here are the ones from the AgentManager + * which are the values read from the configurations file. + * + * @return the PKCS10CertificationRequest object created using the client specific configs and the generated + * PublicKey + * @throws AgentCoreOperationException if an error occurs when creating a content signer to sign the CSR. + */ + private PKCS10CertificationRequest generateCertSignRequest() throws AgentCoreOperationException { + // Build the CN for the cert that's being requested. + X500NameBuilder nameBld = new X500NameBuilder(BCStyle.INSTANCE); + nameBld.addRDN(BCStyle.CN, AgentManager.getInstance().getAgentConfigs().getServerName()); + nameBld.addRDN(BCStyle.O, AgentManager.getInstance().getAgentConfigs().getDeviceOwner()); + nameBld.addRDN(BCStyle.OU, AgentManager.getInstance().getAgentConfigs().getDeviceOwner()); + nameBld.addRDN(BCStyle.UNIQUE_IDENTIFIER, AgentManager.getInstance().getAgentConfigs().getDeviceId()); + nameBld.addRDN(BCStyle.SERIALNUMBER, AgentManager.getInstance().getAgentConfigs().getDeviceId()); + X500Name principal = nameBld.build(); + + JcaContentSignerBuilder contentSignerBuilder = new JcaContentSignerBuilder(SIGNATURE_ALG).setProvider(PROVIDER); + ContentSigner contentSigner; + + try { + contentSigner = contentSignerBuilder.build(this.privateKey); + } catch (OperatorCreationException e) { + String errorMsg = "Could not create content signer with private key."; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + + // Generate the certificate signing request (csr = PKCS10) + PKCS10CertificationRequestBuilder reqBuilder = new JcaPKCS10CertificationRequestBuilder(principal, + this.publicKey); + return reqBuilder.build(contentSigner); + } + + + /** + * This method connects to the SCEP Server to fetch the signed SCEP Certificate. + * + * @param tempCert the temporary self-signed certificate of the client required for the initial CSR + * request against the SCEP Server. + * @param certSignRequest the PKCS10 Certificate-Sign-Request that is to be sent to the SCEP Server. + * @return the SCEP-Certificate for the client signed by the SCEP-Server. + * @throws AgentCoreOperationException if the SCEPUrl is invalid or if the flow of sending the CSR and getting + * the signed certificate fails or if the signed certificate cannot be + * retrieved from the reply from the server. + */ + private X509Certificate getSignedCertificateFromServer(X509Certificate tempCert, + PKCS10CertificationRequest certSignRequest) + throws AgentCoreOperationException { + + X509Certificate signedSCEPCertificate = null; + URL url; + EnrollmentResponse enrolResponse; + CertStore certStore; + + try { + // The URL where we are going to request our cert from + url = new URL(this.SCEPUrl); + + /* // This is called when we get the certificate for our CSR signed by CA + // Implement this handler to check the CA cert in prod. We can do cert pinning here + CallbackHandler cb = new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + //throw new UnsupportedOperationException("Not supported yet."); //To change body of generated + methods, choose Tools | Templates. + } + };*/ + + // Implement verification of the CA cert. VERIFY the CA + CertificateVerifier ocv = new OptimisticCertificateVerifier(); + + // Instantiate our SCEP client + Client scepClient = new Client(url, ocv); + + // Submit our cert for signing. SCEP server should allow the client to specify + // the SCEP CA to issue the request against, if there are multiple CAs + enrolResponse = scepClient.enrol(tempCert, this.privateKey, certSignRequest); + + // Verify we got what we want, and just print out the cert. + certStore = enrolResponse.getCertStore(); + + for (Certificate x509Certificate : certStore.getCertificates(null)) { + if (log.isDebugEnabled()) { + log.debug(x509Certificate.toString()); + } + signedSCEPCertificate = (X509Certificate) x509Certificate; + } + + } catch (MalformedURLException ex) { + String errorMsg = "Could not create valid URL from given SCEP URI: " + SCEPUrl; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, ex); + } catch (TransactionException | ClientException e) { + String errorMsg = "Enrollment process to SCEP Server at: " + SCEPUrl + " failed."; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } catch (CertStoreException e) { + String errorMsg = "Could not retrieve [Signed-Certificate] from the response message from SCEP-Server."; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + + return signedSCEPCertificate; + } + + + /** + * Gets the Public Key of the SCEP-Server and initializes it for later use. This method contacts the SCEP Server + * and fetches its CA Cert and extracts the Public Key of the server from the received reply. + * + * @return the public key of the SCEP Server which is to be used to encrypt pyloads. + * @throws AgentCoreOperationException if the SCEPUrl is invalid or if the flow of sending the CSR and getting + * the signed certificate fails or if the signed certificate cannot be + * retrieved from the reply from the server. + */ + private PublicKey initPublicKeyOfServer() throws AgentCoreOperationException { + URL url; + CertStore certStore; + PublicKey serverCertPublicKey = null; + + try { + // The URL where we are going to request our cert from + url = new URL(this.SCEPUrl); + + /* // This is called when we get the certificate for our CSR signed by CA + // Implement this handler to check the CA cert in prod. We can do cert pinning here + CallbackHandler cb = new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + //throw new UnsupportedOperationException("Not supported yet."); //To change body of generated + methods, choose Tools | Templates. + } + };*/ + + // Implement verification of the CA cert. VERIFY the CA + CertificateVerifier ocv = new OptimisticCertificateVerifier(); + + // Instantiate our SCEP client + Client scepClient = new Client(url, ocv); + + // Get the CA capabilities. Should return SHA1withRSA for strongest hash and sig. Returns MD5. + if (log.isDebugEnabled()) { + Capabilities cap = scepClient.getCaCapabilities(); + log.debug(String.format( + "\nStrongestCipher: %s,\nStrongestMessageDigest: %s,\nStrongestSignatureAlgorithm: %s," + + "\nIsRenewalSupported: %s,\nIsRolloverSupported: %s", + cap.getStrongestCipher(), cap.getStrongestMessageDigest(), cap.getStrongestSignatureAlgorithm(), + cap.isRenewalSupported(), cap.isRolloverSupported())); + } + + certStore = scepClient.getCaCertificate(); + + for (Certificate cert : certStore.getCertificates(null)) { + if (cert instanceof X509Certificate) { + if (log.isDebugEnabled()) { + log.debug(((X509Certificate) cert).getIssuerDN().getName()); + } + + //TODO: Need to identify the correct certificate. + // I have chosen the CA cert based on its BasicConstraint criticality being set to "true" + if (((X509CertImpl) cert).getBasicConstraintsExtension().isCritical()) { + serverCertPublicKey = cert.getPublicKey(); + } + } + } + + } catch (MalformedURLException ex) { + String errorMsg = "Could not create valid URL from given SCEP URI: " + SCEPUrl; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, ex); + } catch (ClientException e) { + String errorMsg = "Could not retrieve [Server-Certificate] from the SCEP-Server."; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } catch (CertStoreException e) { + String errorMsg = "Could not retrieve [Server-Certificates] from the response message from SCEP-Server."; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + + return serverCertPublicKey; + } + + + /** + * Gets the Public-Key of the client. + * + * @return the public key of the client. + */ + public PublicKey getPublicKey() { + return publicKey; + } + + /** + * Gets the Private-Key of the client. + * + * @return the private key of the client. + */ + public PrivateKey getPrivateKey() { + return privateKey; + } + + /** + * Gets the SCEP-Certificate of the client. + * + * @return the SCEP Certificate of the client. + */ + public X509Certificate getSCEPCertificate() { + return SCEPCertificate; + } + + /** + * Gets the Public-Key of the Server. + * + * @return the pubic key of the server. + */ + public PublicKey getServerPublicKey() { + return serverPublicKey; + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/exception/AgentCoreOperationException.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/exception/AgentCoreOperationException.java new file mode 100644 index 000000000..7aa102042 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/exception/AgentCoreOperationException.java @@ -0,0 +1,57 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.advanced.exception; + + +public class AgentCoreOperationException extends Exception { + private static final long serialVersionUID = 2736466230451105710L; + + private String errorMessage; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public AgentCoreOperationException(String msg, Exception nestedEx) { + super(msg, nestedEx); + setErrorMessage(msg); + } + + public AgentCoreOperationException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public AgentCoreOperationException(String msg) { + super(msg); + setErrorMessage(msg); + } + + public AgentCoreOperationException() { + super(); + } + + public AgentCoreOperationException(Throwable cause) { + super(cause); + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/sidhdhi/SidhdhiQuery.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/sidhdhi/SidhdhiQuery.java new file mode 100644 index 000000000..94e6bd525 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/sidhdhi/SidhdhiQuery.java @@ -0,0 +1,205 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.advanced.sidhdhi; + +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.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.core.AgentConstants; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.core.AgentManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.core.AgentUtilOperations; +import org.wso2.siddhi.core.SiddhiManager; +import org.wso2.siddhi.core.event.Event; +import org.wso2.siddhi.core.stream.input.InputHandler; +import org.wso2.siddhi.core.stream.output.StreamCallback; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +/** + * This class reads the humidity reading and injects values + * to the siddhiEngine for processing on a routine basis + * also if the siddhiquery is updated the class takes + * care of re-initializing same. + */ +public class SidhdhiQuery implements Runnable { + private static final Log log = LogFactory.getLog(SidhdhiQuery.class); + public static final String sidhdhiQueryPath = AgentManager.getInstance().getRootPath() + AgentConstants.CEP_FILE_NAME; + + //Bam data push client + private static SiddhiManager siddhiManager = new SiddhiManager(); + + public static SiddhiManager getSiddhiManager() { + return siddhiManager; + } + + public static void setSiddhiManager(SiddhiManager siddhiManager) { + SidhdhiQuery.siddhiManager = siddhiManager; + } + + public void run() { + + //Start the execution plan with pre-defined or previously persisted Siddhi query + File f = new File(sidhdhiQueryPath); + + if (!f.exists()) { + AgentUtilOperations.writeToFile(AgentConstants.CEP_QUERY, sidhdhiQueryPath); + } + + StartExecutionPlan startExecutionPlan = new StartExecutionPlan().invoke(); + + while (true) { + + //Check if there is new policy update available + if (AgentManager.isUpdated()) { + System.out.print("### Policy Update Detected!"); + //Restart execution plan with new query + restartSiddhi(); + startExecutionPlan = new StartExecutionPlan().invoke(); + } + InputHandler inputHandler = startExecutionPlan.getInputHandler(); + + //Sending events to Siddhi + try { + int humidityReading = AgentManager.getInstance().getTemperature(); + inputHandler.send(new Object[]{"FIRE_1", humidityReading}); + Thread.sleep(3000); + } catch (InterruptedException e) { + e.printStackTrace(); + break; + } + } + } + + /** + * Re-Initialize SiddhiManager + */ + private void restartSiddhi() { + siddhiManager.shutdown(); + siddhiManager = new SiddhiManager(); + } + + + /** + * Read content from a given file and return as a string + * + * @param path + * @param encoding + * @return + */ + public static String readFile(String path, Charset encoding) { + byte[] encoded = new byte[0]; + try { + encoded = Files.readAllBytes(Paths.get(path)); + } catch (IOException e) { + log.error("Error reading Sidhdhi query from file."); + } + return new String(encoded, encoding); + } + + + /** + * Read humidity data from API URL + * + * @param humidityAPIUrl + * @return + */ + private String readHumidityData(String humidityAPIUrl) { + HttpClient client = new DefaultHttpClient(); + HttpGet request = new HttpGet(humidityAPIUrl); + String responseStr = null; + try { + HttpResponse response = client.execute(request); + log.debug("Response Code : " + response); + InputStream input = response.getEntity().getContent(); + BufferedReader br = new BufferedReader(new InputStreamReader(input, "UTF-8")); + responseStr = String.valueOf(br.readLine()); + br.close(); + + } catch (IOException e) { + //log.error("Exception encountered while trying to make get request."); + log.error("Error while reading humidity reading from file!"); + return responseStr; + } + return responseStr; + } + + /** + * Initialize SiddhiExecution plan + */ + private static class StartExecutionPlan { + private InputHandler inputHandler; + + public InputHandler getInputHandler() { + return inputHandler; + } + + public StartExecutionPlan invoke() { + String executionPlan; + + executionPlan = readFile(sidhdhiQueryPath, StandardCharsets.UTF_8); + + //Generating runtime + siddhiManager.addExecutionPlan(executionPlan); + + siddhiManager.addCallback("bulbOnStream", new StreamCallback() { + @Override + public void receive(Event[] events) { + System.out.println("Bulb on Event Fired!"); + if (events.length > 0) { + if (!AgentManager.getInstance().isAlarmOn()) { + AgentManager.getInstance().changeAlarmStatus(true); + System.out.println("#### Performed HTTP call! ON."); + } + } + } + }); + + siddhiManager.addCallback("bulbOffStream", new StreamCallback() { + @Override + public void receive(Event[] inEvents) { + System.out.println("Bulb off Event Fired"); + if (AgentManager.getInstance().isAlarmOn()) { + AgentManager.getInstance().changeAlarmStatus(false); + System.out.println("#### Performed HTTP call! OFF."); + } + } + + }); + + //Retrieving InputHandler to push events into Siddhi + inputHandler = siddhiManager.getInputHandler("fireAlarmEventStream"); + + //Starting event processing + System.out.println("Execution Plan Started!"); + return this; + } + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/CommunicationUtils.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/CommunicationUtils.java new file mode 100644 index 000000000..e6e16ba52 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/CommunicationUtils.java @@ -0,0 +1,226 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.advanced.transport; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; + +/** + * This is a utility class which contains methods common to the communication process of a client and the server. The + * methods include encryption/decryption of payloads and signing/verification of payloads received and to be sent. + */ +public class CommunicationUtils { + private static final Log log = LogFactory.getLog(TransportUtils.class); + + // The Signature Algorithm used. + private static final String SIGNATURE_ALG = "SHA1withRSA"; + // The Encryption Algorithm and the Padding used. + private static final String CIPHER_PADDING = "RSA/ECB/PKCS1Padding"; + + + /** + * Encrypts the message with the key that's passed in. + * + * @param message the message to be encrypted. + * @param encryptionKey the key to use for the encryption of the message. + * @return the encrypted message in String format. + * @throws TransportHandlerException if an error occurs with the encryption flow which can be due to Padding + * issues, encryption key being invalid or the algorithm used is unrecognizable. + */ + public static String encryptMessage(String message, Key encryptionKey) throws TransportHandlerException { + Cipher encrypter; + byte[] cipherData; + + try { + encrypter = Cipher.getInstance(CIPHER_PADDING); + encrypter.init(Cipher.ENCRYPT_MODE, encryptionKey); + cipherData = encrypter.doFinal(message.getBytes(StandardCharsets.UTF_8)); + + } catch (NoSuchAlgorithmException e) { + String errorMsg = "Algorithm not found exception occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (NoSuchPaddingException e) { + String errorMsg = "No Padding error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (InvalidKeyException e) { + String errorMsg = "InvalidKey exception occurred for encryptionKey \n[\n" + encryptionKey + "\n]\n"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (BadPaddingException e) { + String errorMsg = "Bad Padding error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (IllegalBlockSizeException e) { + String errorMsg = "Illegal blockSize error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } + + return Base64.encodeBase64String(cipherData); + } + +///TODO:: Exception needs to change according to the common package + + /** + * Signed a given message using the PrivateKey that's passes in. + * + * @param message the message to be signed. Ideally some encrypted payload. + * @param signatureKey the PrivateKey with which the message is to be signed. + * @return the Base64Encoded String of the signed payload. + * @throws TransportHandlerException if some error occurs with the signing process which may be related to the + * signature algorithm used or the key used for signing. + */ + public static String signMessage(String message, PrivateKey signatureKey) throws TransportHandlerException { + + Signature signature; + String signedEncodedString; + + try { + signature = Signature.getInstance(SIGNATURE_ALG); + signature.initSign(signatureKey); + signature.update(Base64.decodeBase64(message)); + + byte[] signatureBytes = signature.sign(); + signedEncodedString = Base64.encodeBase64String(signatureBytes); + + } catch (NoSuchAlgorithmException e) { + String errorMsg = + "Algorithm not found exception occurred for Signature instance of [" + SIGNATURE_ALG + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (SignatureException e) { + String errorMsg = "Signature exception occurred for Signature instance of [" + SIGNATURE_ALG + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (InvalidKeyException e) { + String errorMsg = "InvalidKey exception occurred for signatureKey \n[\n" + signatureKey + "\n]\n"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } + + return signedEncodedString; + } + + + /** + * Verifies some signed-data against the a Public-Key to ensure that it was produced by the holder of the + * corresponding Private Key. + * + * @param data the actual payoad which was signed by some Private Key. + * @param signedData the signed data produced by signing the payload using a Private Key. + * @param verificationKey the corresponding Public Key which is an exact pair of the Private-Key with we expect + * the data to be signed by. + * @return true if the signed data verifies to be signed by the corresponding Private Key. + * @throws TransportHandlerException if some error occurs with the verification process which may be related to + * the signature algorithm used or the key used for signing. + */ + public static boolean verifySignature(String data, String signedData, PublicKey verificationKey) + throws TransportHandlerException { + + Signature signature; + boolean verified; + + try { + signature = Signature.getInstance(SIGNATURE_ALG); + signature.initVerify(verificationKey); + signature.update(Base64.decodeBase64(data)); + + verified = signature.verify(Base64.decodeBase64(signedData)); + + } catch (NoSuchAlgorithmException e) { + String errorMsg = + "Algorithm not found exception occurred for Signature instance of [" + SIGNATURE_ALG + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (SignatureException e) { + String errorMsg = "Signature exception occurred for Signature instance of [" + SIGNATURE_ALG + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (InvalidKeyException e) { + String errorMsg = "InvalidKey exception occurred for signatureKey \n[\n" + verificationKey + "\n]\n"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } + + return verified; + } + + + /** + * Encrypts the message with the key that's passed in. + * + * @param encryptedMessage the encrypted message that is supposed to be decrypted. + * @param decryptKey the key to use in the decryption process. + * @return the decrypted message in String format. + * @throws TransportHandlerException if an error occurs with the encryption flow which can be due to Padding + * issues, encryption key being invalid or the algorithm used is unrecognizable. + */ + public static String decryptMessage(String encryptedMessage, Key decryptKey) throws TransportHandlerException { + + Cipher decrypter; + String decryptedMessage; + + try { + + decrypter = Cipher.getInstance(CIPHER_PADDING); + decrypter.init(Cipher.DECRYPT_MODE, decryptKey); + decryptedMessage = new String(decrypter.doFinal(Base64.decodeBase64(encryptedMessage)), + StandardCharsets.UTF_8); + + } catch (NoSuchAlgorithmException e) { + String errorMsg = "Algorithm not found exception occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (NoSuchPaddingException e) { + String errorMsg = "No Padding error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (InvalidKeyException e) { + String errorMsg = "InvalidKey exception occurred for encryptionKey \n[\n" + decryptKey + "\n]\n"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (BadPaddingException e) { + String errorMsg = "Bad Padding error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (IllegalBlockSizeException e) { + String errorMsg = "Illegal blockSize error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } + + return decryptedMessage; + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/TransportHandler.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/TransportHandler.java new file mode 100644 index 000000000..8034f66e2 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/TransportHandler.java @@ -0,0 +1,47 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.advanced.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 thees methods that implement the logic related to the devices + * using the protocol. + * + * @param a message type specific to the protocol implemented + */ +public interface TransportHandler { + int DEFAULT_TIMEOUT_INTERVAL = 5000; // millis ~ 10 sec + + void connect(); + + boolean isConnected(); + + //TODO:: Any errors needs to be thrown ahead + void processIncomingMessage(T message, String... messageParams); + + void processIncomingMessage(); + + void publishDeviceData(String... publishData); + + void publishDeviceData(); + + void disconnect(); +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/TransportHandlerException.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/TransportHandlerException.java new file mode 100644 index 000000000..aeccc0fbc --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/TransportHandlerException.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.device.mgt.iot.virtualfirealarm.agent.advanced.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/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/TransportUtils.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/TransportUtils.java new file mode 100644 index 000000000..a9b6676a3 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/TransportUtils.java @@ -0,0 +1,302 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.advanced.transport; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.DatagramSocket; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.NetworkInterface; +import java.net.ServerSocket; +import java.net.SocketException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +public class TransportUtils { + private static final Log log = LogFactory.getLog(TransportUtils.class); + + public static final int MIN_PORT_NUMBER = 9000; + public static final int MAX_PORT_NUMBER = 11000; + + /** + * Given a server endpoint as a String, this method splits it into Protocol, Host and Port + * + * @param ipString a network endpoint in the format - '://:' + * @return a map with keys "Protocol", "Host" & "Port" for the related values from the ipString + * @throws TransportHandlerException + */ + public static Map getHostAndPort(String ipString) + throws TransportHandlerException { + Map ipPortMap = new HashMap(); + String[] ipPortArray = ipString.split(":"); + + if (ipPortArray.length != 3) { + String errorMsg = + "The IP String - '" + ipString + + "' is invalid. It needs to be in format '://:'."; + log.info(errorMsg); + throw new TransportHandlerException(errorMsg); + } + + ipPortMap.put("Protocol", ipPortArray[0]); + ipPortMap.put("Host", ipPortArray[1].replace(File.separator, "")); + ipPortMap.put("Port", ipPortArray[2]); + return ipPortMap; + } + + /** + * This method validates whether a specific IP Address is of IPv4 type + * + * @param ipAddress the IP Address which needs to be validated + * @return true if it is of IPv4 type and false otherwise + */ + public static boolean validateIPv4(String ipAddress) { + try { + if (ipAddress == null || ipAddress.isEmpty()) { + return false; + } + + String[] parts = ipAddress.split("\\."); + if (parts.length != 4) { + return false; + } + + for (String s : parts) { + int i = Integer.parseInt(s); + if ((i < 0) || (i > 255)) { + return false; + } + } + return !ipAddress.endsWith("."); + + } catch (NumberFormatException nfe) { + log.warn("The IP Address: " + ipAddress + " could not " + + "be validated against IPv4-style"); + return false; + } + } + + + public static Map getInterfaceIPMap() throws TransportHandlerException { + + Map interfaceToIPMap = new HashMap(); + Enumeration networkInterfaces; + String networkInterfaceName = ""; + String ipAddress; + + try { + networkInterfaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException exception) { + String errorMsg = + "Error encountered whilst trying to get the list of network-interfaces"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, exception); + } + + try { + for (; networkInterfaces.hasMoreElements(); ) { + networkInterfaceName = networkInterfaces.nextElement().getName(); + + if (log.isDebugEnabled()) { + log.debug("Network Interface: " + networkInterfaceName); + log.debug("------------------------------------------"); + } + + Enumeration interfaceIPAddresses = NetworkInterface.getByName( + networkInterfaceName).getInetAddresses(); + + for (; interfaceIPAddresses.hasMoreElements(); ) { + ipAddress = interfaceIPAddresses.nextElement().getHostAddress(); + + if (log.isDebugEnabled()) { + log.debug("IP Address: " + ipAddress); + } + + if (TransportUtils.validateIPv4(ipAddress)) { + interfaceToIPMap.put(networkInterfaceName, ipAddress); + } + } + + if (log.isDebugEnabled()) { + log.debug("------------------------------------------"); + } + } + } catch (SocketException exception) { + String errorMsg = + "Error encountered whilst trying to get the IP Addresses of the network " + + "interface: " + networkInterfaceName; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, exception); + } + + return interfaceToIPMap; + } + + + /** + * Attempts to find a free port between the MIN_PORT_NUMBER(9000) and MAX_PORT_NUMBER(11000). + * Tries 'RANDOMLY picked' port numbers between this range up-until "randomAttempts" number of + * times. If still fails, then tries each port in descending order from the MAX_PORT_NUMBER + * whilst skipping already attempted ones via random selection. + * + * @param randomAttempts no of times to TEST port numbers picked randomly over the given range + * @return an available/free port + */ + public static synchronized int getAvailablePort(int randomAttempts) { + ArrayList failedPorts = new ArrayList(randomAttempts); + + Random randomNum = new Random(); + int randomPort = MAX_PORT_NUMBER; + + while (randomAttempts > 0) { + randomPort = randomNum.nextInt(MAX_PORT_NUMBER - MIN_PORT_NUMBER) + MIN_PORT_NUMBER; + + if (checkIfPortAvailable(randomPort)) { + return randomPort; + } + failedPorts.add(randomPort); + randomAttempts--; + } + + randomPort = MAX_PORT_NUMBER; + + while (true) { + if (!failedPorts.contains(randomPort) && checkIfPortAvailable(randomPort)) { + return randomPort; + } + randomPort--; + } + } + + + private static boolean checkIfPortAvailable(int port) { + ServerSocket tcpSocket = null; + DatagramSocket udpSocket = null; + + try { + tcpSocket = new ServerSocket(port); + tcpSocket.setReuseAddress(true); + + udpSocket = new DatagramSocket(port); + udpSocket.setReuseAddress(true); + return true; + } catch (IOException ex) { + // denotes the port is in use + } finally { + if (tcpSocket != null) { + try { + tcpSocket.close(); + } catch (IOException e) { + /* not to be thrown */ + } + } + + if (udpSocket != null) { + udpSocket.close(); + } + } + + return false; + } + + + /** + * This is a utility method that creates and returns a HTTP connection object. + * + * @param urlString the URL pattern to which the connection needs to be created + * @return an HTTPConnection object which cn be used to send HTTP requests + * @throws TransportHandlerException if errors occur when creating the HTTP connection with + * the given URL string + */ + public static HttpURLConnection getHttpConnection(String urlString) throws + TransportHandlerException { + URL connectionUrl; + HttpURLConnection httpConnection; + + try { + connectionUrl = new URL(urlString); + httpConnection = (HttpURLConnection) connectionUrl.openConnection(); + } catch (MalformedURLException e) { + String errorMsg = "Error occured whilst trying to form HTTP-URL from string: " + urlString; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (IOException exception) { + String errorMsg = "Error occured whilst trying to open a connection to: " + urlString; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, exception); + } + return httpConnection; + } + + /** + * This is a utility method that reads and returns the response from a HTTP connection + * + * @param httpConnection the connection from which a response is expected + * @return the response (as a string) from the given HTTP connection + * @throws TransportHandlerException if any errors occur whilst reading the response from + * the connection stream + */ + public static String readResponseFromHttpRequest(HttpURLConnection httpConnection) + throws TransportHandlerException { + BufferedReader bufferedReader; + try { + bufferedReader = new BufferedReader(new InputStreamReader( + httpConnection.getInputStream(), StandardCharsets.UTF_8)); + } catch (IOException exception) { + String errorMsg = "There is an issue with connecting the reader to the input stream at: " + + httpConnection.getURL(); + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, exception); + } + + String responseLine; + StringBuilder completeResponse = new StringBuilder(); + + try { + while ((responseLine = bufferedReader.readLine()) != null) { + completeResponse.append(responseLine); + } + } catch (IOException exception) { + String errorMsg = "Error occured whilst trying read from the connection stream at: " + + httpConnection.getURL(); + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, exception); + } + try { + bufferedReader.close(); + } catch (IOException exception) { + log.error( + "Could not succesfully close the bufferedReader to the connection at: " + httpConnection.getURL()); + } + return completeResponse.toString(); + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/http/HTTPTransportHandler.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/http/HTTPTransportHandler.java new file mode 100644 index 000000000..6abee0b09 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/http/HTTPTransportHandler.java @@ -0,0 +1,91 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.advanced.transport.http; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.eclipse.jetty.server.Server; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.transport.TransportHandler; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.transport.TransportUtils; + +/** + * 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 HTTP-Transport specific implementations. The class implements utility methods for the + * case of a HTTP 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 HTTP specific functionality (ideally a device API writer who would like to communicate to the device + * via HTTP Protocol). + */ +public abstract class HTTPTransportHandler implements TransportHandler { + private static final Log log = LogFactory.getLog(HTTPTransportHandler.class); + + protected Server server; + protected int port; + protected int timeoutInterval; + + protected HTTPTransportHandler() { + this.port = TransportUtils.getAvailablePort(10); + this.server = new Server(port); + timeoutInterval = DEFAULT_TIMEOUT_INTERVAL; + } + + protected HTTPTransportHandler(int port) { + this.port = port; + this.server = new Server(this.port); + timeoutInterval = DEFAULT_TIMEOUT_INTERVAL; + } + + protected HTTPTransportHandler(int port, int timeoutInterval) { + this.port = port; + this.server = new Server(this.port); + this.timeoutInterval = timeoutInterval; + } + + public void setTimeoutInterval(int timeoutInterval) { + this.timeoutInterval = timeoutInterval; + } + + /** + * Checks whether the HTTP server is up and listening for incoming requests. + * + * @return true if the server is up & listening for requests, else false. + */ + public boolean isConnected() { + return server.isStarted(); + } + + + protected void incrementPort() { + this.port = this.port + 1; + server = new Server(port); + } + + /** + * Shuts-down the HTTP Server. + */ + public void closeConnection() throws Exception { + if (server != null && isConnected()) { + server.stop(); + } + } + + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/mqtt/MQTTTransportHandler.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/mqtt/MQTTTransportHandler.java new file mode 100644 index 000000000..d4626f6df --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/mqtt/MQTTTransportHandler.java @@ -0,0 +1,360 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.advanced.transport.mqtt; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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.eclipse.paho.client.mqttv3.MqttSecurityException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.transport.TransportHandler; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.transport.TransportHandlerException; + +import java.io.File; +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 HTTP-Transport specific implementations. The class implements utility methods for the + * case of a HTTP 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 HTTP specific functionality (ideally a device API writer who would like to communicate to the device + * via HTTP 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 processing incoming messages. Makes use of the 'Paho-MQTT' + * library provided by Eclipse Org. + */ +public abstract class MQTTTransportHandler + implements MqttCallback, TransportHandler { + private static final Log log = LogFactory.getLog(MQTTTransportHandler.class); + + public static final int DEFAULT_MQTT_QUALITY_OF_SERVICE = 0; + + private MqttClient client; + private String clientId; + private MqttConnectOptions options; + private String clientWillTopic; + + protected String mqttBrokerEndPoint; + protected int timeoutInterval; + protected String subscribeTopic; + + /** + * Constructor for the MQTTTransportHandler which takes in the owner, type of the device + * and the MQTT Broker URL and the topic to subscribe. + * + * @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 + */ + protected MQTTTransportHandler(String deviceOwner, String deviceType, + String mqttBrokerEndPoint, + String subscribeTopic) { + this.clientId = deviceOwner + ":" + deviceType; + this.subscribeTopic = subscribeTopic; + this.clientWillTopic = deviceType + File.separator + "disconnection"; + this.mqttBrokerEndPoint = mqttBrokerEndPoint; + this.timeoutInterval = DEFAULT_TIMEOUT_INTERVAL; + this.initSubscriber(); + } + + /** + * 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 successive + * 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; + //TODO:: Use constant strings + this.clientWillTopic = deviceType + File.separator + "disconnection"; + this.mqttBrokerEndPoint = mqttBrokerEndPoint; + this.timeoutInterval = intervalInMillis; + this.initSubscriber(); + } + + public void setTimeoutInterval(int timeoutInterval) { + this.timeoutInterval = timeoutInterval; + } + + /** + * 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 call-back this current class. + */ + private void initSubscriber() { + try { + client = new MqttClient(this.mqttBrokerEndPoint, clientId, null); + //TODO:: Need to check for debug + log.info("MQTT subscriber was created with ClientID : " + clientId); + } catch (MqttException ex) { + //TODO:: Remove unnecessary formatting and print exception + String errorMsg = "MQTT Client Error\n" + "\tReason: " + ex.getReasonCode() + + "\n\tMessage: " + ex.getMessage() + "\n\tLocalMsg: " + + ex.getLocalizedMessage() + "\n\tCause: " + ex.getCause() + + "\n\tException: " + ex; + log.error(errorMsg); + //TODO:: Throw the error out + } + + options = new MqttConnectOptions(); + options.setCleanSession(false); + //TODO:: Use constant strings + options.setWill(clientWillTopic, "Connection-Lost".getBytes(StandardCharsets.UTF_8), 2, + true); + client.setCallback(this); + } + + /** + * Checks whether the connection to the MQTT-Broker persists. + * + * @return true if the client is connected to the MQTT-Broker, else false. + */ + @Override + public boolean isConnected() { + return client.isConnected(); + } + + + /** + * Connects to the MQTT-Broker and if successfully established connection. + * + * @throws TransportHandlerException in the event of 'Connecting to' the MQTT broker fails. + */ + protected void connectToQueue() throws TransportHandlerException { + try { + client.connect(options); + + if (log.isDebugEnabled()) { + log.debug("Subscriber connected to queue at: " + this.mqttBrokerEndPoint); + } + } catch (MqttSecurityException ex) { + String errorMsg = "MQTT Security Exception when connecting to queue\n" + "\tReason: " + + " " + + ex.getReasonCode() + "\n\tMessage: " + ex.getMessage() + + "\n\tLocalMsg: " + ex.getLocalizedMessage() + "\n\tCause: " + + ex.getCause() + "\n\tException: " + ex; + //TODO:: Compulsory log of errors and remove formatted error + if (log.isDebugEnabled()) { + log.debug(errorMsg); + } + throw new TransportHandlerException(errorMsg, ex); + + } catch (MqttException ex) { + //TODO:: Compulsory log of errors and remove formatted error + String errorMsg = "MQTT Exception when connecting to queue\n" + "\tReason: " + + ex.getReasonCode() + "\n\tMessage: " + ex.getMessage() + + "\n\tLocalMsg: " + ex.getLocalizedMessage() + "\n\tCause: " + + ex.getCause() + "\n\tException: " + ex; + if (log.isDebugEnabled()) { + log.debug(errorMsg); + } + throw new TransportHandlerException(errorMsg, ex); + } + } + + /** + * Subscribes to the MQTT-Topic specific to this MQTT Client. (The MQTT-Topic specific to the + * device is taken in as a constructor parameter of this class) . + * + * @throws TransportHandlerException in the event of 'Subscribing to' the MQTT broker + * fails. + */ + protected void subscribeToQueue() throws TransportHandlerException { + try { + //TODO:: QoS Level take it from a variable + client.subscribe(subscribeTopic, 0); + log.info("Subscriber '" + clientId + "' subscribed to topic: " + subscribeTopic); + } catch (MqttException ex) { + //TODO:: Compulsory log of errors and remove formatted error + String errorMsg = "MQTT Exception when trying to subscribe to topic: " + + subscribeTopic + "\n\tReason: " + ex.getReasonCode() + + "\n\tMessage: " + ex.getMessage() + "\n\tLocalMsg: " + + ex.getLocalizedMessage() + "\n\tCause: " + ex.getCause() + + "\n\tException: " + ex; + if (log.isDebugEnabled()) { + log.debug(errorMsg); + } + + throw new TransportHandlerException(errorMsg, ex); + } + } + + + /** + * This method is used to publish reply-messages for the control signals received. + * Invocation of this method calls its overloaded-method with a QoS equal to that of the + * default value. + * + * @param topic the topic to which the reply message is to be published. + * @param payLoad the reply-message (payload) of the MQTT publish action. + */ + protected void publishToQueue(String topic, String payLoad) + throws TransportHandlerException { + publishToQueue(topic, payLoad, DEFAULT_MQTT_QUALITY_OF_SERVICE, false); + } + + /** + * This is an overloaded method that publishes MQTT reply-messages for control signals + * received form the IoT-Server. + * + * @param topic the topic to which the reply message is to be published + * @param payLoad the reply-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) + */ + protected void publishToQueue(String topic, String payLoad, int qos, boolean retained) + throws TransportHandlerException { + try { + client.publish(topic, payLoad.getBytes(StandardCharsets.UTF_8), qos, retained); + if (log.isDebugEnabled()) { + log.debug("Message: " + payLoad + " to MQTT topic [" + topic + + "] published successfully"); + } + } catch (MqttException ex) { + String errorMsg = + "MQTT Client Error" + "\n\tReason: " + ex.getReasonCode() + "\n\tMessage: " + + ex.getMessage() + "\n\tLocalMsg: " + ex.getLocalizedMessage() + + "\n\tCause: " + ex.getCause() + "\n\tException: " + ex; + log.info(errorMsg); + throw new TransportHandlerException(errorMsg, ex); + } + } + + + protected void publishToQueue(String topic, MqttMessage message) + throws TransportHandlerException { + try { + client.publish(topic, message); + if (log.isDebugEnabled()) { + log.debug("Message: " + message.toString() + " to MQTT topic [" + topic + + "] published successfully"); + } + } catch (MqttException ex) { + //TODO:: Compulsory log of errors and remove formatted error + String errorMsg = + "MQTT Client Error" + "\n\tReason: " + ex.getReasonCode() + "\n\tMessage: " + + ex.getMessage() + "\n\tLocalMsg: " + ex.getLocalizedMessage() + + "\n\tCause: " + ex.getCause() + "\n\tException: " + ex; + log.info(errorMsg); + throw new TransportHandlerException(errorMsg, ex); + } + } + + + /** + * 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.warn("Lost Connection for client: " + this.clientId + + " to " + this.mqttBrokerEndPoint + ".\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 was subscribed to. + * @param mqttMessage the actual MQTT-Message that was received from the broker. + */ + @Override + public void messageArrived(final String topic, final MqttMessage mqttMessage) { + if (log.isDebugEnabled()) { + log.info("Got an MQTT message '" + mqttMessage.toString() + "' for topic '" + topic + "'."); + } + + Thread messageProcessorThread = new Thread() { + public void run() { + processIncomingMessage(mqttMessage, topic); + } + }; + 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 message = ""; + try { + message = iMqttDeliveryToken.getMessage().toString(); + } catch (MqttException e) { + //TODO:: Throw errors + log.error( + "Error occurred whilst trying to read the message from the MQTT delivery " + + "token."); + } + String topic = iMqttDeliveryToken.getTopics()[0]; + String client = iMqttDeliveryToken.getClient().getClientId(); + + if (log.isDebugEnabled()) { + log.debug("Message - '" + message + "' of client [" + client + "] for the topic (" + + topic + ") was delivered successfully."); + } + } + + /** + * Closes the connection to the MQTT Broker. + */ + public void closeConnection() throws MqttException { + if (client != null && isConnected()) { + client.disconnect(); + } + } +} + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/xmpp/XMPPTransportHandler.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/xmpp/XMPPTransportHandler.java new file mode 100644 index 000000000..ba8320c7c --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/transport/xmpp/XMPPTransportHandler.java @@ -0,0 +1,366 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.advanced.transport.xmpp; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.FromContainsFilter; +import org.jivesoftware.smack.filter.OrFilter; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.filter.PacketTypeFilter; +import org.jivesoftware.smack.filter.ToContainsFilter; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.transport.TransportHandler; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.transport.TransportHandlerException; + +/** + * 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 HTTP-Transport specific implementations. The class implements utility methods for the + * case of a HTTP 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 HTTP specific functionality (ideally a device API writer who would like to communicate to the device + * via HTTP Protocol). + *

      + * This class contains the IoT-Server specific implementation for all the XMPP functionality. This includes + * connecting to a XMPP Server & Login-In using the device's/server's XMPP-Account, Setting listeners and filters on + * incoming XMPP messages and Sending XMPP replies for messages received. Makes use of the 'Smack-XMPP' library + * provided by jivesoftware/igniterealtime. + */ +public abstract class XMPPTransportHandler implements TransportHandler { + private static final Log log = LogFactory.getLog(XMPPTransportHandler.class); + + protected String server; + protected int timeoutInterval; // millis + + //TODO:: Shouldnt be hard-coded. Need to be read from configs + private static final int DEFAULT_XMPP_PORT = 5222; + private XMPPConnection connection; + private int port; + private ConnectionConfiguration config; + private PacketFilter filter; + private PacketListener listener; + + + /** + * Constructor for XMPPTransportHandler passing only the server-IP. + * + * @param server the IP of the XMPP server. + */ + protected XMPPTransportHandler(String server) { + this.server = server; + this.port = DEFAULT_XMPP_PORT; + this.timeoutInterval = DEFAULT_TIMEOUT_INTERVAL; + initXMPPClient(); + } + + /** + * Constructor for XMPPTransportHandler passing server-IP and the XMPP-port. + * + * @param server the IP of the XMPP server. + * @param port the XMPP server's port to connect to. (default - 5222) + */ + protected XMPPTransportHandler(String server, int port) { + this.server = server; + this.port = port; + this.timeoutInterval = DEFAULT_TIMEOUT_INTERVAL; + initXMPPClient(); + } + + /** + * Constructor for XMPPTransportHandler passing server-IP, the XMPP-port and the + * timeoutInterval used by listeners to the server and for reconnection schedules. + * + * @param server the IP of the XMPP server. + * @param port the XMPP server's port to connect to. (default - 5222) + * @param timeoutInterval the timeout interval to use for the connection and reconnection + */ + protected XMPPTransportHandler(String server, int port, int timeoutInterval) { + this.server = server; + this.port = port; + this.timeoutInterval = timeoutInterval; + initXMPPClient(); + } + + /** + * Sets the client's time-out-limit whilst waiting for XMPP-replies from server. + * + * @param millis the time in millis to be set as the time-out-limit whilst waiting for a + * XMPP-reply. + */ + public void setTimeoutInterval(int millis) { + this.timeoutInterval = millis; + } + + /** + * Checks whether the connection to the XMPP-Server persists. + * + * @return true if the client is connected to the XMPP-Server, else false. + */ + @Override + public boolean isConnected() { + return connection.isConnected(); + } + + /** + * Initializes the XMPP Client. Sets the time-out-limit whilst waiting for XMPP-replies from + * server. Sets the XMPP configurations to connect to the server and creates the + * XMPPConnection object used for connecting and Logging-In. + */ + private void initXMPPClient() { + log.info(String.format("Initializing connection to XMPP Server at %1$s via port " + + "%2$d.", server, port)); + SmackConfiguration.setPacketReplyTimeout(timeoutInterval); + config = new ConnectionConfiguration(server, port); +// TODO:: Need to enable SASL-Authentication appropriately + config.setSASLAuthenticationEnabled(false); + config.setSecurityMode(ConnectionConfiguration.SecurityMode.disabled); + connection = new XMPPConnection(config); + } + +//TODO:: Re-check all exception handling + + /** + * Connects to the XMPP-Server and if attempt unsuccessful, then throws exception. + * + * @throws TransportHandlerException in the event of 'Connecting to' the XMPP server fails. + */ + protected void connectToServer() throws TransportHandlerException { + try { + connection.connect(); + log.info(String.format("Connection to XMPP Server at %1$s established successfully......", server)); + + } catch (XMPPException xmppExcepion) { + String errorMsg = "Connection attempt to the XMPP Server at " + server + " via port " + port + " failed."; + log.info(errorMsg); + throw new TransportHandlerException(errorMsg, xmppExcepion); + } + } + + /** + * If successfully established connection, then tries to Log in using the device's XMPP + * Account credentials. + * + * @param username the username of the device's XMPP-Account. + * @param password the password of the device's XMPP-Account. + * @param resource the resource the resource, specific to the XMPP-Account to which the login + * is made to + * @throws TransportHandlerException in the event of 'Logging into' the XMPP server fails. + */ + protected void loginToServer(String username, String password, String resource) + throws TransportHandlerException { + if (isConnected()) { + try { + if (resource == null) { + connection.login(username, password); + log.info(String.format("Logged into XMPP Server at %1$s as user %2$s......", server, username)); + } else { + connection.login(username, password, resource); + log.info(String.format("Logged into XMPP Server at %1$s as user %2$s on resource %3$s......", + server, username, resource)); + } + } catch (XMPPException xmppException) { + String errorMsg = + "Login attempt to the XMPP Server at " + server + " with username - " + username + " failed."; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, xmppException); + } + } else { + String errorMsg = "Not connected to XMPP-Server to attempt Login. Please 'connectToServer' before Login"; + throw new TransportHandlerException(errorMsg); + } + } + + + /** + * Sets a filter for all the incoming XMPP-Messages on the Sender's JID (XMPP-Account ID). + * Also creates a listener for the incoming messages and connects the listener to the + * XMPPConnection alongside the set filter. + * + * @param senderJID the JID (XMPP-Account ID of the sender) to which the filter is to be set. + */ + protected void setFilterOnSender(String senderJID) { + filter = new AndFilter(new PacketTypeFilter(Message.class), new FromContainsFilter( + senderJID)); + listener = new PacketListener() { + @Override + public void processPacket(Packet packet) { + if (packet instanceof Message) { + final Message xmppMessage = (Message) packet; + Thread msgProcessThread = new Thread() { + public void run() { + processIncomingMessage(xmppMessage); + } + }; + msgProcessThread.setDaemon(true); + msgProcessThread.start(); + } + } + }; + + connection.addPacketListener(listener, filter); + } + + + /** + * Sets a filter for all the incoming XMPP-Messages on the Receiver's JID (XMPP-Account ID). + * Also creates a listener for the incoming messages and connects the listener to the + * XMPPConnection alongside the set filter. + * + * @param receiverJID the JID (XMPP-Account ID of the receiver) to which the filter is to be + * set. + */ + protected void setFilterOnReceiver(String receiverJID) { + filter = new AndFilter(new PacketTypeFilter(Message.class), new ToContainsFilter( + receiverJID)); + listener = new PacketListener() { + @Override + public void processPacket(Packet packet) { + if (packet instanceof Message) { + final Message xmppMessage = (Message) packet; + Thread msgProcessThread = new Thread() { + public void run() { + processIncomingMessage(xmppMessage); + } + }; + msgProcessThread.setDaemon(true); + msgProcessThread.start(); + } + } + }; + + connection.addPacketListener(listener, filter); + } + + + /** + * Sets a filter for all the incoming XMPP-Messages on the From-JID & To-JID (XMPP-Account IDs) + * passed in. Also creates a listener for the incoming messages and connects the listener to + * the XMPPConnection alongside the set filter. + * + * @param senderJID the From-JID (XMPP-Account ID) to which the filter is to be set. + * @param receiverJID the To-JID (XMPP-Account ID) to which the filter is to be set. + * @param andCondition if true: then filter is set with 'AND' operator (senderJID && + * receiverJID), + * if false: then the filter is set with 'OR' operator (senderJID | + * receiverJID) + */ + protected void setMessageFilterAndListener(String senderJID, String receiverJID, boolean + andCondition) { + PacketFilter jidFilter; + + if (andCondition) { + jidFilter = new AndFilter(new FromContainsFilter(senderJID), new ToContainsFilter( + receiverJID)); + } else { + jidFilter = new OrFilter(new FromContainsFilter(senderJID), new ToContainsFilter( + receiverJID)); + } + + filter = new AndFilter(new PacketTypeFilter(Message.class), jidFilter); + listener = new PacketListener() { + @Override + public void processPacket(Packet packet) { + if (packet instanceof Message) { + final Message xmppMessage = (Message) packet; + Thread msgProcessThread = new Thread() { + public void run() { + processIncomingMessage(xmppMessage); + } + }; + msgProcessThread.setDaemon(true); + msgProcessThread.start(); + } + } + }; + + connection.addPacketListener(listener, filter); + } + + + /** + * Sends an XMPP message. Calls the overloaded method with Subject set to "Reply-From-Device" + * + * @param JID the JID (XMPP Account ID) to which the message is to be sent to. + * @param message the XMPP-Message that is to be sent. + */ + protected void sendXMPPMessage(String JID, String message) { + sendXMPPMessage(JID, message, "XMPP-Message"); + } + + + /** + * Overloaded method to send an XMPP message. Includes the subject to be mentioned in the + * message that is sent. + * + * @param JID the JID (XMPP Account ID) to which the message is to be sent to. + * @param message the XMPP-Message that is to be sent. + * @param subject the subject that the XMPP-Message would carry. + */ + protected void sendXMPPMessage(String JID, String message, String subject) { + Message xmppMessage = new Message(); + xmppMessage.setTo(JID); + xmppMessage.setSubject(subject); + xmppMessage.setBody(message); + xmppMessage.setType(Message.Type.chat); + sendXMPPMessage(JID, xmppMessage); + } + + + /** + * Sends an XMPP message. + * + * @param JID the JID (XMPP Account ID) to which the message is to be sent to. + * @param xmppMessage the XMPP-Message that is to be sent. + */ + protected void sendXMPPMessage(String JID, Message xmppMessage) { + connection.sendPacket(xmppMessage); + if (log.isDebugEnabled()) { + log.debug("Message: '" + xmppMessage.getBody() + "' sent to XMPP JID [" + JID + + "] sent successfully."); + } + } + + + /** + * Disables default debugger provided by the XMPPConnection. + */ + protected void disableDebugger() { + connection.DEBUG_ENABLED = false; + } + + + /** + * Closes the connection to the XMPP Server. + */ + public void closeConnection() { + if (connection != null && isConnected()) { + connection.disconnect(); + } + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/virtual/VirtualHardwareManager.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/virtual/VirtualHardwareManager.java new file mode 100644 index 000000000..59a13bb87 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/virtual/VirtualHardwareManager.java @@ -0,0 +1,218 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.advanced.virtual; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.core.AgentConstants; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.core.AgentUtilOperations; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.virtual.ui.AgentUI; + +import javax.sound.midi.InvalidMidiDataException; +import javax.sound.midi.MidiSystem; +import javax.sound.midi.MidiUnavailableException; +import javax.sound.midi.Sequence; +import javax.sound.midi.Sequencer; +import javax.sound.sampled.Clip; +import javax.swing.*; +import java.io.IOException; +import java.io.InputStream; + +/** + * This class use to emulate virtual hardware functionality + */ +public class VirtualHardwareManager { + + private static final Log log = LogFactory.getLog(VirtualHardwareManager.class); + + private static VirtualHardwareManager virtualHardwareManager; + + private AgentUI agentUI; + private Sequencer sequencer = null; + + private int temperature = 30, humidity = 30; + private int temperatureMin = 20, temperatureMax = 50, humidityMin = 20, humidityMax = 50; + private int temperatureSVF = 50, humiditySVF = 50; + private boolean isTemperatureRandomized, isHumidityRandomized; + private boolean isTemperatureSmoothed, isHumiditySmoothed; + + private VirtualHardwareManager(){ + } + + public static VirtualHardwareManager getInstance(){ + if (virtualHardwareManager == null){ + virtualHardwareManager = new VirtualHardwareManager(); + } + return virtualHardwareManager; + } + + public void init(){ + try { + // Set System L&F for Device UI + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (UnsupportedLookAndFeelException e) { + log.error( + "'UnsupportedLookAndFeelException' error occurred whilst initializing the" + + " Agent UI."); + } catch (ClassNotFoundException e) { + log.error( + "'ClassNotFoundException' error occurred whilst initializing the Agent UI."); + } catch (InstantiationException e) { + log.error( + "'InstantiationException' error occurred whilst initializing the Agent UI."); + } catch (IllegalAccessException e) { + log.error( + "'IllegalAccessException' error occurred whilst initializing the Agent UI."); + } + java.awt.EventQueue.invokeLater(new Runnable() { + public void run() { + agentUI = new AgentUI(); + agentUI.setVisible(true); + } + }); + setAudioSequencer(); + } + + /** + * Get temperature from emulated device + * @return Temperature + */ + public int getTemperature() { + if (isTemperatureRandomized) { + temperature = getRandom(temperatureMax, temperatureMin, temperature, + isTemperatureSmoothed, temperatureSVF); + agentUI.updateTemperature(temperature); + } + return temperature; + } + + public void setTemperature(int temperature) { + this.temperature = temperature; + } + + /** + * Get humidity from emulated device + * @return Humidity + */ + public int getHumidity() { + if (isHumidityRandomized) { + humidity = getRandom(humidityMax, humidityMin, humidity, isHumiditySmoothed, + humiditySVF); + agentUI.updateHumidity(humidity); + } + return humidity; + } + + public void setHumidity(int humidity) { + this.humidity = humidity; + } + + public void setTemperatureMin(int temperatureMin) { + this.temperatureMin = temperatureMin; + } + + public void setTemperatureMax(int temperatureMax) { + this.temperatureMax = temperatureMax; + } + + public void setHumidityMin(int humidityMin) { + this.humidityMin = humidityMin; + } + + public void setHumidityMax(int humidityMax) { + this.humidityMax = humidityMax; + } + + public void setIsHumidityRandomized(boolean isHumidityRandomized) { + this.isHumidityRandomized = isHumidityRandomized; + } + + public void setIsTemperatureRandomized(boolean isTemperatureRandomized) { + this.isTemperatureRandomized = isTemperatureRandomized; + } + + public void setTemperatureSVF(int temperatureSVF) { + this.temperatureSVF = temperatureSVF; + } + + public void setHumiditySVF(int humiditySVF) { + this.humiditySVF = humiditySVF; + } + + public void setIsTemperatureSmoothed(boolean isTemperatureSmoothed) { + this.isTemperatureSmoothed = isTemperatureSmoothed; + } + + public void setIsHumiditySmoothed(boolean isHumiditySmoothed) { + this.isHumiditySmoothed = isHumiditySmoothed; + } + + public void changeAlarmStatus(boolean isOn) { + agentUI.setAlarmStatus(isOn); + + if (isOn) { + sequencer.start(); + } else { + sequencer.stop(); + } + } + + public void addToPolicyLog(String policy) { + agentUI.addToPolicyLog(policy); + } + + + private int getRandom(int max, int min, int current, boolean isSmoothed, int svf) { + + if (isSmoothed) { + int offset = (max - min) * svf / 100; + double mx = current + offset; + max = (mx > max) ? max : (int) Math.round(mx); + + double mn = current - offset; + min = (mn < min) ? min : (int) Math.round(mn); + } + + double rnd = Math.random() * (max - min) + min; + return (int) Math.round(rnd); + + } + + private void setAudioSequencer() { + InputStream audioSrc = AgentUtilOperations.class.getResourceAsStream( + "/" + AgentConstants.AUDIO_FILE_NAME); + Sequence sequence; + + try { + sequence = MidiSystem.getSequence(audioSrc); + sequencer = MidiSystem.getSequencer(); + sequencer.open(); + sequencer.setSequence(sequence); + } catch (InvalidMidiDataException e) { + log.error("AudioReader: Error whilst setting MIDI Audio reader sequence"); + } catch (IOException e) { + log.error("AudioReader: Error whilst getting audio sequence from stream"); + } catch (MidiUnavailableException e) { + log.error("AudioReader: Error whilst openning MIDI Audio reader sequencer"); + } + + sequencer.setLoopCount(Clip.LOOP_CONTINUOUSLY); + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/virtual/ui/AgentUI.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/virtual/ui/AgentUI.java new file mode 100644 index 000000000..96546e5b6 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/advanced/virtual/ui/AgentUI.java @@ -0,0 +1,1085 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.advanced.virtual.ui; + +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.core.AgentConstants; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.core.AgentManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.core.AgentUtilOperations; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.virtual.VirtualHardwareManager; + +import javax.swing.*; +import java.awt.*; +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; + +public class AgentUI extends JFrame { + + private boolean isTemperatureRandomized, isHumidityRandomized; + private boolean isTemperatureSmoothed, isHumiditySmoothed; + + private volatile boolean isAlarmOn = false; + + private final Object _lock = new Object(); + + private JLabel picLabelBulbOn, picLabelBulbOff; + + private volatile java.util.List policyLogs = new ArrayList<>(); + + // Variables declaration - do not modify + private JButton btnControl; + private JButton btnView; + private JCheckBox chkbxEmulate; + private JCheckBox chkbxHumidityRandom; + private JCheckBox chkbxHumiditySmooth; + private JCheckBox chkbxTemperatureRandom; + private JCheckBox chkbxTemperatureSmooth; + private JComboBox cmbInterface; + private JComboBox cmbPeriod; + private JComboBox cmbProtocol; + private JLabel jLabel1; + private JLabel jLabel10; + private JLabel jLabel11; + private JLabel jLabel12; + private JLabel jLabel2; + private JLabel jLabel20; + private JLabel jLabel23; + private JLabel jLabel24; + private JLabel jLabel25; + private JLabel jLabel3; + private JLabel jLabel4; + private JLabel jLabel5; + private JLabel jLabel6; + private JLabel jLabel7; + private JLabel jLabel8; + private JLabel jLabel9; + private JPanel jPanel1; + private JPanel jPanel2; + private JPanel jPanel3; + private JPanel jPanel4; + private JPanel jPanel6; + private JPanel jPanel7; + private JPanel jPanel8; + private JPanel jPanel9; + private JScrollPane jScrollPane1; + private JSeparator jSeparator1; + private JSeparator jSeparator5; + private JLabel lblAgentName; + private JLabel lblStatus; + private JPanel pnlBulbStatus; + private JSpinner spinnerHumidity; + private JSpinner spinnerInterval; + private JSpinner spinnerTemperature; + private JTextArea txtAreaLogs; + private JTextField txtHumidityMax; + private JTextField txtHumidityMin; + private JTextField txtHumiditySVF; + private JTextField txtTemperatureMax; + private JTextField txtTemperatureMin; + private JTextField txtTemperatureSVF; + // End of variables declaration + + //Update UI from AgentManager changes + private Runnable uiUpdater = new Runnable() { + @Override + public void run() { + while (true) { + EventQueue.invokeLater(new Runnable() { + @Override + public void run() { + pnlBulbStatus.removeAll(); + pnlBulbStatus.add(isAlarmOn ? picLabelBulbOn : picLabelBulbOff); + pnlBulbStatus.updateUI(); + lblStatus.setText(AgentManager.getInstance().getAgentStatus()); + String policy = getPolicyLog(); + if (policy != null){ + txtAreaLogs.append("\n" + policy); + txtAreaLogs.append("\n--------------------------------------------------\n"); + } + if (isTemperatureRandomized) { + txtTemperatureMinActionPerformed(null); + txtTemperatureMaxActionPerformed(null); + if (isTemperatureSmoothed) { + txtTemperatureSVFActionPerformed(null); + } + } + if (isHumidityRandomized) { + txtHumidityMinActionPerformed(null); + txtHumidityMaxActionPerformed(null); + if (isHumiditySmoothed) { + txtHumiditySVFActionPerformed(null); + } + } + } + }); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + break; + } + } + } + }; + + + /** + * Creates new form AgentUI + */ + public AgentUI() { + initComponents(); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + private void initComponents() { + + lblAgentName = new JLabel(); + jLabel2 = new JLabel(); + jPanel1 = new JPanel(); + jLabel3 = new JLabel(); + pnlBulbStatus = new JPanel(); + jPanel2 = new JPanel(); + jLabel4 = new JLabel(); + chkbxTemperatureRandom = new JCheckBox(); + jSeparator1 = new JSeparator(); + jPanel7 = new JPanel(); + jLabel5 = new JLabel(); + txtTemperatureMin = new JTextField(); + jLabel6 = new JLabel(); + txtTemperatureMax = new JTextField(); + jLabel10 = new JLabel(); + txtTemperatureSVF = new JTextField(); + spinnerTemperature = new JSpinner(); + chkbxTemperatureSmooth = new JCheckBox(); + jPanel6 = new JPanel(); + jLabel20 = new JLabel(); + btnView = new JButton(); + btnControl = new JButton(); + lblStatus = new JLabel(); + jPanel8 = new JPanel(); + jLabel23 = new JLabel(); + chkbxHumidityRandom = new JCheckBox(); + jSeparator5 = new JSeparator(); + jPanel9 = new JPanel(); + jLabel24 = new JLabel(); + txtHumidityMin = new JTextField(); + jLabel25 = new JLabel(); + txtHumidityMax = new JTextField(); + txtHumiditySVF = new JTextField(); + jLabel11 = new JLabel(); + spinnerHumidity = new JSpinner(); + chkbxHumiditySmooth = new JCheckBox(); + jPanel3 = new JPanel(); + jLabel7 = new JLabel(); + spinnerInterval = new JSpinner(); + jLabel8 = new JLabel(); + jLabel9 = new JLabel(); + cmbProtocol = new JComboBox(); + jLabel12 = new JLabel(); + cmbInterface = new JComboBox(); + jScrollPane1 = new JScrollPane(); + txtAreaLogs = new JTextArea(); + jPanel4 = new JPanel(); + chkbxEmulate = new JCheckBox(); + cmbPeriod = new JComboBox(); + jLabel1 = new JLabel(); + + setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + setTitle("Fire Alarm Emulator"); + setResizable(false); + Dimension dim = Toolkit.getDefaultToolkit().getScreenSize(); + setLocation(dim.width / 2 - 650 / 2, dim.height / 2 - 440 / 2); + + lblAgentName.setFont(new Font("Cantarell", 1, 24)); // NOI18N + lblAgentName.setHorizontalAlignment(SwingConstants.LEFT); + lblAgentName.setText("Device Name: " + AgentManager.getInstance().getDeviceName()); + + jLabel2.setHorizontalAlignment(SwingConstants.CENTER); + jLabel2.setText("Copyright (c) 2015, WSO2 Inc."); + + jPanel1.setBackground(new Color(220, 220, 220)); + + jLabel3.setFont(new Font("Cantarell", 0, 18)); // NOI18N + jLabel3.setHorizontalAlignment(SwingConstants.CENTER); + jLabel3.setText("Alarm Status"); + + pnlBulbStatus.setBackground(new Color(220, 220, 220)); + + GroupLayout pnlBulbStatusLayout = new GroupLayout(pnlBulbStatus); + pnlBulbStatus.setLayout(pnlBulbStatusLayout); + pnlBulbStatusLayout.setHorizontalGroup( + pnlBulbStatusLayout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGap(0, 0, Short.MAX_VALUE) + ); + pnlBulbStatusLayout.setVerticalGroup( + pnlBulbStatusLayout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGap(0, 167, Short.MAX_VALUE) + ); + + GroupLayout jPanel1Layout = new GroupLayout(jPanel1); + jPanel1.setLayout(jPanel1Layout); + jPanel1Layout.setHorizontalGroup( + jPanel1Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel1Layout.createParallelGroup( + GroupLayout.Alignment.TRAILING) + .addComponent(pnlBulbStatus, + GroupLayout + .DEFAULT_SIZE, + GroupLayout.DEFAULT_SIZE, + Short.MAX_VALUE) + .addComponent(jLabel3, + GroupLayout.DEFAULT_SIZE, + 190, Short.MAX_VALUE)) + .addContainerGap()) + ); + jPanel1Layout.setVerticalGroup( + jPanel1Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel1Layout.createSequentialGroup() + .addContainerGap() + .addComponent(jLabel3) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(pnlBulbStatus, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addContainerGap()) + ); + + jPanel2.setBackground(new Color(220, 220, 220)); + + jLabel4.setFont(new Font("Cantarell", 0, 18)); // NOI18N + jLabel4.setHorizontalAlignment(SwingConstants.CENTER); + jLabel4.setText("Temperature"); + + chkbxTemperatureRandom.setText("Randomize Data"); + chkbxTemperatureRandom.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxTemperatureRandomActionPerformed(evt); + } + }); + + jSeparator1.setOrientation(SwingConstants.VERTICAL); + + jPanel7.setBackground(new Color(220, 220, 220)); + + jLabel5.setHorizontalAlignment(SwingConstants.LEFT); + jLabel5.setText("Min"); + + txtTemperatureMin.setHorizontalAlignment(JTextField.CENTER); + txtTemperatureMin.setText("20"); + txtTemperatureMin.setEnabled(false); + txtTemperatureMin.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtTemperatureMinActionPerformed(evt); + } + }); + + jLabel6.setHorizontalAlignment(SwingConstants.RIGHT); + jLabel6.setText("Max"); + + txtTemperatureMax.setHorizontalAlignment(JTextField.CENTER); + txtTemperatureMax.setText("50"); + txtTemperatureMax.setEnabled(false); + txtTemperatureMax.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtTemperatureMaxActionPerformed(evt); + } + }); + + jLabel10.setHorizontalAlignment(SwingConstants.RIGHT); + jLabel10.setText("SV %"); + + txtTemperatureSVF.setHorizontalAlignment(JTextField.CENTER); + txtTemperatureSVF.setText("50"); + txtTemperatureSVF.setEnabled(false); + txtTemperatureSVF.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtTemperatureSVFActionPerformed(evt); + } + }); + + GroupLayout jPanel7Layout = new GroupLayout(jPanel7); + jPanel7.setLayout(jPanel7Layout); + jPanel7Layout.setHorizontalGroup( + jPanel7Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel7Layout.createSequentialGroup() + .addComponent(jLabel5) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtTemperatureMin, GroupLayout.PREFERRED_SIZE, 45, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel6) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtTemperatureMax, GroupLayout.PREFERRED_SIZE, 45, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel10) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtTemperatureSVF, GroupLayout.PREFERRED_SIZE, 45, GroupLayout.PREFERRED_SIZE) + .addContainerGap(GroupLayout.DEFAULT_SIZE, + Short.MAX_VALUE)) + ); + jPanel7Layout.setVerticalGroup( + jPanel7Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(GroupLayout.Alignment.TRAILING, jPanel7Layout + .createSequentialGroup() + .addContainerGap(GroupLayout.DEFAULT_SIZE, + Short.MAX_VALUE) + .addGroup(jPanel7Layout.createParallelGroup( + GroupLayout.Alignment.BASELINE) + .addComponent(txtTemperatureMin, + GroupLayout + .PREFERRED_SIZE, + GroupLayout + .DEFAULT_SIZE, + GroupLayout.PREFERRED_SIZE) + .addComponent(txtTemperatureMax, + GroupLayout.PREFERRED_SIZE, + GroupLayout.DEFAULT_SIZE, + GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel6) + .addComponent(jLabel5) + .addComponent(jLabel10) + .addComponent(txtTemperatureSVF, + GroupLayout.PREFERRED_SIZE, + GroupLayout.DEFAULT_SIZE, + GroupLayout.PREFERRED_SIZE)) + .addGap(35, 35, 35)) + ); + + spinnerTemperature.setFont(new Font("Cantarell", 1, 24)); // NOI18N + spinnerTemperature.setModel(new SpinnerNumberModel(30, 0, 100, 1)); + spinnerTemperature.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + spinnerTemperatureStateChanged(evt); + } + }); + + chkbxTemperatureSmooth.setText("Smooth Variation"); + chkbxTemperatureSmooth.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxTemperatureSmoothActionPerformed(evt); + } + }); + + GroupLayout jPanel2Layout = new GroupLayout(jPanel2); + jPanel2.setLayout(jPanel2Layout); + jPanel2Layout.setHorizontalGroup( + jPanel2Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel2Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel2Layout.createParallelGroup(GroupLayout.Alignment.LEADING, false) + .addComponent(jLabel4, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(spinnerTemperature)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jSeparator1, GroupLayout.PREFERRED_SIZE, 6, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanel2Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addComponent(jPanel7, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addGroup(jPanel2Layout.createSequentialGroup() + .addComponent(chkbxTemperatureRandom) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(chkbxTemperatureSmooth))) + .addContainerGap()) + ); + jPanel2Layout.setVerticalGroup( + jPanel2Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel2Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel2Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addComponent(jSeparator1) + .addGroup(jPanel2Layout.createSequentialGroup() + .addGroup(jPanel2Layout.createParallelGroup( + GroupLayout.Alignment.BASELINE) + .addComponent( + chkbxTemperatureRandom) + .addComponent( + chkbxTemperatureSmooth)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel7, GroupLayout.PREFERRED_SIZE, 51, GroupLayout.PREFERRED_SIZE) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(jPanel2Layout.createSequentialGroup() + .addComponent(jLabel4, GroupLayout.PREFERRED_SIZE, 23, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(spinnerTemperature))) + .addContainerGap()) + ); + + jPanel6.setBackground(new Color(253, 254, 209)); + + jLabel20.setText("Connection Status:"); + jLabel20.setVerticalTextPosition(SwingConstants.TOP); + + btnView.setText("View Device Data"); + btnView.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + btnViewMouseClicked(evt); + } + }); + + btnControl.setText("Control Device"); + btnControl.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + btnControlMouseClicked(evt); + } + }); + + lblStatus.setFont(new Font("Cantarell", 1, 15)); // NOI18N + lblStatus.setText("Not Connected"); + + GroupLayout jPanel6Layout = new GroupLayout(jPanel6); + jPanel6.setLayout(jPanel6Layout); + jPanel6Layout.setHorizontalGroup( + jPanel6Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel6Layout.createSequentialGroup() + .addContainerGap() + .addComponent(jLabel20) + .addPreferredGap(LayoutStyle + .ComponentPlacement.RELATED) + .addComponent(lblStatus, GroupLayout + .DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(btnControl) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(btnView) + .addContainerGap()) + ); + jPanel6Layout.setVerticalGroup( + jPanel6Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(GroupLayout.Alignment.TRAILING, jPanel6Layout.createSequentialGroup() + .addContainerGap(GroupLayout.DEFAULT_SIZE, + Short.MAX_VALUE) + .addGroup(jPanel6Layout.createParallelGroup( + GroupLayout.Alignment.LEADING, false) + .addComponent(jLabel20, + GroupLayout.DEFAULT_SIZE, + GroupLayout.DEFAULT_SIZE, + Short.MAX_VALUE) + .addGroup(jPanel6Layout.createParallelGroup( + GroupLayout.Alignment.BASELINE) + .addComponent(btnView, + GroupLayout.DEFAULT_SIZE, + GroupLayout.DEFAULT_SIZE, + Short.MAX_VALUE) + .addComponent(btnControl) + .addComponent(lblStatus))) + .addContainerGap()) + ); + + jPanel8.setBackground(new Color(220, 220, 220)); + + jLabel23.setFont(new Font("Cantarell", 0, 18)); // NOI18N + jLabel23.setHorizontalAlignment(SwingConstants.CENTER); + jLabel23.setText("Humidity"); + + chkbxHumidityRandom.setText("Randomize Data"); + chkbxHumidityRandom.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxHumidityRandomActionPerformed(evt); + } + }); + + jSeparator5.setOrientation(SwingConstants.VERTICAL); + + jPanel9.setBackground(new Color(220, 220, 220)); + + jLabel24.setHorizontalAlignment(SwingConstants.LEFT); + jLabel24.setText("Min"); + + txtHumidityMin.setHorizontalAlignment(JTextField.CENTER); + txtHumidityMin.setText("20"); + txtHumidityMin.setEnabled(false); + txtHumidityMin.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtHumidityMinActionPerformed(evt); + } + }); + + jLabel25.setHorizontalAlignment(SwingConstants.RIGHT); + jLabel25.setText("Max"); + + txtHumidityMax.setHorizontalAlignment(JTextField.CENTER); + txtHumidityMax.setText("50"); + txtHumidityMax.setEnabled(false); + txtHumidityMax.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtHumidityMaxActionPerformed(evt); + } + }); + + txtHumiditySVF.setHorizontalAlignment(JTextField.CENTER); + txtHumiditySVF.setText("50"); + txtHumiditySVF.setEnabled(false); + txtHumiditySVF.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtHumiditySVFActionPerformed(evt); + } + }); + + jLabel11.setHorizontalAlignment(SwingConstants.RIGHT); + jLabel11.setText("SV %"); + + GroupLayout jPanel9Layout = new GroupLayout(jPanel9); + jPanel9.setLayout(jPanel9Layout); + jPanel9Layout.setHorizontalGroup( + jPanel9Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel9Layout.createSequentialGroup() + .addComponent(jLabel24) + .addPreferredGap(LayoutStyle + .ComponentPlacement.RELATED) + .addComponent(txtHumidityMin, GroupLayout + .PREFERRED_SIZE, 45, GroupLayout + .PREFERRED_SIZE) + .addPreferredGap(LayoutStyle + .ComponentPlacement.RELATED) + .addComponent(jLabel25) + .addPreferredGap(LayoutStyle + .ComponentPlacement.RELATED) + .addComponent(txtHumidityMax, GroupLayout.PREFERRED_SIZE, 45, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel11) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtHumiditySVF, GroupLayout.PREFERRED_SIZE, 45, GroupLayout.PREFERRED_SIZE) + .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + jPanel9Layout.setVerticalGroup( + jPanel9Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(GroupLayout.Alignment.TRAILING, jPanel9Layout.createSequentialGroup() + .addContainerGap(GroupLayout.DEFAULT_SIZE, + Short.MAX_VALUE) + .addGroup(jPanel9Layout.createParallelGroup( + GroupLayout.Alignment.LEADING) + .addGroup(jPanel9Layout.createParallelGroup( + GroupLayout.Alignment.BASELINE) + .addComponent(jLabel11) + .addComponent(txtHumiditySVF, + GroupLayout.PREFERRED_SIZE, + GroupLayout.DEFAULT_SIZE, + GroupLayout.PREFERRED_SIZE)) + .addGroup(jPanel9Layout.createParallelGroup( + GroupLayout.Alignment.BASELINE) + .addComponent(txtHumidityMin, + GroupLayout.PREFERRED_SIZE, + GroupLayout.DEFAULT_SIZE, + GroupLayout.PREFERRED_SIZE) + .addComponent(txtHumidityMax, + GroupLayout.PREFERRED_SIZE, + GroupLayout.DEFAULT_SIZE, + GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel25) + .addComponent(jLabel24))) + .addGap(35, 35, 35)) + ); + + spinnerHumidity.setFont(new Font("Cantarell", 1, 24)); // NOI18N + spinnerHumidity.setModel(new SpinnerNumberModel(30, 0, 100, 1)); + spinnerHumidity.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + spinnerHumidityStateChanged(evt); + } + }); + + chkbxHumiditySmooth.setText("Smooth Variation"); + chkbxHumiditySmooth.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxHumiditySmoothActionPerformed(evt); + } + }); + + GroupLayout jPanel8Layout = new GroupLayout(jPanel8); + jPanel8.setLayout(jPanel8Layout); + jPanel8Layout.setHorizontalGroup( + jPanel8Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel8Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel8Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addComponent(jLabel23, GroupLayout.PREFERRED_SIZE, 100, GroupLayout.PREFERRED_SIZE) + .addComponent(spinnerHumidity)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jSeparator5, GroupLayout.PREFERRED_SIZE, 6, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanel8Layout.createParallelGroup(GroupLayout.Alignment.LEADING, false) + .addComponent(jPanel9, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addGroup(jPanel8Layout.createSequentialGroup() + .addComponent(chkbxHumidityRandom) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(chkbxHumiditySmooth))) + .addContainerGap()) + ); + jPanel8Layout.setVerticalGroup( + jPanel8Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel8Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel8Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addComponent(jSeparator5) + .addGroup(jPanel8Layout.createSequentialGroup() + .addGroup(jPanel8Layout.createParallelGroup( + GroupLayout.Alignment.BASELINE) + .addComponent( + chkbxHumidityRandom) + .addComponent( + chkbxHumiditySmooth)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel9, GroupLayout.PREFERRED_SIZE, 51, GroupLayout.PREFERRED_SIZE) + .addGap(0, 1, Short.MAX_VALUE)) + .addGroup(jPanel8Layout.createSequentialGroup() + .addComponent(jLabel23, GroupLayout.PREFERRED_SIZE, 23, GroupLayout.PREFERRED_SIZE) + .addPreferredGap + (LayoutStyle.ComponentPlacement.RELATED) + .addComponent + (spinnerHumidity))) + .addContainerGap()) + ); + + jPanel3.setBackground(new Color(207, 233, 234)); + + jLabel7.setText("Data Push Interval:"); + + spinnerInterval.setModel(new SpinnerNumberModel(Integer.valueOf(5), Integer + .valueOf(1), null, Integer.valueOf(1))); + spinnerInterval.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + spinnerIntervalStateChanged(evt); + } + }); + + jLabel8.setText("Seconds"); + + jLabel9.setText("Protocol:"); + + cmbProtocol.setModel(new DefaultComboBoxModel(new String[] { "MQTT", "XMPP", + "HTTP" })); + cmbProtocol.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cmbProtocolActionPerformed(evt); + } + }); + + jLabel12.setText("Interface:"); + + cmbInterface.setModel(new DefaultComboBoxModel(new String[] { "eth0" })); + cmbInterface.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cmbInterfaceActionPerformed(evt); + } + }); + + GroupLayout jPanel3Layout = new GroupLayout(jPanel3); + jPanel3.setLayout(jPanel3Layout); + jPanel3Layout.setHorizontalGroup( + jPanel3Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel3Layout.createSequentialGroup() + .addContainerGap() + .addComponent(jLabel7) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(spinnerInterval, GroupLayout.PREFERRED_SIZE, 55, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel8) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jLabel12) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cmbInterface, GroupLayout.PREFERRED_SIZE, 100, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel9) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cmbProtocol, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + ); + jPanel3Layout.setVerticalGroup( + jPanel3Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(GroupLayout.Alignment.TRAILING, jPanel3Layout + .createSequentialGroup() + .addContainerGap(GroupLayout.DEFAULT_SIZE, + Short.MAX_VALUE) + .addGroup(jPanel3Layout.createParallelGroup( + GroupLayout.Alignment.LEADING) + .addGroup(jPanel3Layout.createParallelGroup( + GroupLayout.Alignment.BASELINE) + .addComponent(jLabel12) + .addComponent(cmbInterface, + GroupLayout.PREFERRED_SIZE, + GroupLayout.DEFAULT_SIZE, + GroupLayout.PREFERRED_SIZE)) + .addGroup(jPanel3Layout.createParallelGroup( + GroupLayout.Alignment.BASELINE) + .addComponent(jLabel7) + .addComponent(spinnerInterval, + GroupLayout.PREFERRED_SIZE, + GroupLayout.DEFAULT_SIZE, + GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel8) + .addComponent(jLabel9) + .addComponent(cmbProtocol, + GroupLayout.PREFERRED_SIZE, + GroupLayout.DEFAULT_SIZE, + GroupLayout.PREFERRED_SIZE))) + .addContainerGap()) + ); + + txtAreaLogs.setBackground(new Color(1, 1, 1)); + txtAreaLogs.setColumns(20); + txtAreaLogs.setFont(new Font("Courier 10 Pitch", Font.BOLD, 9)); // NOI18N + txtAreaLogs.setForeground(new Color(0, 255, 0)); + txtAreaLogs.setRows(5); + jScrollPane1.setViewportView(txtAreaLogs); + + jPanel4.setBackground(new Color(169, 253, 173)); + + chkbxEmulate.setText("Emulate data"); + chkbxEmulate.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxEmulateActionPerformed(evt); + } + }); + + cmbPeriod.setModel(new DefaultComboBoxModel(new String[] { "1 hour", "1 day", "1 week", "1 month " })); + cmbPeriod.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cmbPeriodActionPerformed(evt); + } + }); + + jLabel1.setText("Emulation Period"); + + GroupLayout jPanel4Layout = new GroupLayout(jPanel4); + jPanel4.setLayout(jPanel4Layout); + jPanel4Layout.setHorizontalGroup( + jPanel4Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel4Layout.createSequentialGroup() + .addContainerGap() + .addComponent(chkbxEmulate) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jLabel1) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cmbPeriod, GroupLayout.PREFERRED_SIZE, 162, GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + ); + jPanel4Layout.setVerticalGroup( + jPanel4Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(GroupLayout.Alignment.TRAILING, jPanel4Layout.createSequentialGroup() + .addContainerGap(GroupLayout.DEFAULT_SIZE, + Short.MAX_VALUE) + .addGroup(jPanel4Layout.createParallelGroup( + GroupLayout.Alignment.BASELINE) + .addComponent(chkbxEmulate) + .addComponent(cmbPeriod, + GroupLayout.PREFERRED_SIZE, + GroupLayout.DEFAULT_SIZE, + GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel1)) + .addContainerGap()) + ); + + GroupLayout layout = new GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addComponent(jScrollPane1) + .addComponent(lblAgentName, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanel6, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(jPanel1, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup( + GroupLayout.Alignment.LEADING) + .addComponent( + jPanel8, + GroupLayout.DEFAULT_SIZE, + GroupLayout.DEFAULT_SIZE, + Short.MAX_VALUE) + .addComponent( + jPanel2, + GroupLayout.DEFAULT_SIZE, + GroupLayout.DEFAULT_SIZE, + Short.MAX_VALUE))) + .addComponent(jLabel2, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanel4, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanel3, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(lblAgentName, GroupLayout.PREFERRED_SIZE, 53, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel6, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(jPanel2, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel8, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) + .addComponent(jPanel1, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jScrollPane1, GroupLayout.PREFERRED_SIZE, 115, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel3, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel4, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel2, GroupLayout.PREFERRED_SIZE, 28, GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + ); + + pack(); + + chkbxTemperatureSmooth.setEnabled(false); + chkbxTemperatureSmooth.setEnabled(false); + + cmbInterface.removeAllItems(); + for (String item : AgentManager.getInstance().getInterfaceList()){ + cmbInterface.addItem(item); + } + cmbInterface.setEnabled(false); + + cmbProtocol.removeAllItems(); + for (String item : AgentManager.getInstance().getProtocolList()){ + cmbProtocol.addItem(item); + } + cmbProtocol.setSelectedItem(AgentConstants.DEFAULT_PROTOCOL); + + URL urlAlarmOn = this.getClass().getResource("/alarm-on.gif"); + ImageIcon imageIconAlarmOn = new ImageIcon(urlAlarmOn); + + URL urlAlarmOff = this.getClass().getResource("/alarm-off.gif"); + ImageIcon imageIconAlarmOff = new ImageIcon(urlAlarmOff); + + picLabelBulbOn = new JLabel(imageIconAlarmOn); + picLabelBulbOn.setSize(pnlBulbStatus.getSize()); + + picLabelBulbOff = new JLabel(imageIconAlarmOff); + picLabelBulbOff.setSize(pnlBulbStatus.getSize()); + + addToPolicyLog(AgentUtilOperations.formatMessage(AgentManager.getInstance().getInitialPolicy())); + new Thread(uiUpdater).start(); + + AgentManager.getInstance().setDeviceReady(true); + } + + private void btnControlMouseClicked(java.awt.event.MouseEvent evt) { + Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null; + if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) { + try { + URI uri = new URI(AgentManager.getInstance().getDeviceMgtControlUrl()); + desktop.browse(uri); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private void btnViewMouseClicked(java.awt.event.MouseEvent evt) { + Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null; + if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) { + try { + URI uri = new URI(AgentManager.getInstance().getDeviceMgtAnalyticUrl()); + desktop.browse(uri); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private void chkbxTemperatureRandomActionPerformed(java.awt.event.ActionEvent evt) { + isTemperatureRandomized = chkbxTemperatureRandom.isSelected(); + VirtualHardwareManager.getInstance().setIsTemperatureRandomized(isTemperatureRandomized); + spinnerTemperature.setEnabled(!isTemperatureRandomized); + txtTemperatureMax.setEnabled(isTemperatureRandomized); + txtTemperatureMin.setEnabled(isTemperatureRandomized); + chkbxTemperatureSmooth.setEnabled(isTemperatureRandomized); + txtTemperatureSVF.setEnabled(isTemperatureRandomized && isTemperatureSmoothed); + } + + private void chkbxHumidityRandomActionPerformed(java.awt.event.ActionEvent evt) { + isHumidityRandomized = chkbxHumidityRandom.isSelected(); + VirtualHardwareManager.getInstance().setIsHumidityRandomized(isHumidityRandomized); + spinnerHumidity.setEnabled(!isHumidityRandomized); + txtHumidityMax.setEnabled(isHumidityRandomized); + txtHumidityMin.setEnabled(isHumidityRandomized); + chkbxHumiditySmooth.setEnabled(isHumidityRandomized); + txtTemperatureSVF.setEnabled(isHumidityRandomized && isHumiditySmoothed); + } + + private void spinnerTemperatureStateChanged(javax.swing.event.ChangeEvent evt) { + if (!isTemperatureRandomized) { + try { + int temperature = Integer.parseInt(spinnerTemperature.getValue().toString()); + VirtualHardwareManager.getInstance().setTemperature(temperature); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(this, "Invalid temperature value", "Error", JOptionPane.ERROR_MESSAGE); + spinnerTemperature.setValue(VirtualHardwareManager.getInstance().getTemperature()); + } + } + } + + private void spinnerHumidityStateChanged(javax.swing.event.ChangeEvent evt) { + if (!isHumidityRandomized) { + try { + int humidity = Integer.parseInt(spinnerHumidity.getValue().toString()); + VirtualHardwareManager.getInstance().setHumidity(humidity); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(this, "Invalid humidity value", "Error", JOptionPane.ERROR_MESSAGE); + spinnerHumidity.setValue(VirtualHardwareManager.getInstance().getHumidity()); + } + } + } + + private void txtTemperatureMinActionPerformed(java.awt.event.ActionEvent evt) { + try { + int temperature = Integer.parseInt(txtTemperatureMin.getText()); + VirtualHardwareManager.getInstance().setTemperatureMin(temperature); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(this, "Invalid temperature value", "Error", JOptionPane.ERROR_MESSAGE); + txtTemperatureMin.setText("20"); + } + } + + private void txtTemperatureMaxActionPerformed(java.awt.event.ActionEvent evt) { + try { + int temperature = Integer.parseInt(txtTemperatureMax.getText()); + VirtualHardwareManager.getInstance().setTemperatureMax(temperature); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(this, "Invalid temperature value", "Error", JOptionPane.ERROR_MESSAGE); + txtTemperatureMax.setText("50"); + } + } + + private void txtHumidityMinActionPerformed(java.awt.event.ActionEvent evt) { + try { + int humidity = Integer.parseInt(txtHumidityMin.getText()); + VirtualHardwareManager.getInstance().setHumidityMin(humidity); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(this, "Invalid humidity value", "Error", JOptionPane.ERROR_MESSAGE); + txtHumidityMin.setText("20"); + } + } + + private void txtHumidityMaxActionPerformed(java.awt.event.ActionEvent evt) { + try { + int humidity = Integer.parseInt(txtHumidityMax.getText()); + VirtualHardwareManager.getInstance().setHumidityMax(humidity); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(this, "Invalid humidity value", "Error", JOptionPane.ERROR_MESSAGE); + txtHumidityMax.setText("50"); + } + } + + private void spinnerIntervalStateChanged(javax.swing.event.ChangeEvent evt) { + try { + int interval = Integer.parseInt(spinnerInterval.getValue().toString()); + AgentManager.getInstance().setPushInterval(interval); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(this, "Invalid time interval value", "Error", JOptionPane.ERROR_MESSAGE); + spinnerInterval.setValue(5); + } + } + + private void cmbInterfaceActionPerformed(java.awt.event.ActionEvent evt) { + AgentManager.getInstance().setInterface(cmbInterface.getSelectedIndex()); + } + + private void cmbProtocolActionPerformed(java.awt.event.ActionEvent evt) { + if (cmbProtocol.getSelectedIndex() != -1 && cmbProtocol.getItemAt( + cmbProtocol.getSelectedIndex()).equals(AgentConstants.HTTP_PROTOCOL)) { + cmbInterface.setEnabled(true); + } else { + cmbInterface.setEnabled(false); + } + + AgentManager.getInstance().setProtocol(cmbProtocol.getSelectedIndex()); + + } + + private void txtTemperatureSVFActionPerformed(java.awt.event.ActionEvent evt) { + try { + int temperatureSVF = Integer.parseInt(txtTemperatureSVF.getText()); + VirtualHardwareManager.getInstance().setTemperatureSVF(temperatureSVF); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(this, "Invalid value", "Error", JOptionPane.ERROR_MESSAGE); + txtTemperatureSVF.setText("50"); + } + } + + private void txtHumiditySVFActionPerformed(java.awt.event.ActionEvent evt) { + try { + int humiditySVF = Integer.parseInt(txtHumiditySVF.getText()); + VirtualHardwareManager.getInstance().setHumiditySVF(humiditySVF); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(this, "Invalid value", "Error", JOptionPane.ERROR_MESSAGE); + txtHumiditySVF.setText("50"); + } + } + + private void chkbxTemperatureSmoothActionPerformed(java.awt.event.ActionEvent evt) { + isTemperatureSmoothed = chkbxTemperatureSmooth.isSelected(); + txtTemperatureSVF.setEnabled(isTemperatureSmoothed); + VirtualHardwareManager.getInstance().setIsTemperatureSmoothed(isTemperatureSmoothed); + } + + private void chkbxHumiditySmoothActionPerformed(java.awt.event.ActionEvent evt) { + isHumiditySmoothed = chkbxHumiditySmooth.isSelected(); + txtHumiditySVF.setEnabled(isHumiditySmoothed); + VirtualHardwareManager.getInstance().setIsHumiditySmoothed(isHumiditySmoothed); + } + + private void cmbPeriodActionPerformed(java.awt.event.ActionEvent evt) { + // TODO add your handling code here: + } + + private void chkbxEmulateActionPerformed(java.awt.event.ActionEvent evt) { + // TODO add your handling code here: + } + + public void setAlarmStatus(boolean isAlarmOn) { + this.isAlarmOn = isAlarmOn; + } + + public void updateTemperature(int temperature) { + spinnerTemperature.setValue(temperature); + spinnerTemperature.updateUI(); + } + + public void updateHumidity(int humidity) { + spinnerHumidity.setValue(humidity); + spinnerHumidity.updateUI(); + } + + public void addToPolicyLog(String policy) { + synchronized (this._lock) { + policyLogs.add(policy); + } + } + + private String getPolicyLog() { + synchronized (this._lock) { + if (policyLogs.size() > 0) { + String policy = policyLogs.get(0); + policyLogs.remove(0); + return policy; + } else { + return null; + } + } + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/resources/alarm-off.gif b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/resources/alarm-off.gif new file mode 100644 index 0000000000000000000000000000000000000000..c346605ad00627e4bda392cd37ab24bde7643395 GIT binary patch literal 4265 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 C_s;DA literal 0 HcmV?d00001 diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/resources/cep_query.txt b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/resources/cep_query.txt new file mode 100644 index 000000000..17546ece2 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/resources/cep_query.txt @@ -0,0 +1,11 @@ +define stream fireAlarmEventStream (deviceID string, temp int); +from fireAlarmEventStream#window.time(7886776 sec) +select deviceID, max(temp) as maxValue +group by deviceID +insert into analyzeStream for expired-events; +from analyzeStream[maxValue < 88] +select maxValue +insert into bulbOnStream; +from fireAlarmEventStream[temp > 1093] +select deviceID, temp +insert into bulbOffStream; \ No newline at end of file diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/resources/deviceConfig.properties b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/resources/deviceConfig.properties new file mode 100644 index 000000000..930742a2a --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/resources/deviceConfig.properties @@ -0,0 +1,33 @@ +# +# 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. +# +# + +#[Device-Configurations] +server-name=WSO2IoTServer +owner=shabirmean +deviceId=t4ctwq8qfl11 +device-name=SMEAN_t4ctwq8qfl11 +controller-context=/virtual_firealarm/controller +https-ep=https://localhost:9443 +http-ep=http://localhost:9763 +apim-ep=http://192.168.67.21:8281 +mqtt-ep=tcp://192.168.67.21:1883 +xmpp-ep=http://204.232.188.215:5222 +auth-method=token +auth-token=79d68b50ae5f5a06e812889979b3453 +refresh-token=8bdda6359dddad218cff3354d5a8cb3b +network-interface=en0 +push-interval=14 diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/resources/fireAlarmSound.mid b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/resources/fireAlarmSound.mid new file mode 100644 index 0000000000000000000000000000000000000000..d1a2241b2d87d668b9cfa999f05be6a8c477d895 GIT binary patch literal 6719 zcmeI%&rc&|7zglY+L=<>4Yh|&?8cA@YYBz~Q>>7XY15%lD8d3Q;20ul)6PO!VUezl zB_jA>{7SQ2H^mm&olGP zJ2TJw%nQsdZ6t}vMjSb~-{+QgFS9L)3btqF;=7mCUGc&NDl7>iQ=)>x^OooARCwK@ zZyniZ(aJrzM}?QDl;au|toEq-*4yfKLZt$Kc4|(XP&287Dtca_g2?sIL#L*l;W)d+ zZng7vo84jmhaR3M@uFtoI5r@T74j&7tuA3Ml+h^A(|nnUn9RJ^%MC?8rCSRNy9|p zqwdy-9l}CxB6d_wl!bhVvXHM)v{UEGKYv0mOzOaOv1ji`S+G)I72q!yM?-gK$&)2t zmcm)RCfpglEIb+g@%u8YJ}8`lr{QUM8lHylz<1y~@E!OTd<(t>--2(#H{qM`P53%| z9lj1TiJ4 zj(9>kV%4&Dy!ooa<(m1j>>~!u@{XW|v0Y0q%^(uMW@~Y*AfaKy!a%p(<8= zd21G`#eBKg#|dqJWIv&dl|INX{!Gp~EJrK5)l2?--C-@~s?XkXs_T1CZdgz0B~~6h zO_`L?%X$e}Tn9!8eYYqauoC){0(8)@jx2vXvW|*LbFoiS8eP@)-n*(Sq?CSROI}FH z=YAol0rla^&c}=8Z6-5oD$g7KkjA<1 zdEr*qLgV&p)f;(FRr3|a+V)$_NrxooVxJ~7+R*lL357P4jIkxthOGWdP6O)0m7R|l z%deOuZPPi;#`?rgd5PW7XN%Xie~vw-2nC}w5uwQ_g(5T+rRj^ggnnb$GYng3rkAFB zX|k8bZ6tsFPUmmRiG2{Ir>z&bMB@?#C7O_EQlgMVQxZ)}6qablORshppNDWr(+-++ z(72W4$DEh=OCayMU7nZ14+8%e`MKN8mPL>8IxFVP#Xe(mGNtY1*_=!%8DmSPl&n4= zrvdfh%Ff4&RvUB~RX0hU}ZIZNn zolU}Kd0UOO>6BH|#_TtVQT=shbKNnf&5vo}PO~Y)-GAh(te7ts`+QN`dw)^M6o1O< zZ{)0@Qpnh)c00dPb3h9@YIC-b;>O5-AI4L1DODvg(tL&EVKI?&xbBup!Y2Imu5g!? ygz*a{MNtMz>YM7$#+7aMZ + + + + + + + + + + Builds, tests, and runs the project VirtualAgentUI. + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/manifest.mf b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/manifest.mf new file mode 100644 index 000000000..328e8e5bc --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/manifest.mf @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +X-COMMENT: Main-Class will be added automatically by build + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/build-impl.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/build-impl.xml new file mode 100644 index 000000000..112454207 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/build-impl.xml @@ -0,0 +1,1413 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set src.dir + Must set test.src.dir + Must set build.dir + Must set dist.dir + Must set build.classes.dir + Must set dist.javadoc.dir + Must set build.test.classes.dir + Must set build.test.results.dir + Must set build.classes.excludes + Must set dist.jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + No tests executed. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set JVM to use for profiling in profiler.info.jvm + Must set profiler agent JVM arguments in profiler.info.jvmargs.agent + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + To run this application from the command line without Ant, try: + + java -jar "${dist.jar.resolved}" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + Must select one file in the IDE or set run.class + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set debug.class + + + + + Must select one file in the IDE or set debug.class + + + + + Must set fix.includes + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + Must select one file in the IDE or set profile.class + This target only works when run from inside the NetBeans IDE. + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + + + Must select some files in the IDE or set test.includes + + + + + Must select one file in the IDE or set run.class + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + Some tests failed; see details above. + + + + + + + + + Must select some files in the IDE or set test.includes + + + + Some tests failed; see details above. + + + + Must select some files in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + Some tests failed; see details above. + + + + + Must select one file in the IDE or set test.class + + + + Must select one file in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + + + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/genfiles.properties b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/genfiles.properties new file mode 100644 index 000000000..a6df38fd8 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/genfiles.properties @@ -0,0 +1,8 @@ +build.xml.data.CRC32=e60df945 +build.xml.script.CRC32=7c331eea +build.xml.stylesheet.CRC32=8064a381@1.75.2.48 +# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. +# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. +nbproject/build-impl.xml.data.CRC32=e60df945 +nbproject/build-impl.xml.script.CRC32=4fa004f7 +nbproject/build-impl.xml.stylesheet.CRC32=876e7a8f@1.75.2.48 diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/private/private.properties b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/private/private.properties new file mode 100644 index 000000000..e59ac1df6 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/private/private.properties @@ -0,0 +1,2 @@ +compile.on.save=true +user.properties.file=/home/charitha/.netbeans/8.0.2/build.properties diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/private/private.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/private/private.xml new file mode 100644 index 000000000..97b7e92bd --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/private/private.xml @@ -0,0 +1,9 @@ + + + + + + file:/home/charitha/NetBeansProjects/VirtualAgentUI/src/org/wso2/carbon/device/mgt/iot/agent/virtual/ui/AgentUI.java + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/project.properties b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/project.properties new file mode 100644 index 000000000..fb798f614 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/project.properties @@ -0,0 +1,73 @@ +annotation.processing.enabled=true +annotation.processing.enabled.in.editor=false +annotation.processing.processor.options= +annotation.processing.processors.list= +annotation.processing.run.all.processors=true +annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output +build.classes.dir=${build.dir}/classes +build.classes.excludes=**/*.java,**/*.form +# This directory is removed when the project is cleaned: +build.dir=build +build.generated.dir=${build.dir}/generated +build.generated.sources.dir=${build.dir}/generated-sources +# Only compile against the classpath explicitly listed here: +build.sysclasspath=ignore +build.test.classes.dir=${build.dir}/test/classes +build.test.results.dir=${build.dir}/test/results +# Uncomment to specify the preferred debugger connection transport: +#debug.transport=dt_socket +debug.classpath=\ + ${run.classpath} +debug.test.classpath=\ + ${run.test.classpath} +# Files in build.classes.dir which should be excluded from distribution jar +dist.archive.excludes= +# This directory is removed when the project is cleaned: +dist.dir=dist +dist.jar=${dist.dir}/VirtualAgentUI.jar +dist.javadoc.dir=${dist.dir}/javadoc +excludes= +includes=** +jar.compress=false +javac.classpath= +# Space-separated list of extra javac options +javac.compilerargs= +javac.deprecation=false +javac.processorpath=\ + ${javac.classpath} +javac.source=1.8 +javac.target=1.8 +javac.test.classpath=\ + ${javac.classpath}:\ + ${build.classes.dir} +javac.test.processorpath=\ + ${javac.test.classpath} +javadoc.additionalparam= +javadoc.author=false +javadoc.encoding=${source.encoding} +javadoc.noindex=false +javadoc.nonavbar=false +javadoc.notree=false +javadoc.private=false +javadoc.splitindex=true +javadoc.use=true +javadoc.version=false +javadoc.windowtitle= +main.class=org.wso2.carbon.device.mgt.iot.agent.virtual.VirtualAgentUI +manifest.file=manifest.mf +meta.inf.dir=${src.dir}/META-INF +mkdist.disabled=false +platform.active=default_platform +run.classpath=\ + ${javac.classpath}:\ + ${build.classes.dir} +# Space-separated list of JVM arguments used when running the project. +# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value. +# To set system properties for unit tests define test-sys-prop.name=value: +run.jvmargs= +run.test.classpath=\ + ${javac.test.classpath}:\ + ${build.test.classes.dir} +source.encoding=UTF-8 +src.dir=src +test.src.dir=test diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/project.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/project.xml new file mode 100644 index 000000000..438f1e6a3 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/nbproject/project.xml @@ -0,0 +1,15 @@ + + + org.netbeans.modules.java.j2seproject + + + VirtualAgentUI + + + + + + + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/src/bulb-on.jpg b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/src/bulb-on.jpg new file mode 100644 index 0000000000000000000000000000000000000000..51d40cd834db53f1bf305997832ad4c5f7a00356 GIT binary patch literal 6942 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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/VirtualAgentUI.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/VirtualAgentUI.java new file mode 100644 index 000000000..a2aa2116a --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/VirtualAgentUI.java @@ -0,0 +1,37 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.wso2.carbon.device.mgt.iot.agent.virtual; + +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; +import org.wso2.carbon.device.mgt.iot.agent.virtual.ui.AgentUI; + +/** + * + * @author charitha + */ +public class VirtualAgentUI { + + /** + * @param args the command line arguments + */ + public static void main(String[] args) { + try { + // Set System L&F + UIManager.setLookAndFeel( + UIManager.getSystemLookAndFeelClassName()); + } catch (UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException e) { + // handle exception + } + + java.awt.EventQueue.invokeLater(new Runnable() { + public void run() { + new AgentUI().setVisible(true); + } + }); + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/ui/AgentUI.form b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/ui/AgentUI.form new file mode 100644 index 000000000..0778bef15 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/ui/AgentUI.form @@ -0,0 +1,830 @@ + + +

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/ui/AgentUI.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/ui/AgentUI.java new file mode 100644 index 000000000..27a177492 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/ui/AgentUI.java @@ -0,0 +1,758 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.wso2.carbon.device.mgt.iot.agent.virtual.ui; + +/** + * + * @author charitha + */ +public class AgentUI extends javax.swing.JFrame { + + /** + * Creates new form AgentUI + */ + public AgentUI() { + initComponents(); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + lblAgentName = new javax.swing.JLabel(); + jLabel2 = new javax.swing.JLabel(); + jPanel1 = new javax.swing.JPanel(); + jLabel3 = new javax.swing.JLabel(); + pnlBulbStatus = new javax.swing.JPanel(); + jPanel2 = new javax.swing.JPanel(); + jLabel4 = new javax.swing.JLabel(); + chkbxTemperatureRandom = new javax.swing.JCheckBox(); + jSeparator1 = new javax.swing.JSeparator(); + jPanel7 = new javax.swing.JPanel(); + jLabel5 = new javax.swing.JLabel(); + txtTemperatureMin = new javax.swing.JTextField(); + jLabel6 = new javax.swing.JLabel(); + txtTemperatureMax = new javax.swing.JTextField(); + jLabel10 = new javax.swing.JLabel(); + txtTemperatureSVF = new javax.swing.JTextField(); + spinnerTemperature = new javax.swing.JSpinner(); + chkbxTemperatureSmooth = new javax.swing.JCheckBox(); + jPanel6 = new javax.swing.JPanel(); + jLabel20 = new javax.swing.JLabel(); + btnView = new javax.swing.JButton(); + btnControl = new javax.swing.JButton(); + lblStatus = new javax.swing.JLabel(); + jPanel8 = new javax.swing.JPanel(); + jLabel23 = new javax.swing.JLabel(); + chkbxHumidityRandom = new javax.swing.JCheckBox(); + jSeparator5 = new javax.swing.JSeparator(); + jPanel9 = new javax.swing.JPanel(); + jLabel24 = new javax.swing.JLabel(); + txtHumidityMin = new javax.swing.JTextField(); + jLabel25 = new javax.swing.JLabel(); + txtHumidityMax = new javax.swing.JTextField(); + txtHumiditySVF = new javax.swing.JTextField(); + jLabel11 = new javax.swing.JLabel(); + spinnerHumidity = new javax.swing.JSpinner(); + chkbxHumiditySmooth = new javax.swing.JCheckBox(); + jPanel3 = new javax.swing.JPanel(); + jLabel7 = new javax.swing.JLabel(); + spinnerInterval = new javax.swing.JSpinner(); + jLabel8 = new javax.swing.JLabel(); + jLabel9 = new javax.swing.JLabel(); + cmbProtocol = new javax.swing.JComboBox(); + jLabel12 = new javax.swing.JLabel(); + cmbInterface = new javax.swing.JComboBox(); + jScrollPane1 = new javax.swing.JScrollPane(); + txtAreaLogs = new javax.swing.JTextArea(); + jPanel4 = new javax.swing.JPanel(); + chkbxEmulate = new javax.swing.JCheckBox(); + cmbPeriod = new javax.swing.JComboBox(); + jLabel1 = new javax.swing.JLabel(); + + setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); + setTitle("Fire Alarm Emulator"); + setResizable(false); + + lblAgentName.setFont(new java.awt.Font("Cantarell", 1, 24)); // NOI18N + lblAgentName.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); + lblAgentName.setText("Device Name: WSO2 IoT Virtual Agent"); + + jLabel2.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + jLabel2.setText("Copyright (c) 2015, WSO2 Inc."); + + jPanel1.setBackground(new java.awt.Color(220, 220, 220)); + + jLabel3.setFont(new java.awt.Font("Cantarell", 0, 18)); // NOI18N + jLabel3.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + jLabel3.setText("Bulb Status"); + + pnlBulbStatus.setBackground(new java.awt.Color(220, 220, 220)); + + javax.swing.GroupLayout pnlBulbStatusLayout = new javax.swing.GroupLayout(pnlBulbStatus); + pnlBulbStatus.setLayout(pnlBulbStatusLayout); + pnlBulbStatusLayout.setHorizontalGroup( + pnlBulbStatusLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 0, Short.MAX_VALUE) + ); + pnlBulbStatusLayout.setVerticalGroup( + pnlBulbStatusLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 167, Short.MAX_VALUE) + ); + + javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1); + jPanel1.setLayout(jPanel1Layout); + jPanel1Layout.setHorizontalGroup( + jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(pnlBulbStatus, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jLabel3, javax.swing.GroupLayout.DEFAULT_SIZE, 190, Short.MAX_VALUE)) + .addContainerGap()) + ); + jPanel1Layout.setVerticalGroup( + jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel1Layout.createSequentialGroup() + .addContainerGap() + .addComponent(jLabel3) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(pnlBulbStatus, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addContainerGap()) + ); + + jPanel2.setBackground(new java.awt.Color(220, 220, 220)); + + jLabel4.setFont(new java.awt.Font("Cantarell", 0, 18)); // NOI18N + jLabel4.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + jLabel4.setText("Temperature"); + + chkbxTemperatureRandom.setText("Randomize Data"); + chkbxTemperatureRandom.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxTemperatureRandomActionPerformed(evt); + } + }); + + jSeparator1.setOrientation(javax.swing.SwingConstants.VERTICAL); + + jPanel7.setBackground(new java.awt.Color(220, 220, 220)); + + jLabel5.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); + jLabel5.setText("Min"); + + txtTemperatureMin.setHorizontalAlignment(javax.swing.JTextField.CENTER); + txtTemperatureMin.setText("20"); + txtTemperatureMin.setEnabled(false); + txtTemperatureMin.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtTemperatureMinActionPerformed(evt); + } + }); + + jLabel6.setHorizontalAlignment(javax.swing.SwingConstants.RIGHT); + jLabel6.setText("Max"); + + txtTemperatureMax.setHorizontalAlignment(javax.swing.JTextField.CENTER); + txtTemperatureMax.setText("50"); + txtTemperatureMax.setEnabled(false); + txtTemperatureMax.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtTemperatureMaxActionPerformed(evt); + } + }); + + jLabel10.setHorizontalAlignment(javax.swing.SwingConstants.RIGHT); + jLabel10.setText("SV %"); + + txtTemperatureSVF.setHorizontalAlignment(javax.swing.JTextField.CENTER); + txtTemperatureSVF.setText("50"); + txtTemperatureSVF.setEnabled(false); + txtTemperatureSVF.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtTemperatureSVFActionPerformed(evt); + } + }); + + javax.swing.GroupLayout jPanel7Layout = new javax.swing.GroupLayout(jPanel7); + jPanel7.setLayout(jPanel7Layout); + jPanel7Layout.setHorizontalGroup( + jPanel7Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel7Layout.createSequentialGroup() + .addComponent(jLabel5) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtTemperatureMin, javax.swing.GroupLayout.PREFERRED_SIZE, 45, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel6) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtTemperatureMax, javax.swing.GroupLayout.PREFERRED_SIZE, 45, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel10) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtTemperatureSVF, javax.swing.GroupLayout.PREFERRED_SIZE, 45, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + jPanel7Layout.setVerticalGroup( + jPanel7Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel7Layout.createSequentialGroup() + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(jPanel7Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(txtTemperatureMin, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(txtTemperatureMax, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel6) + .addComponent(jLabel5) + .addComponent(jLabel10) + .addComponent(txtTemperatureSVF, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGap(35, 35, 35)) + ); + + spinnerTemperature.setFont(new java.awt.Font("Cantarell", 1, 24)); // NOI18N + spinnerTemperature.setModel(new javax.swing.SpinnerNumberModel(30, 0, 100, 1)); + spinnerTemperature.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + spinnerTemperatureStateChanged(evt); + } + }); + + chkbxTemperatureSmooth.setText("Smooth Variation"); + chkbxTemperatureSmooth.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxTemperatureSmoothActionPerformed(evt); + } + }); + + javax.swing.GroupLayout jPanel2Layout = new javax.swing.GroupLayout(jPanel2); + jPanel2.setLayout(jPanel2Layout); + jPanel2Layout.setHorizontalGroup( + jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel2Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(jLabel4, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(spinnerTemperature)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jSeparator1, javax.swing.GroupLayout.PREFERRED_SIZE, 6, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jPanel7, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(jPanel2Layout.createSequentialGroup() + .addComponent(chkbxTemperatureRandom) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(chkbxTemperatureSmooth))) + .addContainerGap()) + ); + jPanel2Layout.setVerticalGroup( + jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel2Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jSeparator1) + .addGroup(jPanel2Layout.createSequentialGroup() + .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(chkbxTemperatureRandom) + .addComponent(chkbxTemperatureSmooth)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel7, javax.swing.GroupLayout.PREFERRED_SIZE, 51, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(jPanel2Layout.createSequentialGroup() + .addComponent(jLabel4, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(spinnerTemperature))) + .addContainerGap()) + ); + + jPanel6.setBackground(new java.awt.Color(253, 254, 209)); + + jLabel20.setText("Connection Status:"); + jLabel20.setVerticalTextPosition(javax.swing.SwingConstants.TOP); + + btnView.setText("View Device Data"); + btnView.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + btnViewMouseClicked(evt); + } + }); + + btnControl.setText("Control Device"); + btnControl.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + btnControlMouseClicked(evt); + } + }); + + lblStatus.setFont(new java.awt.Font("Cantarell", 1, 15)); // NOI18N + lblStatus.setText("Not Connected"); + + javax.swing.GroupLayout jPanel6Layout = new javax.swing.GroupLayout(jPanel6); + jPanel6.setLayout(jPanel6Layout); + jPanel6Layout.setHorizontalGroup( + jPanel6Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel6Layout.createSequentialGroup() + .addContainerGap() + .addComponent(jLabel20) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(lblStatus, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(btnControl) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(btnView) + .addContainerGap()) + ); + jPanel6Layout.setVerticalGroup( + jPanel6Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel6Layout.createSequentialGroup() + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(jPanel6Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(jLabel20, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(jPanel6Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(btnView, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(btnControl) + .addComponent(lblStatus))) + .addContainerGap()) + ); + + jPanel8.setBackground(new java.awt.Color(220, 220, 220)); + + jLabel23.setFont(new java.awt.Font("Cantarell", 0, 18)); // NOI18N + jLabel23.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + jLabel23.setText("Humidity"); + + chkbxHumidityRandom.setText("Randomize Data"); + chkbxHumidityRandom.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxHumidityRandomActionPerformed(evt); + } + }); + + jSeparator5.setOrientation(javax.swing.SwingConstants.VERTICAL); + + jPanel9.setBackground(new java.awt.Color(220, 220, 220)); + + jLabel24.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); + jLabel24.setText("Min"); + + txtHumidityMin.setHorizontalAlignment(javax.swing.JTextField.CENTER); + txtHumidityMin.setText("20"); + txtHumidityMin.setEnabled(false); + txtHumidityMin.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtHumidityMinActionPerformed(evt); + } + }); + + jLabel25.setHorizontalAlignment(javax.swing.SwingConstants.RIGHT); + jLabel25.setText("Max"); + + txtHumidityMax.setHorizontalAlignment(javax.swing.JTextField.CENTER); + txtHumidityMax.setText("50"); + txtHumidityMax.setEnabled(false); + txtHumidityMax.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtHumidityMaxActionPerformed(evt); + } + }); + + txtHumiditySVF.setHorizontalAlignment(javax.swing.JTextField.CENTER); + txtHumiditySVF.setText("50"); + txtHumiditySVF.setEnabled(false); + txtHumiditySVF.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtHumiditySVFActionPerformed(evt); + } + }); + + jLabel11.setHorizontalAlignment(javax.swing.SwingConstants.RIGHT); + jLabel11.setText("SV %"); + + javax.swing.GroupLayout jPanel9Layout = new javax.swing.GroupLayout(jPanel9); + jPanel9.setLayout(jPanel9Layout); + jPanel9Layout.setHorizontalGroup( + jPanel9Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel9Layout.createSequentialGroup() + .addComponent(jLabel24) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtHumidityMin, javax.swing.GroupLayout.PREFERRED_SIZE, 45, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel25) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtHumidityMax, javax.swing.GroupLayout.PREFERRED_SIZE, 45, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel11) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtHumiditySVF, javax.swing.GroupLayout.PREFERRED_SIZE, 45, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + jPanel9Layout.setVerticalGroup( + jPanel9Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel9Layout.createSequentialGroup() + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(jPanel9Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel9Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jLabel11) + .addComponent(txtHumiditySVF, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(jPanel9Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(txtHumidityMin, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(txtHumidityMax, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel25) + .addComponent(jLabel24))) + .addGap(35, 35, 35)) + ); + + spinnerHumidity.setFont(new java.awt.Font("Cantarell", 1, 24)); // NOI18N + spinnerHumidity.setModel(new javax.swing.SpinnerNumberModel(30, 0, 100, 1)); + spinnerHumidity.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + spinnerHumidityStateChanged(evt); + } + }); + + chkbxHumiditySmooth.setText("Smooth Variation"); + chkbxHumiditySmooth.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxHumiditySmoothActionPerformed(evt); + } + }); + + javax.swing.GroupLayout jPanel8Layout = new javax.swing.GroupLayout(jPanel8); + jPanel8.setLayout(jPanel8Layout); + jPanel8Layout.setHorizontalGroup( + jPanel8Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel8Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel8Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jLabel23, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(spinnerHumidity)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jSeparator5, javax.swing.GroupLayout.PREFERRED_SIZE, 6, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanel8Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(jPanel9, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(jPanel8Layout.createSequentialGroup() + .addComponent(chkbxHumidityRandom) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(chkbxHumiditySmooth))) + .addContainerGap()) + ); + jPanel8Layout.setVerticalGroup( + jPanel8Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel8Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel8Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jSeparator5) + .addGroup(jPanel8Layout.createSequentialGroup() + .addGroup(jPanel8Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(chkbxHumidityRandom) + .addComponent(chkbxHumiditySmooth)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel9, javax.swing.GroupLayout.PREFERRED_SIZE, 51, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(0, 1, Short.MAX_VALUE)) + .addGroup(jPanel8Layout.createSequentialGroup() + .addComponent(jLabel23, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(spinnerHumidity))) + .addContainerGap()) + ); + + jPanel3.setBackground(new java.awt.Color(207, 233, 234)); + + jLabel7.setText("Data Push Interval:"); + + spinnerInterval.setModel(new javax.swing.SpinnerNumberModel(Integer.valueOf(5), Integer.valueOf(1), null, Integer.valueOf(1))); + spinnerInterval.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + spinnerIntervalStateChanged(evt); + } + }); + + jLabel8.setText("Seconds"); + + jLabel9.setText("Protocol:"); + + cmbProtocol.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "MQTT", "XMPP", "HTTP" })); + cmbProtocol.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cmbProtocolActionPerformed(evt); + } + }); + + jLabel12.setText("Interface:"); + + cmbInterface.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "eth0" })); + cmbInterface.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cmbInterfaceActionPerformed(evt); + } + }); + + javax.swing.GroupLayout jPanel3Layout = new javax.swing.GroupLayout(jPanel3); + jPanel3.setLayout(jPanel3Layout); + jPanel3Layout.setHorizontalGroup( + jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel3Layout.createSequentialGroup() + .addContainerGap() + .addComponent(jLabel7) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(spinnerInterval, javax.swing.GroupLayout.PREFERRED_SIZE, 55, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel8) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jLabel12) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cmbInterface, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel9) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cmbProtocol, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + ); + jPanel3Layout.setVerticalGroup( + jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel3Layout.createSequentialGroup() + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jLabel12) + .addComponent(cmbInterface, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jLabel7) + .addComponent(spinnerInterval, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel8) + .addComponent(jLabel9) + .addComponent(cmbProtocol, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addContainerGap()) + ); + + txtAreaLogs.setBackground(new java.awt.Color(1, 1, 1)); + txtAreaLogs.setColumns(20); + txtAreaLogs.setFont(new java.awt.Font("Courier 10 Pitch", 1, 18)); // NOI18N + txtAreaLogs.setForeground(new java.awt.Color(0, 255, 0)); + txtAreaLogs.setRows(5); + jScrollPane1.setViewportView(txtAreaLogs); + + jPanel4.setBackground(new java.awt.Color(169, 253, 173)); + + chkbxEmulate.setText("Emulate data"); + chkbxEmulate.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxEmulateActionPerformed(evt); + } + }); + + cmbPeriod.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "1 hour", "1 day", "1 week", "1 month " })); + cmbPeriod.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cmbPeriodActionPerformed(evt); + } + }); + + jLabel1.setText("Emulation Period"); + + javax.swing.GroupLayout jPanel4Layout = new javax.swing.GroupLayout(jPanel4); + jPanel4.setLayout(jPanel4Layout); + jPanel4Layout.setHorizontalGroup( + jPanel4Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel4Layout.createSequentialGroup() + .addContainerGap() + .addComponent(chkbxEmulate) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jLabel1) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cmbPeriod, javax.swing.GroupLayout.PREFERRED_SIZE, 162, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + ); + jPanel4Layout.setVerticalGroup( + jPanel4Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel4Layout.createSequentialGroup() + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(jPanel4Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(chkbxEmulate) + .addComponent(cmbPeriod, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel1)) + .addContainerGap()) + ); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jScrollPane1) + .addComponent(lblAgentName, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanel6, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jPanel8, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanel2, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))) + .addComponent(jLabel2, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanel4, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanel3, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(lblAgentName, javax.swing.GroupLayout.PREFERRED_SIZE, 53, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel6, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(jPanel2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel8, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 115, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel4, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel2, javax.swing.GroupLayout.PREFERRED_SIZE, 28, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + ); + + pack(); + }// //GEN-END:initComponents + + private void btnControlMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_btnControlMouseClicked + // TODO add your handling code here: + }//GEN-LAST:event_btnControlMouseClicked + + private void btnViewMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_btnViewMouseClicked + // TODO add your handling code here: + }//GEN-LAST:event_btnViewMouseClicked + + private void chkbxTemperatureRandomActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chkbxTemperatureRandomActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_chkbxTemperatureRandomActionPerformed + + private void chkbxHumidityRandomActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chkbxHumidityRandomActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_chkbxHumidityRandomActionPerformed + + private void spinnerTemperatureStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_spinnerTemperatureStateChanged + // TODO add your handling code here: + }//GEN-LAST:event_spinnerTemperatureStateChanged + + private void spinnerHumidityStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_spinnerHumidityStateChanged + // TODO add your handling code here: + }//GEN-LAST:event_spinnerHumidityStateChanged + + private void txtTemperatureMinActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_txtTemperatureMinActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_txtTemperatureMinActionPerformed + + private void txtTemperatureMaxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_txtTemperatureMaxActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_txtTemperatureMaxActionPerformed + + private void txtHumidityMinActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_txtHumidityMinActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_txtHumidityMinActionPerformed + + private void txtHumidityMaxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_txtHumidityMaxActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_txtHumidityMaxActionPerformed + + private void spinnerIntervalStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_spinnerIntervalStateChanged + // TODO add your handling code here: + }//GEN-LAST:event_spinnerIntervalStateChanged + + private void cmbInterfaceActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cmbInterfaceActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_cmbInterfaceActionPerformed + + private void cmbProtocolActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cmbProtocolActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_cmbProtocolActionPerformed + + private void txtTemperatureSVFActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_txtTemperatureSVFActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_txtTemperatureSVFActionPerformed + + private void txtHumiditySVFActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_txtHumiditySVFActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_txtHumiditySVFActionPerformed + + private void chkbxTemperatureSmoothActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chkbxTemperatureSmoothActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_chkbxTemperatureSmoothActionPerformed + + private void chkbxHumiditySmoothActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chkbxHumiditySmoothActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_chkbxHumiditySmoothActionPerformed + + private void cmbPeriodActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cmbPeriodActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_cmbPeriodActionPerformed + + private void chkbxEmulateActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chkbxEmulateActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_chkbxEmulateActionPerformed + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton btnControl; + private javax.swing.JButton btnView; + private javax.swing.JCheckBox chkbxEmulate; + private javax.swing.JCheckBox chkbxHumidityRandom; + private javax.swing.JCheckBox chkbxHumiditySmooth; + private javax.swing.JCheckBox chkbxTemperatureRandom; + private javax.swing.JCheckBox chkbxTemperatureSmooth; + private javax.swing.JComboBox cmbInterface; + private javax.swing.JComboBox cmbPeriod; + private javax.swing.JComboBox cmbProtocol; + private javax.swing.JLabel jLabel1; + private javax.swing.JLabel jLabel10; + private javax.swing.JLabel jLabel11; + private javax.swing.JLabel jLabel12; + private javax.swing.JLabel jLabel2; + private javax.swing.JLabel jLabel20; + private javax.swing.JLabel jLabel23; + private javax.swing.JLabel jLabel24; + private javax.swing.JLabel jLabel25; + private javax.swing.JLabel jLabel3; + private javax.swing.JLabel jLabel4; + private javax.swing.JLabel jLabel5; + private javax.swing.JLabel jLabel6; + private javax.swing.JLabel jLabel7; + private javax.swing.JLabel jLabel8; + private javax.swing.JLabel jLabel9; + private javax.swing.JPanel jPanel1; + private javax.swing.JPanel jPanel2; + private javax.swing.JPanel jPanel3; + private javax.swing.JPanel jPanel4; + private javax.swing.JPanel jPanel6; + private javax.swing.JPanel jPanel7; + private javax.swing.JPanel jPanel8; + private javax.swing.JPanel jPanel9; + private javax.swing.JScrollPane jScrollPane1; + private javax.swing.JSeparator jSeparator1; + private javax.swing.JSeparator jSeparator5; + private javax.swing.JLabel lblAgentName; + private javax.swing.JLabel lblStatus; + private javax.swing.JPanel pnlBulbStatus; + private javax.swing.JSpinner spinnerHumidity; + private javax.swing.JSpinner spinnerInterval; + private javax.swing.JSpinner spinnerTemperature; + private javax.swing.JTextArea txtAreaLogs; + private javax.swing.JTextField txtHumidityMax; + private javax.swing.JTextField txtHumidityMin; + private javax.swing.JTextField txtHumiditySVF; + private javax.swing.JTextField txtTemperatureMax; + private javax.swing.JTextField txtTemperatureMin; + private javax.swing.JTextField txtTemperatureSVF; + // End of variables declaration//GEN-END:variables +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/pom.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/pom.xml new file mode 100644 index 000000000..4ec107071 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/pom.xml @@ -0,0 +1,232 @@ + + + + + 4.0.0 + + + virtual-fire-alarm-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl + WSO2 Carbon - IoT Server VirtualFireAlarm Agent + WSO2 Carbon - VirtualFireAlarm Device Agent Implementation + http://wso2.org + + + + + org.apache.maven.plugins + maven-compiler-plugin + + UTF-8 + ${wso2.maven.compiler.source} + ${wso2.maven.compiler.target} + + 2.3.2 + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.Bootstrap + + + + + jar-with-dependencies + + wso2-firealarm-virtual-agent + false + + + + make-assembly + + package + + + single + + + + + + + + + + + + + log4j + log4j + ${log4j.version} + + + + + + org.eclipse.jetty + jetty-server + ${jetty.version} + + + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + ${paho.mqtt.version} + + + + + org.igniterealtime.smack.wso2 + smack + ${smack.wso2.version} + + + org.igniterealtime.smack.wso2 + smackx + ${smackx.wso2.version} + + + + + org.bouncycastle.wso2 + bcprov-jdk15on + ${bcprov.wso2.version} + + + org.bouncycastle.wso2 + bcpkix-jdk15on + ${bcpkix.wso2.version} + + + + + com.google.code.jscep.wso2 + jscep + ${jscep.version} + + + + commons-codec.wso2 + commons-codec + + + + commons-lang + commons-lang + ${commons-lang.version} + + + + commons-logging + commons-logging + ${common-logging.version} + + + + commons-io + commons-io + ${commons.io} + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-simple + ${slf4j.version} + + + + org.json.wso2 + json + + + + + + + + wso2-nexus + WSO2 internal Repository + http://maven.wso2.org/nexus/content/groups/wso2-public/ + + true + daily + ignore + + + + wso2-maven2-repository + http://dist.wso2.org/maven2 + + + + + + + 1.7 + 1.7 + + + 8.1.3.v20120416 + + + 1.0.2 + + + 3.0.4.wso2v1 + 3.0.4.wso2v1 + + + 1.49.wso2v1 + 1.49.wso2v1 + + + 2.0.2.wso2v2 + + + 1.2.17 + 1.2 + 2.4 + 1.7 + 2.6 + + + 1.7.13 + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/Bootstrap.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/Bootstrap.java new file mode 100644 index 000000000..63d4b2595 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/Bootstrap.java @@ -0,0 +1,36 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent; + +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.core.AgentManager; + +public class Bootstrap { + + /** + * @param args the command line arguments + */ + public static void main(String[] args) { + System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.SimpleLog"); + System.setProperty("org.apache.commons.logging.simplelog.defaultlog", "info"); + System.setProperty("org.apache.commons.logging.simplelog.showdatetime", "true"); + System.setProperty("org.apache.commons.logging.simplelog.dateTimeFormat", "HH:mm:ss"); + AgentManager.getInstance().init(); + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/communication/http/FireAlarmHTTPCommunicator.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/communication/http/FireAlarmHTTPCommunicator.java new file mode 100644 index 000000000..0f920e0d0 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/communication/http/FireAlarmHTTPCommunicator.java @@ -0,0 +1,495 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.communication.http; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.transport.TransportHandlerException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.transport.TransportUtils; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.transport.http.HTTPTransportHandler; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.core.AgentConstants; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.core.AgentManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.exception.AgentCoreOperationException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.ProtocolException; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Enumeration; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class FireAlarmHTTPCommunicator extends HTTPTransportHandler { + private static final Log log = LogFactory.getLog(FireAlarmHTTPCommunicator.class); + + private ScheduledExecutorService service = Executors.newScheduledThreadPool(2); + private ScheduledFuture dataPushServiceHandler; + private ScheduledFuture connectorServiceHandler; + + public FireAlarmHTTPCommunicator() { + super(); + } + + public FireAlarmHTTPCommunicator(int port) { + super(port); + } + + public FireAlarmHTTPCommunicator(int port, int reconnectionInterval) { + super(port, reconnectionInterval); + } + + public ScheduledFuture getDataPushServiceHandler() { + return dataPushServiceHandler; + } + + public void connect() { + Runnable connect = new Runnable() { + public void run() { + if (!isConnected()) { + try { + processIncomingMessage(); + server.start(); + registerThisDevice(); + publishDeviceData(); + log.info("HTTP Server started at port: " + port); + + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.warn("Unable to 'START' HTTP server. Will retry after " + + timeoutInterval / 1000 + " seconds."); + } + } + } + } + }; + + connectorServiceHandler = service.scheduleAtFixedRate(connect, 0, timeoutInterval, + TimeUnit.MILLISECONDS); + } + + + @Override + public void processIncomingMessage() { + server.setHandler(new AbstractHandler() { + public void handle(String s, Request request, HttpServletRequest + httpServletRequest, + HttpServletResponse httpServletResponse) + throws IOException, ServletException { + httpServletResponse.setContentType("text/html;charset=utf-8"); + httpServletResponse.setStatus(HttpServletResponse.SC_OK); + request.setHandled(true); + + AgentManager agentManager = AgentManager.getInstance(); + String pathContext = request.getPathInfo(); + String separator = File.separator; + + if (pathContext.toUpperCase().contains( + separator + AgentConstants.TEMPERATURE_CONTROL)) { + httpServletResponse.getWriter().println( + agentManager.getTemperature()); + + } else if (pathContext.toUpperCase().contains( + separator + AgentConstants.HUMIDITY_CONTROL)) { + httpServletResponse.getWriter().println( + agentManager.getHumidity()); + + } else if (pathContext.toUpperCase().contains( + separator + AgentConstants.BULB_CONTROL)) { + String[] pathVariables = pathContext.split(separator); + + if (pathVariables.length != 3) { + httpServletResponse.getWriter().println( + "Invalid BULB-control received by the device. Need to be in " + + "'{host}:{port}/BULB/{ON|OFF}' format."); + return; + } + + String switchState = pathVariables[2]; + + if (switchState == null) { + httpServletResponse.getWriter().println( + "Please specify switch-status of the BULB."); + } else { + boolean status = switchState.toUpperCase().equals( + AgentConstants.CONTROL_ON); + agentManager.changeAlarmStatus(status); + httpServletResponse.getWriter().println("Bulb is " + (status ? + AgentConstants.CONTROL_ON : AgentConstants.CONTROL_OFF)); + } + } else { + httpServletResponse.getWriter().println( + "Invalid control command received by the device."); + } + } + }); + } + + @Override + public void publishDeviceData() { + final AgentManager agentManager = AgentManager.getInstance(); + int publishInterval = agentManager.getPushInterval(); + final String deviceOwner = agentManager.getAgentConfigs().getDeviceOwner(); + final String deviceID = agentManager.getAgentConfigs().getDeviceId(); + boolean simulationMode = false; + int duration = 2 * 60; + int frequency = 5; + + Runnable pushDataRunnable = new Runnable() { + @Override + public void run() { + + String pushDataPayload = String.format(AgentConstants.PUSH_DATA_PAYLOAD, deviceOwner, + deviceID, (agentManager.getDeviceIP() + ":" + port), + agentManager.getTemperature()); + executeDataPush(pushDataPayload); + } + }; + + if (!simulationMode) { + dataPushServiceHandler = service.scheduleAtFixedRate(pushDataRunnable, publishInterval, + publishInterval, + TimeUnit.SECONDS); + } else { + String pushDataPayload = String.format(AgentConstants.PUSH_SIMULATION_DATA_PAYLOAD, deviceOwner, + deviceID, (agentManager.getDeviceIP() + ":" + port), + agentManager.getTemperature(), true, duration, frequency); + executeDataPush(pushDataPayload); + + } + } + + + private void executeDataPush(String pushDataPayload) { + AgentManager agentManager = AgentManager.getInstance(); + int responseCode = -1; + String pushDataEndPointURL = agentManager.getPushDataAPIEP(); + HttpURLConnection httpConnection = null; + + try { + httpConnection = TransportUtils.getHttpConnection(agentManager.getPushDataAPIEP()); + httpConnection.setRequestMethod(AgentConstants.HTTP_POST); + httpConnection.setRequestProperty("Authorization", "Bearer " + + agentManager.getAgentConfigs().getAuthToken()); + httpConnection.setRequestProperty("Content-Type", + AgentConstants.APPLICATION_JSON_TYPE); + + httpConnection.setDoOutput(true); + DataOutputStream dataOutPutWriter = new DataOutputStream( + httpConnection.getOutputStream()); + dataOutPutWriter.writeBytes(pushDataPayload); + dataOutPutWriter.flush(); + dataOutPutWriter.close(); + + responseCode = httpConnection.getResponseCode(); + httpConnection.disconnect(); + + log.info(AgentConstants.LOG_APPENDER + "Message - '" + pushDataPayload + + "' was published to server at: " + httpConnection.getURL()); + + } catch (ProtocolException exception) { + String errorMsg = + "Protocol specific error occurred when trying to set method to " + + AgentConstants.HTTP_POST + " for:" + pushDataEndPointURL; + log.error(AgentConstants.LOG_APPENDER + errorMsg); + + } catch (IOException exception) { + String errorMsg = + "An IO error occurred whilst trying to get the response code from: " + + pushDataEndPointURL + " for a " + AgentConstants.HTTP_POST + + " " + "method."; + log.error(AgentConstants.LOG_APPENDER + errorMsg); + + } catch (TransportHandlerException exception) { + log.error(AgentConstants.LOG_APPENDER + + "Error encountered whilst trying to create HTTP-Connection " + + "to IoT-Server EP at: " + + pushDataEndPointURL); + } + + if (responseCode == HttpStatus.CONFLICT_409 || + responseCode == HttpStatus.PRECONDITION_FAILED_412) { + log.warn(AgentConstants.LOG_APPENDER + + "DeviceIP is being Re-Registered due to Push-Data failure " + + "with response code: " + + responseCode); + registerThisDevice(); + + } else if (responseCode != HttpStatus.NO_CONTENT_204) { + if (log.isDebugEnabled()) { + log.error(AgentConstants.LOG_APPENDER + "Status Code: " + responseCode + + " encountered whilst trying to Push-Device-Data to IoT " + + "Server at: " + + agentManager.getPushDataAPIEP()); + } + agentManager.updateAgentStatus(AgentConstants.SERVER_NOT_RESPONDING); + } + + if (log.isDebugEnabled()) { + log.debug(AgentConstants.LOG_APPENDER + "Push-Data call with payload - " + + pushDataPayload + ", to IoT Server returned status " + + responseCode); + } + } + + @Override + public void disconnect() { + Runnable stopConnection = new Runnable() { + public void run() { + while (isConnected()) { + try { + dataPushServiceHandler.cancel(true); + connectorServiceHandler.cancel(true); + closeConnection(); + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.warn(AgentConstants.LOG_APPENDER + + "Unable to 'STOP' HTTP server at port: " + port); + } + + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException e1) { + log.error(AgentConstants.LOG_APPENDER + + "HTTP-Termination: Thread Sleep Interrupt " + + "Exception"); + } + } + } + } + }; + + Thread terminatorThread = new Thread(stopConnection); + terminatorThread.setDaemon(true); + terminatorThread.start(); + } + + @Override + public void processIncomingMessage(Object message, String... messageParams) { + + } + + @Override + public void publishDeviceData(String... publishData) { + + } + + public void registerThisDevice() { + final AgentManager agentManager = AgentManager.getInstance(); + agentManager.updateAgentStatus("Registering..."); + + final Runnable ipRegistration = new Runnable() { + @Override + public void run() { + while (isConnected()) { + try { + int responseCode = registerDeviceIP( + agentManager.getAgentConfigs().getDeviceOwner(), + agentManager.getAgentConfigs().getDeviceId()); + + if (responseCode == HttpStatus.OK_200) { + agentManager.updateAgentStatus(AgentConstants.REGISTERED); + break; + } else { + log.error(AgentConstants.LOG_APPENDER + + "Device Registration with IoT Server at:" + " " + + agentManager.getIpRegistrationEP() + + " failed with response - '" + responseCode + ":" + + HttpStatus.getMessage(responseCode) + "'"); + agentManager.updateAgentStatus(AgentConstants.RETRYING_TO_REGISTER); + } + } catch (AgentCoreOperationException exception) { + log.error(AgentConstants.LOG_APPENDER + + "Error encountered whilst trying to register the " + + "Device's IP at: " + + agentManager.getIpRegistrationEP() + + ".\nCheck whether the network-interface provided is " + + "accurate"); + agentManager.updateAgentStatus(AgentConstants.REGISTRATION_FAILED); + } + + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException e1) { + log.error(AgentConstants.LOG_APPENDER + + "Device Registration: Thread Sleep Interrupt Exception"); + } + } + } + }; + + Thread ipRegisterThread = new Thread(ipRegistration); + ipRegisterThread.setDaemon(true); + ipRegisterThread.start(); + } + + + /** + * This method calls the "Register-API" of the IoT Server in order to register the device's IP + * against its ID. + * + * @param deviceOwner the owner of the device by whose name the agent was downloaded. + * (Read from configuration file) + * @param deviceID the deviceId that is auto-generated whilst downloading the agent. + * (Read from configuration file) + * @return the status code of the HTTP-Post call to the Register-API of the IoT-Server + * @throws AgentCoreOperationException if any errors occur when an HTTPConnection session is + * created + */ + private int registerDeviceIP(String deviceOwner, String deviceID) + throws AgentCoreOperationException { + int responseCode = -1; + final AgentManager agentManager = AgentManager.getInstance(); + + String networkInterface = agentManager.getNetworkInterface(); + String deviceIPAddress = getDeviceIP(networkInterface); + + if (deviceIPAddress == null) { + throw new AgentCoreOperationException( + "An IP address could not be retrieved for the selected network interface - '" + + networkInterface + "."); + } + + agentManager.setDeviceIP(deviceIPAddress); + log.info(AgentConstants.LOG_APPENDER + "Device IP Address: " + deviceIPAddress); + + String deviceIPRegistrationEP = agentManager.getIpRegistrationEP(); + String registerEndpointURLString = + deviceIPRegistrationEP + File.separator + deviceOwner + File.separator + deviceID + + File.separator + deviceIPAddress + File.separator + port; + + if (log.isDebugEnabled()) { + log.debug(AgentConstants.LOG_APPENDER + "DeviceIP Registration EndPoint: " + + registerEndpointURLString); + } + + HttpURLConnection httpConnection; + try { + httpConnection = TransportUtils.getHttpConnection(registerEndpointURLString); + } catch (TransportHandlerException e) { + String errorMsg = + "Protocol specific error occurred when trying to fetch an HTTPConnection to:" + + " " + + registerEndpointURLString; + log.error(AgentConstants.LOG_APPENDER + errorMsg); + throw new AgentCoreOperationException(); + } + + try { + httpConnection.setRequestMethod(AgentConstants.HTTP_POST); + httpConnection.setRequestProperty("Authorization", "Bearer " + + agentManager.getAgentConfigs().getAuthToken()); + httpConnection.setDoOutput(true); + responseCode = httpConnection.getResponseCode(); + + } catch (ProtocolException exception) { + String errorMsg = "Protocol specific error occurred when trying to set method to " + + AgentConstants.HTTP_POST + " for:" + registerEndpointURLString; + log.error(AgentConstants.LOG_APPENDER + errorMsg); + throw new AgentCoreOperationException(errorMsg, exception); + + } catch (IOException exception) { + String errorMsg = "An IO error occurred whilst trying to get the response code from:" + + " " + + registerEndpointURLString + " for a " + AgentConstants.HTTP_POST + " method."; + log.error(AgentConstants.LOG_APPENDER + errorMsg); + throw new AgentCoreOperationException(errorMsg, exception); + } + + log.info(AgentConstants.LOG_APPENDER + "DeviceIP - " + deviceIPAddress + + ", registration with IoT Server at : " + + agentManager.getAgentConfigs().getHTTPS_ServerEndpoint() + + " returned status " + + responseCode); + + return responseCode; + } + + /*------------------------------------------------------------------------------------------*/ + /* Utility methods relevant to creating and sending HTTP requests to the Iot-Server */ + /*------------------------------------------------------------------------------------------*/ + + /** + * This method is used to get the IP of the device in which the agent is run on. + * + * @return the IP Address of the device + * @throws AgentCoreOperationException if any errors occur whilst trying to get the IP address + */ + private String getDeviceIP() throws AgentCoreOperationException { + try { + return Inet4Address.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + String errorMsg = "Error encountered whilst trying to get the device IP address."; + log.error(AgentConstants.LOG_APPENDER + errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + } + + /** + * This is an overloaded method that fetches the public IPv4 address of the given network + * interface + * + * @param networkInterfaceName the network-interface of whose IPv4 address is to be retrieved + * @return the IP Address iof the device + * @throws AgentCoreOperationException if any errors occur whilst trying to get details of the + * given network interface + */ + private String getDeviceIP(String networkInterfaceName) throws + AgentCoreOperationException { + String ipAddress = null; + try { + Enumeration interfaceIPAddresses = NetworkInterface.getByName( + networkInterfaceName).getInetAddresses(); + for (; interfaceIPAddresses.hasMoreElements(); ) { + InetAddress ip = interfaceIPAddresses.nextElement(); + ipAddress = ip.getHostAddress(); + if (log.isDebugEnabled()) { + log.debug(AgentConstants.LOG_APPENDER + "IP Address: " + ipAddress); + } + + if (TransportUtils.validateIPv4(ipAddress)) { + return ipAddress; + } + } + } catch (SocketException | NullPointerException exception) { + String errorMsg = + "Error encountered whilst trying to get IP Addresses of the network interface: " + + networkInterfaceName + + ".\nPlease check whether the name of the network interface used is correct"; + log.error(AgentConstants.LOG_APPENDER + errorMsg); + throw new AgentCoreOperationException(errorMsg, exception); + } + + return ipAddress; + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/communication/mqtt/FireAlarmMQTTCommunicator.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/communication/mqtt/FireAlarmMQTTCommunicator.java new file mode 100644 index 000000000..cb74b048b --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/communication/mqtt/FireAlarmMQTTCommunicator.java @@ -0,0 +1,265 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.communication.mqtt; + +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.device.mgt.iot.virtualfirealarm.agent.core.AgentConstants; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.core.AgentManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.core.AgentUtilOperations; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.exception.AgentCoreOperationException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.transport.TransportHandlerException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.transport.mqtt.MQTTTransportHandler; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +//TODO:: Lincense heade, comments and SPECIFIC class name since its not generic +public class FireAlarmMQTTCommunicator extends MQTTTransportHandler { + + private static final Log log = LogFactory.getLog(FireAlarmMQTTCommunicator.class); + + private ScheduledExecutorService service = Executors.newScheduledThreadPool(2); + private ScheduledFuture dataPushServiceHandler; + + public FireAlarmMQTTCommunicator(String deviceOwner, String deviceType, + String mqttBrokerEndPoint, String subscribeTopic) { + super(deviceOwner, deviceType, mqttBrokerEndPoint, subscribeTopic); + } + + @SuppressWarnings("unused") + public FireAlarmMQTTCommunicator(String deviceOwner, String deviceType, + String mqttBrokerEndPoint, String subscribeTopic, + int intervalInMillis) { + super(deviceOwner, deviceType, mqttBrokerEndPoint, subscribeTopic, intervalInMillis); + } + + public ScheduledFuture getDataPushServiceHandler() { + return dataPushServiceHandler; + } + + //TODO:: Terminate logs with a period + //TODO: Need to print exceptions + @Override + public void connect() { + final AgentManager agentManager = AgentManager.getInstance(); + Runnable connector = new Runnable() { + public void run() { + while (!isConnected()) { + try { + connectToQueue(); + agentManager.updateAgentStatus("Connected to MQTT Queue"); + } catch (TransportHandlerException e) { + log.warn(AgentConstants.LOG_APPENDER + "Connection to MQTT Broker at: " + mqttBrokerEndPoint + + " failed.\n Will retry in " + timeoutInterval + " milli-seconds."); + } + + try{ + subscribeToQueue(); + agentManager.updateAgentStatus("Subscribed to MQTT Queue"); + publishDeviceData(); + + } catch (TransportHandlerException e) { + log.warn(AgentConstants.LOG_APPENDER + "Subscription to MQTT Broker at: " + + mqttBrokerEndPoint + " failed"); + agentManager.updateAgentStatus("Subscription to broker failed."); + } + + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException ex) { + log.error(AgentConstants.LOG_APPENDER + "MQTT: Connect-Thread Sleep Interrupt Exception."); + } + } + } + }; + + Thread connectorThread = new Thread(connector); + connectorThread.setDaemon(true); + connectorThread.start(); + } + + + @Override + public void processIncomingMessage(MqttMessage message, String... messageParams) { + final AgentManager agentManager = AgentManager.getInstance(); + String serverName = agentManager.getAgentConfigs().getServerName(); + String deviceOwner = agentManager.getAgentConfigs().getDeviceOwner(); + String deviceID = agentManager.getAgentConfigs().getDeviceId(); + String receivedMessage; + String replyMessage; + String securePayLoad; + + try { + receivedMessage = AgentUtilOperations.extractMessageFromPayload(message.toString()); + log.info(AgentConstants.LOG_APPENDER + "Message [" + receivedMessage + "] was received"); + } catch (AgentCoreOperationException e) { + log.warn(AgentConstants.LOG_APPENDER + "Could not extract message from payload.", e); + return; + } + + + String[] controlSignal = receivedMessage.split(":"); + // message- ":" format.(ex: "BULB:ON", "TEMPERATURE", "HUMIDITY") + + try { + switch (controlSignal[0].toUpperCase()) { + case AgentConstants.BULB_CONTROL: + boolean stateToSwitch = controlSignal[1].equals(AgentConstants.CONTROL_ON); + agentManager.changeAlarmStatus(stateToSwitch); + log.info(AgentConstants.LOG_APPENDER + "Bulb was switched to state: '" + controlSignal[1] + "'"); + break; + + case AgentConstants.TEMPERATURE_CONTROL: + int currentTemperature = agentManager.getTemperature(); + + String replyTemperature = "Current temperature was read as: '" + currentTemperature + "C'"; + log.info(AgentConstants.LOG_APPENDER + replyTemperature); + + String tempPublishTopic = String.format(AgentConstants.MQTT_PUBLISH_TOPIC, + serverName, deviceOwner, deviceID); + + replyMessage = AgentConstants.TEMPERATURE_CONTROL + ":" + currentTemperature; + securePayLoad = AgentUtilOperations.prepareSecurePayLoad(replyMessage); + publishToQueue(tempPublishTopic, securePayLoad); + break; + + case AgentConstants.HUMIDITY_CONTROL: + int currentHumidity = agentManager.getHumidity(); + + String replyHumidity = "Current humidity was read as: '" + currentHumidity + "%'"; + log.info(AgentConstants.LOG_APPENDER + replyHumidity); + + String humidPublishTopic = String.format( + AgentConstants.MQTT_PUBLISH_TOPIC, serverName, deviceOwner, deviceID); + + replyMessage = AgentConstants.HUMIDITY_CONTROL + ":" + currentHumidity; + securePayLoad = AgentUtilOperations.prepareSecurePayLoad(replyMessage); + publishToQueue(humidPublishTopic, securePayLoad); + break; + + default: + log.warn(AgentConstants.LOG_APPENDER + "'" + controlSignal[0] + + "' is invalid and not-supported for this device-type"); + break; + } + } catch (AgentCoreOperationException e) { + log.warn(AgentConstants.LOG_APPENDER + "Preparing Secure payload failed", e); + } catch (TransportHandlerException e) { + log.error(AgentConstants.LOG_APPENDER + + "MQTT - Publishing, reply message to the MQTT Queue at: " + + agentManager.getAgentConfigs().getMqttBrokerEndpoint() + " failed"); + } + + } + + @Override + public void publishDeviceData() { + final AgentManager agentManager = AgentManager.getInstance(); + int publishInterval = agentManager.getPushInterval(); + Runnable pushDataRunnable = new Runnable() { + @Override + public void run() { + int currentTemperature = agentManager.getTemperature(); + String message = "PUBLISHER:" + AgentConstants.TEMPERATURE_CONTROL + ":" + currentTemperature; + + try { + String payLoad = AgentUtilOperations.prepareSecurePayLoad(message); + + MqttMessage pushMessage = new MqttMessage(); + pushMessage.setPayload(payLoad.getBytes(StandardCharsets.UTF_8)); + pushMessage.setQos(DEFAULT_MQTT_QUALITY_OF_SERVICE); + pushMessage.setRetained(false); + + String topic = String.format(AgentConstants.MQTT_PUBLISH_TOPIC, + agentManager.getAgentConfigs().getServerName(), + agentManager.getAgentConfigs().getDeviceOwner(), + agentManager.getAgentConfigs().getDeviceId()); + + publishToQueue(topic, pushMessage); + log.info(AgentConstants.LOG_APPENDER + "Message: '" + message + "' published to MQTT Queue at [" + + agentManager.getAgentConfigs().getMqttBrokerEndpoint() + "] under topic [" + + topic + "]"); + + } catch (TransportHandlerException e) { + log.warn(AgentConstants.LOG_APPENDER + "Data Publish attempt to topic - [" + + AgentConstants.MQTT_PUBLISH_TOPIC + "] failed for payload [" + message + "]"); + } catch (AgentCoreOperationException e) { + log.warn(AgentConstants.LOG_APPENDER + "Preparing Secure payload failed", e); + } + } + }; + + dataPushServiceHandler = service.scheduleAtFixedRate(pushDataRunnable, publishInterval, publishInterval, + TimeUnit.SECONDS); + } + + + @Override + public void disconnect() { + Runnable stopConnection = new Runnable() { + public void run() { + while (isConnected()) { + + if (dataPushServiceHandler != null) { + dataPushServiceHandler.cancel(true); + } + + try { + closeConnection(); + + } catch (MqttException e) { + if (log.isDebugEnabled()) { + log.warn(AgentConstants.LOG_APPENDER + + "Unable to 'STOP' MQTT connection at broker at: " + + mqttBrokerEndPoint); + } + + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException e1) { + log.error(AgentConstants.LOG_APPENDER + + "MQTT-Terminator: Thread Sleep Interrupt Exception"); + } + } + } + } + }; + + Thread terminatorThread = new Thread(stopConnection); + terminatorThread.setDaemon(true); + terminatorThread.start(); + } + + @Override + public void processIncomingMessage() { + + } + + @Override + public void publishDeviceData(String... publishData) { + + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/communication/xmpp/FireAlarmXMPPCommunicator.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/communication/xmpp/FireAlarmXMPPCommunicator.java new file mode 100644 index 000000000..86c4a0f7b --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/communication/xmpp/FireAlarmXMPPCommunicator.java @@ -0,0 +1,265 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.communication.xmpp; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jivesoftware.smack.packet.Message; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.core.AgentConstants; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.core.AgentManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.core.AgentUtilOperations; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.exception.AgentCoreOperationException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.transport.TransportHandlerException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.transport.xmpp.XMPPTransportHandler; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class FireAlarmXMPPCommunicator extends XMPPTransportHandler { + + private static final Log log = LogFactory.getLog(FireAlarmXMPPCommunicator.class); + + private ScheduledExecutorService service = Executors.newScheduledThreadPool(2); + private ScheduledFuture dataPushServiceHandler; + private ScheduledFuture connectorServiceHandler; + + private String username; + private String password; + private String resource; + private String xmppAdminJID; + private String xmppDeviceJID; + + public FireAlarmXMPPCommunicator(String server) { + super(server); + } + + public FireAlarmXMPPCommunicator(String server, int port) { + super(server, port); + } + + public FireAlarmXMPPCommunicator(String server, int port, int timeout) { + super(server, port, timeout); + } + + public ScheduledFuture getDataPushServiceHandler() { + return dataPushServiceHandler; + } + + @Override + public void connect() { + final AgentManager agentManager = AgentManager.getInstance(); + username = agentManager.getAgentConfigs().getDeviceId(); + password = agentManager.getAgentConfigs().getAuthToken(); + resource = agentManager.getAgentConfigs().getDeviceOwner(); + + xmppDeviceJID = username + "@" + server; + xmppAdminJID = agentManager.getAgentConfigs().getServerName() + "_" + AgentConstants.DEVICE_TYPE + "@" + server; + + + Runnable connect = new Runnable() { + public void run() { + if (!isConnected()) { + try { + connectToServer(); + } catch (TransportHandlerException e) { + log.warn(AgentConstants.LOG_APPENDER + "Connection to XMPP server at: " + server + " failed"); + } + + try { + loginToServer(username, password, resource); + agentManager.updateAgentStatus("Connected to XMPP Server"); + setMessageFilterAndListener(xmppAdminJID, xmppDeviceJID, true); + publishDeviceData(); + } catch (TransportHandlerException e) { + log.warn(AgentConstants.LOG_APPENDER + "Login to XMPP server at: " + server + " failed"); + agentManager.updateAgentStatus("No XMPP Account for Device"); + } + } + } + }; + + connectorServiceHandler = service.scheduleAtFixedRate(connect, 0, timeoutInterval, TimeUnit.MILLISECONDS); + } + + /** + * This is an abstract method used for post processing the received XMPP-message. This + * method will be implemented as per requirement at the time of creating an object of this + * class. + * + * @param xmppMessage the xmpp message received by the listener. + */ + @Override + public void processIncomingMessage(Message xmppMessage, String... messageParams) { + final AgentManager agentManager = AgentManager.getInstance(); + String from = xmppMessage.getFrom(); + String message = xmppMessage.getBody(); + String receivedMessage; + String replyMessage; + String securePayLoad; + + try { + receivedMessage = AgentUtilOperations.extractMessageFromPayload(message); + log.info(AgentConstants.LOG_APPENDER + "Message [" + receivedMessage + "] was received"); + } catch (AgentCoreOperationException e) { + log.warn(AgentConstants.LOG_APPENDER + "Could not extract message from payload.", e); + return; + } + + String[] controlSignal = receivedMessage.split(":"); + //message- ":" format. (ex: "BULB:ON", "TEMPERATURE", "HUMIDITY") + + try { + switch (controlSignal[0].toUpperCase()) { + case AgentConstants.BULB_CONTROL: + if (controlSignal.length != 2) { + replyMessage = "BULB controls need to be in the form - 'BULB:{ON|OFF}'"; + log.warn(replyMessage); + securePayLoad = AgentUtilOperations.prepareSecurePayLoad(replyMessage); + sendXMPPMessage(xmppAdminJID, securePayLoad, "CONTROL-REPLY"); + break; + } + + agentManager.changeAlarmStatus(controlSignal[1].equals(AgentConstants.CONTROL_ON)); + log.info(AgentConstants.LOG_APPENDER + "Bulb was switched to state: '" + controlSignal[1] + "'"); + break; + + case AgentConstants.TEMPERATURE_CONTROL: + int currentTemperature = agentManager.getTemperature(); + + String replyTemperature = + "The current temperature was read to be: '" + currentTemperature + + "C'"; + log.info(AgentConstants.LOG_APPENDER + replyTemperature); + + replyMessage = AgentConstants.TEMPERATURE_CONTROL + ":" + currentTemperature; + securePayLoad = AgentUtilOperations.prepareSecurePayLoad(replyMessage); + sendXMPPMessage(xmppAdminJID, securePayLoad, "CONTROL-REPLY"); + break; + + case AgentConstants.HUMIDITY_CONTROL: + int currentHumidity = agentManager.getHumidity(); + + String replyHumidity = "The current humidity was read to be: '" + currentHumidity + "%'"; + log.info(AgentConstants.LOG_APPENDER + replyHumidity); + + replyMessage = AgentConstants.HUMIDITY_CONTROL + ":" + currentHumidity; + securePayLoad = AgentUtilOperations.prepareSecurePayLoad(replyMessage); + sendXMPPMessage(xmppAdminJID, securePayLoad, "CONTROL-REPLY"); + break; + + default: + replyMessage = "'" + controlSignal[0] + "' is invalid and not-supported for this device-type"; + log.warn(replyMessage); + securePayLoad = AgentUtilOperations.prepareSecurePayLoad(replyMessage); + sendXMPPMessage(xmppAdminJID, securePayLoad, "CONTROL-ERROR"); + break; + } + } catch (AgentCoreOperationException e) { + log.warn(AgentConstants.LOG_APPENDER + "Preparing Secure payload failed", e); + } + + } + + + @Override + public void publishDeviceData() { + final AgentManager agentManager = AgentManager.getInstance(); + int publishInterval = agentManager.getPushInterval(); + + Runnable pushDataRunnable = new Runnable() { + @Override + public void run() { + Message xmppMessage = new Message(); + + try { + int currentTemperature = agentManager.getTemperature(); + + String message = AgentConstants.TEMPERATURE_CONTROL + ":" + currentTemperature; + String payLoad = AgentUtilOperations.prepareSecurePayLoad(message); + + xmppMessage.setTo(xmppAdminJID); + xmppMessage.setSubject("PUBLISHER"); + xmppMessage.setBody(payLoad); + xmppMessage.setType(Message.Type.chat); + + sendXMPPMessage(xmppAdminJID, xmppMessage); + log.info(AgentConstants.LOG_APPENDER + "Message: '" + message + "' sent to XMPP JID - " + + "[" + xmppAdminJID + "] under subject [" + xmppMessage.getSubject() + "]."); + } catch (AgentCoreOperationException e) { + log.warn(AgentConstants.LOG_APPENDER + "Preparing Secure payload failed for XMPP JID - " + + "[" + xmppAdminJID + "] with subject - [" + xmppMessage.getSubject() + "]."); + } + } + }; + + dataPushServiceHandler = service.scheduleAtFixedRate(pushDataRunnable, publishInterval, + publishInterval, TimeUnit.SECONDS); + } + + + @Override + public void disconnect() { + Runnable stopConnection = new Runnable() { + public void run() { + + if (dataPushServiceHandler != null) { + dataPushServiceHandler.cancel(true); + } + + if (connectorServiceHandler != null) { + connectorServiceHandler.cancel(true); + } + + while (isConnected()) { + closeConnection(); + + if (log.isDebugEnabled()) { + log.warn(AgentConstants.LOG_APPENDER + + "Unable to 'STOP' connection to XMPP server at: " + server); + } + + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException e1) { + log.error(AgentConstants.LOG_APPENDER + "XMPP-Terminator: Thread Sleep Interrupt Exception"); + } + + } + } + }; + + Thread terminatorThread = new Thread(stopConnection); + terminatorThread.setDaemon(true); + terminatorThread.start(); + } + + + @Override + public void processIncomingMessage() { + + } + + @Override + public void publishDeviceData(String... publishData) { + + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/core/AgentConfiguration.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/core/AgentConfiguration.java new file mode 100644 index 000000000..e36b161cb --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/core/AgentConfiguration.java @@ -0,0 +1,155 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.core; + +/** + * A Configuration class that holds all the Agent specific details that are read from the + * 'deviceConfig.properties' file. This file is generated by the IoT-Server at the time of + * downloading the device agent from the IoT-Server. + */ +public class AgentConfiguration { + private String serverName; + private String deviceOwner; + private String deviceId; + private String deviceName; + private String controllerContext; + private String HTTPS_ServerEndpoint; + private String HTTP_ServerEndpoint; + private String apimGatewayEndpoint; + private String mqttBrokerEndpoint; + private String xmppServerEndpoint; + private String authMethod; + private String authToken; + private String refreshToken; + private int dataPushInterval; + + public String getServerName() { + return serverName; + } + + public void setServerName(String serverName) { + this.serverName = serverName; + } + + public String getDeviceOwner() { + return deviceOwner; + } + + public void setDeviceOwner(String deviceOwner) { + this.deviceOwner = deviceOwner; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getDeviceName() { + return deviceName; + } + + public void setDeviceName(String deviceName) { + this.deviceName = deviceName; + } + + public String getControllerContext() { + return controllerContext; + } + + public void setControllerContext(String controllerContext) { + this.controllerContext = controllerContext; + } + + public String getHTTPS_ServerEndpoint() { + return HTTPS_ServerEndpoint; + } + + public void setHTTPS_ServerEndpoint(String HTTPS_ServerEndpoint) { + this.HTTPS_ServerEndpoint = HTTPS_ServerEndpoint; + } + + public String getHTTP_ServerEndpoint() { + return HTTP_ServerEndpoint; + } + + public void setHTTP_ServerEndpoint(String HTTP_ServerEndpoint) { + this.HTTP_ServerEndpoint = HTTP_ServerEndpoint; + } + + public String getApimGatewayEndpoint() { + return apimGatewayEndpoint; + } + + public void setApimGatewayEndpoint(String apimGatewayEndpoint) { + this.apimGatewayEndpoint = apimGatewayEndpoint; + } + + public String getMqttBrokerEndpoint() { + return mqttBrokerEndpoint; + } + + public void setMqttBrokerEndpoint(String mqttBrokerEndpoint) { + this.mqttBrokerEndpoint = mqttBrokerEndpoint; + } + + public String getXmppServerEndpoint() { + return xmppServerEndpoint; + } + + public void setXmppServerEndpoint(String xmppServerEndpoint) { + this.xmppServerEndpoint = xmppServerEndpoint; + } + + public String getAuthMethod() { + return authMethod; + } + + public void setAuthMethod(String authMethod) { + this.authMethod = authMethod; + } + + public String getAuthToken() { + return authToken; + } + + public void setAuthToken(String authToken) { + this.authToken = authToken; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public int getDataPushInterval() { + return dataPushInterval; + } + + public void setDataPushInterval(int dataPushInterval) { + this.dataPushInterval = dataPushInterval; + } +} + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/core/AgentConstants.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/core/AgentConstants.java new file mode 100644 index 000000000..f415ef9e0 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/core/AgentConstants.java @@ -0,0 +1,109 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.core; + +public class AgentConstants { + public static final String DEVICE_TYPE = "virtual_firealarm"; + public static final String LOG_APPENDER = "AGENT_LOG:: "; + public static final String PROPERTIES_FILE_PATH = ""; + public static final int DEFAULT_RETRY_THREAD_INTERVAL = 5000; // time in millis + /* --------------------------------------------------------------------------------------- + IoT-Server specific information + --------------------------------------------------------------------------------------- */ + public static final String DEVICE_CONTROLLER_API_EP = "/virtual_firealarm/controller"; + public static final String DEVICE_ENROLLMENT_API_EP = "/scep"; + public static final String DEVICE_REGISTER_API_EP = "/register"; + public static final String DEVICE_PUSH_TEMPERATURE_API_EP = "/temperature"; + public static final String PUSH_DATA_PAYLOAD = + "{\"owner\":\"%s\",\"deviceId\":\"%s\",\"reply\":\"%s\",\"value\":\"%s\"}"; + + public static final String PUSH_SIMULATION_DATA_PAYLOAD = + "{\"owner\":\"%s\",\"deviceId\":\"%s\",\"reply\":\"%s\",\"value\":\"%s\",\"isSimulated\":\"%s\",\"duration\":\"%s\",\"frequency\":\"%s\"}"; + + public static final String AGENT_CONTROL_APP_EP = "/devicemgt/device/%s?id=%s"; + public static final String DEVICE_DETAILS_PAGE_EP = "/devicemgt/device/%s?id=%s"; + public static final String DEVICE_ANALYTICS_PAGE_URL = "/devicemgt/analytics?deviceId=%s&deviceType=%s&deviceName=%s"; + /* --------------------------------------------------------------------------------------- + HTTP Connection specific information for communicating with IoT-Server + --------------------------------------------------------------------------------------- */ + public static final String HTTP_POST = "POST"; + public static final String HTTP_GET = "GET"; + public static final String APPLICATION_JSON_TYPE = "application/json"; + public static final String REGISTERED = "Registered"; + public static final String NOT_REGISTERED = "Not-Registered"; + public static final String REGISTRATION_FAILED = "Registration Failed"; + public static final String RETRYING_TO_REGISTER = "Registration Failed. Re-trying.."; + public static final String SERVER_NOT_RESPONDING = "Server not responding.."; + + /* --------------------------------------------------------------------------------------- + MQTT Connection specific information + --------------------------------------------------------------------------------------- */ + public static final int DEFAULT_MQTT_RECONNECTION_INTERVAL = 2; // time in seconds + public static final int DEFAULT_MQTT_QUALITY_OF_SERVICE = 0; + public static final String MQTT_SUBSCRIBE_TOPIC = "%s/%s/" + DEVICE_TYPE + "/%s"; + public static final String MQTT_PUBLISH_TOPIC = "%s/%s/" + DEVICE_TYPE + "/%s/publisher"; + /* --------------------------------------------------------------------------------------- + XMPP Connection specific information + --------------------------------------------------------------------------------------- */ + public static final String XMPP_ADMIN_ACCOUNT_UNAME = "admin"; + /* --------------------------------------------------------------------------------------- + Device/Agent specific properties to be read from the 'deviceConfig.properties' file + --------------------------------------------------------------------------------------- */ + public static final String AGENT_PROPERTIES_FILE_NAME = "deviceConfig.properties"; + public static final String SERVER_NAME_PROPERTY = "server-name"; + public static final String DEVICE_OWNER_PROPERTY = "owner"; + public static final String DEVICE_ID_PROPERTY = "deviceId"; + public static final String DEVICE_NAME_PROPERTY = "device-name"; + public static final String DEVICE_CONTROLLER_CONTEXT_PROPERTY = "controller-context"; + public static final String SERVER_HTTPS_EP_PROPERTY = "https-ep"; + public static final String SERVER_HTTP_EP_PROPERTY = "http-ep"; + public static final String APIM_GATEWAY_EP_PROPERTY = "apim-ep"; + public static final String MQTT_BROKER_EP_PROPERTY = "mqtt-ep"; + public static final String XMPP_SERVER_EP_PROPERTY = "xmpp-ep"; + public static final String AUTH_METHOD_PROPERTY = "auth-method"; + public static final String AUTH_TOKEN_PROPERTY = "auth-token"; + public static final String REFRESH_TOKEN_PROPERTY = "refresh-token"; + public static final String NETWORK_INTERFACE_PROPERTY = "network-interface"; + public static final String PUSH_INTERVAL_PROPERTY = "push-interval"; + /* --------------------------------------------------------------------------------------- + Default values for the Device/Agent specific configurations listed above + --------------------------------------------------------------------------------------- */ + public static final String DEFAULT_NETWORK_INTERFACE = "en0"; + public static final int DEFAULT_DATA_PUBLISH_INTERVAL = 15; // seconds + public static final String DEFAULT_PROTOCOL = "MQTT"; + /* --------------------------------------------------------------------------------------- + Control Signal specific constants to match the request context + --------------------------------------------------------------------------------------- */ + public static final String BULB_CONTROL = "BULB"; + public static final String TEMPERATURE_CONTROL = "TEMPERATURE"; + public static final String POLICY_SIGNAL = "POLICY"; + public static final String HUMIDITY_CONTROL = "HUMIDITY"; + public static final String CONTROL_ON = "ON"; + public static final String CONTROL_OFF = "OFF"; + public static final String AUDIO_FILE_NAME = "fireAlarmSound.mid"; + /* --------------------------------------------------------------------------------------- + Communication protocol specific Strings + --------------------------------------------------------------------------------------- */ + public static final String TCP_PREFIX = "tcp://"; + public static final String HTTP_PREFIX = "http://"; + public static final String HTTPS_PREFIX = "https://"; + public static final String HTTP_PROTOCOL = "HTTP"; + public static final String MQTT_PROTOCOL = "MQTT"; + public static final String XMPP_PROTOCOL = "XMPP"; +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/core/AgentManager.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/core/AgentManager.java new file mode 100644 index 000000000..b1d204d62 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/core/AgentManager.java @@ -0,0 +1,347 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.core; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.enrollment.EnrollmentManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.exception.AgentCoreOperationException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.transport.TransportHandler; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.transport.TransportHandlerException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.transport.TransportUtils; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.communication.http.FireAlarmHTTPCommunicator; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.communication.mqtt.FireAlarmMQTTCommunicator; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.communication.xmpp.FireAlarmXMPPCommunicator; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.virtual.VirtualHardwareManager; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class AgentManager { + + private static final Log log = LogFactory.getLog(AgentManager.class); + private static AgentManager agentManager; + private String rootPath = ""; + + private boolean deviceReady = false; + private boolean isAlarmOn = false; + + private String deviceMgtControlUrl, deviceMgtAnalyticUrl; + private String deviceName, agentStatus; + + private int pushInterval; // seconds + private String prevProtocol, protocol; + + private String networkInterface; + private List interfaceList, protocolList; + private Map agentCommunicator; + + private AgentConfiguration agentConfigs; + + private String deviceIP; + private String enrollmentEP; + private String ipRegistrationEP; + private String pushDataAPIEP; + + private AgentManager() { + } + + public static AgentManager getInstance() { + if (agentManager == null) { + agentManager = new AgentManager(); + } + return agentManager; + } + + public void init() { + + agentCommunicator = new HashMap<>(); + + // Read IoT-Server specific configurations from the 'deviceConfig.properties' file + try { + this.agentConfigs = AgentUtilOperations.readIoTServerConfigs(); + } catch (AgentCoreOperationException e) { + log.error("Reading device configuration from configd file failed:\n"); + log.error(e); + System.exit(0); + } + + // Initialise IoT-Server URL endpoints from the configuration read from file + AgentUtilOperations.initializeServerEndPoints(); + + String analyticsPageContext = String.format(AgentConstants.DEVICE_ANALYTICS_PAGE_URL, + agentConfigs.getDeviceId(), + AgentConstants.DEVICE_TYPE, + agentConfigs.getDeviceName()); + + String controlPageContext = String.format(AgentConstants.DEVICE_DETAILS_PAGE_EP, + AgentConstants.DEVICE_TYPE, + agentConfigs.getDeviceId()); + + this.deviceMgtAnalyticUrl = agentConfigs.getHTTPS_ServerEndpoint() + analyticsPageContext; + this.deviceMgtControlUrl = agentConfigs.getHTTPS_ServerEndpoint() + controlPageContext; + + this.agentStatus = AgentConstants.NOT_REGISTERED; + this.deviceName = this.agentConfigs.getDeviceName(); + + this.pushInterval = this.agentConfigs.getDataPushInterval(); + this.networkInterface = AgentConstants.DEFAULT_NETWORK_INTERFACE; + + this.protocol = AgentConstants.DEFAULT_PROTOCOL; + this.prevProtocol = protocol; + + Map xmppIPPortMap; + try { + xmppIPPortMap = TransportUtils.getHostAndPort(agentConfigs.getXmppServerEndpoint()); + String xmppServer = xmppIPPortMap.get("Host"); + int xmppPort = Integer.parseInt(xmppIPPortMap.get("Port")); + + TransportHandler xmppCommunicator = new FireAlarmXMPPCommunicator(xmppServer, xmppPort); + agentCommunicator.put(AgentConstants.XMPP_PROTOCOL, xmppCommunicator); + + } catch (TransportHandlerException e) { + log.error("XMPP Endpoint String - " + agentConfigs.getXmppServerEndpoint() + + ", provided in the configuration file is invalid."); + } + String mqttTopic = String.format(AgentConstants.MQTT_SUBSCRIBE_TOPIC, + agentConfigs.getServerName(), + agentConfigs.getDeviceOwner(), + agentConfigs.getDeviceId()); + +// TransportHandler httpCommunicator = new FireAlarmHTTPCommunicator(); + TransportHandler mqttCommunicator = new FireAlarmMQTTCommunicator(agentConfigs.getDeviceOwner(), + agentConfigs.getDeviceId(), + agentConfigs.getMqttBrokerEndpoint(), + mqttTopic); + +// agentCommunicator.put(AgentConstants.HTTP_PROTOCOL, httpCommunicator); + agentCommunicator.put(AgentConstants.MQTT_PROTOCOL, mqttCommunicator); + + try { + interfaceList = new ArrayList<>(TransportUtils.getInterfaceIPMap().keySet()); + protocolList = new ArrayList<>(agentCommunicator.keySet()); + } catch (TransportHandlerException e) { + log.error("An error occurred whilst retrieving all NetworkInterface-IP mappings"); + } + + //Initializing hardware at that point + //AgentManger.setDeviceReady() method should invoked from hardware after initialization + VirtualHardwareManager.getInstance().init(); + + //Wait till hardware get ready + while (!deviceReady) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + log.info(AgentConstants.LOG_APPENDER + "Sleep error in 'device ready-flag' checking thread"); + } + } + + try { + EnrollmentManager.getInstance().beginEnrollmentFlow(); + } catch (AgentCoreOperationException e) { + log.error("Device Enrollment Failed:\n"); + log.error(e); + System.exit(0); + } + + //Start agent communication + agentCommunicator.get(protocol).connect(); + } + + private void switchCommunicator(String stopProtocol, String startProtocol) { + agentCommunicator.get(stopProtocol).disconnect(); + + while (agentCommunicator.get(stopProtocol).isConnected()) { + try { + Thread.sleep(250); + } catch (InterruptedException e) { + log.info(AgentConstants.LOG_APPENDER + + "Sleep error in 'Switch-Communicator' Thread's shutdown wait."); + } + } + + agentCommunicator.get(startProtocol).connect(); + } + + public void setInterface(int interfaceId) { + if (interfaceId != -1) { + String newInterface = interfaceList.get(interfaceId); + + if (!newInterface.equals(networkInterface)) { + networkInterface = newInterface; + + if (protocol.equals(AgentConstants.HTTP_PROTOCOL) && !protocol.equals( + prevProtocol)) { + switchCommunicator(protocol, protocol); + } + } + } + } + + public void setProtocol(int protocolId) { + if (protocolId != -1) { + String newProtocol = protocolList.get(protocolId); + + if (!protocol.equals(newProtocol)) { + prevProtocol = protocol; + protocol = newProtocol; + switchCommunicator(prevProtocol, protocol); + } + } + } + + public void changeAlarmStatus(boolean isOn) { + VirtualHardwareManager.getInstance().changeAlarmStatus(isOn); + isAlarmOn = isOn; + } + + public void updateAgentStatus(String status) { + this.agentStatus = status; + } + + /*------------------------------------------------------------------------------------------*/ + /* Getter and Setter Methods for the private variables */ + /*------------------------------------------------------------------------------------------*/ + + public void setRootPath(String rootPath) { + this.rootPath = rootPath; + } + + public void setDeviceReady(boolean deviceReady) { + this.deviceReady = deviceReady; + } + + public String getDeviceMgtControlUrl() { + return deviceMgtControlUrl; + } + + public String getDeviceMgtAnalyticUrl() { + return deviceMgtAnalyticUrl; + } + + public AgentConfiguration getAgentConfigs() { + return agentConfigs; + } + + public String getDeviceIP() { + return deviceIP; + } + + public void setDeviceIP(String deviceIP) { + this.deviceIP = deviceIP; + } + + public String getEnrollmentEP() { + return enrollmentEP; + } + + public void setEnrollmentEP(String enrollmentEP) { + this.enrollmentEP = enrollmentEP; + } + + public String getIpRegistrationEP() { + return ipRegistrationEP; + } + + public void setIpRegistrationEP(String ipRegistrationEP) { + this.ipRegistrationEP = ipRegistrationEP; + } + + public String getPushDataAPIEP() { + return pushDataAPIEP; + } + + public void setPushDataAPIEP(String pushDataAPIEP) { + this.pushDataAPIEP = pushDataAPIEP; + } + + public String getDeviceName() { + return deviceName; + } + + public String getNetworkInterface() { + return networkInterface; + } + + public String getAgentStatus() { + return agentStatus; + } + + public int getPushInterval() { + return pushInterval; + } + + public void setPushInterval(int pushInterval) { + this.pushInterval = pushInterval; + TransportHandler transportHandler = agentCommunicator.get(protocol); + + switch (protocol) { + case AgentConstants.HTTP_PROTOCOL: + ((FireAlarmHTTPCommunicator) transportHandler).getDataPushServiceHandler() + .cancel(true); + break; + case AgentConstants.MQTT_PROTOCOL: + ((FireAlarmMQTTCommunicator) transportHandler).getDataPushServiceHandler() + .cancel(true); + break; + case AgentConstants.XMPP_PROTOCOL: + ((FireAlarmXMPPCommunicator) transportHandler).getDataPushServiceHandler() + .cancel(true); + break; + default: + log.warn("Unknown protocol " + protocol); + } + transportHandler.publishDeviceData(); + + if (log.isDebugEnabled()) { + log.debug("The Data Publish Interval was changed to: " + pushInterval); + } + } + + public List getInterfaceList() { + return interfaceList; + } + + public List getProtocolList() { + return protocolList; + } + + /** + * Get temperature reading from device + * @return Temperature + */ + public int getTemperature() { + return VirtualHardwareManager.getInstance().getTemperature(); + } + + /** + * Get humidity reading from device + * @return Humidity + */ + public int getHumidity(){ + return VirtualHardwareManager.getInstance().getHumidity(); + } + + public boolean isAlarmOn() { + return isAlarmOn; + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/core/AgentUtilOperations.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/core/AgentUtilOperations.java new file mode 100644 index 000000000..d12baaa2b --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/core/AgentUtilOperations.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.device.mgt.iot.virtualfirealarm.agent.core; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONObject; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.enrollment.EnrollmentManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.exception.AgentCoreOperationException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.transport.CommunicationUtils; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.transport.TransportHandlerException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Properties; + +/** + * This class contains all the core operations of the FireAlarm agent that are common to both + * Virtual and Real Scenarios. These operations include, connecting to and subscribing to an MQTT + * queue and to a XMPP Server. Pushing temperature data to the IoT-Server at timely intervals. + * Reading device specific configuration from a configs file etc.... + */ +public class AgentUtilOperations { + + private static final Log log = LogFactory.getLog(AgentUtilOperations.class); + private static final String JSON_MESSAGE_KEY = "Msg"; + private static final String JSON_SIGNATURE_KEY = "Sig"; + + /** + * This method reads the agent specific configurations for the device from the + * "deviceConfigs.properties" file found at /repository/conf folder. + * If the properties file is not found in the specified path, then the configuration values + * are set to the default ones in the 'AgentConstants' class. + * + * @return an object of type 'AgentConfiguration' which contains all the necessary + * configuration attributes + */ + public static AgentConfiguration readIoTServerConfigs() throws AgentCoreOperationException { + AgentManager agentManager = AgentManager.getInstance(); + AgentConfiguration iotServerConfigs = new AgentConfiguration(); + Properties properties = new Properties(); + InputStream propertiesInputStream = null; + String propertiesFileName = AgentConstants.AGENT_PROPERTIES_FILE_NAME; + + try { + ClassLoader loader = AgentUtilOperations.class.getClassLoader(); + URL path = loader.getResource(propertiesFileName); + System.out.println(path); + String root = path.getPath().replace("wso2-firealarm-virtual-agent.jar!/deviceConfig.properties", "") + .replace("jar:", "").replace("file:", ""); + + root = URLDecoder.decode(root, StandardCharsets.UTF_8.toString()); + agentManager.setRootPath(root); + + String deviceConfigFilePath = root + AgentConstants.AGENT_PROPERTIES_FILE_NAME; + propertiesInputStream = new FileInputStream(deviceConfigFilePath); + + //load a properties file from class path, inside static method + properties.load(propertiesInputStream); + + iotServerConfigs.setServerName(properties.getProperty( + AgentConstants.SERVER_NAME_PROPERTY)); + iotServerConfigs.setDeviceOwner(properties.getProperty( + AgentConstants.DEVICE_OWNER_PROPERTY)); + iotServerConfigs.setDeviceId(properties.getProperty( + AgentConstants.DEVICE_ID_PROPERTY)); + iotServerConfigs.setDeviceName(properties.getProperty( + AgentConstants.DEVICE_NAME_PROPERTY)); + iotServerConfigs.setControllerContext(properties.getProperty( + AgentConstants.DEVICE_CONTROLLER_CONTEXT_PROPERTY)); + iotServerConfigs.setHTTPS_ServerEndpoint(properties.getProperty( + AgentConstants.SERVER_HTTPS_EP_PROPERTY)); + iotServerConfigs.setHTTP_ServerEndpoint(properties.getProperty( + AgentConstants.SERVER_HTTP_EP_PROPERTY)); + iotServerConfigs.setApimGatewayEndpoint(properties.getProperty( + AgentConstants.APIM_GATEWAY_EP_PROPERTY)); + iotServerConfigs.setMqttBrokerEndpoint(properties.getProperty( + AgentConstants.MQTT_BROKER_EP_PROPERTY)); + iotServerConfigs.setXmppServerEndpoint(properties.getProperty( + AgentConstants.XMPP_SERVER_EP_PROPERTY)); + iotServerConfigs.setAuthMethod(properties.getProperty( + AgentConstants.AUTH_METHOD_PROPERTY)); + iotServerConfigs.setAuthToken(properties.getProperty( + AgentConstants.AUTH_TOKEN_PROPERTY)); + iotServerConfigs.setRefreshToken(properties.getProperty( + AgentConstants.REFRESH_TOKEN_PROPERTY)); + iotServerConfigs.setDataPushInterval(Integer.parseInt(properties.getProperty( + AgentConstants.PUSH_INTERVAL_PROPERTY))); + + log.info(AgentConstants.LOG_APPENDER + "Server name: " + + iotServerConfigs.getServerName()); + log.info(AgentConstants.LOG_APPENDER + "Device Owner: " + + iotServerConfigs.getDeviceOwner()); + log.info(AgentConstants.LOG_APPENDER + "Device ID: " + iotServerConfigs.getDeviceId()); + log.info(AgentConstants.LOG_APPENDER + "Device Name: " + + iotServerConfigs.getDeviceName()); + log.info(AgentConstants.LOG_APPENDER + "Device Controller Context: " + + iotServerConfigs.getControllerContext()); + log.info(AgentConstants.LOG_APPENDER + "IoT Server HTTPS EndPoint: " + + iotServerConfigs.getHTTPS_ServerEndpoint()); + log.info(AgentConstants.LOG_APPENDER + "IoT Server HTTP EndPoint: " + + iotServerConfigs.getHTTP_ServerEndpoint()); + log.info(AgentConstants.LOG_APPENDER + "API-Manager Gateway EndPoint: " + + iotServerConfigs.getApimGatewayEndpoint()); + log.info(AgentConstants.LOG_APPENDER + "MQTT Broker EndPoint: " + + iotServerConfigs.getMqttBrokerEndpoint()); + log.info(AgentConstants.LOG_APPENDER + "XMPP Server EndPoint: " + + iotServerConfigs.getXmppServerEndpoint()); + log.info(AgentConstants.LOG_APPENDER + "Authentication Method: " + + iotServerConfigs.getAuthMethod()); + log.info(AgentConstants.LOG_APPENDER + "Authentication Token: " + + iotServerConfigs.getAuthToken()); + log.info(AgentConstants.LOG_APPENDER + "Refresh Token: " + + iotServerConfigs.getRefreshToken()); + log.info(AgentConstants.LOG_APPENDER + "Data Push Interval: " + + iotServerConfigs.getDataPushInterval()); + + } catch (FileNotFoundException ex) { + String errorMsg = "[" + propertiesFileName + "] file not found at: " + AgentConstants.PROPERTIES_FILE_PATH; + log.error(AgentConstants.LOG_APPENDER + errorMsg); + throw new AgentCoreOperationException(errorMsg); + + } catch (IOException ex) { + String errorMsg = "Error occurred whilst trying to fetch [" + propertiesFileName + "] from: " + + AgentConstants.PROPERTIES_FILE_PATH; + log.error(AgentConstants.LOG_APPENDER + errorMsg); + throw new AgentCoreOperationException(errorMsg); + } finally { + if (propertiesInputStream != null) { + try { + propertiesInputStream.close(); + } catch (IOException e) { + log.error(AgentConstants.LOG_APPENDER + + "Error occurred whilst trying to close InputStream resource used to read the '" + + propertiesFileName + "' file"); + } + } + } + return iotServerConfigs; + } + + /** + * This method constructs the URLs for each of the API Endpoints called by the device agent + * Ex: Register API, Push-Data API + * + * @throws AgentCoreOperationException if any error occurs at socket level whilst trying to + * retrieve the deviceIP of the network-interface read + * from the configs file + */ + public static void initializeServerEndPoints() { + AgentManager agentManager = AgentManager.getInstance(); + String serverSecureEndpoint = agentManager.getAgentConfigs().getHTTPS_ServerEndpoint(); + String serverUnSecureEndpoint = agentManager.getAgentConfigs().getHTTP_ServerEndpoint(); + String backEndContext = agentManager.getAgentConfigs().getControllerContext(); + + String deviceControllerAPIEndpoint = serverSecureEndpoint + backEndContext; + + String deviceEnrollmentEndpoint = + serverUnSecureEndpoint + backEndContext + AgentConstants.DEVICE_ENROLLMENT_API_EP; + agentManager.setEnrollmentEP(deviceEnrollmentEndpoint); + + String registerEndpointURL = + deviceControllerAPIEndpoint + AgentConstants.DEVICE_REGISTER_API_EP; + agentManager.setIpRegistrationEP(registerEndpointURL); + + String pushDataEndPointURL = + deviceControllerAPIEndpoint + AgentConstants.DEVICE_PUSH_TEMPERATURE_API_EP; + agentManager.setPushDataAPIEP(pushDataEndPointURL); + + log.info(AgentConstants.LOG_APPENDER + "IoT Server's Device Controller API Endpoint: " + + deviceControllerAPIEndpoint); + log.info(AgentConstants.LOG_APPENDER + "Device Enrollment EndPoint: " + + registerEndpointURL); + log.info(AgentConstants.LOG_APPENDER + "DeviceIP Registration EndPoint: " + + registerEndpointURL); + log.info(AgentConstants.LOG_APPENDER + "Push-Data API EndPoint: " + pushDataEndPointURL); + } + + + public static String prepareSecurePayLoad(String message) throws AgentCoreOperationException { + PublicKey serverPublicKey = EnrollmentManager.getInstance().getServerPublicKey(); + PrivateKey devicePrivateKey = EnrollmentManager.getInstance().getPrivateKey(); + + String encryptedMsg; + try { + encryptedMsg = CommunicationUtils.encryptMessage(message, serverPublicKey); + } catch (TransportHandlerException e) { + String errorMsg = "Error occurred whilst trying to encryptMessage: [" + message + "]"; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + + String signedPayload; + try { + signedPayload = CommunicationUtils.signMessage(encryptedMsg, devicePrivateKey); + } catch (TransportHandlerException e) { + String errorMsg = "Error occurred whilst trying to sign encrypted message of: [" + message + "]"; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + + JSONObject jsonPayload = new JSONObject(); + jsonPayload.put(JSON_MESSAGE_KEY, encryptedMsg); + jsonPayload.put(JSON_SIGNATURE_KEY, signedPayload); + + return jsonPayload.toString(); + } + + + public static String extractMessageFromPayload(String message) throws AgentCoreOperationException { + String actualMessage; + + PublicKey serverPublicKey = EnrollmentManager.getInstance().getServerPublicKey(); + PrivateKey devicePrivateKey = EnrollmentManager.getInstance().getPrivateKey(); + + JSONObject jsonPayload = new JSONObject(message); + Object encryptedMessage = jsonPayload.get(JSON_MESSAGE_KEY); + Object signedPayload = jsonPayload.get(JSON_SIGNATURE_KEY); + boolean verification; + + if (encryptedMessage != null && signedPayload != null) { + try { + verification = CommunicationUtils.verifySignature( + encryptedMessage.toString(), signedPayload.toString(), serverPublicKey); + } catch (TransportHandlerException e) { + String errorMsg = + "Error occurred whilst trying to verify signature on received message: [" + message + "]"; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + } else { + String errorMsg = "The received message is in an INVALID format. " + + "Need to be JSON - {\"Msg\":\"\", \"Sig\":\"\"}."; + throw new AgentCoreOperationException(errorMsg); + } + + try { + if (verification) { + actualMessage = CommunicationUtils.decryptMessage(encryptedMessage.toString(), devicePrivateKey); + } else { + String errorMsg = "Could not verify payload signature. The message was not signed by a valid client"; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg); + } + } catch (TransportHandlerException e) { + String errorMsg = "Error occurred whilst trying to decrypt received message: [" + encryptedMessage + "]"; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + + return actualMessage; + } + + +} + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/enrollment/EnrollmentManager.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/enrollment/EnrollmentManager.java new file mode 100644 index 000000000..a4347cbae --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/enrollment/EnrollmentManager.java @@ -0,0 +1,436 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.enrollment; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.X500NameBuilder; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; +import org.jscep.client.Client; +import org.jscep.client.ClientException; +import org.jscep.client.EnrollmentResponse; +import org.jscep.client.verification.CertificateVerifier; +import org.jscep.client.verification.OptimisticCertificateVerifier; +import org.jscep.transaction.TransactionException; +import org.jscep.transport.response.Capabilities; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.core.AgentConstants; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.core.AgentManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.exception.AgentCoreOperationException; +import sun.security.x509.X509CertImpl; + +import java.math.BigInteger; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.CertStore; +import java.security.cert.CertStoreException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Date; + +/** + * This class controls the entire SCEP enrolment process of the client. It is a singleton for any single client which + * has the agent code running in it. The main functionality of this class includes generating a Private-Public Key + * Pair for the enrollment flow, creating the Certificate-Sign-Request using the generated Public-Key to send to the + * SEP server, Contacting the SCEP server to receive the Signed Certificate and requesting for the server's public + * key for encrypting the payloads. + * The provider for all Cryptographic functions used in this class are "BouncyCastle" and the Asymmetric-Key pair + * algorithm used is "RSA" with a key size of 2048. The signature algorithm used is "SHA1withRSA". + * This class also holds the "SCEPUrl" (Server Url read from the configs file), the Private-Public Keys of the + * client, Signed SCEP certificate and the server's public certificate. + */ + +//TODO: Need to save cert and keys to file after initial enrollment... +public class EnrollmentManager { + private static final Log log = LogFactory.getLog(EnrollmentManager.class); + private static EnrollmentManager enrollmentManager; + + private static final String KEY_PAIR_ALGORITHM = "RSA"; + private static final String PROVIDER = "BC"; + private static final String SIGNATURE_ALG = "SHA1withRSA"; + private static final int KEY_SIZE = 2048; + + // Seed to our PRNG. Make sure this is initialised randomly, NOT LIKE THIS + private static final byte[] SEED = ")(*&^%$#@!".getBytes(); + private static final int CERT_VALIDITY = 730; + + // URL of our SCEP server + private String SCEPUrl; + private PrivateKey privateKey; + private PublicKey publicKey; + private PublicKey serverPublicKey; + private X509Certificate SCEPCertificate; + + + /** + * Constructor of the EnrollmentManager. Initializes the SCEPUrl as read from the configuration file by the + * AgentManager. + */ + private EnrollmentManager() { + this.SCEPUrl = AgentManager.getInstance().getEnrollmentEP(); + } + + /** + * Method to return the current singleton instance of the EnrollmentManager. + * + * @return the current singleton instance if available and if not initializes a new instance and returns it. + */ + public static EnrollmentManager getInstance() { + if (enrollmentManager == null) { + enrollmentManager = new EnrollmentManager(); + } + return enrollmentManager; + } + + + /** + * Method to control the entire enrollment flow. This method calls the method to create the Private-Public Key + * Pair, calls the specific method to generate the Certificate-Sign-Request, creates a one time self signed + * certificate to present to the SCEP server with the initial CSR, calls the specific method to connect to the + * SCEP Server and to get the SCEP Certificate and also calls the method that requests the SCEP Server for its + * PublicKey for future payload encryption. + * + * @throws AgentCoreOperationException if the private method generateCertSignRequest() fails with an error or if + * there is an error creating a self-sign certificate to present to the + * server (whilst trying to get the CSR signed) + */ + public void beginEnrollmentFlow() throws AgentCoreOperationException { + Security.addProvider(new BouncyCastleProvider()); + + KeyPair keyPair = generateKeyPair(); + this.privateKey = keyPair.getPrivate(); + this.publicKey = keyPair.getPublic(); + + if (log.isDebugEnabled()) { + log.info(AgentConstants.LOG_APPENDER + "DevicePrivateKey:\n[\n" + privateKey + "\n]\n"); + log.info(AgentConstants.LOG_APPENDER + "DevicePublicKey:\n[\n" + publicKey + "\n]\n"); + } + + PKCS10CertificationRequest certSignRequest = generateCertSignRequest(); + + /** + * ----------------------------------------------------------------------------------------------- + * Generate an ephemeral self-signed certificate. This is needed to present to the CA in the SCEP request. + * In the future, add proper EKU and attributes in the request. The CA does NOT have to honour any of this. + * ----------------------------------------------------------------------------------------------- + */ + X500Name issuer = new X500Name("CN=Temporary Issuer"); + BigInteger serial = new BigInteger(32, new SecureRandom()); + Date fromDate = new Date(); + Date toDate = new Date(System.currentTimeMillis() + (CERT_VALIDITY * 86400000L)); + + // Build the self-signed cert using BC, sign it with our private key (self-signed) + X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(issuer, serial, fromDate, toDate, + certSignRequest.getSubject(), + certSignRequest.getSubjectPublicKeyInfo()); + ContentSigner sigGen; + X509Certificate tmpCert; + + try { + sigGen = new JcaContentSignerBuilder(SIGNATURE_ALG).setProvider(PROVIDER).build(keyPair.getPrivate()); + tmpCert = new JcaX509CertificateConverter().setProvider(PROVIDER).getCertificate(certBuilder.build(sigGen)); + } catch (OperatorCreationException e) { + String errorMsg = "Error occurred whilst creating a ContentSigner for the Temp-Self-Signed Certificate."; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } catch (CertificateException e) { + String errorMsg = "Error occurred whilst trying to create Temp-Self-Signed Certificate."; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + /** + * ----------------------------------------------------------------------------------------------- + */ + + this.SCEPCertificate = getSignedCertificateFromServer(tmpCert, certSignRequest); + this.serverPublicKey = initPublicKeyOfServer(); + + if (log.isDebugEnabled()) { + log.info(AgentConstants.LOG_APPENDER + "TemporaryCertPublicKey:\n[\n" + tmpCert.getPublicKey() + "\n]\n"); + log.info(AgentConstants.LOG_APPENDER + "ServerPublicKey:\n[\n" + serverPublicKey + "\n]\n"); + } + + } + + + /** + * This method creates the Public-Private Key pair for the current client. + * + * @return the generated KeyPair object + * @throws AgentCoreOperationException when the given Security Provider does not exist or the Algorithmn used to + * generate the key pair is invalid. + */ + private KeyPair generateKeyPair() throws AgentCoreOperationException { + + // Generate key pair + KeyPairGenerator keyPairGenerator; + try { + keyPairGenerator = KeyPairGenerator.getInstance(KEY_PAIR_ALGORITHM, PROVIDER); + keyPairGenerator.initialize(KEY_SIZE, new SecureRandom(SEED)); + } catch (NoSuchAlgorithmException e) { + String errorMsg = "Algorithm [" + KEY_PAIR_ALGORITHM + "] provided for KeyPairGenerator is invalid."; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } catch (NoSuchProviderException e) { + String errorMsg = "Provider [" + PROVIDER + "] provided for KeyPairGenerator does not exist."; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + + return keyPairGenerator.genKeyPair(); + } + + + /** + * This method creates the PKCS10 Certificate Sign Request which is to be sent to the SCEP Server using the + * generated PublicKey of the client. The certificate parameters used here are the ones from the AgentManager + * which are the values read from the configurations file. + * + * @return the PKCS10CertificationRequest object created using the client specific configs and the generated + * PublicKey + * @throws AgentCoreOperationException if an error occurs when creating a content signer to sign the CSR. + */ + private PKCS10CertificationRequest generateCertSignRequest() throws AgentCoreOperationException { + // Build the CN for the cert that's being requested. + X500NameBuilder nameBld = new X500NameBuilder(BCStyle.INSTANCE); + nameBld.addRDN(BCStyle.CN, AgentManager.getInstance().getAgentConfigs().getServerName()); + nameBld.addRDN(BCStyle.O, AgentManager.getInstance().getAgentConfigs().getDeviceOwner()); + nameBld.addRDN(BCStyle.OU, AgentManager.getInstance().getAgentConfigs().getDeviceOwner()); + nameBld.addRDN(BCStyle.UNIQUE_IDENTIFIER, AgentManager.getInstance().getAgentConfigs().getDeviceId()); + nameBld.addRDN(BCStyle.SERIALNUMBER, AgentManager.getInstance().getAgentConfigs().getDeviceId()); + X500Name principal = nameBld.build(); + + JcaContentSignerBuilder contentSignerBuilder = new JcaContentSignerBuilder(SIGNATURE_ALG).setProvider(PROVIDER); + ContentSigner contentSigner; + + try { + contentSigner = contentSignerBuilder.build(this.privateKey); + } catch (OperatorCreationException e) { + String errorMsg = "Could not create content signer with private key."; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + + // Generate the certificate signing request (csr = PKCS10) + PKCS10CertificationRequestBuilder reqBuilder = new JcaPKCS10CertificationRequestBuilder(principal, + this.publicKey); + return reqBuilder.build(contentSigner); + } + + + /** + * This method connects to the SCEP Server to fetch the signed SCEP Certificate. + * + * @param tempCert the temporary self-signed certificate of the client required for the initial CSR + * request against the SCEP Server. + * @param certSignRequest the PKCS10 Certificate-Sign-Request that is to be sent to the SCEP Server. + * @return the SCEP-Certificate for the client signed by the SCEP-Server. + * @throws AgentCoreOperationException if the SCEPUrl is invalid or if the flow of sending the CSR and getting + * the signed certificate fails or if the signed certificate cannot be + * retrieved from the reply from the server. + */ + private X509Certificate getSignedCertificateFromServer(X509Certificate tempCert, + PKCS10CertificationRequest certSignRequest) + throws AgentCoreOperationException { + + X509Certificate signedSCEPCertificate = null; + URL url; + EnrollmentResponse enrolResponse; + CertStore certStore; + + try { + // The URL where we are going to request our cert from + url = new URL(this.SCEPUrl); + + /* // This is called when we get the certificate for our CSR signed by CA + // Implement this handler to check the CA cert in prod. We can do cert pinning here + CallbackHandler cb = new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + //throw new UnsupportedOperationException("Not supported yet."); //To change body of generated + methods, choose Tools | Templates. + } + };*/ + + // Implement verification of the CA cert. VERIFY the CA + CertificateVerifier ocv = new OptimisticCertificateVerifier(); + + // Instantiate our SCEP client + Client scepClient = new Client(url, ocv); + + // Submit our cert for signing. SCEP server should allow the client to specify + // the SCEP CA to issue the request against, if there are multiple CAs + enrolResponse = scepClient.enrol(tempCert, this.privateKey, certSignRequest); + + // Verify we got what we want, and just print out the cert. + certStore = enrolResponse.getCertStore(); + + for (Certificate x509Certificate : certStore.getCertificates(null)) { + if (log.isDebugEnabled()) { + log.debug(x509Certificate.toString()); + } + signedSCEPCertificate = (X509Certificate) x509Certificate; + } + + } catch (MalformedURLException ex) { + String errorMsg = "Could not create valid URL from given SCEP URI: " + SCEPUrl; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, ex); + } catch (TransactionException | ClientException e) { + String errorMsg = "Enrollment process to SCEP Server at: " + SCEPUrl + " failed."; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } catch (CertStoreException e) { + String errorMsg = "Could not retrieve [Signed-Certificate] from the response message from SCEP-Server."; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + + return signedSCEPCertificate; + } + + + /** + * Gets the Public Key of the SCEP-Server and initializes it for later use. This method contacts the SCEP Server + * and fetches its CA Cert and extracts the Public Key of the server from the received reply. + * + * @return the public key of the SCEP Server which is to be used to encrypt pyloads. + * @throws AgentCoreOperationException if the SCEPUrl is invalid or if the flow of sending the CSR and getting + * the signed certificate fails or if the signed certificate cannot be + * retrieved from the reply from the server. + */ + private PublicKey initPublicKeyOfServer() throws AgentCoreOperationException { + URL url; + CertStore certStore; + PublicKey serverCertPublicKey = null; + + try { + // The URL where we are going to request our cert from + url = new URL(this.SCEPUrl); + + /* // This is called when we get the certificate for our CSR signed by CA + // Implement this handler to check the CA cert in prod. We can do cert pinning here + CallbackHandler cb = new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + //throw new UnsupportedOperationException("Not supported yet."); //To change body of generated + methods, choose Tools | Templates. + } + };*/ + + // Implement verification of the CA cert. VERIFY the CA + CertificateVerifier ocv = new OptimisticCertificateVerifier(); + + // Instantiate our SCEP client + Client scepClient = new Client(url, ocv); + + // Get the CA capabilities. Should return SHA1withRSA for strongest hash and sig. Returns MD5. + if (log.isDebugEnabled()) { + Capabilities cap = scepClient.getCaCapabilities(); + log.debug(String.format( + "\nStrongestCipher: %s,\nStrongestMessageDigest: %s,\nStrongestSignatureAlgorithm: %s," + + "\nIsRenewalSupported: %s,\nIsRolloverSupported: %s", + cap.getStrongestCipher(), cap.getStrongestMessageDigest(), cap.getStrongestSignatureAlgorithm(), + cap.isRenewalSupported(), cap.isRolloverSupported())); + } + + certStore = scepClient.getCaCertificate(); + + for (Certificate cert : certStore.getCertificates(null)) { + if (cert instanceof X509Certificate) { + if (log.isDebugEnabled()) { + log.debug(((X509Certificate) cert).getIssuerDN().getName()); + } + + //TODO: Need to identify the correct certificate. + // I have chosen the CA cert based on its BasicConstraint criticality being set to "true" + if (((X509CertImpl) cert).getBasicConstraintsExtension().isCritical()) { + serverCertPublicKey = cert.getPublicKey(); + } + } + } + + } catch (MalformedURLException ex) { + String errorMsg = "Could not create valid URL from given SCEP URI: " + SCEPUrl; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, ex); + } catch (ClientException e) { + String errorMsg = "Could not retrieve [Server-Certificate] from the SCEP-Server."; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } catch (CertStoreException e) { + String errorMsg = "Could not retrieve [Server-Certificates] from the response message from SCEP-Server."; + log.error(errorMsg); + throw new AgentCoreOperationException(errorMsg, e); + } + + return serverCertPublicKey; + } + + + /** + * Gets the Public-Key of the client. + * @return the public key of the client. + */ + public PublicKey getPublicKey() { + return publicKey; + } + + /** + * Gets the Private-Key of the client. + * @return the private key of the client. + */ + public PrivateKey getPrivateKey() { + return privateKey; + } + + /** + * Gets the SCEP-Certificate of the client. + * @return the SCEP Certificate of the client. + */ + public X509Certificate getSCEPCertificate() { + return SCEPCertificate; + } + + /** + * Gets the Public-Key of the Server. + * @return the pubic key of the server. + */ + public PublicKey getServerPublicKey() { + return serverPublicKey; + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/exception/AgentCoreOperationException.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/exception/AgentCoreOperationException.java new file mode 100644 index 000000000..fac321ef9 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/exception/AgentCoreOperationException.java @@ -0,0 +1,57 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.exception; + + +public class AgentCoreOperationException extends Exception{ + private static final long serialVersionUID = 2736466230451105710L; + + private String errorMessage; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public AgentCoreOperationException(String msg, Exception nestedEx) { + super(msg, nestedEx); + setErrorMessage(msg); + } + + public AgentCoreOperationException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public AgentCoreOperationException(String msg) { + super(msg); + setErrorMessage(msg); + } + + public AgentCoreOperationException() { + super(); + } + + public AgentCoreOperationException(Throwable cause) { + super(cause); + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/CommunicationUtils.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/CommunicationUtils.java new file mode 100644 index 000000000..bb445a3d9 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/CommunicationUtils.java @@ -0,0 +1,225 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.transport; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; + +/** + * This is a utility class which contains methods common to the communication process of a client and the server. The + * methods include encryption/decryption of payloads and signing/verification of payloads received and to be sent. + */ +public class CommunicationUtils { + private static final Log log = LogFactory.getLog(TransportUtils.class); + + // The Signature Algorithm used. + private static final String SIGNATURE_ALG = "SHA1withRSA"; + // The Encryption Algorithm and the Padding used. + private static final String CIPHER_PADDING = "RSA/ECB/PKCS1Padding"; + + + /** + * Encrypts the message with the key that's passed in. + * + * @param message the message to be encrypted. + * @param encryptionKey the key to use for the encryption of the message. + * @return the encrypted message in String format. + * @throws TransportHandlerException if an error occurs with the encryption flow which can be due to Padding + * issues, encryption key being invalid or the algorithm used is unrecognizable. + */ + public static String encryptMessage(String message, Key encryptionKey) throws TransportHandlerException { + Cipher encrypter; + byte[] cipherData; + + try { + encrypter = Cipher.getInstance(CIPHER_PADDING); + encrypter.init(Cipher.ENCRYPT_MODE, encryptionKey); + cipherData = encrypter.doFinal(message.getBytes(StandardCharsets.UTF_8)); + + } catch (NoSuchAlgorithmException e) { + String errorMsg = "Algorithm not found exception occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (NoSuchPaddingException e) { + String errorMsg = "No Padding error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (InvalidKeyException e) { + String errorMsg = "InvalidKey exception occurred for encryptionKey \n[\n" + encryptionKey + "\n]\n"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (BadPaddingException e) { + String errorMsg = "Bad Padding error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (IllegalBlockSizeException e) { + String errorMsg = "Illegal blockSize error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } + + return Base64.encodeBase64String(cipherData); + } + +///TODO:: Exception needs to change according to the common package + /** + * Signed a given message using the PrivateKey that's passes in. + * + * @param message the message to be signed. Ideally some encrypted payload. + * @param signatureKey the PrivateKey with which the message is to be signed. + * @return the Base64Encoded String of the signed payload. + * @throws TransportHandlerException if some error occurs with the signing process which may be related to the + * signature algorithm used or the key used for signing. + */ + public static String signMessage(String message, PrivateKey signatureKey) throws TransportHandlerException { + + Signature signature; + String signedEncodedString; + + try { + signature = Signature.getInstance(SIGNATURE_ALG); + signature.initSign(signatureKey); + signature.update(Base64.decodeBase64(message)); + + byte[] signatureBytes = signature.sign(); + signedEncodedString = Base64.encodeBase64String(signatureBytes); + + } catch (NoSuchAlgorithmException e) { + String errorMsg = + "Algorithm not found exception occurred for Signature instance of [" + SIGNATURE_ALG + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (SignatureException e) { + String errorMsg = "Signature exception occurred for Signature instance of [" + SIGNATURE_ALG + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (InvalidKeyException e) { + String errorMsg = "InvalidKey exception occurred for signatureKey \n[\n" + signatureKey + "\n]\n"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } + + return signedEncodedString; + } + + + /** + * Verifies some signed-data against the a Public-Key to ensure that it was produced by the holder of the + * corresponding Private Key. + * + * @param data the actual payoad which was signed by some Private Key. + * @param signedData the signed data produced by signing the payload using a Private Key. + * @param verificationKey the corresponding Public Key which is an exact pair of the Private-Key with we expect + * the data to be signed by. + * @return true if the signed data verifies to be signed by the corresponding Private Key. + * @throws TransportHandlerException if some error occurs with the verification process which may be related to + * the signature algorithm used or the key used for signing. + */ + public static boolean verifySignature(String data, String signedData, PublicKey verificationKey) + throws TransportHandlerException { + + Signature signature; + boolean verified; + + try { + signature = Signature.getInstance(SIGNATURE_ALG); + signature.initVerify(verificationKey); + signature.update(Base64.decodeBase64(data)); + + verified = signature.verify(Base64.decodeBase64(signedData)); + + } catch (NoSuchAlgorithmException e) { + String errorMsg = + "Algorithm not found exception occurred for Signature instance of [" + SIGNATURE_ALG + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (SignatureException e) { + String errorMsg = "Signature exception occurred for Signature instance of [" + SIGNATURE_ALG + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (InvalidKeyException e) { + String errorMsg = "InvalidKey exception occurred for signatureKey \n[\n" + verificationKey + "\n]\n"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } + + return verified; + } + + + /** + * Encrypts the message with the key that's passed in. + * + * @param encryptedMessage the encrypted message that is supposed to be decrypted. + * @param decryptKey the key to use in the decryption process. + * @return the decrypted message in String format. + * @throws TransportHandlerException if an error occurs with the encryption flow which can be due to Padding + * issues, encryption key being invalid or the algorithm used is unrecognizable. + */ + public static String decryptMessage(String encryptedMessage, Key decryptKey) throws TransportHandlerException { + + Cipher decrypter; + String decryptedMessage; + + try { + + decrypter = Cipher.getInstance(CIPHER_PADDING); + decrypter.init(Cipher.DECRYPT_MODE, decryptKey); + decryptedMessage = new String(decrypter.doFinal(Base64.decodeBase64(encryptedMessage)), + StandardCharsets.UTF_8); + + } catch (NoSuchAlgorithmException e) { + String errorMsg = "Algorithm not found exception occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (NoSuchPaddingException e) { + String errorMsg = "No Padding error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (InvalidKeyException e) { + String errorMsg = "InvalidKey exception occurred for encryptionKey \n[\n" + decryptKey + "\n]\n"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (BadPaddingException e) { + String errorMsg = "Bad Padding error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (IllegalBlockSizeException e) { + String errorMsg = "Illegal blockSize error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } + + return decryptedMessage; + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/TransportHandler.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/TransportHandler.java new file mode 100644 index 000000000..fa45cdbb0 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/TransportHandler.java @@ -0,0 +1,47 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.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 thees methods that implement the logic related to the devices + * using the protocol. + * + * @param a message type specific to the protocol implemented + */ +public interface TransportHandler { + int DEFAULT_TIMEOUT_INTERVAL = 5000; // millis ~ 10 sec + + void connect(); + + boolean isConnected(); + + //TODO:: Any errors needs to be thrown ahead + void processIncomingMessage(T message, String... messageParams); + + void processIncomingMessage(); + + void publishDeviceData(String... publishData); + + void publishDeviceData(); + + void disconnect(); +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/TransportHandlerException.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/TransportHandlerException.java new file mode 100644 index 000000000..9ae69a998 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/TransportHandlerException.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.device.mgt.iot.virtualfirealarm.agent.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/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/TransportUtils.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/TransportUtils.java new file mode 100644 index 000000000..a5c20bbf0 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/TransportUtils.java @@ -0,0 +1,302 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.transport; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.transport.TransportHandlerException; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.DatagramSocket; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.NetworkInterface; +import java.net.ServerSocket; +import java.net.SocketException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +public class TransportUtils { + private static final Log log = LogFactory.getLog(TransportUtils.class); + + public static final int MIN_PORT_NUMBER = 9000; + public static final int MAX_PORT_NUMBER = 11000; + + /** + * Given a server endpoint as a String, this method splits it into Protocol, Host and Port + * + * @param ipString a network endpoint in the format - '://:' + * @return a map with keys "Protocol", "Host" & "Port" for the related values from the ipString + * @throws TransportHandlerException + */ + public static Map getHostAndPort(String ipString) + throws TransportHandlerException { + Map ipPortMap = new HashMap(); + String[] ipPortArray = ipString.split(":"); + + if (ipPortArray.length != 3) { + String errorMsg = + "The IP String - '" + ipString + + "' is invalid. It needs to be in format '://:'."; + log.info(errorMsg); + throw new TransportHandlerException(errorMsg); + } + + ipPortMap.put("Protocol", ipPortArray[0]); + ipPortMap.put("Host", ipPortArray[1].replace(File.separator, "")); + ipPortMap.put("Port", ipPortArray[2]); + return ipPortMap; + } + + /** + * This method validates whether a specific IP Address is of IPv4 type + * + * @param ipAddress the IP Address which needs to be validated + * @return true if it is of IPv4 type and false otherwise + */ + public static boolean validateIPv4(String ipAddress) { + try { + if (ipAddress == null || ipAddress.isEmpty()) { + return false; + } + + String[] parts = ipAddress.split("\\."); + if (parts.length != 4) { + return false; + } + + for (String s : parts) { + int i = Integer.parseInt(s); + if ((i < 0) || (i > 255)) { + return false; + } + } + return !ipAddress.endsWith("."); + + } catch (NumberFormatException nfe) { + log.warn("The IP Address: " + ipAddress + " could not " + + "be validated against IPv4-style"); + return false; + } + } + + + public static Map getInterfaceIPMap() throws TransportHandlerException { + + Map interfaceToIPMap = new HashMap(); + Enumeration networkInterfaces; + String networkInterfaceName = ""; + String ipAddress; + + try { + networkInterfaces = NetworkInterface.getNetworkInterfaces(); + } catch (SocketException exception) { + String errorMsg = + "Error encountered whilst trying to get the list of network-interfaces"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, exception); + } + + try { + for (; networkInterfaces.hasMoreElements(); ) { + networkInterfaceName = networkInterfaces.nextElement().getName(); + + if (log.isDebugEnabled()) { + log.debug("Network Interface: " + networkInterfaceName); + log.debug("------------------------------------------"); + } + + Enumeration interfaceIPAddresses = NetworkInterface.getByName( + networkInterfaceName).getInetAddresses(); + + for (; interfaceIPAddresses.hasMoreElements(); ) { + ipAddress = interfaceIPAddresses.nextElement().getHostAddress(); + + if (log.isDebugEnabled()) { + log.debug("IP Address: " + ipAddress); + } + + if (TransportUtils.validateIPv4(ipAddress)) { + interfaceToIPMap.put(networkInterfaceName, ipAddress); + } + } + + if (log.isDebugEnabled()) { + log.debug("------------------------------------------"); + } + } + } catch (SocketException exception) { + String errorMsg = + "Error encountered whilst trying to get the IP Addresses of the network " + + "interface: " + networkInterfaceName; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, exception); + } + + return interfaceToIPMap; + } + + + /** + * Attempts to find a free port between the MIN_PORT_NUMBER(9000) and MAX_PORT_NUMBER(11000). + * Tries 'RANDOMLY picked' port numbers between this range up-until "randomAttempts" number of + * times. If still fails, then tries each port in descending order from the MAX_PORT_NUMBER + * whilst skipping already attempted ones via random selection. + * + * @param randomAttempts no of times to TEST port numbers picked randomly over the given range + * @return an available/free port + */ + public static synchronized int getAvailablePort(int randomAttempts) { + ArrayList failedPorts = new ArrayList(randomAttempts); + + Random randomNum = new Random(); + int randomPort = MAX_PORT_NUMBER; + + while (randomAttempts > 0) { + randomPort = randomNum.nextInt(MAX_PORT_NUMBER - MIN_PORT_NUMBER) + MIN_PORT_NUMBER; + + if (checkIfPortAvailable(randomPort)) { + return randomPort; + } + failedPorts.add(randomPort); + randomAttempts--; + } + + randomPort = MAX_PORT_NUMBER; + + while (true) { + if (!failedPorts.contains(randomPort) && checkIfPortAvailable(randomPort)) { + return randomPort; + } + randomPort--; + } + } + + + private static boolean checkIfPortAvailable(int port) { + ServerSocket tcpSocket = null; + DatagramSocket udpSocket = null; + + try { + tcpSocket = new ServerSocket(port); + tcpSocket.setReuseAddress(true); + + udpSocket = new DatagramSocket(port); + udpSocket.setReuseAddress(true); + return true; + } catch (IOException ex) { + // denotes the port is in use + } finally { + if (tcpSocket != null) { + try { + tcpSocket.close(); + } catch (IOException e) { + /* not to be thrown */ + } + } + + if (udpSocket != null) { + udpSocket.close(); + } + } + + return false; + } + + + /** + * This is a utility method that creates and returns a HTTP connection object. + * + * @param urlString the URL pattern to which the connection needs to be created + * @return an HTTPConnection object which cn be used to send HTTP requests + * @throws TransportHandlerException if errors occur when creating the HTTP connection with + * the given URL string + */ + public static HttpURLConnection getHttpConnection(String urlString) throws + TransportHandlerException { + URL connectionUrl; + HttpURLConnection httpConnection; + + try { + connectionUrl = new URL(urlString); + httpConnection = (HttpURLConnection) connectionUrl.openConnection(); + } catch (MalformedURLException e) { + String errorMsg = "Error occured whilst trying to form HTTP-URL from string: " + urlString; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } catch (IOException exception) { + String errorMsg = "Error occured whilst trying to open a connection to: " + urlString; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, exception); + } + return httpConnection; + } + + /** + * This is a utility method that reads and returns the response from a HTTP connection + * + * @param httpConnection the connection from which a response is expected + * @return the response (as a string) from the given HTTP connection + * @throws TransportHandlerException if any errors occur whilst reading the response from + * the connection stream + */ + public static String readResponseFromHttpRequest(HttpURLConnection httpConnection) + throws TransportHandlerException { + BufferedReader bufferedReader; + try { + bufferedReader = new BufferedReader(new InputStreamReader( + httpConnection.getInputStream(), StandardCharsets.UTF_8)); + } catch (IOException exception) { + String errorMsg = "There is an issue with connecting the reader to the input stream at: " + + httpConnection.getURL(); + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, exception); + } + + String responseLine; + StringBuilder completeResponse = new StringBuilder(); + + try { + while ((responseLine = bufferedReader.readLine()) != null) { + completeResponse.append(responseLine); + } + } catch (IOException exception) { + String errorMsg = "Error occured whilst trying read from the connection stream at: " + + httpConnection.getURL(); + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, exception); + } + try { + bufferedReader.close(); + } catch (IOException exception) { + log.error("Could not succesfully close the bufferedReader to the connection at: " + httpConnection.getURL()); + } + return completeResponse.toString(); + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/http/HTTPTransportHandler.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/http/HTTPTransportHandler.java new file mode 100644 index 000000000..c771eb8ed --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/http/HTTPTransportHandler.java @@ -0,0 +1,91 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.transport.http; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.eclipse.jetty.server.Server; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.transport.TransportHandler; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.transport.TransportUtils; + +/** + * 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 HTTP-Transport specific implementations. The class implements utility methods for the + * case of a HTTP 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 HTTP specific functionality (ideally a device API writer who would like to communicate to the device + * via HTTP Protocol). + */ +public abstract class HTTPTransportHandler implements TransportHandler { + private static final Log log = LogFactory.getLog(HTTPTransportHandler.class); + + protected Server server; + protected int port; + protected int timeoutInterval; + + protected HTTPTransportHandler() { + this.port = TransportUtils.getAvailablePort(10); + this.server = new Server(port); + timeoutInterval = DEFAULT_TIMEOUT_INTERVAL; + } + + protected HTTPTransportHandler(int port) { + this.port = port; + this.server = new Server(this.port); + timeoutInterval = DEFAULT_TIMEOUT_INTERVAL; + } + + protected HTTPTransportHandler(int port, int timeoutInterval) { + this.port = port; + this.server = new Server(this.port); + this.timeoutInterval = timeoutInterval; + } + + public void setTimeoutInterval(int timeoutInterval) { + this.timeoutInterval = timeoutInterval; + } + + /** + * Checks whether the HTTP server is up and listening for incoming requests. + * + * @return true if the server is up & listening for requests, else false. + */ + public boolean isConnected() { + return server.isStarted(); + } + + + protected void incrementPort() { + this.port = this.port + 1; + server = new Server(port); + } + + /** + * Shuts-down the HTTP Server. + */ + public void closeConnection() throws Exception { + if (server != null && isConnected()) { + server.stop(); + } + } + + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/mqtt/MQTTTransportHandler.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/mqtt/MQTTTransportHandler.java new file mode 100644 index 000000000..597f50a4d --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/mqtt/MQTTTransportHandler.java @@ -0,0 +1,360 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.transport.mqtt; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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.eclipse.paho.client.mqttv3.MqttSecurityException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.transport.TransportHandler; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.transport.TransportHandlerException; + +import java.io.File; +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 HTTP-Transport specific implementations. The class implements utility methods for the + * case of a HTTP 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 HTTP specific functionality (ideally a device API writer who would like to communicate to the device + * via HTTP 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 processing incoming messages. Makes use of the 'Paho-MQTT' + * library provided by Eclipse Org. + */ +public abstract class MQTTTransportHandler + implements MqttCallback, TransportHandler { + private static final Log log = LogFactory.getLog(MQTTTransportHandler.class); + + public static final int DEFAULT_MQTT_QUALITY_OF_SERVICE = 0; + + private MqttClient client; + private String clientId; + private MqttConnectOptions options; + private String clientWillTopic; + + protected String mqttBrokerEndPoint; + protected int timeoutInterval; + protected String subscribeTopic; + + /** + * Constructor for the MQTTTransportHandler which takes in the owner, type of the device + * and the MQTT Broker URL and the topic to subscribe. + * + * @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 + */ + protected MQTTTransportHandler(String deviceOwner, String deviceType, + String mqttBrokerEndPoint, + String subscribeTopic) { + this.clientId = deviceOwner + ":" + deviceType; + this.subscribeTopic = subscribeTopic; + this.clientWillTopic = deviceType + File.separator + "disconnection"; + this.mqttBrokerEndPoint = mqttBrokerEndPoint; + this.timeoutInterval = DEFAULT_TIMEOUT_INTERVAL; + this.initSubscriber(); + } + + /** + * 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 successive + * 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; + //TODO:: Use constant strings + this.clientWillTopic = deviceType + File.separator + "disconnection"; + this.mqttBrokerEndPoint = mqttBrokerEndPoint; + this.timeoutInterval = intervalInMillis; + this.initSubscriber(); + } + + public void setTimeoutInterval(int timeoutInterval) { + this.timeoutInterval = timeoutInterval; + } + + /** + * 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 call-back this current class. + */ + private void initSubscriber() { + try { + client = new MqttClient(this.mqttBrokerEndPoint, clientId, null); + //TODO:: Need to check for debug + log.info("MQTT subscriber was created with ClientID : " + clientId); + } catch (MqttException ex) { + //TODO:: Remove unnecessary formatting and print exception + String errorMsg = "MQTT Client Error\n" + "\tReason: " + ex.getReasonCode() + + "\n\tMessage: " + ex.getMessage() + "\n\tLocalMsg: " + + ex.getLocalizedMessage() + "\n\tCause: " + ex.getCause() + + "\n\tException: " + ex; + log.error(errorMsg); + //TODO:: Throw the error out + } + + options = new MqttConnectOptions(); + options.setCleanSession(false); + //TODO:: Use constant strings + options.setWill(clientWillTopic, "Connection-Lost".getBytes(StandardCharsets.UTF_8), 2, + true); + client.setCallback(this); + } + + /** + * Checks whether the connection to the MQTT-Broker persists. + * + * @return true if the client is connected to the MQTT-Broker, else false. + */ + @Override + public boolean isConnected() { + return client.isConnected(); + } + + + /** + * Connects to the MQTT-Broker and if successfully established connection. + * + * @throws TransportHandlerException in the event of 'Connecting to' the MQTT broker fails. + */ + protected void connectToQueue() throws TransportHandlerException { + try { + client.connect(options); + + if (log.isDebugEnabled()) { + log.debug("Subscriber connected to queue at: " + this.mqttBrokerEndPoint); + } + } catch (MqttSecurityException ex) { + String errorMsg = "MQTT Security Exception when connecting to queue\n" + "\tReason: " + + " " + + ex.getReasonCode() + "\n\tMessage: " + ex.getMessage() + + "\n\tLocalMsg: " + ex.getLocalizedMessage() + "\n\tCause: " + + ex.getCause() + "\n\tException: " + ex; + //TODO:: Compulsory log of errors and remove formatted error + if (log.isDebugEnabled()) { + log.debug(errorMsg); + } + throw new TransportHandlerException(errorMsg, ex); + + } catch (MqttException ex) { + //TODO:: Compulsory log of errors and remove formatted error + String errorMsg = "MQTT Exception when connecting to queue\n" + "\tReason: " + + ex.getReasonCode() + "\n\tMessage: " + ex.getMessage() + + "\n\tLocalMsg: " + ex.getLocalizedMessage() + "\n\tCause: " + + ex.getCause() + "\n\tException: " + ex; + if (log.isDebugEnabled()) { + log.debug(errorMsg); + } + throw new TransportHandlerException(errorMsg, ex); + } + } + + /** + * Subscribes to the MQTT-Topic specific to this MQTT Client. (The MQTT-Topic specific to the + * device is taken in as a constructor parameter of this class) . + * + * @throws TransportHandlerException in the event of 'Subscribing to' the MQTT broker + * fails. + */ + protected void subscribeToQueue() throws TransportHandlerException { + try { + //TODO:: QoS Level take it from a variable + client.subscribe(subscribeTopic, 0); + log.info("Subscriber '" + clientId + "' subscribed to topic: " + subscribeTopic); + } catch (MqttException ex) { + //TODO:: Compulsory log of errors and remove formatted error + String errorMsg = "MQTT Exception when trying to subscribe to topic: " + + subscribeTopic + "\n\tReason: " + ex.getReasonCode() + + "\n\tMessage: " + ex.getMessage() + "\n\tLocalMsg: " + + ex.getLocalizedMessage() + "\n\tCause: " + ex.getCause() + + "\n\tException: " + ex; + if (log.isDebugEnabled()) { + log.debug(errorMsg); + } + + throw new TransportHandlerException(errorMsg, ex); + } + } + + + /** + * This method is used to publish reply-messages for the control signals received. + * Invocation of this method calls its overloaded-method with a QoS equal to that of the + * default value. + * + * @param topic the topic to which the reply message is to be published. + * @param payLoad the reply-message (payload) of the MQTT publish action. + */ + protected void publishToQueue(String topic, String payLoad) + throws TransportHandlerException { + publishToQueue(topic, payLoad, DEFAULT_MQTT_QUALITY_OF_SERVICE, false); + } + + /** + * This is an overloaded method that publishes MQTT reply-messages for control signals + * received form the IoT-Server. + * + * @param topic the topic to which the reply message is to be published + * @param payLoad the reply-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) + */ + protected void publishToQueue(String topic, String payLoad, int qos, boolean retained) + throws TransportHandlerException { + try { + client.publish(topic, payLoad.getBytes(StandardCharsets.UTF_8), qos, retained); + if (log.isDebugEnabled()) { + log.debug("Message: " + payLoad + " to MQTT topic [" + topic + + "] published successfully"); + } + } catch (MqttException ex) { + String errorMsg = + "MQTT Client Error" + "\n\tReason: " + ex.getReasonCode() + "\n\tMessage: " + + ex.getMessage() + "\n\tLocalMsg: " + ex.getLocalizedMessage() + + "\n\tCause: " + ex.getCause() + "\n\tException: " + ex; + log.info(errorMsg); + throw new TransportHandlerException(errorMsg, ex); + } + } + + + protected void publishToQueue(String topic, MqttMessage message) + throws TransportHandlerException { + try { + client.publish(topic, message); + if (log.isDebugEnabled()) { + log.debug("Message: " + message.toString() + " to MQTT topic [" + topic + + "] published successfully"); + } + } catch (MqttException ex) { + //TODO:: Compulsory log of errors and remove formatted error + String errorMsg = + "MQTT Client Error" + "\n\tReason: " + ex.getReasonCode() + "\n\tMessage: " + + ex.getMessage() + "\n\tLocalMsg: " + ex.getLocalizedMessage() + + "\n\tCause: " + ex.getCause() + "\n\tException: " + ex; + log.info(errorMsg); + throw new TransportHandlerException(errorMsg, ex); + } + } + + + /** + * 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.warn("Lost Connection for client: " + this.clientId + + " to " + this.mqttBrokerEndPoint + ".\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 was subscribed to. + * @param mqttMessage the actual MQTT-Message that was received from the broker. + */ + @Override + public void messageArrived(final String topic, final MqttMessage mqttMessage) { + if (log.isDebugEnabled()) { + log.info("Got an MQTT message '" + mqttMessage.toString() + "' for topic '" + topic + "'."); + } + + Thread messageProcessorThread = new Thread() { + public void run() { + processIncomingMessage(mqttMessage, topic); + } + }; + 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 message = ""; + try { + message = iMqttDeliveryToken.getMessage().toString(); + } catch (MqttException e) { + //TODO:: Throw errors + log.error( + "Error occurred whilst trying to read the message from the MQTT delivery " + + "token."); + } + String topic = iMqttDeliveryToken.getTopics()[0]; + String client = iMqttDeliveryToken.getClient().getClientId(); + + if (log.isDebugEnabled()) { + log.debug("Message - '" + message + "' of client [" + client + "] for the topic (" + + topic + ") was delivered successfully."); + } + } + + /** + * Closes the connection to the MQTT Broker. + */ + public void closeConnection() throws MqttException { + if (client != null && isConnected()) { + client.disconnect(); + } + } +} + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/xmpp/XMPPTransportHandler.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/xmpp/XMPPTransportHandler.java new file mode 100644 index 000000000..4ac76f45b --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/transport/xmpp/XMPPTransportHandler.java @@ -0,0 +1,366 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.transport.xmpp; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.FromContainsFilter; +import org.jivesoftware.smack.filter.OrFilter; +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.filter.PacketTypeFilter; +import org.jivesoftware.smack.filter.ToContainsFilter; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.transport.TransportHandler; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.transport.TransportHandlerException; + +/** + * 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 HTTP-Transport specific implementations. The class implements utility methods for the + * case of a HTTP 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 HTTP specific functionality (ideally a device API writer who would like to communicate to the device + * via HTTP Protocol). + *

      + * This class contains the IoT-Server specific implementation for all the XMPP functionality. This includes + * connecting to a XMPP Server & Login-In using the device's/server's XMPP-Account, Setting listeners and filters on + * incoming XMPP messages and Sending XMPP replies for messages received. Makes use of the 'Smack-XMPP' library + * provided by jivesoftware/igniterealtime. + */ +public abstract class XMPPTransportHandler implements TransportHandler { + private static final Log log = LogFactory.getLog(XMPPTransportHandler.class); + + protected String server; + protected int timeoutInterval; // millis + + //TODO:: Shouldnt be hard-coded. Need to be read from configs + private static final int DEFAULT_XMPP_PORT = 5222; + private XMPPConnection connection; + private int port; + private ConnectionConfiguration config; + private PacketFilter filter; + private PacketListener listener; + + + /** + * Constructor for XMPPTransportHandler passing only the server-IP. + * + * @param server the IP of the XMPP server. + */ + protected XMPPTransportHandler(String server) { + this.server = server; + this.port = DEFAULT_XMPP_PORT; + this.timeoutInterval = DEFAULT_TIMEOUT_INTERVAL; + initXMPPClient(); + } + + /** + * Constructor for XMPPTransportHandler passing server-IP and the XMPP-port. + * + * @param server the IP of the XMPP server. + * @param port the XMPP server's port to connect to. (default - 5222) + */ + protected XMPPTransportHandler(String server, int port) { + this.server = server; + this.port = port; + this.timeoutInterval = DEFAULT_TIMEOUT_INTERVAL; + initXMPPClient(); + } + + /** + * Constructor for XMPPTransportHandler passing server-IP, the XMPP-port and the + * timeoutInterval used by listeners to the server and for reconnection schedules. + * + * @param server the IP of the XMPP server. + * @param port the XMPP server's port to connect to. (default - 5222) + * @param timeoutInterval the timeout interval to use for the connection and reconnection + */ + protected XMPPTransportHandler(String server, int port, int timeoutInterval) { + this.server = server; + this.port = port; + this.timeoutInterval = timeoutInterval; + initXMPPClient(); + } + + /** + * Sets the client's time-out-limit whilst waiting for XMPP-replies from server. + * + * @param millis the time in millis to be set as the time-out-limit whilst waiting for a + * XMPP-reply. + */ + public void setTimeoutInterval(int millis) { + this.timeoutInterval = millis; + } + + /** + * Checks whether the connection to the XMPP-Server persists. + * + * @return true if the client is connected to the XMPP-Server, else false. + */ + @Override + public boolean isConnected() { + return connection.isConnected(); + } + + /** + * Initializes the XMPP Client. Sets the time-out-limit whilst waiting for XMPP-replies from + * server. Sets the XMPP configurations to connect to the server and creates the + * XMPPConnection object used for connecting and Logging-In. + */ + private void initXMPPClient() { + log.info(String.format("Initializing connection to XMPP Server at %1$s via port " + + "%2$d.", server, port)); + SmackConfiguration.setPacketReplyTimeout(timeoutInterval); + config = new ConnectionConfiguration(server, port); +// TODO:: Need to enable SASL-Authentication appropriately + config.setSASLAuthenticationEnabled(false); + config.setSecurityMode(ConnectionConfiguration.SecurityMode.disabled); + connection = new XMPPConnection(config); + } + +//TODO:: Re-check all exception handling + + /** + * Connects to the XMPP-Server and if attempt unsuccessful, then throws exception. + * + * @throws TransportHandlerException in the event of 'Connecting to' the XMPP server fails. + */ + protected void connectToServer() throws TransportHandlerException { + try { + connection.connect(); + log.info(String.format("Connection to XMPP Server at %1$s established successfully......", server)); + + } catch (XMPPException xmppExcepion) { + String errorMsg = "Connection attempt to the XMPP Server at " + server + " via port " + port + " failed."; + log.info(errorMsg); + throw new TransportHandlerException(errorMsg, xmppExcepion); + } + } + + /** + * If successfully established connection, then tries to Log in using the device's XMPP + * Account credentials. + * + * @param username the username of the device's XMPP-Account. + * @param password the password of the device's XMPP-Account. + * @param resource the resource the resource, specific to the XMPP-Account to which the login + * is made to + * @throws TransportHandlerException in the event of 'Logging into' the XMPP server fails. + */ + protected void loginToServer(String username, String password, String resource) + throws TransportHandlerException { + if (isConnected()) { + try { + if (resource == null) { + connection.login(username, password); + log.info(String.format("Logged into XMPP Server at %1$s as user %2$s......", server, username)); + } else { + connection.login(username, password, resource); + log.info(String.format("Logged into XMPP Server at %1$s as user %2$s on resource %3$s......", + server, username, resource)); + } + } catch (XMPPException xmppException) { + String errorMsg = + "Login attempt to the XMPP Server at " + server + " with username - " + username + " failed."; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, xmppException); + } + } else { + String errorMsg = "Not connected to XMPP-Server to attempt Login. Please 'connectToServer' before Login"; + throw new TransportHandlerException(errorMsg); + } + } + + + /** + * Sets a filter for all the incoming XMPP-Messages on the Sender's JID (XMPP-Account ID). + * Also creates a listener for the incoming messages and connects the listener to the + * XMPPConnection alongside the set filter. + * + * @param senderJID the JID (XMPP-Account ID of the sender) to which the filter is to be set. + */ + protected void setFilterOnSender(String senderJID) { + filter = new AndFilter(new PacketTypeFilter(Message.class), new FromContainsFilter( + senderJID)); + listener = new PacketListener() { + @Override + public void processPacket(Packet packet) { + if (packet instanceof Message) { + final Message xmppMessage = (Message) packet; + Thread msgProcessThread = new Thread() { + public void run() { + processIncomingMessage(xmppMessage); + } + }; + msgProcessThread.setDaemon(true); + msgProcessThread.start(); + } + } + }; + + connection.addPacketListener(listener, filter); + } + + + /** + * Sets a filter for all the incoming XMPP-Messages on the Receiver's JID (XMPP-Account ID). + * Also creates a listener for the incoming messages and connects the listener to the + * XMPPConnection alongside the set filter. + * + * @param receiverJID the JID (XMPP-Account ID of the receiver) to which the filter is to be + * set. + */ + protected void setFilterOnReceiver(String receiverJID) { + filter = new AndFilter(new PacketTypeFilter(Message.class), new ToContainsFilter( + receiverJID)); + listener = new PacketListener() { + @Override + public void processPacket(Packet packet) { + if (packet instanceof Message) { + final Message xmppMessage = (Message) packet; + Thread msgProcessThread = new Thread() { + public void run() { + processIncomingMessage(xmppMessage); + } + }; + msgProcessThread.setDaemon(true); + msgProcessThread.start(); + } + } + }; + + connection.addPacketListener(listener, filter); + } + + + /** + * Sets a filter for all the incoming XMPP-Messages on the From-JID & To-JID (XMPP-Account IDs) + * passed in. Also creates a listener for the incoming messages and connects the listener to + * the XMPPConnection alongside the set filter. + * + * @param senderJID the From-JID (XMPP-Account ID) to which the filter is to be set. + * @param receiverJID the To-JID (XMPP-Account ID) to which the filter is to be set. + * @param andCondition if true: then filter is set with 'AND' operator (senderJID && + * receiverJID), + * if false: then the filter is set with 'OR' operator (senderJID | + * receiverJID) + */ + protected void setMessageFilterAndListener(String senderJID, String receiverJID, boolean + andCondition) { + PacketFilter jidFilter; + + if (andCondition) { + jidFilter = new AndFilter(new FromContainsFilter(senderJID), new ToContainsFilter( + receiverJID)); + } else { + jidFilter = new OrFilter(new FromContainsFilter(senderJID), new ToContainsFilter( + receiverJID)); + } + + filter = new AndFilter(new PacketTypeFilter(Message.class), jidFilter); + listener = new PacketListener() { + @Override + public void processPacket(Packet packet) { + if (packet instanceof Message) { + final Message xmppMessage = (Message) packet; + Thread msgProcessThread = new Thread() { + public void run() { + processIncomingMessage(xmppMessage); + } + }; + msgProcessThread.setDaemon(true); + msgProcessThread.start(); + } + } + }; + + connection.addPacketListener(listener, filter); + } + + + /** + * Sends an XMPP message. Calls the overloaded method with Subject set to "Reply-From-Device" + * + * @param JID the JID (XMPP Account ID) to which the message is to be sent to. + * @param message the XMPP-Message that is to be sent. + */ + protected void sendXMPPMessage(String JID, String message) { + sendXMPPMessage(JID, message, "XMPP-Message"); + } + + + /** + * Overloaded method to send an XMPP message. Includes the subject to be mentioned in the + * message that is sent. + * + * @param JID the JID (XMPP Account ID) to which the message is to be sent to. + * @param message the XMPP-Message that is to be sent. + * @param subject the subject that the XMPP-Message would carry. + */ + protected void sendXMPPMessage(String JID, String message, String subject) { + Message xmppMessage = new Message(); + xmppMessage.setTo(JID); + xmppMessage.setSubject(subject); + xmppMessage.setBody(message); + xmppMessage.setType(Message.Type.chat); + sendXMPPMessage(JID, xmppMessage); + } + + + /** + * Sends an XMPP message. + * + * @param JID the JID (XMPP Account ID) to which the message is to be sent to. + * @param xmppMessage the XMPP-Message that is to be sent. + */ + protected void sendXMPPMessage(String JID, Message xmppMessage) { + connection.sendPacket(xmppMessage); + if (log.isDebugEnabled()) { + log.debug("Message: '" + xmppMessage.getBody() + "' sent to XMPP JID [" + JID + + "] sent successfully."); + } + } + + + /** + * Disables default debugger provided by the XMPPConnection. + */ + protected void disableDebugger() { + connection.DEBUG_ENABLED = false; + } + + + /** + * Closes the connection to the XMPP Server. + */ + public void closeConnection() { + if (connection != null && isConnected()) { + connection.disconnect(); + } + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/virtual/VirtualHardwareManager.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/virtual/VirtualHardwareManager.java new file mode 100644 index 000000000..3b777cf75 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/virtual/VirtualHardwareManager.java @@ -0,0 +1,213 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.virtual; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.core.AgentConstants; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.core.AgentUtilOperations; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.virtual.ui.AgentUI; + +import javax.sound.midi.InvalidMidiDataException; +import javax.sound.midi.MidiSystem; +import javax.sound.midi.MidiUnavailableException; +import javax.sound.midi.Sequence; +import javax.sound.midi.Sequencer; +import javax.sound.sampled.Clip; +import javax.swing.*; +import java.io.IOException; +import java.io.InputStream; + +/** + * This class use to emulate virtual hardware functionality + */ +public class VirtualHardwareManager { + + private static final Log log = LogFactory.getLog(VirtualHardwareManager.class); + + private static VirtualHardwareManager virtualHardwareManager; + + private AgentUI agentUI; + private Sequencer sequencer = null; + + private int temperature = 30, humidity = 30; + private int temperatureMin = 20, temperatureMax = 50, humidityMin = 20, humidityMax = 50; + private int temperatureSVF = 50, humiditySVF = 50; + private boolean isTemperatureRandomized, isHumidityRandomized; + private boolean isTemperatureSmoothed, isHumiditySmoothed; + + private VirtualHardwareManager(){ + } + + public static VirtualHardwareManager getInstance(){ + if (virtualHardwareManager == null){ + virtualHardwareManager = new VirtualHardwareManager(); + } + return virtualHardwareManager; + } + + public void init(){ + try { + // Set System L&F for Device UI + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (UnsupportedLookAndFeelException e) { + log.error( + "'UnsupportedLookAndFeelException' error occurred whilst initializing the" + + " Agent UI."); + } catch (ClassNotFoundException e) { + log.error( + "'ClassNotFoundException' error occurred whilst initializing the Agent UI."); + } catch (InstantiationException e) { + log.error( + "'InstantiationException' error occurred whilst initializing the Agent UI."); + } catch (IllegalAccessException e) { + log.error( + "'IllegalAccessException' error occurred whilst initializing the Agent UI."); + } + java.awt.EventQueue.invokeLater(new Runnable() { + public void run() { + agentUI = new AgentUI(); + agentUI.setVisible(true); + } + }); + setAudioSequencer(); + } + + /** + * Get temperature from emulated device + * @return Temperature + */ + public int getTemperature() { + if (isTemperatureRandomized) { + temperature = getRandom(temperatureMax, temperatureMin, temperature, + isTemperatureSmoothed, temperatureSVF); + agentUI.updateTemperature(temperature); + } + return temperature; + } + + public void setTemperature(int temperature) { + this.temperature = temperature; + } + + /** + * Get humidity from emulated device + * @return Humidity + */ + public int getHumidity() { + if (isHumidityRandomized) { + humidity = getRandom(humidityMax, humidityMin, humidity, isHumiditySmoothed, + humiditySVF); + agentUI.updateHumidity(humidity); + } + return humidity; + } + + public void setHumidity(int humidity) { + this.humidity = humidity; + } + + public void setTemperatureMin(int temperatureMin) { + this.temperatureMin = temperatureMin; + } + + public void setTemperatureMax(int temperatureMax) { + this.temperatureMax = temperatureMax; + } + + public void setHumidityMin(int humidityMin) { + this.humidityMin = humidityMin; + } + + public void setHumidityMax(int humidityMax) { + this.humidityMax = humidityMax; + } + + public void setIsHumidityRandomized(boolean isHumidityRandomized) { + this.isHumidityRandomized = isHumidityRandomized; + } + + public void setIsTemperatureRandomized(boolean isTemperatureRandomized) { + this.isTemperatureRandomized = isTemperatureRandomized; + } + + public void setTemperatureSVF(int temperatureSVF) { + this.temperatureSVF = temperatureSVF; + } + + public void setHumiditySVF(int humiditySVF) { + this.humiditySVF = humiditySVF; + } + + public void setIsTemperatureSmoothed(boolean isTemperatureSmoothed) { + this.isTemperatureSmoothed = isTemperatureSmoothed; + } + + public void setIsHumiditySmoothed(boolean isHumiditySmoothed) { + this.isHumiditySmoothed = isHumiditySmoothed; + } + + public void changeAlarmStatus(boolean isOn) { + agentUI.setAlarmStatus(isOn); + + if (isOn) { + sequencer.start(); + } else { + sequencer.stop(); + } + } + + private int getRandom(int max, int min, int current, boolean isSmoothed, int svf) { + + if (isSmoothed) { + int offset = (max - min) * svf / 100; + double mx = current + offset; + max = (mx > max) ? max : (int) Math.round(mx); + + double mn = current - offset; + min = (mn < min) ? min : (int) Math.round(mn); + } + + double rnd = Math.random() * (max - min) + min; + return (int) Math.round(rnd); + + } + + private void setAudioSequencer() { + InputStream audioSrc = AgentUtilOperations.class.getResourceAsStream( + "/" + AgentConstants.AUDIO_FILE_NAME); + Sequence sequence; + + try { + sequence = MidiSystem.getSequence(audioSrc); + sequencer = MidiSystem.getSequencer(); + sequencer.open(); + sequencer.setSequence(sequence); + } catch (InvalidMidiDataException e) { + log.error("AudioReader: Error whilst setting MIDI Audio reader sequence"); + } catch (IOException e) { + log.error("AudioReader: Error whilst getting audio sequence from stream"); + } catch (MidiUnavailableException e) { + log.error("AudioReader: Error whilst openning MIDI Audio reader sequencer"); + } + + sequencer.setLoopCount(Clip.LOOP_CONTINUOUSLY); + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/virtual/ui/AgentUI.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/virtual/ui/AgentUI.java new file mode 100644 index 000000000..4a9b76c86 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/agent/virtual/ui/AgentUI.java @@ -0,0 +1,954 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.agent.virtual.ui; + +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.core.AgentConstants; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.core.AgentManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.virtual.VirtualHardwareManager; + +import javax.swing.*; +import java.awt.*; +import java.net.URI; +import java.net.URL; + +public class AgentUI extends JFrame { + + private boolean isTemperatureRandomized, isHumidityRandomized; + private boolean isTemperatureSmoothed, isHumiditySmoothed; + + private volatile boolean isAlarmOn = false; + + private JLabel picLabelBulbOn, picLabelBulbOff; + + // Variables declaration - do not modify + private JButton btnControl; + private JButton btnView; + private JCheckBox chkbxEmulate; + private JCheckBox chkbxHumidityRandom; + private JCheckBox chkbxHumiditySmooth; + private JCheckBox chkbxTemperatureRandom; + private JCheckBox chkbxTemperatureSmooth; + private JComboBox cmbInterface; + private JComboBox cmbPeriod; + private JComboBox cmbProtocol; + private JLabel jLabel1; + private JLabel jLabel10; + private JLabel jLabel11; + private JLabel jLabel12; + private JLabel jLabel2; + private JLabel jLabel20; + private JLabel jLabel23; + private JLabel jLabel24; + private JLabel jLabel25; + private JLabel jLabel3; + private JLabel jLabel4; + private JLabel jLabel5; + private JLabel jLabel6; + private JLabel jLabel7; + private JLabel jLabel8; + private JLabel jLabel9; + private JPanel jPanel1; + private JPanel jPanel2; + private JPanel jPanel3; + private JPanel jPanel4; + private JPanel jPanel6; + private JPanel jPanel7; + private JPanel jPanel8; + private JPanel jPanel9; + private JSeparator jSeparator1; + private JSeparator jSeparator5; + private JLabel lblAgentName; + private JLabel lblStatus; + private JPanel pnlBulbStatus; + private JSpinner spinnerHumidity; + private JSpinner spinnerInterval; + private JSpinner spinnerTemperature; + private JTextField txtHumidityMax; + private JTextField txtHumidityMin; + private JTextField txtHumiditySVF; + private JTextField txtTemperatureMax; + private JTextField txtTemperatureMin; + private JTextField txtTemperatureSVF; + // End of variables declaration + + private Runnable uiUpdater = new Runnable() { + @Override + public void run() { + while (true) { + EventQueue.invokeLater(new Runnable() { + @Override + public void run() { + pnlBulbStatus.removeAll(); + pnlBulbStatus.add(isAlarmOn ? picLabelBulbOn : picLabelBulbOff); + pnlBulbStatus.updateUI(); + lblStatus.setText(AgentManager.getInstance().getAgentStatus()); + if (isTemperatureRandomized) { + txtTemperatureMinActionPerformed(null); + txtTemperatureMaxActionPerformed(null); + if (isTemperatureSmoothed) { + txtTemperatureSVFActionPerformed(null); + } + } + if (isHumidityRandomized) { + txtHumidityMinActionPerformed(null); + txtHumidityMaxActionPerformed(null); + if (isHumiditySmoothed) { + txtHumiditySVFActionPerformed(null); + } + } + } + }); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + break; + } + } + } + }; + + /** + * Creates new form AgentUI + */ + public AgentUI() { + initComponents(); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + private void initComponents() { + + lblAgentName = new JLabel(); + jLabel2 = new JLabel(); + jPanel1 = new JPanel(); + jLabel3 = new JLabel(); + pnlBulbStatus = new JPanel(); + jPanel2 = new JPanel(); + jLabel4 = new JLabel(); + chkbxTemperatureRandom = new JCheckBox(); + jSeparator1 = new JSeparator(); + jPanel7 = new JPanel(); + jLabel5 = new JLabel(); + txtTemperatureMin = new JTextField(); + jLabel6 = new JLabel(); + txtTemperatureMax = new JTextField(); + jLabel10 = new JLabel(); + txtTemperatureSVF = new JTextField(); + spinnerTemperature = new JSpinner(); + chkbxTemperatureSmooth = new JCheckBox(); + jPanel6 = new JPanel(); + jLabel20 = new JLabel(); + btnView = new JButton(); + btnControl = new JButton(); + lblStatus = new JLabel(); + jPanel8 = new JPanel(); + jLabel23 = new JLabel(); + chkbxHumidityRandom = new JCheckBox(); + jSeparator5 = new JSeparator(); + jPanel9 = new JPanel(); + jLabel24 = new JLabel(); + txtHumidityMin = new JTextField(); + jLabel25 = new JLabel(); + txtHumidityMax = new JTextField(); + txtHumiditySVF = new JTextField(); + jLabel11 = new JLabel(); + spinnerHumidity = new JSpinner(); + chkbxHumiditySmooth = new JCheckBox(); + jPanel3 = new JPanel(); + jLabel7 = new JLabel(); + spinnerInterval = new JSpinner(); + jLabel8 = new JLabel(); + jLabel9 = new JLabel(); + cmbProtocol = new JComboBox(); + jLabel12 = new JLabel(); + cmbInterface = new JComboBox(); + jPanel4 = new JPanel(); + chkbxEmulate = new JCheckBox(); + cmbPeriod = new JComboBox(); + jLabel1 = new JLabel(); + + setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + setTitle("Fire Alarm Emulator"); + setResizable(false); + Dimension dim = Toolkit.getDefaultToolkit().getScreenSize(); + setLocation(dim.width / 2 - 650 / 2, dim.height / 2 - 440 / 2); + + lblAgentName.setFont(new Font("Cantarell", 1, 24)); // NOI18N + lblAgentName.setHorizontalAlignment(SwingConstants.LEFT); + lblAgentName.setText("Device Name: " + AgentManager.getInstance().getDeviceName()); + + jLabel2.setHorizontalAlignment(SwingConstants.CENTER); + jLabel2.setText("Copyright (c) 2015, WSO2 Inc."); + + jPanel1.setBackground(new Color(220, 220, 220)); + + jLabel3.setFont(new Font("Cantarell", 0, 18)); // NOI18N + jLabel3.setHorizontalAlignment(SwingConstants.CENTER); + jLabel3.setText("Alarm Status"); + + pnlBulbStatus.setBackground(new Color(220, 220, 220)); + + GroupLayout pnlBulbStatusLayout = new GroupLayout(pnlBulbStatus); + pnlBulbStatus.setLayout(pnlBulbStatusLayout); + pnlBulbStatusLayout.setHorizontalGroup( + pnlBulbStatusLayout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGap(0, 0, Short.MAX_VALUE) + ); + pnlBulbStatusLayout.setVerticalGroup( + pnlBulbStatusLayout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGap(0, 167, Short.MAX_VALUE) + ); + + GroupLayout jPanel1Layout = new GroupLayout(jPanel1); + jPanel1.setLayout(jPanel1Layout); + jPanel1Layout.setHorizontalGroup( + jPanel1Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel1Layout.createParallelGroup(GroupLayout.Alignment.TRAILING) + .addComponent(pnlBulbStatus, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jLabel3, GroupLayout.DEFAULT_SIZE, 190, Short.MAX_VALUE)) + .addContainerGap()) + ); + jPanel1Layout.setVerticalGroup( + jPanel1Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel1Layout.createSequentialGroup() + .addContainerGap() + .addComponent(jLabel3) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(pnlBulbStatus, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addContainerGap()) + ); + + jPanel2.setBackground(new Color(220, 220, 220)); + + jLabel4.setFont(new Font("Cantarell", 0, 18)); // NOI18N + jLabel4.setHorizontalAlignment(SwingConstants.CENTER); + jLabel4.setText("Temperature"); + + chkbxTemperatureRandom.setText("Randomize Data"); + chkbxTemperatureRandom.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxTemperatureRandomActionPerformed(evt); + } + }); + + jSeparator1.setOrientation(SwingConstants.VERTICAL); + + jPanel7.setBackground(new Color(220, 220, 220)); + + jLabel5.setHorizontalAlignment(SwingConstants.LEFT); + jLabel5.setText("Min"); + + txtTemperatureMin.setHorizontalAlignment(JTextField.CENTER); + txtTemperatureMin.setText("20"); + txtTemperatureMin.setEnabled(false); + txtTemperatureMin.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtTemperatureMinActionPerformed(evt); + } + }); + + jLabel6.setHorizontalAlignment(SwingConstants.RIGHT); + jLabel6.setText("Max"); + + txtTemperatureMax.setHorizontalAlignment(JTextField.CENTER); + txtTemperatureMax.setText("50"); + txtTemperatureMax.setEnabled(false); + txtTemperatureMax.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtTemperatureMaxActionPerformed(evt); + } + }); + + jLabel10.setHorizontalAlignment(SwingConstants.RIGHT); + jLabel10.setText("SV %"); + + txtTemperatureSVF.setHorizontalAlignment(JTextField.CENTER); + txtTemperatureSVF.setText("50"); + txtTemperatureSVF.setEnabled(false); + txtTemperatureSVF.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtTemperatureSVFActionPerformed(evt); + } + }); + + GroupLayout jPanel7Layout = new GroupLayout(jPanel7); + jPanel7.setLayout(jPanel7Layout); + jPanel7Layout.setHorizontalGroup( + jPanel7Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel7Layout.createSequentialGroup() + .addComponent(jLabel5) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtTemperatureMin, GroupLayout.PREFERRED_SIZE, 45, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel6) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtTemperatureMax, GroupLayout.PREFERRED_SIZE, 45, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel10) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtTemperatureSVF, GroupLayout.PREFERRED_SIZE, 45, GroupLayout.PREFERRED_SIZE) + .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + jPanel7Layout.setVerticalGroup( + jPanel7Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(GroupLayout.Alignment.TRAILING, jPanel7Layout.createSequentialGroup() + .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(jPanel7Layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(txtTemperatureMin, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addComponent(txtTemperatureMax, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel6) + .addComponent(jLabel5) + .addComponent(jLabel10) + .addComponent(txtTemperatureSVF, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) + .addGap(35, 35, 35)) + ); + + spinnerTemperature.setFont(new Font("Cantarell", 1, 24)); // NOI18N + spinnerTemperature.setModel(new SpinnerNumberModel(30, 0, 100, 1)); + spinnerTemperature.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + spinnerTemperatureStateChanged(evt); + } + }); + + chkbxTemperatureSmooth.setText("Smooth Variation"); + chkbxTemperatureSmooth.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxTemperatureSmoothActionPerformed(evt); + } + }); + + GroupLayout jPanel2Layout = new GroupLayout(jPanel2); + jPanel2.setLayout(jPanel2Layout); + jPanel2Layout.setHorizontalGroup( + jPanel2Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel2Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel2Layout.createParallelGroup(GroupLayout.Alignment.LEADING, false) + .addComponent(jLabel4, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(spinnerTemperature)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jSeparator1, GroupLayout.PREFERRED_SIZE, 6, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanel2Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addComponent(jPanel7, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addGroup(jPanel2Layout.createSequentialGroup() + .addComponent(chkbxTemperatureRandom) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(chkbxTemperatureSmooth))) + .addContainerGap()) + ); + jPanel2Layout.setVerticalGroup( + jPanel2Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel2Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel2Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addComponent(jSeparator1) + .addGroup(jPanel2Layout.createSequentialGroup() + .addGroup(jPanel2Layout.createParallelGroup( + + GroupLayout.Alignment.BASELINE) + .addComponent(chkbxTemperatureRandom) + .addComponent(chkbxTemperatureSmooth)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel7, GroupLayout.PREFERRED_SIZE, 51, GroupLayout.PREFERRED_SIZE) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(jPanel2Layout.createSequentialGroup() + .addComponent(jLabel4, GroupLayout.PREFERRED_SIZE, 23, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(spinnerTemperature))) + .addContainerGap()) + ); + + jPanel6.setBackground(new Color(253, 254, 209)); + + jLabel20.setText("Connection Status:"); + jLabel20.setVerticalTextPosition(SwingConstants.TOP); + + btnView.setText("View Device Data"); + btnView.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + btnViewMouseClicked(evt); + } + }); + + btnControl.setText("Control Device"); + btnControl.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + btnControlMouseClicked(evt); + } + }); + + lblStatus.setFont(new Font("Cantarell", 1, 15)); // NOI18N + lblStatus.setText("Not Connected"); + + GroupLayout jPanel6Layout = new GroupLayout(jPanel6); + jPanel6.setLayout(jPanel6Layout); + jPanel6Layout.setHorizontalGroup( + jPanel6Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel6Layout.createSequentialGroup() + .addContainerGap() + .addComponent(jLabel20) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(lblStatus, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(btnControl) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(btnView) + .addContainerGap()) + ); + jPanel6Layout.setVerticalGroup( + jPanel6Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(GroupLayout.Alignment.TRAILING, jPanel6Layout.createSequentialGroup() + .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(jPanel6Layout.createParallelGroup(GroupLayout.Alignment.LEADING, false) + .addComponent(jLabel20, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(jPanel6Layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(btnView, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(btnControl) + .addComponent(lblStatus))) + .addContainerGap()) + ); + + jPanel8.setBackground(new Color(220, 220, 220)); + + jLabel23.setFont(new Font("Cantarell", 0, 18)); // NOI18N + jLabel23.setHorizontalAlignment(SwingConstants.CENTER); + jLabel23.setText("Humidity"); + + chkbxHumidityRandom.setText("Randomize Data"); + chkbxHumidityRandom.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxHumidityRandomActionPerformed(evt); + } + }); + + jSeparator5.setOrientation(SwingConstants.VERTICAL); + + jPanel9.setBackground(new Color(220, 220, 220)); + + jLabel24.setHorizontalAlignment(SwingConstants.LEFT); + jLabel24.setText("Min"); + + txtHumidityMin.setHorizontalAlignment(JTextField.CENTER); + txtHumidityMin.setText("20"); + txtHumidityMin.setEnabled(false); + txtHumidityMin.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtHumidityMinActionPerformed(evt); + } + }); + + jLabel25.setHorizontalAlignment(SwingConstants.RIGHT); + jLabel25.setText("Max"); + + txtHumidityMax.setHorizontalAlignment(JTextField.CENTER); + txtHumidityMax.setText("50"); + txtHumidityMax.setEnabled(false); + txtHumidityMax.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtHumidityMaxActionPerformed(evt); + } + }); + + txtHumiditySVF.setHorizontalAlignment(JTextField.CENTER); + txtHumiditySVF.setText("50"); + txtHumiditySVF.setEnabled(false); + txtHumiditySVF.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtHumiditySVFActionPerformed(evt); + } + }); + + jLabel11.setHorizontalAlignment(SwingConstants.RIGHT); + jLabel11.setText("SV %"); + + GroupLayout jPanel9Layout = new GroupLayout(jPanel9); + jPanel9.setLayout(jPanel9Layout); + jPanel9Layout.setHorizontalGroup( + jPanel9Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel9Layout.createSequentialGroup() + .addComponent(jLabel24) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtHumidityMin, GroupLayout.PREFERRED_SIZE, 45, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel25) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtHumidityMax, GroupLayout.PREFERRED_SIZE, 45, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel11) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtHumiditySVF, GroupLayout.PREFERRED_SIZE, 45, GroupLayout.PREFERRED_SIZE) + .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + jPanel9Layout.setVerticalGroup( + jPanel9Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(GroupLayout.Alignment.TRAILING, jPanel9Layout.createSequentialGroup() + .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(jPanel9Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel9Layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(jLabel11) + .addComponent(txtHumiditySVF, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) + .addGroup(jPanel9Layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(txtHumidityMin, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addComponent(txtHumidityMax, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel25) + .addComponent(jLabel24))) + .addGap(35, 35, 35)) + ); + + spinnerHumidity.setFont(new Font("Cantarell", 1, 24)); // NOI18N + spinnerHumidity.setModel(new SpinnerNumberModel(30, 0, 100, 1)); + spinnerHumidity.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + spinnerHumidityStateChanged(evt); + } + }); + + chkbxHumiditySmooth.setText("Smooth Variation"); + chkbxHumiditySmooth.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxHumiditySmoothActionPerformed(evt); + } + }); + + GroupLayout jPanel8Layout = new GroupLayout(jPanel8); + jPanel8.setLayout(jPanel8Layout); + jPanel8Layout.setHorizontalGroup( + jPanel8Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel8Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel8Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addComponent(jLabel23, GroupLayout.PREFERRED_SIZE, 100, GroupLayout.PREFERRED_SIZE) + .addComponent(spinnerHumidity)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jSeparator5, GroupLayout.PREFERRED_SIZE, 6, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanel8Layout.createParallelGroup(GroupLayout.Alignment.LEADING, false) + .addComponent(jPanel9, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addGroup(jPanel8Layout.createSequentialGroup() + .addComponent(chkbxHumidityRandom) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(chkbxHumiditySmooth))) + .addContainerGap()) + ); + jPanel8Layout.setVerticalGroup( + jPanel8Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel8Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel8Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addComponent(jSeparator5) + .addGroup(jPanel8Layout.createSequentialGroup() + .addGroup(jPanel8Layout.createParallelGroup( + GroupLayout.Alignment.BASELINE) + .addComponent(chkbxHumidityRandom) + .addComponent(chkbxHumiditySmooth)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel9, GroupLayout.PREFERRED_SIZE, 51, GroupLayout.PREFERRED_SIZE) + .addGap(0, 1, Short.MAX_VALUE)) + .addGroup(jPanel8Layout.createSequentialGroup() + .addComponent(jLabel23, GroupLayout.PREFERRED_SIZE, 23, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(spinnerHumidity))) + .addContainerGap()) + ); + + jPanel3.setBackground(new Color(207, 233, 234)); + + jLabel7.setText("Data Push Interval:"); + + spinnerInterval.setModel(new SpinnerNumberModel(Integer.valueOf(AgentManager.getInstance().getPushInterval()), Integer.valueOf(1), null, Integer.valueOf(1))); + spinnerInterval.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + spinnerIntervalStateChanged(evt); + } + }); + + jLabel8.setText("Seconds"); + + jLabel9.setText("Protocol:"); + + cmbProtocol.setModel(new DefaultComboBoxModel(new String[] { "MQTT", "XMPP", "HTTP" })); + cmbProtocol.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cmbProtocolActionPerformed(evt); + } + }); + + jLabel12.setText("Interface:"); + + cmbInterface.setModel(new DefaultComboBoxModel(new String[] { "eth0" })); + cmbInterface.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cmbInterfaceActionPerformed(evt); + } + }); + + GroupLayout jPanel3Layout = new GroupLayout(jPanel3); + jPanel3.setLayout(jPanel3Layout); + jPanel3Layout.setHorizontalGroup( + jPanel3Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel3Layout.createSequentialGroup() + .addContainerGap() + .addComponent(jLabel7) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(spinnerInterval, GroupLayout.PREFERRED_SIZE, 55, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel8) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jLabel12) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cmbInterface, GroupLayout.PREFERRED_SIZE, 100, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel9) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cmbProtocol, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + ); + jPanel3Layout.setVerticalGroup( + jPanel3Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(GroupLayout.Alignment.TRAILING, jPanel3Layout.createSequentialGroup() + .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(jPanel3Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel3Layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(jLabel12) + .addComponent(cmbInterface, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) + .addGroup(jPanel3Layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(jLabel7) + .addComponent(spinnerInterval, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel8) + .addComponent(jLabel9) + .addComponent(cmbProtocol, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE))) + .addContainerGap()) + ); + + jPanel4.setBackground(new Color(169, 253, 173)); + + chkbxEmulate.setText("Emulate data"); + chkbxEmulate.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxEmulateActionPerformed(evt); + } + }); + + cmbPeriod.setModel(new DefaultComboBoxModel(new String[] { "1 hour", "1 day", "1 week", "1 month " })); + cmbPeriod.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cmbPeriodActionPerformed(evt); + } + }); + + jLabel1.setText("Emulation Period"); + + GroupLayout jPanel4Layout = new GroupLayout(jPanel4); + jPanel4.setLayout(jPanel4Layout); + jPanel4Layout.setHorizontalGroup( + jPanel4Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(jPanel4Layout.createSequentialGroup() + .addContainerGap() + .addComponent(chkbxEmulate) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jLabel1) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cmbPeriod, GroupLayout.PREFERRED_SIZE, 162, GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + ); + jPanel4Layout.setVerticalGroup( + jPanel4Layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(GroupLayout.Alignment.TRAILING, jPanel4Layout.createSequentialGroup() + .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(jPanel4Layout.createParallelGroup(GroupLayout.Alignment.BASELINE) + .addComponent(chkbxEmulate) + .addComponent(cmbPeriod, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel1)) + .addContainerGap()) + ); + + GroupLayout layout = new GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addComponent(lblAgentName, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanel6, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(jPanel1, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup( + GroupLayout.Alignment.LEADING) + .addComponent(jPanel8, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanel2, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))) + .addComponent(jLabel2, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanel4, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanel3, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(lblAgentName, GroupLayout.PREFERRED_SIZE, 53, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel6, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(jPanel2, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel8, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) + .addComponent(jPanel1, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel3, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel4, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel2, GroupLayout.PREFERRED_SIZE, 28, GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + ); + + pack(); + + chkbxTemperatureSmooth.setEnabled(false); + chkbxTemperatureSmooth.setEnabled(false); + + cmbInterface.removeAllItems(); + for (String item : AgentManager.getInstance().getInterfaceList()){ + cmbInterface.addItem(item); + } + cmbInterface.setEnabled(false); + + cmbProtocol.removeAllItems(); + for (String item : AgentManager.getInstance().getProtocolList()){ + cmbProtocol.addItem(item); + } + cmbProtocol.setSelectedItem(AgentConstants.DEFAULT_PROTOCOL); + + URL urlAlarmOn = this.getClass().getResource("/alarm-on.gif"); + ImageIcon imageIconAlarmOn = new ImageIcon(urlAlarmOn); + + URL urlAlarmOff = this.getClass().getResource("/alarm-off.gif"); + ImageIcon imageIconAlarmOff = new ImageIcon(urlAlarmOff); + + picLabelBulbOn = new JLabel(imageIconAlarmOn); + picLabelBulbOn.setSize(pnlBulbStatus.getSize()); + + picLabelBulbOff = new JLabel(imageIconAlarmOff); + picLabelBulbOff.setSize(pnlBulbStatus.getSize()); + + new Thread(uiUpdater).start(); + + AgentManager.getInstance().setDeviceReady(true); + } + + private void btnControlMouseClicked(java.awt.event.MouseEvent evt) { + Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null; + if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) { + try { + URI uri = new URI(AgentManager.getInstance().getDeviceMgtControlUrl()); + desktop.browse(uri); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private void btnViewMouseClicked(java.awt.event.MouseEvent evt) { + Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null; + if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) { + try { + URI uri = new URI(AgentManager.getInstance().getDeviceMgtAnalyticUrl()); + desktop.browse(uri); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private void chkbxTemperatureRandomActionPerformed(java.awt.event.ActionEvent evt) { + isTemperatureRandomized = chkbxTemperatureRandom.isSelected(); + VirtualHardwareManager.getInstance().setIsTemperatureRandomized(isTemperatureRandomized); + spinnerTemperature.setEnabled(!isTemperatureRandomized); + txtTemperatureMax.setEnabled(isTemperatureRandomized); + txtTemperatureMin.setEnabled(isTemperatureRandomized); + chkbxTemperatureSmooth.setEnabled(isTemperatureRandomized); + txtTemperatureSVF.setEnabled(isTemperatureRandomized && isTemperatureSmoothed); + } + + private void chkbxHumidityRandomActionPerformed(java.awt.event.ActionEvent evt) { + isHumidityRandomized = chkbxHumidityRandom.isSelected(); + VirtualHardwareManager.getInstance().setIsHumidityRandomized(isHumidityRandomized); + spinnerHumidity.setEnabled(!isHumidityRandomized); + txtHumidityMax.setEnabled(isHumidityRandomized); + txtHumidityMin.setEnabled(isHumidityRandomized); + chkbxHumiditySmooth.setEnabled(isHumidityRandomized); + txtTemperatureSVF.setEnabled(isHumidityRandomized && isHumiditySmoothed); + } + + private void spinnerTemperatureStateChanged(javax.swing.event.ChangeEvent evt) { + if (!isTemperatureRandomized) { + try { + int temperature = Integer.parseInt(spinnerTemperature.getValue().toString()); + VirtualHardwareManager.getInstance().setTemperature(temperature); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(this, "Invalid temperature value", "Error", JOptionPane.ERROR_MESSAGE); + spinnerTemperature.setValue(VirtualHardwareManager.getInstance().getTemperature()); + } + } + } + + private void spinnerHumidityStateChanged(javax.swing.event.ChangeEvent evt) { + if (!isHumidityRandomized) { + try { + int humidity = Integer.parseInt(spinnerHumidity.getValue().toString()); + VirtualHardwareManager.getInstance().setHumidity(humidity); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(this, "Invalid humidity value", "Error", JOptionPane.ERROR_MESSAGE); + spinnerHumidity.setValue(VirtualHardwareManager.getInstance().getHumidity()); + } + } + } + + private void txtTemperatureMinActionPerformed(java.awt.event.ActionEvent evt) { + try { + int temperature = Integer.parseInt(txtTemperatureMin.getText()); + VirtualHardwareManager.getInstance().setTemperatureMin(temperature); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(this, "Invalid temperature value", "Error", JOptionPane.ERROR_MESSAGE); + txtTemperatureMin.setText("20"); + } + } + + private void txtTemperatureMaxActionPerformed(java.awt.event.ActionEvent evt) { + try { + int temperature = Integer.parseInt(txtTemperatureMax.getText()); + VirtualHardwareManager.getInstance().setTemperatureMax(temperature); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(this, "Invalid temperature value", "Error", JOptionPane.ERROR_MESSAGE); + txtTemperatureMax.setText("50"); + } + } + + private void txtHumidityMinActionPerformed(java.awt.event.ActionEvent evt) { + try { + int humidity = Integer.parseInt(txtHumidityMin.getText()); + VirtualHardwareManager.getInstance().setHumidityMin(humidity); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(this, "Invalid humidity value", "Error", JOptionPane.ERROR_MESSAGE); + txtHumidityMin.setText("20"); + } + } + + private void txtHumidityMaxActionPerformed(java.awt.event.ActionEvent evt) { + try { + int humidity = Integer.parseInt(txtHumidityMax.getText()); + VirtualHardwareManager.getInstance().setHumidityMax(humidity); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(this, "Invalid humidity value", "Error", JOptionPane.ERROR_MESSAGE); + txtHumidityMax.setText("50"); + } + } + + private void spinnerIntervalStateChanged(javax.swing.event.ChangeEvent evt) { + try { + int interval = Integer.parseInt(spinnerInterval.getValue().toString()); + AgentManager.getInstance().setPushInterval(interval); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(this, "Invalid time interval value", "Error", JOptionPane.ERROR_MESSAGE); + spinnerInterval.setValue(5); + } + } + + private void cmbInterfaceActionPerformed(java.awt.event.ActionEvent evt) { + AgentManager.getInstance().setInterface(cmbInterface.getSelectedIndex()); + } + + private void cmbProtocolActionPerformed(java.awt.event.ActionEvent evt) { + if (cmbProtocol.getSelectedIndex() != -1 && cmbProtocol.getItemAt( + cmbProtocol.getSelectedIndex()).equals(AgentConstants.HTTP_PROTOCOL)) { + cmbInterface.setEnabled(true); + } else { + cmbInterface.setEnabled(false); + } + + AgentManager.getInstance().setProtocol(cmbProtocol.getSelectedIndex()); + + } + + private void txtTemperatureSVFActionPerformed(java.awt.event.ActionEvent evt) { + try { + int temperatureSVF = Integer.parseInt(txtTemperatureSVF.getText()); + VirtualHardwareManager.getInstance().setTemperatureSVF(temperatureSVF); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(this, "Invalid value", "Error", JOptionPane.ERROR_MESSAGE); + txtTemperatureSVF.setText("50"); + } + } + + private void txtHumiditySVFActionPerformed(java.awt.event.ActionEvent evt) { + try { + int humiditySVF = Integer.parseInt(txtHumiditySVF.getText()); + VirtualHardwareManager.getInstance().setHumiditySVF(humiditySVF); + } catch (NumberFormatException e) { + JOptionPane.showMessageDialog(this, "Invalid value", "Error", JOptionPane.ERROR_MESSAGE); + txtHumiditySVF.setText("50"); + } + } + + private void chkbxTemperatureSmoothActionPerformed(java.awt.event.ActionEvent evt) { + isTemperatureSmoothed = chkbxTemperatureSmooth.isSelected(); + txtTemperatureSVF.setEnabled(isTemperatureSmoothed); + VirtualHardwareManager.getInstance().setIsTemperatureSmoothed(isTemperatureSmoothed); + } + + private void chkbxHumiditySmoothActionPerformed(java.awt.event.ActionEvent evt) { + isHumiditySmoothed = chkbxHumiditySmooth.isSelected(); + txtHumiditySVF.setEnabled(isHumiditySmoothed); + VirtualHardwareManager.getInstance().setIsHumiditySmoothed(isHumiditySmoothed); + } + + private void cmbPeriodActionPerformed(java.awt.event.ActionEvent evt) { + // TODO add your handling code here: + } + + private void chkbxEmulateActionPerformed(java.awt.event.ActionEvent evt) { + // TODO add your handling code here: + } + + public void setAlarmStatus(boolean isAlarmOn) { + this.isAlarmOn = isAlarmOn; + } + + public void updateTemperature(int temperature) { + spinnerTemperature.setValue(temperature); + spinnerTemperature.updateUI(); + } + + public void updateHumidity(int humidity) { + spinnerHumidity.setValue(humidity); + spinnerHumidity.updateUI(); + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/resources/alarm-off.gif b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/resources/alarm-off.gif new file mode 100644 index 0000000000000000000000000000000000000000..c346605ad00627e4bda392cd37ab24bde7643395 GIT binary patch literal 4265 zcmcgu`Bzhi62A9lyJWoy1j2p^TTsM=O`@ntSQ-H_EFyx?fC`9O0|=s6y&-^TPy>Rc ziZ(&qu?>repf+J~DQa<{;>HW8h>9ANTCFWS-aqlaU*^pD&dhx0n{(#OH%r2UglSiR zlfYE~n4X><7#Nt2jh$|6oSsRqUWH-U%L}cpuAXjd<8U|vfq=t7>gwuxdwUxj8!unJ z{OQxDqN1YdEnAM_aPQu|yLIbUYisN1=x9SjLqtS`N~Iba8cI)3&(6*ki^XcSIyN>| zC=}-9EJMnudIT>!8hhNp*yra7GRGm_HN zy}Z1pKYc=Ac%~*PHg>9^LEz<8S6wX>2$GUw)oN8yUfy#6KoB@0LM#>v^M%46)avx4 zq`bUrwMvzpJ)%p`f?-a^C-h2^(kR*3}6FUa_$e1GjFeR7I_=jh_JEa%=01 zC;vCNe8GbZU?<=W%;0}J5s{`apjcH>TDGfv_a1e{-hKNul~o6-YYzTU`{SX*bw`f= zq}A0QYdC)5q+*Y*iYCm3noDy`t_jX5|A7m zZ^27@UVST|DVT+f~Q_Dd|=7UV?p{0$_BkJV+)A`YQpPs)8Jzm)v>3;! z@EX&WSZ9yv%014E)mH?*9y_K9pY^JtIwAYj@!E_tuTIo?*cr?0pS%K-F7qUj1c#u0 z5y@Gj%>ZtT%_1PJ;7kM_tR;b5Yd*98mjuj=1#Zc4ZD3RWeT~QW$xRL<&jJNg0|&{^ zdR|fJL1}4_Ids9o>wc0-O7)38+$j6B0iM9?fEsS0+2GSM$6%nacn=JcWz}t3g3afX zXY>yex+)1y{xT`iqgG=X2lh^i*h{BYp*k9T^OF+aGZb*Z>bO$g1p{imp^?Ee7`kBx z?EQYLy=JkRz@}Od?~1faTtD8Vf?USpkBjYin$!yGa((>zb17)oDy}RaqxS1{6vr?`75*6ZqI3Zr^dL?mvM1D*78xd@pr z4CvbQ>)OAZAEhs5iMQ?eUY^oK^L96aS;t{C$r-za;tVH>7T#eiwmFiRgwG= zXyKh3(G2NNebT6VeRibH(}wZr{AvDwZ(75@Y~*8 zC7k*!p|4uKq6`zkI|h{0AUCHcCS>R5IPJb0$3PXeoakh3RG}EY>^TDaPkok9MIY2$ zP^mh@zj({`6+U5b=&82`v^;C71Qz&v>dL~D zi|i8(6bS&OE5DmHdFTddd)3_XLQ|^Sy(p^ErqeL#GW^3C?pN@6*Hx2^;{LkcU% zYqYqv<5G622w#oI&slj2ibwT8ai9qo+4&f^*%q>Fy@qf34WrS~NLuSLky_}bcJhm1 zyz2+ur1Cj@5!63E0?`h$A+se$8^pxtJCmEjO;f?v=G8b}ZSo?yCqzWRlY4X)%l{R2 z3152ywM`f;^=|lWeJI|e{I_KedH`86;6pD|$Nk%daj!qe%~tQC4=RDLTQ}l+*8$GT zmyOQNu%APDGMkzU-aQg}qDTyo$!qF6oqE89zZop350y4I$@oxRBW=~L_Wu+bX4zjS zS~%9XP)^MO9AyTO8v7G2ad_1em~7F}qGXJft(%=cL~OOJ|1vJY9XU2m;qY6CXf$*= zWIWx5$L_!QdS&9~T0cKSXDvDaI4J;C6u1aJ)qOt8?`$mA_JdQ_Jd>owLJ3jLCYrx|kto~Hwn2_r^2G;jP>?K>afmUe z$o5%xc7&s0wu0zVcDrKRSi@ezm$mqgu%_Iq3rVw%WQGtwoDg4m##Hy z@-8LYJBi|!Egy`3b0!nKZRs24lMNL(U_VCQ=*4XVa2y#*eAn__FseWB@TnRF9Z-wW z21om(>N7+9Oc>GEF3OvO>ufpKQW5qmX*)iDgd$gmBgN2%-5j{qmoZzvYGFW&{pPAa zN`JinsOMGmmN^i3Y%v0ToH!kA-^b%r8mkf0>FBoy1Y_7{FdFx0i7N_%Z|~1S6!L)+ z7*W4#>3I5-{nx?k(QVB?+#1hV@bzK;zP8qrOW$TEe|9jM9xEOr@T=?=q%jb$U3+z}0FYDm_fMwOK zG$~p%G{wbHV5z0=r-#Me`6;os;sqmIr5Bdv6beh3(RUr*DFKh7kGJkd@^s9oYVUDT zb4bSuyz7r5{HXS)+*8GDM(B_i!*L-}HK5t0P(>C!W84VIqU3J(irRJ2iyM~FK)=r7 zmhF>Zi>o@e<~QM@w~DK12O8D?P+B*2|A!dAv1^FJ(x^-rF;PZ9$GgCsIDjSp3!|G& zgxFTajiSDzhphLQm#NhQ5JJp0G85nfv2C3{Z^7 zk@ZP7Sqb*laKf*-m$_N`rvV3bln0Yg{;)q)0rcbr6Rx|V`X!$Ci@+&G(9;q0<_jR$ zg&+VypC+4@w1Cnc7KO_gT>}J~=KgsesCFliOweTgPO5B<#KVttitpaTeB=v_Vc^YY zkT(bh53AlN@sCYlUhg77{e1BRCln#XG&wB_vPw#uIcESDAao$mTr;%&qpO4&zrSgV zb7*w&nmH;N7^wg}$$BVa30<40g~mo1!Jtd)wo|;rHhz_g7eH;)uvO0UZxeJjmqI1`^WvSgE(I&%XO*)$^s4^zHgpr7 zCtrs51nu-dpo~BfAs?&4sG3yhc;(h-cF{2@U&WShT|r01;lPg$kM)*om(_UIg7Z5C4`ck;y;btojbgoB4_LbG8Jh$183`fys8cj5R zx!w-p?^`4;EV>6;2z}#>i)LBQbxxf7S7qviKAy<;UBQYjmfpt4w@B}v>nXn5HaVdz)80{2VZ?%V4V=d7W1bEOyVE!{B;rjFtx zjkLjhXK~2lWDzcoKl}GIC`bX}CkkxzOQQ~E`Q&aLM441LgspMz3EA^BC)i57$Nf$T zO}^rT9*5TwBqA^ZThL~+m}jzDu9^QVZqdsSs54XImzGkxyKV>QH;+Vcdl$tPKHOy$d&-N;IunuVTIH8()DUEt3J*xVF}H*k}Z>@y;Kq! zU$4>s;eN(NEBFlb=sXZ~{;p@4pSsz4*amI z-oR{XdF9s2^qe+2uYlYe{#bPJ#QOY$8=lWRDO%4VljD}jJmHA_xj)zaUFm-8pr@Y% zBK}4DJtMd8d{f@l%m;R%`MI7}p`y%Y&zJTZpvb<4T@}A zyYHc6RqWyF2sU{q4uZ%veLy^AT_hI0OJX zoxZTJK)17_x3tjzRmaAVNTjqhuB4=de(xSHFRzl45-%^Nrlw|ebhM?VrN6)b@bEA( zF_9h{dkqHL-QAs@p6=@ET3=tUtE)3NH&01PSzTTA^70A{4Aj%p%gM>Hv$NCG)QpOX za&vPliHgGgOaDg1VCd`Xe>FAfIXTie9DR4!&fNT8!w>)@A(8aeRXQ(k_diKqUeeOi z^ut3868W!>o1NX?x;iCk>6(%fO(i8aH@lpil*Fj0H2}b1kmlxkdYYOSH8sm~a=hH! zqM`zGQc?l~|7mgi*YdB_)KnTB?T`A8FwK^Ka%*aol%(zK%onDoQ&JMUx?27%NPkxs z@c%Fg)0wS}9PADBY>bo@6k*`MsfB;Q5o~}vAom{`|Jf#hkxm3iDXD4c8JStxIk|cH z1?0k_;*!#`@`}o;>YA(9u2XK*-mI%{xYgLye7mLfPFwrkj?S*`dp*7PA3W^qe>6bt zRhO3@9hH|?ADnC%Rz{#^VbAAbvk1h*RK~Eh^0TF-H{=TfAP&36n59KV5Y( zuj{@KKJ-(5)MbjFXgmYQrjp9h+zZ?BCw6B`!Z zX&t`MCJdkRdsVs_@B#JY?=^v+=MIM@6&b@GOh6A=k*SB|o~*I9#@~O_RB^|E{Z31s`FL7uvyFkTtcKIFygU+*zZU=094J}%<~&@G zTh*K|{DMJsu%Y|Zc(8>+|M>P*4#N*#K?%MuC*tMT15Kflrv%wkn@JW;mHU})Alt=9gE z@L5l}x51+`wi&qXrrh348fz()Q%dkUWMH3cu?n{Vi??O!aj^F0PMq$YdnbWkocqg8 zbcRbafKp@de&7>w9a*O^RK82mNxJo- zbaOfxfx(9l?z9>ZKt`{P{s$k4QFg;uKXeN$!&MlgDrOZ=AKMpo_ogPm*yHddhU1yI zKa9uyF(gKI9PZZ}Pevda#v9xL(f1mmD}5&zxlRwi$~6rggt3$NK@>ZYoD3_CqV^zo ze1IXOaK#%WzoItF$Gj`7zNvF04@*G&h6> zjmQv9xQy#1zByn_W`qN&#W**ZXm7dW)oD`UBn@du0ojK3N%6(*s2h?X*Y@5eHX8!V z6O&=XaRMV(7n}h{OGd;2&}nx=O#h)Os}2s%%nQPQaub@vU!hOdx z3Xn$+5!m2!ziSyV#M3W67qY`NJ&uVgjj|rtWM$60{2=B>y?YT|;8`sIox~Y4f5+w& zQQzW@4<@s?v(fXi8mXMv866(rMi_nu&$xXAtmw@V+xv)&T4az}1Ax%IctQ27Y$Tje zghYBEH5JpOzSDql!v^?IiDVyt(8*TYZkKQnM4qMqsyx$yhI za$#X^OCnW!Bg;|}4_yDrOhoBXwN$hKN)r;$kPk<)ni6Y;XI2fIC+3t%IQT&c5pmcs zkgEn%|1Or2(lcjbwd|mz5dq6Wv&jj)`HOu108~-~4RW5Xeg^LX$ z1jr!VhfsdL-w=+&LvX4hgp3<>*KJSh+a@ATXe2|H?L_oJ2zRzr1q!i1vd?X*EtNz;GA*?r`F{}y52wL410v($1i+9<=_!E? z1?DlijF}VL%|C4i*DnzJ3d2ww-grno315DO1E2Y}Ly18gVb;Fp+5kPQXE z#u0`~f~XMhK56H$72HUIK#L?0?cM{j_$rAr$T?6RxWMEZA&krhzy{|*$s~C&#_dD0 zp%n1e=m!QZ0tBm|4VFFxIC#TJQZ!Y-94~=Ypm_9>2p~^|BGReF*JM5qhRakE7HJ2c zB;W=#{%wRv4VCO_zmBWlhSd^T)?{x0Hhf zP|w+^PJ*DvDI+;Ak^;1)zQ!ZV==)k5W|Xqkmqa8BhNR?1g;^d3^BGZzuvcO8tQrTf z-;Wp0s*{2MDlxK@7t}gV%LU_zJ{!D5`Aj@W%Dab*3@5U6sQ?i=K(p1^F$^y-i`}Jy zE);#Dx-;y}vF>;i!Weq|#j9-IaKMb2`n1*Zdw7`PKIHb!ap3VZ zfMOw}NGH;0-hD()RvPK|ZY7vhq6#<0Si^}FkkpmUi#RH*rU%r&qO`N92B7v=0EyvhCP@MwS;POZ^d%ddeI|77lkq*C0w9%_ z06zQ7luR$Yl?0Q(lf-oiDQH{|Ok{jNU_b#BF?^etdFKA0d?+ZU!NXL-Jji^WoV;{j z0WL<~R6Y{Y?>KjCD|MWRvfK}AK8{agYt-jJ>O#l%JHg{*s=aeypfrX&*!qZwofjzg9nAVfo7679eeTErp91$jj@f7W`1OfQFdiaS7e_^s+++&b2Q=RB- zhr<{?@P&ldfYURPtD*?C8Lf+@msiqL*?ht2;>&8`7N>0BaYj(AlO{3-nkq|6?*ikF z&b6qk^~8~jeULDC>l@wCh>U@{tHrT|P0 zAPdFYOETse5;}%+@{m&FQVfcoR|RP>Cfd1wcV;*jz)t~ExZ=5Skf28X2XSai5*oh< zXwh`#7{h*VBz~2$H9pJ{EHCg-2|BS>;79>ZQ!SY&VBBcro9A(u@C?=;HmvOcD=}_- z6j*kN!@W>94TmHso?nrWpfrdky0|qIh-8^?6JX!di)8qK;Dw@mlVT;5V@NL;iv(G0 z;HzK3nMHLioGV^BMqd*ea4GpArS`kAbWAG{!KJm;?|pa`*tpW&U!l;HIgrb2@4>C7`)dCtaF8u@6Wj5Aa%xT~g$33T~8 z#6pB`nbmv|1r=i=R-D03Sbz{-yS)S){1&Ehm;w?m6iEfPy(hDgt?DK17yutu#SlH3 z_4H`OR(z!}xti}MJ$tS0G#>?2L0I%R0PDN}ay{g8nPb_!mdp`|-b!9K!fP96@Kf~p zh|toqx)m*kO@?^zTqnKB*LWf8h!YURRC_yIW*YTwu{Rc@#nGZuKQodZHw4Ft$J8}| z3^+*hF=cTP*b1mw@-90!;dR?UQ>fj4Ar+`bgi+EEsTuAa27Zszfva4U8mW+^5kmQe zz`8LP`c^m##tMyTmKeD7SPR8)jW{{9$$RAeX9T}4lz)FKl z&CoCIiGuv?fSL`0*~abIp%DuW{2mo`Ccf5e$vsh~dG4M$`wW=e>>KWDAqGG#+eTeH zfqd--GST40^*fJT{9M09nwo1eYrr4*S*T}(e~dGFv|wrYq(xHfIyZN#f`oSX0mw6< zj&%yWG<<`J2#<2}i`vl5%Y@|iL7{dq${4Ojs~&O={~(>iKmZO@Lsy4e%*7$qsy5E- zn<*D9{w&zZEQU|l0YClD#|FCHn+HTGpc)mDqr!x7@mL0E@J9zX4bDJ>9OA$*t}w%g z23KSPaLhnY>gfsNbC(Q^)F@XL#~?e4I8jBP-p|Ci z3)ybXt_TSMi4i#<`l5x_J-cJ=11klHg)<>TT9upV9*-_}9{-f9Fuk_tb#2#VqB zOHDP85g`p5#IbPCS}n-Q=d3z^4{1if!{E*pV@;W!Zbu-@VFYSScS>CVKlwvpqqn`c z-33cL%*TLBFV!;x{rF-$Ex1ga)SzEteJOyG^#>ri+T&+8_-+)&X`<-16%8Xz>n<_!V5rQB1BGc1KbBCD?FI+lkNmS9k!(P;jk~Wg9TEvL>pAF&`Vbc@3pSei zM3(67BwBcv+&)T&G^uBeyW6KrhfsZbB_}PynTJXH7HDyYFx6fMF;_+!9Q{~p>`DKs zqx)Gr%$}y-8VG5GL-qApJo|xG?nD#|^bFRgW;t3MQ+qV*k~BL?c=;{L|;^*S`R zL?Le1B20i&UMqT!nN41e0a=odQAzrO`1^wG;I9NCS9tW1DDjd`V5=z6_HoqTQ9D+g ztQ!u_Nr6XcWvHh?(~gL@w};AdpNQd8Hf8OS+=`jQgY_4+nEfI5DQmR4Cff{55rA=G zVC)mOVvm{lP5RW=5Am#n{CKEj2NceDoJpVuCYfgxn~&V}HfH<**>0o=ESRBfASD9a zme4s67ucbb0P~+-teKvE4xZbX&WZ+?qfXB`88#e|*y};`VG^67W}8YVj!+PF*y(3M z?rs@sZhqG3K&Wo<-Nfj`z)qWN0EHG7A8OLfO1ne4t6sxEY3^D0GU$STB9mk#+-~04 zW%4VVl_VyvZ39!GX=GD~!;QMi<#w+AGhVTCJOq4?XiQdvyNB%a7cBTi??>tLHDsC^ zJz2{n3Rd-QHcllbb<@FhGQzdD(9Fu;q{@GELBCjb;fy8J=yz6-V9LOadT0Pp3=CwV zj!`1D@?UteC!AL2f{vDE{Lj%{6xYE{hsFFnU31^*!V9`W`1_Goj>qJYKlQap1dOtX zR$&C2vLO(hg51i`_2mhT{1fc28@d$`tyW##VO64XNncg>zSzFbYfIEml3EH;R+Y9G zm@F_5#YQp!-2Ysfa9YMGPCR`Mnv~D8nb#Ojw(uOD1|6P9%P$T-_Ai=w+4x0UTb|h| z0wX%S41~WT2x?+xKrx%;6;aQ{I`d_fn)!3DjT!L^UsqqspH{0`P9rZqdcEB8S-C2e z$q7VrzLyZ|JRbYyWC0~~`4yOE9{X4HG{^bnj{fQL{x_voi)S}m(6raZWT;w&c2KDF zOOMUoo0;4Dc>Q}piYOhn8A-#^a?Vz~zWcbAR;jqM z`ANHQRy%;C+zAMO(33!)*QydxDWh4O=zi0qJDeJ0f!4iwr~y4duSwOdRZ&hnuN4|p zmXi}Ut{1TO@>{%AD3+ z9gk9PAP5$F(WR9BN?9s%KYO|F43i{IgUwHQLq8AMG65g&A3iC{#Lcf2C znzcq!Mfgs?6WP$$<7nqwq%u2W*%;aP-B{J{2y;Z`@73AvhCby_6co|M?3aAm?j^jN zi&S%(J8ldEicSGP9-)5y{Z#Q(;7sc0+pEkG2C|&b1vbtL*bm9J6M2pxVB)X#S6i&E zJP=(AJzQ0T$dWQRfMbTwsf`c8jnEcFM#T|tKC1%mn2l9$vuRm9@5|jL!EarXsL*{2`r@s=~IIQOY7uQ>{ z$;-&h2>RhI8-9ZmUs4D7VQ=`}wxbE`XhxEtn1|qVTV^SC$w0bb&KqVn*zq(-n$rt? zZ-4(`Ava3+sH;$6edfpTtaR|rW0?g^4+_6d3&Tp!2wdM|4lNddy?Ix0{l`7UFI*iw ztBSxAWpK)aZ|M}X_CD`plH8fMLf@3OdMU?VU=&}^Z~(JtA(HqBp>OwzKLd(hofqd7 zyvw{;Amn)6MJ9zefc^Uo%AXDTZ{k~)FV`i$Ut!w5!Y*gcdU}?A_A`eYo#oecsXqm@ z_j{a*$-np`&~sF*(%!FMXE2+4%qG06Wvs0;#fyZ|#$ z2;>6*1D#3?HejX-=Tzp>D$!j<5V&I{aXUF?I+QWgEYz$0 zWot|p>FZX*st-)o%llt28bVyk&N0#^WTan^MakRb>Pu^rtJ7u6$|-L0DK#0YvROIu zu)6wcRp}cYE{HqzvdWi~Tkln)I;jq_{XKVs5Pc&VvV+qm6ZNY1T~SYJiY{I}PndhU z3U<#dHaVgCZ=tN^jcnxQm3Q7uC&$Gj5)jcIBdTlm5AR59{t#=U6~@`yafbV}+6>5W z)Y>YW7S2A*te3F7e=)ZiB<3C8a+h-SGPsm6zHnRQ_rV2(1cpbJ;-`G?ElZ&grRU`j zlD2%T;5wgskm$`i#N;JT&2nz17Or=0q|{(FL#SZc7Y=Yb`3on9T9%bbPIq(DnPa^x zcz_tsCR|#Y)cH)$Y$7G_6D((vZpmO1k} z&uKhIW$6RG<~UUs*O8l03(VdTsb*t!a4I-n*IrMr17i3Mb$Fl z0#=%MY_}gaK4y37`#UFM=ZwPgj<__2C+x=8goQ96tM7=Qfg=grIjUE{gK<_PK{>+* z;C$lVz|m{6zPMRZ&fdr?FAIEphfidEF=}A2ZKRj=v&qOgaTZUs2+0mL^eQH)ExJBI z2XI1Nv9NvcTfs>M*W1JyWwb?l=`$Th%vqinjz}C9(Dlx9>AlG?ZFki$=~CAcW1;+s z75JT^6E9!3%3o*-+_5+kuy$@HuLeH@HSX+soue*9OWyhZE?0f``+Ks9!jH8Q=g=P? zD*W#JSg(oQ{jovGQ24o7S04IvtMS&IpW7`xyFYi@A1nO&*tHl+|MjVNXXynz0(V8c1T9OLO`QLjnB*vpe@B9R5;J59r6FUpDD6^;1Q~VbRu>?o%sY zX?8OEBzFM&Z18Di&SNTkGRr;z+8I=)kdG+}cRp}jH{iwRYoV$gPi6kSXc{I@!9)4t4qKF zKUvdk`*TaO$i32ME;VzGpo1ViRVK%~60pZzf#FV`#b#=6o39{0uy}o+hL5>5c{bA2 zAw`%y)uhPRA-T0xM2@`#V{i`}>2rE2NNi1gI%50|f^4+T1uUSD`Jpc807u^(yxRj7 z_(h^=tDTs3s$Ate)&$-==S;~^Hm{e|~cnGB&)-cZd|{e%&$ z$b>BizqU&V7e45ffrGWg3zEYk*N{)ykBP}7I4UVU!$uaml0x+3fJtW>Da+>ORlqrlYtp4< zDl1et(u&J&G^!+CZEau~_+Vu$7#gRrC*gJc~^Az}6gh{*k zL#aO<^)g)bX@H+LD|bz%luM9FB|{$)l%;emn^7c;IAZ0 z3vdA%QQL4@$1O5lu+jl1TJ)^YQcAmc`(Vqs$xcR`QAq%bnqiW*?)>7DZM0)ArA}09 z_FVJC=Nt50_^{&m>1p}}Mm>&NyO~_6(8Z4aO7#FvOIaHybk~-t$F0O4Lgy0P>P z|JPxO?_<~N#H`#;M)^C^&4GQdes7>7446Ll`_TZk&o73O_r!52(WMAp;6(BPqdRWB z-gpFks2E4bxg}oQ^h{*k=ElXtk$RrELDO;J6x6&kF~M@AXAzCa-#iau81RCQDad56 zbj|u`TJHCV^7BlV_b?{;d!LaM4?~t8?qqD>=18(|gvlscK6CTDDX46c&OYK<@!^90 zD*^#BO9((DGekP9dz;s_8=SK5XfjFBoyam`wi)9gRC$wC4Vhpz8uRfQ56Xc|5^EqW z#)0*%3%U(m2q*?|YmSh#&B2CZ`8u~Ptg_*xxHuN#Te|_Ub>R`R?$T0y~jFj1I%vs z=Ja#A_?nj4Jr6>nZrOBvJrx25VbruN?S@rc}D{p76Ch&-<+H;0X59>)nU1N7Mz@ecc*#$x?HQA6C&bSFTB!3M<%}RUv4138Co&mU zp0%R#54_~S$&|^d-EAWvGM!ZC+esgdJ_&s$moaF5ZP{vQc zmmGPgh*RetG0%_UO?mXXtT2v!rI}-8*Z6-t6Bj5d++X5BDJ#(5vR`;;hns7xFTNoc z&SGHG)5S?lLeh|HZ$+yiSCfG>*-HUi-lukJtMF7g{N*J6H$$BDdgdManVj$iNUQ;z z@=rbwJx}2GxqXH~#LPcwVGQnI4xg6N4)dA(vK~b-!v>Vaa>6sc`1DY2;fBU*OrnWd z*N-FYt&EZRa>-^EY#&%OF9+L{*JK^jOHI|0so+sZ_+WqHq$(e{`yTm_N%$>Bqwh-g z*$rk^qOWMz8t27N-DtQDn5MJj-JLk0Vyv;JaYHIYX9&mXXsOTeR8~m9kS9kGZ9b>s z8Fv$%@Yjr}vn_{{(3*N>!!b*T&nrxJOqn_crp~xH+<5tRxxv)gt*V%vgNbJjSs!R_ z|ArzdlW%$~j$dguS*yzrlcx_U+rb)N<8CE~3ppXP9deocfg8e9m1Z)wRU$0X=C$|n z2QvIq*vuA|R+5ga80JZcUPdJb&`Pdj3qpeBW}C-~#UxNCTPX~#=n$FGlO^{mH%J8k=$uhRo_=I)Vf zvcrx2Hn^Sc?GiiEH%*{*Y(aO^+uOHazr$=%Rhz4Q70YIfL_Ul$Td_QcCcX)!6; z#+m7liE5m!EvLzVUQ*-bo&{xiLq* zO*4ILsII)b_v`tNlk%@sE`OzG1~rNv`ZjzV!5n?zr($aqftnq zEeC0YVe%{DYnS^u$4sOrJA8v06!o$TLs%t}%JE|p^m2cvP#@P&57QdARKo|h#9L=5 zlp@s+_wSy!yIq+Lz@^y(rIIACEA^Gm&l->&`vX_vcO^#UT`K*JoN@AnLCW`+9W6e{ z>2y@mxKY}aAZ(;|;5>%f8<7VlrJX!?*J~)1OS|rq@2}vSyn_ZhRXcul)YP#?+cTMR zqN>~c9Qi`nKe=6Yy_B@zOJ$V69Kky&%)C1niQx+u3oN;I?tfemp1is&)FYztk?U#9 zN9xm~E@1RA;-kjgyP9CjBisx^of6>e&bc`)N;|*3aHQlSf^qRY2i63hw zC`RGW6+T}`ip_0Rd}AiTQ~^)$!}(Z;8PQyZqg7*s4Khqb{H+V%UPkZ`L)KY0@4tG5 zP>~tCuxMFSJ@3~oW#NSl>;omb1=swMO9~gU;&zeVy6(@`x zQ@Us1h8k*;0$uN@4Aw?$ZyJ(v7YMu|@t5D0Q;hHx0Z{0W>+n^OWoA=Rxe?f^mSJ%- zWD;CS-x451n8Q;|xhXQBifO_bgNg9YDW->CwzpgJDy7`cB+`ayRjUi(BR3;X9rIGw zPL;WZr7B$z!RyZQIONOBK&^nE0ls}j;bnx89ME={n zgLBBY1sA{8Z!g?qKYd#yWXK;b`ILtoF8kkVJzNRy`E>X){IUGe|JmYwi<44+8zi=FPCsxbw zzU?dR6a0(jDf)c)d%&LClA7_8p;Pv&ew?~u2uGnSQ_fCS(^>C*#`Zte@64W@q0}<_ zc5PX4ATccoJXJh-SxH1!{dC-y*0HW2lkN8lvGdB;P@F*wy~VkRvt7bCgqab04drht zSwhl5(gBY7_dQOL;_@ko&Go#NQ}-4B>l=m?{6ip>m+|U1!!h6Sn^yrJ$Dx^f9-dz7KUPzcHkWgx-qEK z4%&8)M>otCC+jfH&AE6<@#qUz*)YVN&iQ$er3Tvwh^0WPuaHm6iU#;8#+1c|~bebxFmwnzU=RIh6W_o3%BKjSa2Mw<}BAZ{F!H?rbXR z?Yh^W_wYKIdjHXA%JAKR&atV&y2lSETBhf3wdX!xSgF6AzWkEbHd6L}weKx$W9`X} z#m(y1b5B0(OzvGhT>maL6?Fu zH$_G-^Q!!mIm+@hEff_j^ND5mov>iy!FD0p8!RH8X(W7fQSm!d#kj?FP-#{_T_L|0 zA~0buIeGJRnoM2>Cw@NN`L>#Fw91=jE#E5K^0W>c6mxBEoZ`%xZ;`vHNx!5;I{QMN z+NyoO>5uOt4m^G5sI=cgZZ?<}@^PhPWnc<4!agZ>TBE$F2K%KffO@mi2hrcmM0g z%uS2y*8LB@?JRW$y?NUI@Mw=VoD+7d|4YF1%2c)S8(D?Kxm~3cua-yAzn|r74IH6r`5U|NO=%?8)uu|28$!*0(H!xHa-3(@U@n zn<>2OuxP(-CSIgvnOyI6_6zN(HcO?b;%0u=^cpTj?cN3UH1>CmD;fsem7FeD6eXJ0 z?06aK7E0XYmrI=mRC$ZEDG_Ebu1E~|^OeY4mRNT_vf)FE|Kgzkb=ZLaNm_%zKlqvvMs!aGwEqX< C_s;DA literal 0 HcmV?d00001 diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/resources/deviceConfig.properties b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/resources/deviceConfig.properties new file mode 100644 index 000000000..930742a2a --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/resources/deviceConfig.properties @@ -0,0 +1,33 @@ +# +# 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. +# +# + +#[Device-Configurations] +server-name=WSO2IoTServer +owner=shabirmean +deviceId=t4ctwq8qfl11 +device-name=SMEAN_t4ctwq8qfl11 +controller-context=/virtual_firealarm/controller +https-ep=https://localhost:9443 +http-ep=http://localhost:9763 +apim-ep=http://192.168.67.21:8281 +mqtt-ep=tcp://192.168.67.21:1883 +xmpp-ep=http://204.232.188.215:5222 +auth-method=token +auth-token=79d68b50ae5f5a06e812889979b3453 +refresh-token=8bdda6359dddad218cff3354d5a8cb3b +network-interface=en0 +push-interval=14 diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/resources/fireAlarmSound.mid b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/resources/fireAlarmSound.mid new file mode 100644 index 0000000000000000000000000000000000000000..d1a2241b2d87d668b9cfa999f05be6a8c477d895 GIT binary patch literal 6719 zcmeI%&rc&|7zglY+L=<>4Yh|&?8cA@YYBz~Q>>7XY15%lD8d3Q;20ul)6PO!VUezl zB_jA>{7SQ2H^mm&olGP zJ2TJw%nQsdZ6t}vMjSb~-{+QgFS9L)3btqF;=7mCUGc&NDl7>iQ=)>x^OooARCwK@ zZyniZ(aJrzM}?QDl;au|toEq-*4yfKLZt$Kc4|(XP&287Dtca_g2?sIL#L*l;W)d+ zZng7vo84jmhaR3M@uFtoI5r@T74j&7tuA3Ml+h^A(|nnUn9RJ^%MC?8rCSRNy9|p zqwdy-9l}CxB6d_wl!bhVvXHM)v{UEGKYv0mOzOaOv1ji`S+G)I72q!yM?-gK$&)2t zmcm)RCfpglEIb+g@%u8YJ}8`lr{QUM8lHylz<1y~@E!OTd<(t>--2(#H{qM`P53%| z9lj1TiJ4 zj(9>kV%4&Dy!ooa<(m1j>>~!u@{XW|v0Y0q%^(uMW@~Y*AfaKy!a%p(<8= zd21G`#eBKg#|dqJWIv&dl|INX{!Gp~EJrK5)l2?--C-@~s?XkXs_T1CZdgz0B~~6h zO_`L?%X$e}Tn9!8eYYqauoC){0(8)@jx2vXvW|*LbFoiS8eP@)-n*(Sq?CSROI}FH z=YAol0rla^&c}=8Z6-5oD$g7KkjA<1 zdEr*qLgV&p)f;(FRr3|a+V)$_NrxooVxJ~7+R*lL357P4jIkxthOGWdP6O)0m7R|l z%deOuZPPi;#`?rgd5PW7XN%Xie~vw-2nC}w5uwQ_g(5T+rRj^ggnnb$GYng3rkAFB zX|k8bZ6tsFPUmmRiG2{Ir>z&bMB@?#C7O_EQlgMVQxZ)}6qablORshppNDWr(+-++ z(72W4$DEh=OCayMU7nZ14+8%e`MKN8mPL>8IxFVP#Xe(mGNtY1*_=!%8DmSPl&n4= zrvdfh%Ff4&RvUB~RX0hU}ZIZNn zolU}Kd0UOO>6BH|#_TtVQT=shbKNnf&5vo}PO~Y)-GAh(te7ts`+QN`dw)^M6o1O< zZ{)0@Qpnh)c00dPb3h9@YIC-b;>O5-AI4L1DODvg(tL&EVKI?&xbBup!Y2Imu5g!? ygz*a{MNtMz>YM7$#+7aMZ + + + + + + + + + + Builds, tests, and runs the project VirtualAgentUI. + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/manifest.mf b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/manifest.mf new file mode 100644 index 000000000..328e8e5bc --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/manifest.mf @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +X-COMMENT: Main-Class will be added automatically by build + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/build-impl.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/build-impl.xml new file mode 100644 index 000000000..112454207 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/build-impl.xml @@ -0,0 +1,1413 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set src.dir + Must set test.src.dir + Must set build.dir + Must set dist.dir + Must set build.classes.dir + Must set dist.javadoc.dir + Must set build.test.classes.dir + Must set build.test.results.dir + Must set build.classes.excludes + Must set dist.jar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + No tests executed. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must set JVM to use for profiling in profiler.info.jvm + Must set profiler agent JVM arguments in profiler.info.jvmargs.agent + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + To run this application from the command line without Ant, try: + + java -jar "${dist.jar.resolved}" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + Must select one file in the IDE or set run.class + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set debug.class + + + + + Must select one file in the IDE or set debug.class + + + + + Must set fix.includes + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + Must select one file in the IDE or set profile.class + This target only works when run from inside the NetBeans IDE. + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + This target only works when run from inside the NetBeans IDE. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select one file in the IDE or set run.class + + + + + + Must select some files in the IDE or set test.includes + + + + + Must select one file in the IDE or set run.class + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Must select some files in the IDE or set javac.includes + + + + + + + + + + + + + + + + + + + + Some tests failed; see details above. + + + + + + + + + Must select some files in the IDE or set test.includes + + + + Some tests failed; see details above. + + + + Must select some files in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + Some tests failed; see details above. + + + + + Must select one file in the IDE or set test.class + + + + Must select one file in the IDE or set test.class + Must select some method in the IDE or set test.method + + + + + + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + Must select one file in the IDE or set applet.url + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/genfiles.properties b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/genfiles.properties new file mode 100644 index 000000000..a6df38fd8 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/genfiles.properties @@ -0,0 +1,8 @@ +build.xml.data.CRC32=e60df945 +build.xml.script.CRC32=7c331eea +build.xml.stylesheet.CRC32=8064a381@1.75.2.48 +# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml. +# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you. +nbproject/build-impl.xml.data.CRC32=e60df945 +nbproject/build-impl.xml.script.CRC32=4fa004f7 +nbproject/build-impl.xml.stylesheet.CRC32=876e7a8f@1.75.2.48 diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/private/private.properties b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/private/private.properties new file mode 100644 index 000000000..e59ac1df6 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/private/private.properties @@ -0,0 +1,2 @@ +compile.on.save=true +user.properties.file=/home/charitha/.netbeans/8.0.2/build.properties diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/private/private.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/private/private.xml new file mode 100644 index 000000000..2f9a6910b --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/private/private.xml @@ -0,0 +1,9 @@ + + + + + + file:/home/charitha/git/IoT/iot-server-agents/FireAlarmVirtualAgent/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/ui/AgentUI.java + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/project.properties b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/project.properties new file mode 100644 index 000000000..fb798f614 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/project.properties @@ -0,0 +1,73 @@ +annotation.processing.enabled=true +annotation.processing.enabled.in.editor=false +annotation.processing.processor.options= +annotation.processing.processors.list= +annotation.processing.run.all.processors=true +annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output +build.classes.dir=${build.dir}/classes +build.classes.excludes=**/*.java,**/*.form +# This directory is removed when the project is cleaned: +build.dir=build +build.generated.dir=${build.dir}/generated +build.generated.sources.dir=${build.dir}/generated-sources +# Only compile against the classpath explicitly listed here: +build.sysclasspath=ignore +build.test.classes.dir=${build.dir}/test/classes +build.test.results.dir=${build.dir}/test/results +# Uncomment to specify the preferred debugger connection transport: +#debug.transport=dt_socket +debug.classpath=\ + ${run.classpath} +debug.test.classpath=\ + ${run.test.classpath} +# Files in build.classes.dir which should be excluded from distribution jar +dist.archive.excludes= +# This directory is removed when the project is cleaned: +dist.dir=dist +dist.jar=${dist.dir}/VirtualAgentUI.jar +dist.javadoc.dir=${dist.dir}/javadoc +excludes= +includes=** +jar.compress=false +javac.classpath= +# Space-separated list of extra javac options +javac.compilerargs= +javac.deprecation=false +javac.processorpath=\ + ${javac.classpath} +javac.source=1.8 +javac.target=1.8 +javac.test.classpath=\ + ${javac.classpath}:\ + ${build.classes.dir} +javac.test.processorpath=\ + ${javac.test.classpath} +javadoc.additionalparam= +javadoc.author=false +javadoc.encoding=${source.encoding} +javadoc.noindex=false +javadoc.nonavbar=false +javadoc.notree=false +javadoc.private=false +javadoc.splitindex=true +javadoc.use=true +javadoc.version=false +javadoc.windowtitle= +main.class=org.wso2.carbon.device.mgt.iot.agent.virtual.VirtualAgentUI +manifest.file=manifest.mf +meta.inf.dir=${src.dir}/META-INF +mkdist.disabled=false +platform.active=default_platform +run.classpath=\ + ${javac.classpath}:\ + ${build.classes.dir} +# Space-separated list of JVM arguments used when running the project. +# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value. +# To set system properties for unit tests define test-sys-prop.name=value: +run.jvmargs= +run.test.classpath=\ + ${javac.test.classpath}:\ + ${build.test.classes.dir} +source.encoding=UTF-8 +src.dir=src +test.src.dir=test diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/project.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/project.xml new file mode 100644 index 000000000..438f1e6a3 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/nbproject/project.xml @@ -0,0 +1,15 @@ + + + org.netbeans.modules.java.j2seproject + + + VirtualAgentUI + + + + + + + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/src/bulb-on.jpg b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/src/bulb-on.jpg new file mode 100644 index 0000000000000000000000000000000000000000..51d40cd834db53f1bf305997832ad4c5f7a00356 GIT binary patch literal 6942 zcmb7oby!qg_x23K&{9J;14s=ul$3M`gM`wdq#z|-0s=}62n^jJ(lvlc3DOK8-Aadm zNQa=nZ+zb8{eIu~=eN!|*R{@G`<#8Qv+sM|Ywz2++Z6!0rrHBF01#_1APfMw{R&V9 zfN*gBo>;@ho?rqn7#9~zgpZF$Kte=9LQF(Ve20{h{0=DvDKRlQ4LJpbikh05gp8Js zhKi1oikj;0OMoEkGq_+vFqn|)4)GnT|8KkP0#FbDN&xL3AO!%20tli2-u3{P001B^ z2nhJU3_B7S2M?bB8)X0j0XY9}6a)t1;Nsmb07$UyIOHI5>-!aCTuJ3mRK#sC0#-~Ntlle0_H&yVF!WCoLCKmDsLVVM0l zKHajYnVIuPy^Gy)%xN^Ind8QfKW|r9KU6!RNC4mkV4(Lge`Z>#T{_;$R?4}y7ap{? zTigQf2VGUB_oq3WdQ6R7>`H&Vd@^t1m}cM|yWjkX6X}+EkuKTU@I+v|^pgLEciXv1zHBC$8?hNLzH-hjViwbG7tm5)iy!5|JBM-emmS_afh+SC{fM85tZzr$xB= zo&f-)PO$XWy-R`fmcS6*OQP!=X=IJI8UKXRCpyGRyug&3$)=2VW!1{Fce_}2LKG`om#KFwZi%)Q~O_(@1M#iPE4X0KS_D?@i*r2^E7#H zMnYTP=C#z3sd_oyd$AzvnGXQ)G<}uTI1TbxJ;)hdoN=i1Y59=Nu+wa? zB$vl?wl4{Jr?6l8?h7pbi;WlETe0|MwOsDCtDE&UQ;oj*QKR*V+crQT$A2$Y~%r2YRcie8#^mMK|^^R{z^rwQ}!e@G~2e6$0R3a%ZPDQEi zU%ZRbZJr0MhqpaGR?3)WC|39z{X3#t%Y$$0OT|)Rgkl>fpC<_u{u2P;WWCMu-rlT~ z8u6OE8&~^pD*!GbCpy!xWGJ!qb*BLH?&*Kq!MTj~t!g|cF+u34|F!{0iNpQb{@$@- znJhq#^SAq@9XkOz>QkWch zIu&fu3hmbM3=$jD=Fu8kNBy}8QV7<<$#_#?w2}qd>qaM*b>~AKTRo2VEIeBK!1pwY z!+t>Axk>N2C)GHA@>dV06dS4}aaoQZgfZl$Xx*w+^x1-h!)d~MH-3(YAiKwkskKXP z>N5~3ex}85`yK|jfW-NmbE-49th2cr@0twACo2&p^!{8lBeXmyy#9KR@6n*E>nuFd zw(8|4nHk7qRs9e-Ik&#mGEA#-GfvFfqAN1_VDtD|Ik3;N_UGjI+3RU~1KYVBlBP0O z?h81fU*@bJR}ZqedGJST;=lh>uv*bTvA6o%C$93T)M z7pqABq8N)<3QAmZ2phYIB2-vGj~Yw`x8e}h{Rg5TAW$B-`MCfkBW>$*5A95T(|hF- z;{3(8uLH@Pf+?7*bv`$yY!V}WLrQaO zS2XX+z2;Ps;t#d3{mx*6`uQHS6ZB*(eNX8Fkz@4PApghv8A+9Obpw=^UC@{{0|;N~ zuKl>vYwz|0w7GeZ{=Q}>CNsU#p?ZRHdaT&=nsVQSav(|Be7lMoT4Z^qZRo=6Jj4x) z%Md3_Of>J6wVu+pvu)%`q`OQs$?C3Ecq8GW?Qgtp%shivW za0{>>ClPtaSo>6?oMVTHNcJ92YF(ek%YktI#K*`VYs&mB+$=D-W@Gx=)6WTs_3Gwl zeyS8}qeibypz|~$=o2WcTGf+3!{w#abBhcYl^rGqu{v?vY*r1yJp&smg|2F8J=-Bp z?f5wruCV5mIXmsAx;tUs1!KXB1EK$wMoc2x5)MCWCjAUi2^tA|1b0AUC>J;2OgCNL*Ct=AT^m7e_ciY`KMl|KDVX z&2<#)l)@qqDk!;vA{!j6q^zrF<&ORbNLbLx1Fy}P;&sn5_D%NZu}6B1^O(*s1C+bGm+RwP02c_5d+6;w{jFEJ2!Ti>J-cWz6B6!IqxK_a_Fo2 zJ|yWI-d#=kS_g;xgd?&Wp18*P1rD{lt+eI!zzq>oeAlJbafy8_BCvL{lsnOdd7J0uRk=(YI1LzlXYzAT47mmPCG46 z-gu+e){{k9?6{HC4RMswL5}bEK8YNOQUx5Y%GkMRGUyr9RI}zycN61q8U%+pkp=a9IqxzzjC{i?^E#R0&t=2Dp}v|g z;~@-mcW&&h$4BN>2-tm!qI+8EMOk$m^xdsnyEzhM!+g_6zn~7I%53Lnp)p=YI6M}` z(a;z3-b5O$%zfG{S%)4kBWdO*u7$LN5zem3MwU@MT1{i)JkCS?mwR50@r;Q!9)ZD{ zM>iUA0V{fx2V!Ej$^l$DoTb#z5>$D+{pP z1FnR{O#wL-o4!YABD!;2^pUma1RiHtIO<>B#FNLFaxn`bq6-TCTrTgN~I2a&gCErty%f5TK*A)fh#GCxc- zHsT~LtIwaV2(Sff&J6JUg zQy$m%JjG?44%+<%-q+vJZ3;zWem@(Sbw{8czVN)dzv-j#u3GV@3&AgrZy7(YB_yXN z6$(LfWV?QDUnCzgb;#v;F@5jXym1QNUW0#o1P(CcctRqOvRr1+#%v#gmR?)zjLq*? zGk>(~u=2aoCrZ{+Ct?ufBCE_F&qseoaMT8K{NdkEf6PVP2HPRMQK^17#(cy5tMW&k zp)ba=xUNIDe2L_P8RJ)S7ReYuhz?zRqtmhW`!EfBN7m2IAJb=+zXhZUMz(cL1oQ1Q z5Pwp-8XT5c8bv+hxJmgr20YNxmFt+IW$AAC|tlsI7TM4^|q7T^=5(~yV zSN~g;$+KVakz0a>mpJ^kH$r~tqz z+2k2{lkC@jedz%n{aQ1`*;_b^Yc!fH=YFM-VRwv)5^?S$J8<-8s5ne6rvJFeN+Huj z->Uq9HNRQ(o78=Oxg#O3p5QvBQy03-5b6fTx+dDhfltfMhU|>p3XXsozye2fqoz-G zsnfU6eG-{AfYlHy8MqNr2)USIVC95lt~NM^!#!k5PG9iPnA}=h^M5l ztvseO5K16vxdn7Au#<;P1;fYLV*DNwQ}+5PZQeDPCTZFe94U6WLc-9KeidtG#3U(w z!hxX6kBe}rvdniip|YsVDOX4p*Ukh?xhkGAL)P=^`1>CuZ%4uG4{3W3Ls3~Yx8{x9 zs?%VaItN3-{PoCw3fUmR?zfmVhx618VJ#9*bMLFcN4y1hz*qR+9ZoIHGKKX&SWm2u zT@yAsk%e4o_|e}25VdJS0YuW*DnX1YU4krDuUr1ycNFCHP#XSD;H=22j!Rw62!F$n z`-cKm6YAxx72BHs3p7@2wI!{zX<0&Xv65%pU$iVApR-D!66PYAiCjCAWtG>(y^^xI zhJJ+K#2iMI8%T}gv(b~@m{N>RQ46RlE9l|bLZ?4*{d~RzP5$7zFXt(czOg@a71rO` z1g}md>~f2-!&w?{pT&PxA=Z_`mh}Ga3){@*an1Tj{$QPgH%ICfaxOWmbwjYjs<#mz zaqrv$_)=K)(lYkb1{+0!<@z~z7Dh}pFRbo3RAtAmx;JS5R*Q)TzS8~p{I}L`zSg@+ zV|Wq%g*;|jjNAKK?SfpNOw206)G;~5RPdW2Wm@65YRb!D9`i2t)3~n0NY9X0g2j1 zB9E2kuFIs825I$Apv#e1od2`60Qg%2lB3jY@M);Xoj6gfaa>F$96vcx5A+H2&CpM5 zQBNMLt%0C_-9_nf)B6`myi___1ED{(ir}a6WWRRkAphd_}4-F?SLgWFSZu>TZ!QQ?=}jM zf}CB1O%baixdKvi_74T7EBMpeP(*DNDnCPU6w4@j%84!`KOR__c8ye#?tq^HjUH1H$;tgc4ZtOSRtPEs7sGRcPWhixrycnUcm?b zr#y>%egU-zb zJCvrJ1jUyj&b31b$AIs3(2{$}xk69EPPAVmdD<;3hJ7reXzg@_w@kC&5|lQZ7eWRe zQz5+P_WQN*F+0AnG~Qvmviw(#D$qMSL*vm|Bwr|GtaiA05+3#~M&B1VOI;_EPO6$*)K5K1se;RXGJIvM8e>}^So;BrVPC7Q2G5o(nh z8o_wB88^_GO-NX#Y};YqEyZ2-;<~Iwq#=rIfg49*PSWkq-x`&d0=q+qg$8y_j`yF% z4mRok#f2~y7z&}f?ggFWhl%K;|5KlmpYCnxn{XX!yktE%kPpAQ1!P^a&OmmrG#(gF z$^5M0N@kcjbgn@L8)cfz9g1&A$VGIg*t&+slZ~#(w_O{y;o-oygxcE@0imA$rkm*5PX}h!>+8Ryd|ck>BwI({YVJ^JfAPMJ?>8*&= zs>Gf)vPt>%TY$B(#dZ&i%cu6e^ivO(V%elgb=XS)D?8nR)(G^4>tN?RfDtajQY?{* zw_r06 z2v|Ayc&JVpde|VLy5Efk1ROE?fx^FHB(w%~2GwJork-O|MnH>00>$li{(ceOY-z9R zPw9_9lw!94V(e0Bf==JHf9Jsk=vP>9%2W{*ZU3SC-`V{u>HqNlzncE9q+7YOg(i|0 zV2O|JJUseeO((y~2u4bG{m4p{=VOh&zMmHrd04U*9P|gub-mmb>?cpTeo(SzYQQ{s zBz_Cv1!aNC#jK4gwcb~0ML^$?kK1i)x4(LEfHRA;W-9Ku$2XQ7-?R}*Et_Tohd*nr6rB`HhgGVW7|A})gk+5d6}=?480$J6P1s2Uu`@P4>YLSGo{xB z#T+1LyoehJ)3x>u0?T`-B_W1?9`nv^@5&{Eg}5ZmefkQWPHeJf_x9F$*ry#z4O<3h zDI1C2UaMhV8mGqtWelxezWN0IbV&0-_G6Co84HmZKyRW>_lK^2?pReYhlEoPMLB$M zX9^m%o}V_u(s5A<=5b?GaGsAP(}a>Xr>3V=4)+HaL^}ysiRGv$ZGbG0#fO^DE5C8l zEP*ax8xQxF>6)+@$OtoO5I;Q+*KU>7vo&#;HZx@As@Oj-(m_3{$k6OYEbiXp3Wt2t zP1}twKACH7HT=G$>+D0oRwBI0y|lh8qZ=OE-G_nj!rtElUW0{N?jMJmcQ-_2v`%+9 zE2Uaf1)`q>nC`tQn}kqWF~3yjq)|-9Z$p^LvT+#M%<289jfb&v&P!g^mgOK8r%c}f zH*89Re8KXiTs1FHiTw}1PlAew6`oE$5WyW{MHa|_LOw2nPx^DW>m{#p-`jih94o;H z256}A=QlfJbYUJ^zCc3gKr=z%7hlS?DEENZq*hxy0^JCSPYnXURh|-1#uhf!vW65^ zwe>%DJ%b^NYVG;<%U;D)Q12^`@YC!e+;j-7x@6;x&FhE`5b!(E5&G&~^p-AvhWu#& z5#63@Pp+9XKZxkr;rT6T7I|=Oxop&>3fe!YpW05Yd!0s{2C1*a9r|wUA5bDc9c`kx z)A(p3>zE;EF!&*R%xDC)6X}r@%PrvKfCPPiw>G3zk<&<04zJbUEje0Yek6MR(wvrh z)@SGo{Ks3FGt&Dg)N1-r(lA55Qnu##&ldMbYQDHag(rsM3kBgS+B9zax?%aC7&}>p z6EK}Lthn6)%3qk}HXHD^AR{9yZOBjni->T&_@Y{ogt=-# zzKOEu1x5;?d}(&19dp0lNI^_A7M3Ie*Wpq0zOZe?-Nuw9v#`j!v#mheGHQa<9s0fO z04V`#r`XE2ttb9zq8rFlyj3MbwM`zil{;2)#Ccg9?|7#teAT2=(8;*Pd4x7ax^^@~ z*;F+PPrDtK)3e6#8mks^v}na)UwSQuIGAsuIh@xjzWZ#MF%FQ9;6HWAksND!c z`$H&iK2Eq@?ji)_Hx$roPm(nF3t*YP@NrmSY02*#rDdo0@@Ia$Cw%VE-N_icK&|=={Sb$ literal 0 HcmV?d00001 diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/VirtualAgentUI.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/VirtualAgentUI.java new file mode 100644 index 000000000..a2aa2116a --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/VirtualAgentUI.java @@ -0,0 +1,37 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.wso2.carbon.device.mgt.iot.agent.virtual; + +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; +import org.wso2.carbon.device.mgt.iot.agent.virtual.ui.AgentUI; + +/** + * + * @author charitha + */ +public class VirtualAgentUI { + + /** + * @param args the command line arguments + */ + public static void main(String[] args) { + try { + // Set System L&F + UIManager.setLookAndFeel( + UIManager.getSystemLookAndFeelClassName()); + } catch (UnsupportedLookAndFeelException | ClassNotFoundException | InstantiationException | IllegalAccessException e) { + // handle exception + } + + java.awt.EventQueue.invokeLater(new Runnable() { + public void run() { + new AgentUI().setVisible(true); + } + }); + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/ui/AgentUI.form b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/ui/AgentUI.form new file mode 100644 index 000000000..23b70e036 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/ui/AgentUI.form @@ -0,0 +1,803 @@ + + +

      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/ui/AgentUI.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/ui/AgentUI.java new file mode 100644 index 000000000..470a7d8fe --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl/src/main/ui/src/org/wso2/carbon/device/mgt/iot/agent/virtual/ui/AgentUI.java @@ -0,0 +1,744 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.wso2.carbon.device.mgt.iot.agent.virtual.ui; + +/** + * + * @author charitha + */ +public class AgentUI extends javax.swing.JFrame { + + /** + * Creates new form AgentUI + */ + public AgentUI() { + initComponents(); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + lblAgentName = new javax.swing.JLabel(); + jLabel2 = new javax.swing.JLabel(); + jPanel1 = new javax.swing.JPanel(); + jLabel3 = new javax.swing.JLabel(); + pnlBulbStatus = new javax.swing.JPanel(); + jPanel2 = new javax.swing.JPanel(); + jLabel4 = new javax.swing.JLabel(); + chkbxTemperatureRandom = new javax.swing.JCheckBox(); + jSeparator1 = new javax.swing.JSeparator(); + jPanel7 = new javax.swing.JPanel(); + jLabel5 = new javax.swing.JLabel(); + txtTemperatureMin = new javax.swing.JTextField(); + jLabel6 = new javax.swing.JLabel(); + txtTemperatureMax = new javax.swing.JTextField(); + jLabel10 = new javax.swing.JLabel(); + txtTemperatureSVF = new javax.swing.JTextField(); + spinnerTemperature = new javax.swing.JSpinner(); + chkbxTemperatureSmooth = new javax.swing.JCheckBox(); + jPanel6 = new javax.swing.JPanel(); + jLabel20 = new javax.swing.JLabel(); + btnView = new javax.swing.JButton(); + btnControl = new javax.swing.JButton(); + lblStatus = new javax.swing.JLabel(); + jPanel8 = new javax.swing.JPanel(); + jLabel23 = new javax.swing.JLabel(); + chkbxHumidityRandom = new javax.swing.JCheckBox(); + jSeparator5 = new javax.swing.JSeparator(); + jPanel9 = new javax.swing.JPanel(); + jLabel24 = new javax.swing.JLabel(); + txtHumidityMin = new javax.swing.JTextField(); + jLabel25 = new javax.swing.JLabel(); + txtHumidityMax = new javax.swing.JTextField(); + txtHumiditySVF = new javax.swing.JTextField(); + jLabel11 = new javax.swing.JLabel(); + spinnerHumidity = new javax.swing.JSpinner(); + chkbxHumiditySmooth = new javax.swing.JCheckBox(); + jPanel3 = new javax.swing.JPanel(); + jLabel7 = new javax.swing.JLabel(); + spinnerInterval = new javax.swing.JSpinner(); + jLabel8 = new javax.swing.JLabel(); + jLabel9 = new javax.swing.JLabel(); + cmbProtocol = new javax.swing.JComboBox(); + jLabel12 = new javax.swing.JLabel(); + cmbInterface = new javax.swing.JComboBox(); + jPanel4 = new javax.swing.JPanel(); + chkbxEmulate = new javax.swing.JCheckBox(); + cmbPeriod = new javax.swing.JComboBox(); + jLabel1 = new javax.swing.JLabel(); + + setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); + setTitle("Fire Alarm Emulator"); + setResizable(false); + + lblAgentName.setFont(new java.awt.Font("Cantarell", 1, 24)); // NOI18N + lblAgentName.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); + lblAgentName.setText("Device Name: WSO2 IoT Virtual Agent"); + + jLabel2.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + jLabel2.setText("Copyright (c) 2015, WSO2 Inc."); + + jPanel1.setBackground(new java.awt.Color(220, 220, 220)); + + jLabel3.setFont(new java.awt.Font("Cantarell", 0, 18)); // NOI18N + jLabel3.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + jLabel3.setText("Bulb Status"); + + pnlBulbStatus.setBackground(new java.awt.Color(220, 220, 220)); + + javax.swing.GroupLayout pnlBulbStatusLayout = new javax.swing.GroupLayout(pnlBulbStatus); + pnlBulbStatus.setLayout(pnlBulbStatusLayout); + pnlBulbStatusLayout.setHorizontalGroup( + pnlBulbStatusLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 0, Short.MAX_VALUE) + ); + pnlBulbStatusLayout.setVerticalGroup( + pnlBulbStatusLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 167, Short.MAX_VALUE) + ); + + javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1); + jPanel1.setLayout(jPanel1Layout); + jPanel1Layout.setHorizontalGroup( + jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(pnlBulbStatus, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jLabel3, javax.swing.GroupLayout.DEFAULT_SIZE, 190, Short.MAX_VALUE)) + .addContainerGap()) + ); + jPanel1Layout.setVerticalGroup( + jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel1Layout.createSequentialGroup() + .addContainerGap() + .addComponent(jLabel3) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(pnlBulbStatus, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addContainerGap()) + ); + + jPanel2.setBackground(new java.awt.Color(220, 220, 220)); + + jLabel4.setFont(new java.awt.Font("Cantarell", 0, 18)); // NOI18N + jLabel4.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + jLabel4.setText("Temperature"); + + chkbxTemperatureRandom.setText("Randomize Data"); + chkbxTemperatureRandom.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxTemperatureRandomActionPerformed(evt); + } + }); + + jSeparator1.setOrientation(javax.swing.SwingConstants.VERTICAL); + + jPanel7.setBackground(new java.awt.Color(220, 220, 220)); + + jLabel5.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); + jLabel5.setText("Min"); + + txtTemperatureMin.setHorizontalAlignment(javax.swing.JTextField.CENTER); + txtTemperatureMin.setText("20"); + txtTemperatureMin.setEnabled(false); + txtTemperatureMin.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtTemperatureMinActionPerformed(evt); + } + }); + + jLabel6.setHorizontalAlignment(javax.swing.SwingConstants.RIGHT); + jLabel6.setText("Max"); + + txtTemperatureMax.setHorizontalAlignment(javax.swing.JTextField.CENTER); + txtTemperatureMax.setText("50"); + txtTemperatureMax.setEnabled(false); + txtTemperatureMax.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtTemperatureMaxActionPerformed(evt); + } + }); + + jLabel10.setHorizontalAlignment(javax.swing.SwingConstants.RIGHT); + jLabel10.setText("SV %"); + + txtTemperatureSVF.setHorizontalAlignment(javax.swing.JTextField.CENTER); + txtTemperatureSVF.setText("50"); + txtTemperatureSVF.setEnabled(false); + txtTemperatureSVF.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtTemperatureSVFActionPerformed(evt); + } + }); + + javax.swing.GroupLayout jPanel7Layout = new javax.swing.GroupLayout(jPanel7); + jPanel7.setLayout(jPanel7Layout); + jPanel7Layout.setHorizontalGroup( + jPanel7Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel7Layout.createSequentialGroup() + .addComponent(jLabel5) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtTemperatureMin, javax.swing.GroupLayout.PREFERRED_SIZE, 45, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel6) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtTemperatureMax, javax.swing.GroupLayout.PREFERRED_SIZE, 45, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel10) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtTemperatureSVF, javax.swing.GroupLayout.PREFERRED_SIZE, 45, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + jPanel7Layout.setVerticalGroup( + jPanel7Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel7Layout.createSequentialGroup() + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(jPanel7Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(txtTemperatureMin, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(txtTemperatureMax, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel6) + .addComponent(jLabel5) + .addComponent(jLabel10) + .addComponent(txtTemperatureSVF, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGap(35, 35, 35)) + ); + + spinnerTemperature.setFont(new java.awt.Font("Cantarell", 1, 24)); // NOI18N + spinnerTemperature.setModel(new javax.swing.SpinnerNumberModel(30, 0, 100, 1)); + spinnerTemperature.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + spinnerTemperatureStateChanged(evt); + } + }); + + chkbxTemperatureSmooth.setText("Smooth Variation"); + chkbxTemperatureSmooth.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxTemperatureSmoothActionPerformed(evt); + } + }); + + javax.swing.GroupLayout jPanel2Layout = new javax.swing.GroupLayout(jPanel2); + jPanel2.setLayout(jPanel2Layout); + jPanel2Layout.setHorizontalGroup( + jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel2Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(jLabel4, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(spinnerTemperature)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jSeparator1, javax.swing.GroupLayout.PREFERRED_SIZE, 6, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jPanel7, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(jPanel2Layout.createSequentialGroup() + .addComponent(chkbxTemperatureRandom) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(chkbxTemperatureSmooth))) + .addContainerGap()) + ); + jPanel2Layout.setVerticalGroup( + jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel2Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jSeparator1) + .addGroup(jPanel2Layout.createSequentialGroup() + .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(chkbxTemperatureRandom) + .addComponent(chkbxTemperatureSmooth)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel7, javax.swing.GroupLayout.PREFERRED_SIZE, 51, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(jPanel2Layout.createSequentialGroup() + .addComponent(jLabel4, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(spinnerTemperature))) + .addContainerGap()) + ); + + jPanel6.setBackground(new java.awt.Color(253, 254, 209)); + + jLabel20.setText("Connection Status:"); + jLabel20.setVerticalTextPosition(javax.swing.SwingConstants.TOP); + + btnView.setText("View Device Data"); + btnView.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + btnViewMouseClicked(evt); + } + }); + + btnControl.setText("Control Device"); + btnControl.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + btnControlMouseClicked(evt); + } + }); + + lblStatus.setFont(new java.awt.Font("Cantarell", 1, 15)); // NOI18N + lblStatus.setText("Not Connected"); + + javax.swing.GroupLayout jPanel6Layout = new javax.swing.GroupLayout(jPanel6); + jPanel6.setLayout(jPanel6Layout); + jPanel6Layout.setHorizontalGroup( + jPanel6Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel6Layout.createSequentialGroup() + .addContainerGap() + .addComponent(jLabel20) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(lblStatus, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(btnControl) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(btnView) + .addContainerGap()) + ); + jPanel6Layout.setVerticalGroup( + jPanel6Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel6Layout.createSequentialGroup() + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(jPanel6Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(jLabel20, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(jPanel6Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(btnView, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(btnControl) + .addComponent(lblStatus))) + .addContainerGap()) + ); + + jPanel8.setBackground(new java.awt.Color(220, 220, 220)); + + jLabel23.setFont(new java.awt.Font("Cantarell", 0, 18)); // NOI18N + jLabel23.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + jLabel23.setText("Humidity"); + + chkbxHumidityRandom.setText("Randomize Data"); + chkbxHumidityRandom.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxHumidityRandomActionPerformed(evt); + } + }); + + jSeparator5.setOrientation(javax.swing.SwingConstants.VERTICAL); + + jPanel9.setBackground(new java.awt.Color(220, 220, 220)); + + jLabel24.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); + jLabel24.setText("Min"); + + txtHumidityMin.setHorizontalAlignment(javax.swing.JTextField.CENTER); + txtHumidityMin.setText("20"); + txtHumidityMin.setEnabled(false); + txtHumidityMin.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtHumidityMinActionPerformed(evt); + } + }); + + jLabel25.setHorizontalAlignment(javax.swing.SwingConstants.RIGHT); + jLabel25.setText("Max"); + + txtHumidityMax.setHorizontalAlignment(javax.swing.JTextField.CENTER); + txtHumidityMax.setText("50"); + txtHumidityMax.setEnabled(false); + txtHumidityMax.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtHumidityMaxActionPerformed(evt); + } + }); + + txtHumiditySVF.setHorizontalAlignment(javax.swing.JTextField.CENTER); + txtHumiditySVF.setText("50"); + txtHumiditySVF.setEnabled(false); + txtHumiditySVF.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + txtHumiditySVFActionPerformed(evt); + } + }); + + jLabel11.setHorizontalAlignment(javax.swing.SwingConstants.RIGHT); + jLabel11.setText("SV %"); + + javax.swing.GroupLayout jPanel9Layout = new javax.swing.GroupLayout(jPanel9); + jPanel9.setLayout(jPanel9Layout); + jPanel9Layout.setHorizontalGroup( + jPanel9Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel9Layout.createSequentialGroup() + .addComponent(jLabel24) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtHumidityMin, javax.swing.GroupLayout.PREFERRED_SIZE, 45, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel25) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtHumidityMax, javax.swing.GroupLayout.PREFERRED_SIZE, 45, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel11) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(txtHumiditySVF, javax.swing.GroupLayout.PREFERRED_SIZE, 45, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + jPanel9Layout.setVerticalGroup( + jPanel9Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel9Layout.createSequentialGroup() + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(jPanel9Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel9Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jLabel11) + .addComponent(txtHumiditySVF, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(jPanel9Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(txtHumidityMin, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(txtHumidityMax, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel25) + .addComponent(jLabel24))) + .addGap(35, 35, 35)) + ); + + spinnerHumidity.setFont(new java.awt.Font("Cantarell", 1, 24)); // NOI18N + spinnerHumidity.setModel(new javax.swing.SpinnerNumberModel(30, 0, 100, 1)); + spinnerHumidity.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + spinnerHumidityStateChanged(evt); + } + }); + + chkbxHumiditySmooth.setText("Smooth Variation"); + chkbxHumiditySmooth.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxHumiditySmoothActionPerformed(evt); + } + }); + + javax.swing.GroupLayout jPanel8Layout = new javax.swing.GroupLayout(jPanel8); + jPanel8.setLayout(jPanel8Layout); + jPanel8Layout.setHorizontalGroup( + jPanel8Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel8Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel8Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jLabel23, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(spinnerHumidity)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jSeparator5, javax.swing.GroupLayout.PREFERRED_SIZE, 6, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanel8Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(jPanel9, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(jPanel8Layout.createSequentialGroup() + .addComponent(chkbxHumidityRandom) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(chkbxHumiditySmooth))) + .addContainerGap()) + ); + jPanel8Layout.setVerticalGroup( + jPanel8Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel8Layout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanel8Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jSeparator5) + .addGroup(jPanel8Layout.createSequentialGroup() + .addGroup(jPanel8Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(chkbxHumidityRandom) + .addComponent(chkbxHumiditySmooth)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel9, javax.swing.GroupLayout.PREFERRED_SIZE, 51, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(0, 1, Short.MAX_VALUE)) + .addGroup(jPanel8Layout.createSequentialGroup() + .addComponent(jLabel23, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(spinnerHumidity))) + .addContainerGap()) + ); + + jPanel3.setBackground(new java.awt.Color(207, 233, 234)); + + jLabel7.setText("Data Push Interval:"); + + spinnerInterval.setModel(new javax.swing.SpinnerNumberModel(Integer.valueOf(5), Integer.valueOf(1), null, Integer.valueOf(1))); + spinnerInterval.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + spinnerIntervalStateChanged(evt); + } + }); + + jLabel8.setText("Seconds"); + + jLabel9.setText("Protocol:"); + + cmbProtocol.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "MQTT", "XMPP", "HTTP" })); + cmbProtocol.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cmbProtocolActionPerformed(evt); + } + }); + + jLabel12.setText("Interface:"); + + cmbInterface.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "eth0" })); + cmbInterface.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cmbInterfaceActionPerformed(evt); + } + }); + + javax.swing.GroupLayout jPanel3Layout = new javax.swing.GroupLayout(jPanel3); + jPanel3.setLayout(jPanel3Layout); + jPanel3Layout.setHorizontalGroup( + jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel3Layout.createSequentialGroup() + .addContainerGap() + .addComponent(jLabel7) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(spinnerInterval, javax.swing.GroupLayout.PREFERRED_SIZE, 55, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel8) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jLabel12) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cmbInterface, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel9) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cmbProtocol, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + ); + jPanel3Layout.setVerticalGroup( + jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel3Layout.createSequentialGroup() + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jLabel12) + .addComponent(cmbInterface, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(jPanel3Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jLabel7) + .addComponent(spinnerInterval, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel8) + .addComponent(jLabel9) + .addComponent(cmbProtocol, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addContainerGap()) + ); + + jPanel4.setBackground(new java.awt.Color(169, 253, 173)); + + chkbxEmulate.setText("Emulate data"); + chkbxEmulate.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chkbxEmulateActionPerformed(evt); + } + }); + + cmbPeriod.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "1 hour", "1 day", "1 week", "1 month " })); + cmbPeriod.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cmbPeriodActionPerformed(evt); + } + }); + + jLabel1.setText("Emulation Period"); + + javax.swing.GroupLayout jPanel4Layout = new javax.swing.GroupLayout(jPanel4); + jPanel4.setLayout(jPanel4Layout); + jPanel4Layout.setHorizontalGroup( + jPanel4Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel4Layout.createSequentialGroup() + .addContainerGap() + .addComponent(chkbxEmulate) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jLabel1) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cmbPeriod, javax.swing.GroupLayout.PREFERRED_SIZE, 162, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + ); + jPanel4Layout.setVerticalGroup( + jPanel4Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel4Layout.createSequentialGroup() + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(jPanel4Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(chkbxEmulate) + .addComponent(cmbPeriod, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel1)) + .addContainerGap()) + ); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(lblAgentName, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanel6, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jPanel8, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanel2, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))) + .addComponent(jLabel2, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanel4, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanel3, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(lblAgentName, javax.swing.GroupLayout.PREFERRED_SIZE, 53, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel6, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(jPanel2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel8, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanel4, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel2, javax.swing.GroupLayout.PREFERRED_SIZE, 28, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + ); + + pack(); + }// //GEN-END:initComponents + + private void btnControlMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_btnControlMouseClicked + // TODO add your handling code here: + }//GEN-LAST:event_btnControlMouseClicked + + private void btnViewMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_btnViewMouseClicked + // TODO add your handling code here: + }//GEN-LAST:event_btnViewMouseClicked + + private void chkbxTemperatureRandomActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chkbxTemperatureRandomActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_chkbxTemperatureRandomActionPerformed + + private void chkbxHumidityRandomActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chkbxHumidityRandomActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_chkbxHumidityRandomActionPerformed + + private void spinnerTemperatureStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_spinnerTemperatureStateChanged + // TODO add your handling code here: + }//GEN-LAST:event_spinnerTemperatureStateChanged + + private void spinnerHumidityStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_spinnerHumidityStateChanged + // TODO add your handling code here: + }//GEN-LAST:event_spinnerHumidityStateChanged + + private void txtTemperatureMinActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_txtTemperatureMinActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_txtTemperatureMinActionPerformed + + private void txtTemperatureMaxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_txtTemperatureMaxActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_txtTemperatureMaxActionPerformed + + private void txtHumidityMinActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_txtHumidityMinActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_txtHumidityMinActionPerformed + + private void txtHumidityMaxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_txtHumidityMaxActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_txtHumidityMaxActionPerformed + + private void spinnerIntervalStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_spinnerIntervalStateChanged + // TODO add your handling code here: + }//GEN-LAST:event_spinnerIntervalStateChanged + + private void cmbInterfaceActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cmbInterfaceActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_cmbInterfaceActionPerformed + + private void cmbProtocolActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cmbProtocolActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_cmbProtocolActionPerformed + + private void txtTemperatureSVFActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_txtTemperatureSVFActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_txtTemperatureSVFActionPerformed + + private void txtHumiditySVFActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_txtHumiditySVFActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_txtHumiditySVFActionPerformed + + private void chkbxTemperatureSmoothActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chkbxTemperatureSmoothActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_chkbxTemperatureSmoothActionPerformed + + private void chkbxHumiditySmoothActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chkbxHumiditySmoothActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_chkbxHumiditySmoothActionPerformed + + private void cmbPeriodActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cmbPeriodActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_cmbPeriodActionPerformed + + private void chkbxEmulateActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chkbxEmulateActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_chkbxEmulateActionPerformed + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton btnControl; + private javax.swing.JButton btnView; + private javax.swing.JCheckBox chkbxEmulate; + private javax.swing.JCheckBox chkbxHumidityRandom; + private javax.swing.JCheckBox chkbxHumiditySmooth; + private javax.swing.JCheckBox chkbxTemperatureRandom; + private javax.swing.JCheckBox chkbxTemperatureSmooth; + private javax.swing.JComboBox cmbInterface; + private javax.swing.JComboBox cmbPeriod; + private javax.swing.JComboBox cmbProtocol; + private javax.swing.JLabel jLabel1; + private javax.swing.JLabel jLabel10; + private javax.swing.JLabel jLabel11; + private javax.swing.JLabel jLabel12; + private javax.swing.JLabel jLabel2; + private javax.swing.JLabel jLabel20; + private javax.swing.JLabel jLabel23; + private javax.swing.JLabel jLabel24; + private javax.swing.JLabel jLabel25; + private javax.swing.JLabel jLabel3; + private javax.swing.JLabel jLabel4; + private javax.swing.JLabel jLabel5; + private javax.swing.JLabel jLabel6; + private javax.swing.JLabel jLabel7; + private javax.swing.JLabel jLabel8; + private javax.swing.JLabel jLabel9; + private javax.swing.JPanel jPanel1; + private javax.swing.JPanel jPanel2; + private javax.swing.JPanel jPanel3; + private javax.swing.JPanel jPanel4; + private javax.swing.JPanel jPanel6; + private javax.swing.JPanel jPanel7; + private javax.swing.JPanel jPanel8; + private javax.swing.JPanel jPanel9; + private javax.swing.JSeparator jSeparator1; + private javax.swing.JSeparator jSeparator5; + private javax.swing.JLabel lblAgentName; + private javax.swing.JLabel lblStatus; + private javax.swing.JPanel pnlBulbStatus; + private javax.swing.JSpinner spinnerHumidity; + private javax.swing.JSpinner spinnerInterval; + private javax.swing.JSpinner spinnerTemperature; + private javax.swing.JTextField txtHumidityMax; + private javax.swing.JTextField txtHumidityMin; + private javax.swing.JTextField txtHumiditySVF; + private javax.swing.JTextField txtTemperatureMax; + private javax.swing.JTextField txtTemperatureMin; + private javax.swing.JTextField txtTemperatureSVF; + // End of variables declaration//GEN-END:variables +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/build.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/build.xml new file mode 100644 index 000000000..a877b1141 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/build.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/pom.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/pom.xml new file mode 100644 index 000000000..da9f26ab7 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/pom.xml @@ -0,0 +1,92 @@ + + + + + + + + virtual-fire-alarm-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics + WSO2 Carbon - IoT Server Virtual Firealarm Analytics capp + pom + + + + + maven-clean-plugin + 2.4.1 + + + auto-clean + initialize + + clean + + + + + + maven-antrun-plugin + 1.7 + + + process-resources + + + + + + + run + + + + + + maven-assembly-plugin + 2.5.5 + + ${project.artifactId}-${carbon.device.mgt.version} + false + + src/assembly/src.xml + + + + + create-archive + package + + single + + + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/assembly/src.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/assembly/src.xml new file mode 100644 index 000000000..a5a375010 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/assembly/src.xml @@ -0,0 +1,36 @@ + + + + src + + zip + + false + ${basedir}/src + + + ${basedir}/target/carbonapps + / + true + + + \ No newline at end of file diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/EventReceiver_temperature.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/EventReceiver_temperature.xml new file mode 100644 index 000000000..28b710c27 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/EventReceiver_temperature.xml @@ -0,0 +1,26 @@ + + + + + + false + + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/artifact.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/artifact.xml new file mode 100644 index 000000000..25df56734 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventreceiver_temperature_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + EventReceiver_temperature.xml + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/artifact.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/artifact.xml new file mode 100644 index 000000000..ccfb3b314 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + org_wso2_iot_devices_temperature.xml + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/org_wso2_iot_devices_temperature.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/org_wso2_iot_devices_temperature.xml new file mode 100644 index 000000000..d06f73b14 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstore_temperature_1.0.0/org_wso2_iot_devices_temperature.xml @@ -0,0 +1,62 @@ + + + + + + org.wso2.iot.devices.temperature:1.0.0 + + EVENT_STORE + + + meta_owner + true + true + false + STRING + + + meta_deviceType + true + true + false + STRING + + + meta_deviceId + true + true + false + STRING + + + meta_time + true + true + false + LONG + + + temperature + false + false + false + FLOAT + + + \ No newline at end of file diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/artifact.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/artifact.xml new file mode 100644 index 000000000..27ec69702 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/artifact.xml @@ -0,0 +1,23 @@ + + + + + org.wso2.iot.devices.temperature_1.0.0.json + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/org.wso2.iot.devices.temperature_1.0.0.json b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/org.wso2.iot.devices.temperature_1.0.0.json new file mode 100644 index 000000000..5d94b9821 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Eventstream_temperature_1.0.0/org.wso2.iot.devices.temperature_1.0.0.json @@ -0,0 +1,20 @@ +{ + "name": "org.wso2.iot.devices.temperature", + "version": "1.0.0", + "nickName": "Temperature Data", + "description": "Temperature data received from the Device", + "metaData": [ + {"name":"owner","type":"STRING"}, + {"name":"deviceType","type":"STRING"}, + {"name":"deviceId","type":"STRING"}, + {"name":"time","type":"LONG"} + ], + "payloadData": [ + { + "name": "temperature","type": "FLOAT" + } + ] +} + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/Temperature_Sensor_Script.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/Temperature_Sensor_Script.xml new file mode 100644 index 000000000..41938dd4f --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/Temperature_Sensor_Script.xml @@ -0,0 +1,31 @@ + + + + + IoTServer_Sensor_Script + + 0 * * * * ? + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/artifact.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/artifact.xml new file mode 100644 index 000000000..9b4228e30 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/Sparkscripts_1.0.0/artifact.xml @@ -0,0 +1,22 @@ + + + + + Temperature_Sensor_Script.xml + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/artifacts.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/artifacts.xml new file mode 100644 index 000000000..c4580f909 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics/src/main/resources/carbonapps/Temperature_Sensor/artifacts.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/pom.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/pom.xml new file mode 100644 index 000000000..f9a2c1abc --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/pom.xml @@ -0,0 +1,262 @@ + + + + + + + virtual-fire-alarm-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl + war + WSO2 Carbon - IoT Server VirtualFireAlarm API + WSO2 Carbon - Virtual FireAlarm Service Controller API Implementation + http://wso2.org + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.core + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.analytics + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.certificate.mgt.core + provided + + + commons-codec.wso2 + commons-codec + + + + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + provided + + + org.apache.cxf + 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 + provided + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl + provided + + + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-jaxrs + + + javax + javaee-web-api + provided + + + javax.ws.rs + jsr311-api + provided + + + commons-httpclient.wso2 + commons-httpclient + provided + + + + org.wso2.carbon + org.wso2.carbon.utils + provided + + + org.bouncycastle.wso2 + bcprov-jdk15on + + + org.wso2.carbon + org.wso2.carbon.user.api + + + org.wso2.carbon + org.wso2.carbon.queuing + + + org.wso2.carbon + org.wso2.carbon.base + + + org.apache.axis2.wso2 + axis2 + + + org.igniterealtime.smack.wso2 + smack + + + org.igniterealtime.smack.wso2 + smackx + + + jaxen + jaxen + + + commons-fileupload.wso2 + commons-fileupload + + + org.apache.ant.wso2 + ant + + + org.apache.ant.wso2 + ant + + + commons-httpclient.wso2 + commons-httpclient + + + org.eclipse.equinox + javax.servlet + + + org.wso2.carbon + org.wso2.carbon.registry.api + + + + + + commons-codec.wso2 + commons-codec + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.annotations + provided + + + + org.igniterealtime.smack.wso2 + smack + provided + + + org.igniterealtime.smack.wso2 + smackx + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.extensions + provided + + + + + + + + + maven-compiler-plugin + + UTF-8 + ${wso2.maven.compiler.source} + ${wso2.maven.compiler.target} + + + + maven-war-plugin + + virtual_firealarm + + + + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/VirtualFireAlarmControllerService.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/VirtualFireAlarmControllerService.java new file mode 100644 index 000000000..1318caf7f --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/VirtualFireAlarmControllerService.java @@ -0,0 +1,614 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.controller.service.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.apimgt.annotations.api.API; +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.common.AnalyticsDataRecord; +import org.wso2.carbon.device.mgt.analytics.exception.DeviceManagementAnalyticsException; +import org.wso2.carbon.device.mgt.analytics.service.DeviceAnalyticsService; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.extensions.feature.mgt.annotations.DeviceType; +import org.wso2.carbon.device.mgt.extensions.feature.mgt.annotations.Feature; +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.controller.service.impl.dto.DeviceData; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl.dto.SensorData; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl.exception.VirtualFireAlarmException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl.transport.VirtualFireAlarmMQTTConnector; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl.transport.VirtualFireAlarmXMPPConnector; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl.util.SecurityManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl.util.VirtualFireAlarmServiceUtils; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl.util.scep.ContentType; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl.util.scep.SCEPOperation; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.constants.VirtualFireAlarmConstants; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +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; + +/** + * This class consists the functions/APIs specific to the "actions" of the VirtualFirealarm device-type. These APIs + * include the ones that are used by the [Device] to contact the server (i.e: Enrollment & Publishing Data) and the + * ones used by the [Server/Owner] to contact the [Device] (i.e: sending control signals). This class also initializes + * the transport 'Connectors' [XMPP & MQTT] specific to the VirtualFirealarm device-type in order to communicate with + * such devices and to receive messages form it. + */ +@API(name = "virtual_firealarm", version = "1.0.0", context = "/virtual_firealarm", tags = "virtual_firealarm") +@DeviceType(value = "virtual_firealarm") +@SuppressWarnings("Non-Annoted WebService") +public class VirtualFireAlarmControllerService { + + private static final String XMPP_PROTOCOL = "XMPP"; + private static final String HTTP_PROTOCOL = "HTTP"; + private static final String MQTT_PROTOCOL = "MQTT"; + private static Log log = LogFactory.getLog(VirtualFireAlarmControllerService.class); + @Context //injected response proxy supporting multiple thread + private HttpServletResponse response; + // consists of utility methods related to encrypting and decrypting messages + private SecurityManager securityManager; + // connects to the given MQTT broker and handles MQTT communication + private VirtualFireAlarmMQTTConnector virtualFireAlarmMQTTConnector; + // connects to the given XMPP server and handles XMPP communication + private VirtualFireAlarmXMPPConnector virtualFireAlarmXMPPConnector; + // holds a mapping of the IP addresses to Device-IDs for HTTP communication + private ConcurrentHashMap deviceToIpMap = new ConcurrentHashMap<>(); + + private boolean waitForServerStartup() { + while (!IoTServerStartupListener.isServerReady()) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + return true; + } + } + return false; + } + + /** + * Fetches the `SecurityManager` specific to this VirtualFirealarm controller service. + * + * @return the 'SecurityManager' instance bound to the 'securityManager' variable of this service. + */ + @SuppressWarnings("Unused") + public SecurityManager getSecurityManager() { + return securityManager; + } + + /** + * Sets the `securityManager` variable of this VirtualFirealarm controller service. + * + * @param securityManager a 'SecurityManager' object that handles the encryption, decryption, signing and validation + * of incoming messages from VirtualFirealarm device-types. + */ + @SuppressWarnings("Unused") + public void setSecurityManager(SecurityManager securityManager) { + this.securityManager = securityManager; + securityManager.initVerificationManager(); + } + + /** + * Fetches the `VirtualFireAlarmXMPPConnector` specific to this VirtualFirealarm controller service. + * + * @return the 'VirtualFireAlarmXMPPConnector' instance bound to the 'virtualFireAlarmXMPPConnector' variable of + * this service. + */ + @SuppressWarnings("Unused") + public VirtualFireAlarmXMPPConnector getVirtualFireAlarmXMPPConnector() { + return virtualFireAlarmXMPPConnector; + } + + /** + * Sets the `virtualFireAlarmXMPPConnector` variable of this VirtualFirealarm controller service. + * + * @param virtualFireAlarmXMPPConnector a 'VirtualFireAlarmXMPPConnector' object that handles all XMPP related + * communications of any connected VirtualFirealarm device-type + */ + @SuppressWarnings("Unused") + public void setVirtualFireAlarmXMPPConnector( + final VirtualFireAlarmXMPPConnector virtualFireAlarmXMPPConnector) { + Runnable connector = new Runnable() { + public void run() { + if (waitForServerStartup()) { + return; + } + VirtualFireAlarmControllerService.this.virtualFireAlarmXMPPConnector = virtualFireAlarmXMPPConnector; + + if (XmppConfig.getInstance().isEnabled()) { + Runnable xmppStarter = new Runnable() { + @Override + public void run() { + virtualFireAlarmXMPPConnector.initConnector(); + virtualFireAlarmXMPPConnector.connect(); + } + }; + + Thread xmppStarterThread = new Thread(xmppStarter); + xmppStarterThread.setDaemon(true); + xmppStarterThread.start(); + } else { + log.warn("XMPP disabled in 'devicemgt-config.xml'. Hence, VirtualFireAlarmXMPPConnector not started."); + } + } + }; + Thread connectorThread = new Thread(connector); + connectorThread.setDaemon(true); + connectorThread.start(); + } + + /** + * Fetches the `VirtualFireAlarmMQTTConnector` specific to this VirtualFirealarm controller service. + * + * @return the 'VirtualFireAlarmMQTTConnector' instance bound to the 'virtualFireAlarmMQTTConnector' variable of + * this service. + */ + @SuppressWarnings("Unused") + public VirtualFireAlarmMQTTConnector getVirtualFireAlarmMQTTConnector() { + return virtualFireAlarmMQTTConnector; + } + + /** + * Sets the `virtualFireAlarmMQTTConnector` variable of this VirtualFirealarm controller service. + * + * @param virtualFireAlarmMQTTConnector a 'VirtualFireAlarmMQTTConnector' object that handles all MQTT related + * communications of any connected VirtualFirealarm device-type + */ + @SuppressWarnings("Unused") + public void setVirtualFireAlarmMQTTConnector( + final VirtualFireAlarmMQTTConnector virtualFireAlarmMQTTConnector) { + Runnable connector = new Runnable() { + public void run() { + if (waitForServerStartup()) { + return; + } + VirtualFireAlarmControllerService.this.virtualFireAlarmMQTTConnector = virtualFireAlarmMQTTConnector; + if (MqttConfig.getInstance().isEnabled()) { + virtualFireAlarmMQTTConnector.connect(); + } else { + log.warn("MQTT disabled in 'devicemgt-config.xml'. Hence, VirtualFireAlarmMQTTConnector not started."); + } + } + }; + Thread connectorThread = new Thread(connector); + connectorThread.setDaemon(true); + connectorThread.start(); + } + + /** + * This is an API used/called by the device. It registers the IP of a VirtualFirealarm device against its DeviceID + * when the device connects with the server for the first time. This DeviceID to IP mapping is necessary only for + * cases where HTTP communication is to be used. At such instances, this mapping is used by the server to + * identify the IP of the device to which it has some message to be sent. This method becomes useful only in + * scenarios where HTTP communication is used in a setup where the IoT-Server and the devices communicating with it + * are in the same IP network. + * + * @param deviceId the ID of the VirtualFirealarm device from which this register-IP call was initiated. + * @param deviceIP the IP of the VirtualFirealarm device which has sent this register-IP request. + * @param devicePort the PORT on the VirtualFirealarm device (on this IP) that's open for HTTP communication. + * @param request the HTTP servlet request object received by default as part of the HTTP call to this API. + * @param response the HTTP servlet response object received by default as part of the HTTP call to this API. + * @return a custom message indicating whether the DeviceID to IP mapping was successful. + */ + @POST + @Path("controller/register/{deviceId}/{ip}/{port}") + public String registerDeviceIP(@PathParam("deviceId") String deviceId, @PathParam("ip") String deviceIP, + @PathParam("port") String devicePort, @Context HttpServletRequest request, + @Context HttpServletResponse response) { + try { + String result; + if (log.isDebugEnabled()) { + log.debug("Got register call from IP: " + deviceIP + " for Device ID: " + deviceId); + } + String deviceHttpEndpoint = deviceIP + ":" + devicePort; + deviceToIpMap.put(deviceId, deviceHttpEndpoint); + result = "Device-IP Registered"; + response.setStatus(Response.Status.OK.getStatusCode()); + if (log.isDebugEnabled()) { + log.debug(result); + } + return result; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + /** + * This is an API called/used from within the Server(Front-End) or by a device Owner. It sends a control command to + * the VirtualFirealarm device to switch `ON` or `OFF` its buzzer. The method also takes in the protocol to be used + * to connect-to and send the command to the device. + * + * @param deviceId the ID of the VirtualFirealarm device on which the buzzer needs to switched `ON` or `OFF`. + * @param protocol the protocol (HTTP, MQTT, XMPP) to be used to connect-to & send the message to the device. + * @param state the state to which the buzzer on the device needs to be changed. Either "ON" or "OFF". + * (Case-Insensitive String) + * @param response the HTTP servlet response object received by default as part of the HTTP call to this API. + */ + @POST + @Path("controller/device/{deviceId}/buzz/") + @Feature(code = "buzz", name = "Buzzer On / Off", type = "operation", + description = "Switch on/off Virtual Fire Alarm Buzzer. (On / Off)") + public void switchBuzzer(@PathParam("deviceId") String deviceId, @QueryParam("protocol") String protocol, + @FormParam("state") String state, @Context HttpServletResponse response) { + String switchToState = state.toUpperCase(); + if (!switchToState.equals(VirtualFireAlarmConstants.STATE_ON) && !switchToState.equals( + VirtualFireAlarmConstants.STATE_OFF)) { + log.error("The requested state change shoud be either - 'ON' or 'OFF'"); + response.setStatus(Response.Status.BAD_REQUEST.getStatusCode()); + return; + } + String protocolString = protocol.toUpperCase(); + String callUrlPattern = VirtualFireAlarmConstants.BULB_CONTEXT + switchToState; + if (log.isDebugEnabled()) { + log.debug("Sending request to switch-bulb of device [" + deviceId + "] via " + + protocolString); + } + try { + switch (protocolString) { + case HTTP_PROTOCOL: + String deviceHTTPEndpoint = deviceToIpMap.get(deviceId); + if (deviceHTTPEndpoint == null) { + response.setStatus(Response.Status.PRECONDITION_FAILED.getStatusCode()); + return; + } + VirtualFireAlarmServiceUtils.sendCommandViaHTTP(deviceHTTPEndpoint, callUrlPattern, true); + break; + case XMPP_PROTOCOL: + String xmppResource = VirtualFireAlarmConstants.BULB_CONTEXT.replace("/", ""); + virtualFireAlarmXMPPConnector.publishDeviceData(deviceId, xmppResource, switchToState); + break; + default: + String mqttResource = VirtualFireAlarmConstants.BULB_CONTEXT.replace("/", ""); + virtualFireAlarmMQTTConnector.publishDeviceData(deviceId, mqttResource, switchToState); + break; + } + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceManagementException | TransportHandlerException e) { + log.error("Failed to send switch-bulb request to device [" + deviceId + "] via " + protocolString); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + /** + * This is an API called/used from within the Server(Front-End) or by a device Owner. It sends a control command + * to the VirtualFirealarm device to 'tell what's its current temperature reading'. The method also takes in the + * protocol to be used to connect-to and send the command to the device. + * + * @param deviceId the ID of the VirtualFirealarm device on which the temperature reading is be read-from. + * @param protocol the protocol (HTTP, MQTT, XMPP) to be used to connect-to & send the message to the device. + * @param response the HTTP servlet response object received by default as part of the HTTP call to this API. + * @return an instance of the 'SensorRecord' object that holds the last updated temperature of the VirtualFirealarm + * whose temperature reading was requested. + */ + @GET + @Path("controller/device/{deviceId}/temperature") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Feature(code = "temperature", name = "Temperature", type = "monitor", + description = "Request Temperature reading from Virtual Fire Alarm") + public SensorRecord requestTemperature(@PathParam("deviceId") String deviceId, + @QueryParam("protocol") String protocol, + @Context HttpServletResponse response) { + SensorRecord sensorRecord = null; + String protocolString = protocol.toUpperCase(); + if (log.isDebugEnabled()) { + log.debug("Sending request to read virtual-firealarm-temperature of device " + + "[" + deviceId + "] via " + protocolString); + } + try { + switch (protocolString) { + case HTTP_PROTOCOL: + String deviceHTTPEndpoint = deviceToIpMap.get(deviceId); + if (deviceHTTPEndpoint == null) { + response.setStatus(Response.Status.PRECONDITION_FAILED.getStatusCode()); + } + String temperatureValue = VirtualFireAlarmServiceUtils.sendCommandViaHTTP( + deviceHTTPEndpoint, VirtualFireAlarmConstants.TEMPERATURE_CONTEXT, false); + SensorDataManager.getInstance().setSensorRecord(deviceId, VirtualFireAlarmConstants.SENSOR_TEMP, + temperatureValue, + Calendar.getInstance().getTimeInMillis()); + break; + case XMPP_PROTOCOL: + String xmppResource = VirtualFireAlarmConstants.TEMPERATURE_CONTEXT.replace("/", ""); + virtualFireAlarmMQTTConnector.publishDeviceData(deviceId, xmppResource, ""); + break; + default: + String mqttResource = VirtualFireAlarmConstants.TEMPERATURE_CONTEXT.replace("/", ""); + virtualFireAlarmMQTTConnector.publishDeviceData(deviceId, mqttResource, ""); + break; + } + sensorRecord = SensorDataManager.getInstance().getSensorRecord(deviceId, VirtualFireAlarmConstants + .SENSOR_TEMP); + response.setStatus(Response.Status.OK.getStatusCode()); + } catch (DeviceManagementException | DeviceControllerException | TransportHandlerException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + return sensorRecord; + } + + /** + * This is an API called/used by the VirtualFirealarm device to publish its temperature to the IoT-Server. The + * received data from the device is stored in a 'DeviceRecord' under the device's ID in the 'SensorDataManager' + * of the Server. + * + * @param dataMsg the temperature data received from the device in JSON format complying to type 'DeviceData'. + * @param response the HTTP servlet response object received by default as part of the HTTP call to this API. + */ + @POST + @Path("controller/temperature") + @Consumes(MediaType.APPLICATION_JSON) + public void pushTemperatureData(final DeviceData dataMsg, @Context HttpServletResponse response) { + try { + String deviceId = dataMsg.deviceId; + String deviceIp = dataMsg.reply; + float temperature = dataMsg.value; + String registeredIp = deviceToIpMap.get(deviceId); + if (registeredIp == null) { + log.warn("Unregistered IP: Temperature Data Received from an un-registered IP " + + deviceIp + " for device ID - " + deviceId); + response.setStatus(Response.Status.PRECONDITION_FAILED.getStatusCode()); + return; + } else if (!registeredIp.equals(deviceIp)) { + log.warn("Conflicting IP: Received IP is " + deviceIp + ". Device with ID " + deviceId + + " is already registered under some other IP. Re-registration required"); + response.setStatus(Response.Status.CONFLICT.getStatusCode()); + return; + } + SensorDataManager.getInstance().setSensorRecord(deviceId, VirtualFireAlarmConstants.SENSOR_TEMP, + String.valueOf(temperature), + Calendar.getInstance().getTimeInMillis()); + if (!VirtualFireAlarmServiceUtils.publishToDAS(dataMsg.deviceId, dataMsg.value)) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + /** + * This is an API called/used by the SCEP Client of the VirtualFirealarm device in its SCEP enrollment process. + * This acts as the endpoint exposed as part of the SCEP-Server for use by a SCEP Client. This is one of the two + * method-signatures that takes different parameters according to the SCEP-Operation executed by the SCEP-Client + * of the enrolling device. The API supports 2 SCEP Operations [GetCACert] and [GetCACaps]. + *

      + * Operation [GetCACert] returns the CA cert of the SCEP-Server for the device to verify its authenticity. + * Operation [GetCACaps] returns the CA Capabilities of the SCEP-Server. + * + * @param operation the SCEP operation requested by the client. [GetCACert] or [GetCACaps] + * @param message any messages pertaining to the requested SCEP Operation. + * @return an HTTP Response object with either the CA-Cert or the CA-Capabilities according to the operation. + */ + @GET + @Path("controller/scep") + public Response scepRequest(@QueryParam("operation") String operation, @QueryParam("message") String message) { + try { + if (log.isDebugEnabled()) { + log.debug("Invoking SCEP operation " + operation); + } + if (SCEPOperation.GET_CA_CERT.getValue().equals(operation)) { + if (log.isDebugEnabled()) { + log.debug("Invoking GetCACert"); + } + try { + CertificateManagementService certificateManagementService = + VirtualFireAlarmServiceUtils.getCertificateManagementService(); + SCEPResponse scepResponse = certificateManagementService.getCACertSCEP(); + Response.ResponseBuilder responseBuilder; + switch (scepResponse.getResultCriteria()) { + case CA_CERT_FAILED: + log.error("CA cert failed"); + responseBuilder = Response.serverError(); + break; + case CA_CERT_RECEIVED: + if (log.isDebugEnabled()) { + log.debug("CA certificate received in GetCACert"); + } + responseBuilder = Response.ok(scepResponse.getEncodedResponse(), + ContentType.X_X509_CA_CERT); + break; + case CA_RA_CERT_RECEIVED: + if (log.isDebugEnabled()) { + log.debug("CA and RA certificates received in GetCACert"); + } + responseBuilder = Response.ok(scepResponse.getEncodedResponse(), + ContentType.X_X509_CA_RA_CERT); + break; + default: + log.error("Invalid SCEP request"); + responseBuilder = Response.serverError(); + break; + } + + return responseBuilder.build(); + } catch (VirtualFireAlarmException e) { + log.error("Error occurred while enrolling the VirtualFireAlarm device", e); + } catch (KeystoreException e) { + log.error("Keystore error occurred while enrolling the VirtualFireAlarm device", e); + } + + } else if (SCEPOperation.GET_CA_CAPS.getValue().equals(operation)) { + + if (log.isDebugEnabled()) { + log.debug("Invoking GetCACaps"); + } + + try { + CertificateManagementService certificateManagementService = VirtualFireAlarmServiceUtils. + getCertificateManagementService(); + byte caCaps[] = certificateManagementService.getCACapsSCEP(); + + return Response.ok(caCaps, MediaType.TEXT_PLAIN).build(); + + } catch (VirtualFireAlarmException e) { + log.error("Error occurred while enrolling the device", e); + } + + } else { + log.error("Invalid SCEP operation " + operation); + } + + return Response.serverError().build(); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + /** + * This is an API called/used by the SCEP Client of the VirtualFirealarm device in its SCEP enrollment process. + * This acts as the endpoint exposed as part of the SCEP-Server for use by a SCEP Client. This is one of the two + * method-signatures that takes different parameters according to the SCEP-Operation executed by the SCEP-Client + * of the enrolling device. This API supports the SCEP Operation [PKIOperation]. + *

      + * Operation [PKIOperation] returns a certificate generated by the SCEP-Server for the enrolling device. + * + * @param operation the final SCEP operation executed in the enrollment process - which is [PKIOperation] + * @param inputStream an input stream consisting of the Certificate-Signing-Request (CSR) from the device. + * @return an HTTP Response object with the signed certificate for the device by the CA of the SCEP Server. + */ + @POST + @Path("controller/scep") + public Response scepRequestPost(@QueryParam("operation") String operation, InputStream inputStream) { + try { + if (log.isDebugEnabled()) { + log.debug("Invoking SCEP operation " + operation); + } + if (SCEPOperation.PKI_OPERATION.getValue().equals(operation)) { + if (log.isDebugEnabled()) { + log.debug("Invoking PKIOperation"); + } + try { + CertificateManagementService certificateManagementService = VirtualFireAlarmServiceUtils. + getCertificateManagementService(); + byte pkiMessage[] = certificateManagementService.getPKIMessageSCEP(inputStream); + return Response.ok(pkiMessage, ContentType.X_PKI_MESSAGE).build(); + } catch (VirtualFireAlarmException e) { + log.error("Error occurred while enrolling the device", e); + } catch (KeystoreException e) { + log.error("Keystore error occurred while enrolling the device", e); + } + } + return Response.serverError().build(); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + /** + * Retreive Sensor data for the device type + */ + @Path("controller/stats/device/{deviceId}/sensors/{sensorName}") + @GET + @Consumes("application/json") + @Produces("application/json") + public SensorData[] getVirtualFirealarmStats(@PathParam("deviceId") String deviceId, @PathParam("sensorName") String sensor, + @QueryParam("username") String user, @QueryParam("from") long from, + @QueryParam("to") long to) { + 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); + } + return sensorDatas.toArray(new SensorData[sensorDatas.size()]); + } + } catch (DeviceManagementAnalyticsException e) { + String errorMsg = "Error on retrieving stats on table for sensor name" + sensor; + log.error(errorMsg); + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + return new SensorData[0]; + } + + /** + * get the event table from the sensor name. + */ + private String getSensorEventTableName(String sensorName){ + String sensorEventTableName; + switch (sensorName) { + case VirtualFireAlarmConstants.SENSOR_TEMP: + sensorEventTableName = VirtualFireAlarmConstants.TEMPERATURE_EVENT_TABLE; + break; + case VirtualFireAlarmConstants.SENSOR_HUMIDITY: + sensorEventTableName = VirtualFireAlarmConstants.HUMIDITY_EVENT_TABLE; + break; + default: + sensorEventTableName = null; + } + return sensorEventTableName; + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/dto/DeviceData.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/dto/DeviceData.java new file mode 100644 index 000000000..a6dde796b --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/dto/DeviceData.java @@ -0,0 +1,36 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.controller.service.impl.dto; + +import org.codehaus.jackson.annotate.JsonIgnoreProperties; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement + +@JsonIgnoreProperties(ignoreUnknown = true) +public class DeviceData { + @XmlElement(required = true) public String owner; + @XmlElement(required = true) public String deviceId; + @XmlElement(required = true) public String reply; + @XmlElement public Long time; + @XmlElement public String key; + @XmlElement public float value; +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/dto/SensorData.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/dto/SensorData.java new file mode 100644 index 000000000..a4792fca7 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/dto/SensorData.java @@ -0,0 +1,44 @@ +package org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.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.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/exception/VirtualFireAlarmException.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/exception/VirtualFireAlarmException.java new file mode 100644 index 000000000..4dd6faacb --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/exception/VirtualFireAlarmException.java @@ -0,0 +1,31 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.controller.service.impl.exception; + +public class VirtualFireAlarmException extends Exception { + private static final long serialVersionUID = 118512086957330189L; + + public VirtualFireAlarmException(String errorMessage) { + super(errorMessage); + } + + public VirtualFireAlarmException(String errorMessage, Throwable throwable) { + super(errorMessage, throwable); + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/transport/VirtualFireAlarmMQTTConnector.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/transport/VirtualFireAlarmMQTTConnector.java new file mode 100644 index 000000000..05c0a459e --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/transport/VirtualFireAlarmMQTTConnector.java @@ -0,0 +1,309 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.controller.service.impl.transport; + +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.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.config.server.DeviceManagementConfigurationManager; +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.device.mgt.iot.virtualfirealarm.controller.service.impl.exception.VirtualFireAlarmException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl.util.SecurityManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl.util.VirtualFireAlarmServiceUtils; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.constants.VirtualFireAlarmConstants; +import org.wso2.carbon.utils.multitenancy.MultitenantUtils; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Calendar; +import java.util.UUID; + +/** + * 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 VirtualFirealarm 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 (VirtualFirealarm) 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. + */ +@SuppressWarnings("no JAX-WS annotation") +public class VirtualFireAlarmMQTTConnector extends MQTTTransportHandler { + + private static Log log = LogFactory.getLog(VirtualFireAlarmMQTTConnector.class); + // subscription topic: /+/virtual_firealarm/+/publisher + // 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); + + /** + * Default constructor for the VirtualFirealarmMQTTConnector. + */ + private VirtualFireAlarmMQTTConnector() { + super(iotServerSubscriber, VirtualFireAlarmConstants.DEVICE_TYPE, + MqttConfig.getInstance().getMqttQueueEndpoint(), subscribeTopic); + } + + /** + * {@inheritDoc} + * VirtualFirealarm 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 { + String brokerUsername = MqttConfig.getInstance().getMqttQueueUsername(); + String brokerPassword = MqttConfig.getInstance().getMqttQueuePassword(); + setUsernameAndPassword(brokerUsername, brokerPassword); + 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); + } + } + } + }; + + Thread connectorThread = new Thread(connector); + connectorThread.setDaemon(true); + connectorThread.start(); + } + + /** + * {@inheritDoc} + * VirtualFirealarm 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/"publisher"] + String topic = messageParams[0]; + String[] topicParams = topic.split("/"); + String deviceId = topicParams[2]; + if (log.isDebugEnabled()) { + log.debug("Received MQTT message for: [DEVICE.ID-" + deviceId + "]"); + } + + String actualMessage; + try { + // the hash-code of the deviceId is used as the alias for device certificates during SCEP enrollment. + // hence, the same is used here to fetch the device-specific-certificate from the key store. + PublicKey clientPublicKey = VirtualFireAlarmServiceUtils.getDevicePublicKey(deviceId); + PrivateKey serverPrivateKey = SecurityManager.getServerPrivateKey(); + + // the MQTT-messages from VirtualFireAlarm devices are in the form {"Msg":, "Sig":} + actualMessage = VirtualFireAlarmServiceUtils.extractMessageFromPayload(mqttMessage.toString(), + serverPrivateKey, + clientPublicKey); + if (log.isDebugEnabled()) { + log.debug("MQTT: Received Message [" + actualMessage + "] topic: [" + topic + "]"); + } + + if (actualMessage.contains("PUBLISHER")) { + float temperature = Float.parseFloat(actualMessage.split(":")[2]); + try { + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + DeviceManagementProviderService deviceManagementProviderService = + (DeviceManagementProviderService) ctx + .getOSGiService(DeviceManagementProviderService.class, null); + if (deviceManagementProviderService != null) { + DeviceIdentifier identifier = new DeviceIdentifier(deviceId, + VirtualFireAlarmConstants.DEVICE_TYPE); + Device device = deviceManagementProviderService.getDevice(identifier); + if (device != null) { + String owner = device.getEnrolmentInfo().getOwner(); + ctx.setTenantDomain(MultitenantUtils.getTenantDomain(owner), true); + ctx.setUsername(owner); + if (!VirtualFireAlarmServiceUtils.publishToDAS(deviceId, temperature)) { + log.error("MQTT Subscriber: Publishing data to DAS failed."); + } + } + } + } catch (DeviceManagementException e) { + log.error("Failed to retreive the device managment service for device type " + + VirtualFireAlarmConstants.DEVICE_TYPE, e); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + if (log.isDebugEnabled()) { + log.debug("MQTT Subscriber: Published data to DAS successfully."); + } + + } else if (actualMessage.contains("TEMPERATURE")) { + String temperatureValue = actualMessage.split(":")[1]; + SensorDataManager.getInstance().setSensorRecord(deviceId, VirtualFireAlarmConstants.SENSOR_TEMP, + temperatureValue, + Calendar.getInstance().getTimeInMillis()); + } + } catch (VirtualFireAlarmException e) { + String errorMsg = + "CertificateManagementService failure oo Signature-Verification/Decryption was unsuccessful."; + log.error(errorMsg, e); + } + } else { + String errorMsg = + "MQTT message [" + mqttMessage.toString() + "] was received without the topic information."; + log.warn(errorMsg); + } + } + + /** + * {@inheritDoc} + * VirtualFirealarm 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 != 4) { + String errorMsg = "Incorrect number of arguments received to SEND-MQTT Message. " + + "Need to be [owner, deviceId, resource{BULB/TEMP}, state{ON/OFF or null}]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg); + } + + String deviceOwner = publishData[0]; + String deviceId = publishData[1]; + String resource = publishData[2]; + String state = publishData[3]; + + MqttMessage pushMessage = new MqttMessage(); + String publishTopic = "wso2/" + VirtualFireAlarmConstants.DEVICE_TYPE + "/" + deviceId; + + try { + PublicKey devicePublicKey = VirtualFireAlarmServiceUtils.getDevicePublicKey(deviceId); + PrivateKey serverPrivateKey = SecurityManager.getServerPrivateKey(); + + String actualMessage = resource + ":" + state; + String encryptedMsg = VirtualFireAlarmServiceUtils.prepareSecurePayLoad(actualMessage, + devicePublicKey, + serverPrivateKey); + + pushMessage.setPayload(encryptedMsg.getBytes(StandardCharsets.UTF_8)); + pushMessage.setQos(DEFAULT_MQTT_QUALITY_OF_SERVICE); + pushMessage.setRetained(false); + + publishToQueue(publishTopic, pushMessage); + + } catch (VirtualFireAlarmException e) { + String errorMsg = "Preparing Secure payload failed for device - [" + deviceId + "] of owner - " + + "[" + deviceOwner + "]."; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } + } + + + /** + * {@inheritDoc} + * VirtualFirealarm 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) { + if (log.isDebugEnabled()) { + log.warn("Unable to 'STOP' MQTT connection at broker at: " + mqttBrokerEndPoint + + " for device-type - " + VirtualFireAlarmConstants.DEVICE_TYPE, e); + } + + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException e1) { + log.error("MQTT-Terminator: Thread Sleep Interrupt Exception at device-type - " + + VirtualFireAlarmConstants.DEVICE_TYPE, e1); + } + } + } + } + }; + + Thread terminatorThread = new Thread(stopConnection); + terminatorThread.setDaemon(true); + terminatorThread.start(); + } + + + /** + * {@inheritDoc} + */ + @Override + public void publishDeviceData() { + // nothing to do + } + + /** + * {@inheritDoc} + */ + @Override + public void publishDeviceData(MqttMessage publishData) throws TransportHandlerException { + // nothing to do + } + + /** + * {@inheritDoc} + */ + @Override + public void processIncomingMessage() { + // nothing to do + } + + /** + * {@inheritDoc} + */ + @Override + public void processIncomingMessage(MqttMessage message) throws TransportHandlerException { + // nothing to do + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/transport/VirtualFireAlarmXMPPConnector.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/transport/VirtualFireAlarmXMPPConnector.java new file mode 100644 index 000000000..00697c81a --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/transport/VirtualFireAlarmXMPPConnector.java @@ -0,0 +1,293 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.controller.service.impl.transport; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jivesoftware.smack.packet.Message; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.device.mgt.iot.config.server.DeviceManagementConfigurationManager; +import org.wso2.carbon.device.mgt.iot.controlqueue.xmpp.XmppAccount; +import org.wso2.carbon.device.mgt.iot.controlqueue.xmpp.XmppConfig; +import org.wso2.carbon.device.mgt.iot.controlqueue.xmpp.XmppServerClient; +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.transport.TransportHandlerException; +import org.wso2.carbon.device.mgt.iot.transport.xmpp.XMPPTransportHandler; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.constants.VirtualFireAlarmConstants; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl.exception.VirtualFireAlarmException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl.util.SecurityManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl.util.VirtualFireAlarmServiceUtils; +import org.wso2.carbon.utils.multitenancy.MultitenantUtils; + +import java.io.File; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.Calendar; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +@SuppressWarnings("no JAX-WS annotation") +public class VirtualFireAlarmXMPPConnector extends XMPPTransportHandler { + private static Log log = LogFactory.getLog(VirtualFireAlarmXMPPConnector.class); + + private static String xmppServerIP; + private static String xmppVFireAlarmAdminUsername; + private static String xmppVFireAlarmAdminAccountJID; + private static final String V_FIREALARM_XMPP_PASSWORD = "vfirealarm@123"; + private static final String DEVICEMGT_CONFIG_FILE = "devicemgt-config.xml"; + + private ScheduledFuture connectorServiceHandler; + private ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(); + + private VirtualFireAlarmXMPPConnector() { + super(XmppConfig.getInstance().getXmppServerIP(), XmppConfig.getInstance().getSERVER_CONNECTION_PORT()); + } + + public void initConnector() { + xmppVFireAlarmAdminUsername = "wso2admin_" + VirtualFireAlarmConstants.DEVICE_TYPE; + xmppServerIP = XmppConfig.getInstance().getXmppServerIP(); + xmppVFireAlarmAdminAccountJID = xmppVFireAlarmAdminUsername + "@" + xmppServerIP; + createXMPPAccountForDeviceType(); + } + + public void createXMPPAccountForDeviceType() { + boolean accountExists; + XmppServerClient xmppServerClient = new XmppServerClient(); + xmppServerClient.initControlQueue(); + + try { + accountExists = xmppServerClient.doesXMPPUserAccountExist(xmppVFireAlarmAdminUsername); + + if (!accountExists) { + XmppAccount xmppAccount = new XmppAccount(); + + xmppAccount.setAccountName(xmppVFireAlarmAdminUsername); + xmppAccount.setUsername(xmppVFireAlarmAdminUsername); + xmppAccount.setPassword(V_FIREALARM_XMPP_PASSWORD); + xmppAccount.setEmail(""); + + try { + boolean xmppCreated = xmppServerClient.createXMPPAccount(xmppAccount); + if (!xmppCreated) { + log.warn("Server XMPP Account was not created for device-type - " + + VirtualFireAlarmConstants.DEVICE_TYPE + + ". Check whether XMPP is enabled in \"devicemgt-config.xml\" & restart."); + } else { + log.info("Server XMPP Account [" + xmppVFireAlarmAdminUsername + + "] was not created for device - " + VirtualFireAlarmConstants.DEVICE_TYPE); + } + } catch (DeviceControllerException e) { + String errorMsg = + "An error was encountered whilst trying to create Server XMPP account for device-type - " + + VirtualFireAlarmConstants.DEVICE_TYPE; + log.error(errorMsg, e); + } + } + + } catch (DeviceControllerException e) { + if (e.getMessage().contains(DEVICEMGT_CONFIG_FILE)) { + log.warn("XMPP not Enabled"); + } else { + String errorMsg = "An error was encountered whilst trying to check whether Server XMPP account " + + "exists for device-type - " + VirtualFireAlarmConstants.DEVICE_TYPE ; + log.error(errorMsg, e); + } + } + } + + + @Override + public void connect() { + Runnable connector = new Runnable() { + public void run() { + if (!isConnected()) { + try { + connectToServer(); + loginToServer(xmppVFireAlarmAdminUsername, V_FIREALARM_XMPP_PASSWORD, null); + setFilterOnReceiver(xmppVFireAlarmAdminAccountJID); + + } catch (TransportHandlerException e) { + if (log.isDebugEnabled()) { + log.warn("Connection/Login to XMPP server at: " + server + " as " + + xmppVFireAlarmAdminUsername + " failed for device-type [" + + VirtualFireAlarmConstants.DEVICE_TYPE + "].", e); + } + } + } + } + }; + + connectorServiceHandler = service.scheduleAtFixedRate(connector, 0, timeoutInterval, TimeUnit.MILLISECONDS); + } + + @Override + public void processIncomingMessage(Message xmppMessage) throws TransportHandlerException { + String from = xmppMessage.getFrom(); + String subject = xmppMessage.getSubject(); + String message = xmppMessage.getBody(); + + int indexOfAt = from.indexOf("@"); + int indexOfSlash = from.indexOf("/"); + + if (indexOfAt != -1 && indexOfSlash != -1) { + String deviceId = from.substring(0, indexOfAt); + String owner = from.substring(indexOfSlash + 1, from.length()); + + if (log.isDebugEnabled()) { + log.debug("Received XMPP message for: [OWNER-" + owner + "] & [DEVICE.ID-" + deviceId + "]"); + } + + try { + PublicKey clientPublicKey = VirtualFireAlarmServiceUtils.getDevicePublicKey(deviceId); + PrivateKey serverPrivateKey = SecurityManager.getServerPrivateKey(); + String actualMessage = VirtualFireAlarmServiceUtils.extractMessageFromPayload(message, serverPrivateKey, + clientPublicKey); + if (log.isDebugEnabled()) { + log.debug("XMPP: Received Message [" + actualMessage + "] from: [" + from + "]"); + } + if (subject != null) { + switch (subject) { + case "PUBLISHER": + float temperature = Float.parseFloat(actualMessage.split(":")[1]); + PrivilegedCarbonContext.startTenantFlow(); + String tenantDomain = MultitenantUtils.getTenantDomain(owner); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantDomain(tenantDomain); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setUsername(owner); + if (!VirtualFireAlarmServiceUtils.publishToDAS(deviceId, temperature)) { + log.error("XMPP Connector: Publishing VirtualFirealarm data to DAS failed."); + } + PrivilegedCarbonContext.endTenantFlow(); + if (log.isDebugEnabled()) { + log.debug("XMPP: Publisher Message [" + actualMessage + "] from [" + from + "] " + + "was successfully published to DAS"); + } + break; + case "CONTROL-REPLY": + String tempVal = actualMessage.split(":")[1]; + SensorDataManager.getInstance().setSensorRecord(deviceId, + VirtualFireAlarmConstants.SENSOR_TEMP, + tempVal, + Calendar.getInstance().getTimeInMillis()); + break; + + default: + if (log.isDebugEnabled()) { + log.warn("Unknown XMPP Message [" + actualMessage + "] from [" + from + "] received"); + } + break; + } + } + } catch (VirtualFireAlarmException e) { + String errorMsg = + "CertificateManagementService failure oo Signature-Verification/Decryption was unsuccessful."; + log.error(errorMsg, e); + } + } else { + log.warn("Received XMPP message from client with unexpected JID [" + from + "]."); + } + } + + @Override + public void publishDeviceData(String... publishData) throws TransportHandlerException { + if (publishData.length != 4) { + String errorMsg = "Incorrect number of arguments received to SEND-MQTT Message. " + + "Need to be [owner, deviceId, resource{BULB/TEMP}, state{ON/OFF or null}]"; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg); + } + + String deviceOwner = publishData[0]; + String deviceId = publishData[1]; + String resource = publishData[2]; + String state = publishData[3]; + + try { + PublicKey devicePublicKey = VirtualFireAlarmServiceUtils.getDevicePublicKey(deviceId); + PrivateKey serverPrivateKey = SecurityManager.getServerPrivateKey(); + + String actualMessage = resource + ":" + state; + String encryptedMsg = VirtualFireAlarmServiceUtils.prepareSecurePayLoad(actualMessage, + devicePublicKey, + serverPrivateKey); + + String clientToConnect = deviceId + "@" + xmppServerIP + File.separator + deviceOwner; + sendXMPPMessage(clientToConnect, encryptedMsg, "CONTROL-REQUEST"); + + } catch (VirtualFireAlarmException e) { + String errorMsg = "Preparing Secure payload failed for device - [" + deviceId + "] of owner - " + + "[" + deviceOwner + "]."; + log.error(errorMsg); + throw new TransportHandlerException(errorMsg, e); + } + } + + + @Override + public void disconnect() { + Runnable stopConnection = new Runnable() { + public void run() { + while (isConnected()) { + connectorServiceHandler.cancel(true); + closeConnection(); + if (log.isDebugEnabled()) { + log.warn("Unable to 'STOP' connection to XMPP server at: " + server + + " for user - " + xmppVFireAlarmAdminUsername); + } + + try { + Thread.sleep(timeoutInterval); + } catch (InterruptedException e1) { + log.error("XMPP-Terminator: Thread Sleep Interrupt Exception for " + + VirtualFireAlarmConstants.DEVICE_TYPE + " type.", e1); + } + + } + } + }; + + Thread terminatorThread = new Thread(stopConnection); + terminatorThread.setDaemon(true); + terminatorThread.start(); + } + + + @Override + public void processIncomingMessage(Message message, String... messageParams) throws TransportHandlerException { + // nothing to do + } + + @Override + public void processIncomingMessage() throws TransportHandlerException { + // nothing to do + } + + @Override + public void publishDeviceData() throws TransportHandlerException { + // nothing to do + } + + @Override + public void publishDeviceData(Message publishData) throws TransportHandlerException { + // nothing to do + } +} + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/util/SecurityManager.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/util/SecurityManager.java new file mode 100644 index 000000000..5098e4eab --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/util/SecurityManager.java @@ -0,0 +1,254 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.controller.service.impl.util; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.certificate.mgt.core.exception.KeystoreException; +import org.wso2.carbon.certificate.mgt.core.util.ConfigurationUtil; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl.exception.VirtualFireAlarmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; + + +public class SecurityManager { + private static final Log log = LogFactory.getLog(SecurityManager.class); + + private static PrivateKey serverPrivateKey; + private static final String SIGNATURE_ALG = "SHA1withRSA"; + private static final String CIPHER_PADDING = "RSA/ECB/PKCS1Padding"; + + private SecurityManager() { + + } + + public void initVerificationManager() { + serverPrivateKey = retrievePrivateKey(ConfigurationUtil.CA_CERT_ALIAS, + ConfigurationUtil.KEYSTORE_CA_CERT_PRIV_PASSWORD); + } + + public static PrivateKey retrievePrivateKey(String alias, String password){ + PrivateKey privateKey = null; + InputStream inputStream = null; + KeyStore keyStore; + + try { + keyStore = KeyStore.getInstance(ConfigurationUtil.getConfigEntry(ConfigurationUtil.CERTIFICATE_KEYSTORE)); + inputStream = new FileInputStream(ConfigurationUtil.getConfigEntry( + ConfigurationUtil.PATH_CERTIFICATE_KEYSTORE)); + + keyStore.load(inputStream, ConfigurationUtil.getConfigEntry(ConfigurationUtil.CERTIFICATE_KEYSTORE_PASSWORD) + .toCharArray()); + + privateKey = (PrivateKey) (keyStore.getKey(ConfigurationUtil.getConfigEntry(alias), + ConfigurationUtil.getConfigEntry(password).toCharArray())); + + } catch (KeyStoreException e) { + String errorMsg = "Could not load KeyStore of given type in [certificate-config.xml] file." ; + log.error(errorMsg, e); + } catch (FileNotFoundException e) { + String errorMsg = "KeyStore file could not be loaded from path given in [certificate-config.xml] file."; + log.error(errorMsg, e); + } catch (NoSuchAlgorithmException e) { + String errorMsg = "Algorithm not found when loading KeyStore"; + log.error(errorMsg, e); + } catch (CertificateException e) { + String errorMsg = "CertificateException when loading KeyStore"; + log.error(errorMsg, e); + } catch (IOException e) { + String errorMsg = "Input output issue occurred when loading KeyStore"; + log.error(errorMsg, e); + } catch (KeystoreException e) { + String errorMsg = "An error occurred whilst trying load Configs for KeyStoreReader"; + log.error(errorMsg, e); + } catch (UnrecoverableKeyException e) { + String errorMsg = "Key is unrecoverable when retrieving CA private key"; + log.error(errorMsg, e); + } finally { + try { + if (inputStream != null) { + inputStream.close(); + } + } catch (IOException e) { + log.error("Error closing KeyStore input stream", e); + } + } + + return privateKey; + } + + public static PrivateKey getServerPrivateKey() { + return serverPrivateKey; + } + + public static String encryptMessage(String message, Key encryptionKey) throws VirtualFireAlarmException { + Cipher encrypter; + byte[] cipherData; + + try { + encrypter = Cipher.getInstance(CIPHER_PADDING); + encrypter.init(Cipher.ENCRYPT_MODE, encryptionKey); + cipherData = encrypter.doFinal(message.getBytes(StandardCharsets.UTF_8)); + + } catch (NoSuchAlgorithmException e) { + String errorMsg = "Algorithm not found exception occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new VirtualFireAlarmException(errorMsg, e); + } catch (NoSuchPaddingException e) { + String errorMsg = "No Padding error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new VirtualFireAlarmException(errorMsg, e); + } catch (InvalidKeyException e) { + String errorMsg = "InvalidKey exception occurred for encryptionKey \n[\n" + encryptionKey + "\n]\n"; + log.error(errorMsg); + throw new VirtualFireAlarmException(errorMsg, e); + } catch (BadPaddingException e) { + String errorMsg = "Bad Padding error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new VirtualFireAlarmException(errorMsg, e); + } catch (IllegalBlockSizeException e) { + String errorMsg = "Illegal blockSize error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new VirtualFireAlarmException(errorMsg, e); + } + + return Base64.encodeBase64String(cipherData); + } + + + public static String signMessage(String encryptedData, PrivateKey signatureKey) throws VirtualFireAlarmException { + + Signature signature; + String signedEncodedString; + + try { + signature = Signature.getInstance(SIGNATURE_ALG); + signature.initSign(signatureKey); + signature.update(Base64.decodeBase64(encryptedData)); + + byte[] signatureBytes = signature.sign(); + signedEncodedString = Base64.encodeBase64String(signatureBytes); + + } catch (NoSuchAlgorithmException e) { + String errorMsg = "Algorithm not found exception occurred for Signature instance of [" + SIGNATURE_ALG + "]"; + log.error(errorMsg); + throw new VirtualFireAlarmException(errorMsg, e); + } catch (SignatureException e) { + String errorMsg = "Signature exception occurred for Signature instance of [" + SIGNATURE_ALG + "]"; + log.error(errorMsg); + throw new VirtualFireAlarmException(errorMsg, e); + } catch (InvalidKeyException e) { + String errorMsg = "InvalidKey exception occurred for signatureKey \n[\n" + signatureKey + "\n]\n"; + log.error(errorMsg); + throw new VirtualFireAlarmException(errorMsg, e); + } + + return signedEncodedString; + } + + + public static boolean verifySignature(String data, String signedData, PublicKey verificationKey) + throws VirtualFireAlarmException { + + Signature signature; + boolean verified; + + try { + signature = Signature.getInstance(SIGNATURE_ALG); + signature.initVerify(verificationKey); + signature.update(Base64.decodeBase64(data)); + + verified = signature.verify(Base64.decodeBase64(signedData)); + + } catch (NoSuchAlgorithmException e) { + String errorMsg = "Algorithm not found exception occurred for Signature instance of [" + SIGNATURE_ALG + "]"; + log.error(errorMsg); + throw new VirtualFireAlarmException(errorMsg, e); + } catch (SignatureException e) { + String errorMsg = "Signature exception occurred for Signature instance of [" + SIGNATURE_ALG + "]"; + log.error(errorMsg); + throw new VirtualFireAlarmException(errorMsg, e); + } catch (InvalidKeyException e) { + String errorMsg = "InvalidKey exception occurred for signatureKey \n[\n" + verificationKey + "\n]\n"; + log.error(errorMsg); + throw new VirtualFireAlarmException(errorMsg, e); + } + + return verified; + } + + + public static String decryptMessage(String encryptedMessage, Key decryptKey) throws VirtualFireAlarmException { + + Cipher decrypter; + String decryptedMessage; + + try { + + decrypter = Cipher.getInstance(CIPHER_PADDING); + decrypter.init(Cipher.DECRYPT_MODE, decryptKey); + decryptedMessage = new String(decrypter.doFinal(Base64.decodeBase64(encryptedMessage)), StandardCharsets.UTF_8); + + } catch (NoSuchAlgorithmException e) { + String errorMsg = "Algorithm not found exception occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new VirtualFireAlarmException(errorMsg, e); + } catch (NoSuchPaddingException e) { + String errorMsg = "No Padding error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new VirtualFireAlarmException(errorMsg, e); + } catch (InvalidKeyException e) { + String errorMsg = "InvalidKey exception occurred for encryptionKey \n[\n" + decryptKey + "\n]\n"; + log.error(errorMsg); + throw new VirtualFireAlarmException(errorMsg, e); + } catch (BadPaddingException e) { + String errorMsg = "Bad Padding error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new VirtualFireAlarmException(errorMsg, e); + } catch (IllegalBlockSizeException e) { + String errorMsg = "Illegal blockSize error occurred for Cipher instance of [" + CIPHER_PADDING + "]"; + log.error(errorMsg); + throw new VirtualFireAlarmException(errorMsg, e); + } + + return decryptedMessage; + } + + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/util/VirtualFireAlarmServiceUtils.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/util/VirtualFireAlarmServiceUtils.java new file mode 100644 index 000000000..4126235b1 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/util/VirtualFireAlarmServiceUtils.java @@ -0,0 +1,366 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.controller.service.impl.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.concurrent.FutureCallback; +import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; +import org.apache.http.impl.nio.client.HttpAsyncClients; +import org.json.JSONObject; +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.exception.DataPublisherConfigurationException; +import org.wso2.carbon.device.mgt.analytics.service.DeviceAnalyticsService; +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.sensormgt.SensorDataManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.constants.VirtualFireAlarmConstants; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl.exception.VirtualFireAlarmException; +import org.wso2.carbon.utils.multitenancy.MultitenantUtils; + +import javax.ws.rs.HttpMethod; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.URL; +import java.security.Key; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Future; + +/** + * + */ +public class VirtualFireAlarmServiceUtils { + private static final Log log = LogFactory.getLog(VirtualFireAlarmServiceUtils.class); + + private static final String TEMPERATURE_STREAM_DEFINITION = "org.wso2.iot.devices.temperature"; + private static final String JSON_MESSAGE_KEY = "Msg"; + private static final String JSON_SIGNATURE_KEY = "Sig"; + + /** + * + * @return + * @throws VirtualFireAlarmException + */ + public static CertificateManagementService getCertificateManagementService() throws VirtualFireAlarmException { + + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + CertificateManagementService certificateManagementService = (CertificateManagementService) + ctx.getOSGiService(CertificateManagementService.class, null); + + if (certificateManagementService == null) { + String msg = "EnrollmentService is not initialized"; + log.error(msg); + throw new VirtualFireAlarmException(msg); + } + + return certificateManagementService; + } + + + /** + * + * @param deviceHTTPEndpoint + * @param urlContext + * @param fireAndForgot + * @return + * @throws DeviceManagementException + */ + public static String sendCommandViaHTTP(final String deviceHTTPEndpoint, String urlContext, boolean fireAndForgot) + throws DeviceManagementException { + + String responseMsg = ""; + String urlString = VirtualFireAlarmConstants.URL_PREFIX + deviceHTTPEndpoint + urlContext; + + if (log.isDebugEnabled()) { + log.debug(urlString); + } + + if (!fireAndForgot) { + HttpURLConnection httpConnection = getHttpConnection(urlString); + + try { + httpConnection.setRequestMethod(HttpMethod.GET); + } catch (ProtocolException e) { + String errorMsg = + "Protocol specific error occurred when trying to set method to GET" + + " for:" + urlString; + log.error(errorMsg); + throw new DeviceManagementException(errorMsg, e); + } + + responseMsg = readResponseFromGetRequest(httpConnection); + + } else { + CloseableHttpAsyncClient httpclient = null; + try { + + httpclient = HttpAsyncClients.createDefault(); + httpclient.start(); + HttpGet request = new HttpGet(urlString); + final CountDownLatch latch = new CountDownLatch(1); + Future future = httpclient.execute( + request, new FutureCallback() { + @Override + public void completed(HttpResponse httpResponse) { + latch.countDown(); + } + + @Override + public void failed(Exception e) { + latch.countDown(); + } + + @Override + public void cancelled() { + latch.countDown(); + } + }); + + latch.await(); + + } catch (InterruptedException e) { + if (log.isDebugEnabled()) { + log.debug("Sync Interrupted"); + } + } finally { + try { + if (httpclient != null) { + httpclient.close(); + + } + } catch (IOException e) { + if (log.isDebugEnabled()) { + log.debug("Failed on close"); + } + } + } + } + + return responseMsg; + } + + /* --------------------------------------------------------------------------------------- + Utility methods relevant to creating and sending http requests + --------------------------------------------------------------------------------------- */ + + /* This methods creates and returns a http connection object */ + + /** + * + * @param urlString + * @return + * @throws DeviceManagementException + */ + public static HttpURLConnection getHttpConnection(String urlString) throws DeviceManagementException { + + URL connectionUrl = null; + HttpURLConnection httpConnection; + + try { + connectionUrl = new URL(urlString); + httpConnection = (HttpURLConnection) connectionUrl.openConnection(); + } catch (MalformedURLException e) { + String errorMsg = + "Error occured whilst trying to form HTTP-URL from string: " + urlString; + log.error(errorMsg); + throw new DeviceManagementException(errorMsg, e); + } catch (IOException e) { + String errorMsg = "Error occured whilst trying to open a connection to: " + + connectionUrl.toString(); + log.error(errorMsg); + throw new DeviceManagementException(errorMsg, e); + } + + return httpConnection; + } + + /* This methods reads and returns the response from the connection */ + + public static String readResponseFromGetRequest(HttpURLConnection httpConnection) + throws DeviceManagementException { + BufferedReader bufferedReader; + try { + bufferedReader = new BufferedReader(new InputStreamReader( + httpConnection.getInputStream())); + } catch (IOException e) { + String errorMsg = + "There is an issue with connecting the reader to the input stream at: " + + httpConnection.getURL(); + log.error(errorMsg); + throw new DeviceManagementException(errorMsg, e); + } + + String responseLine; + StringBuilder completeResponse = new StringBuilder(); + + try { + while ((responseLine = bufferedReader.readLine()) != null) { + completeResponse.append(responseLine); + } + } catch (IOException e) { + String errorMsg = + "Error occured whilst trying read from the connection stream at: " + + httpConnection.getURL(); + log.error(errorMsg); + throw new DeviceManagementException(errorMsg, e); + } + try { + bufferedReader.close(); + } catch (IOException e) { + log.error( + "Could not succesfully close the bufferedReader to the connection at: " + + httpConnection.getURL()); + } + + return completeResponse.toString(); + } + + public static boolean publishToDAS(String deviceId, float temperature) { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + DeviceAnalyticsService deviceAnalyticsService = (DeviceAnalyticsService) ctx.getOSGiService( + DeviceAnalyticsService.class, null); + if (deviceAnalyticsService != null) { + String owner = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUsername(); + Object metdaData[] = {owner, VirtualFireAlarmConstants.DEVICE_TYPE, deviceId, System.currentTimeMillis()}; + Object payloadData[] = {temperature}; + try { + deviceAnalyticsService.publishEvent(TEMPERATURE_STREAM_DEFINITION, "1.0.0", metdaData, new Object[0], + payloadData); + } catch (DataPublisherConfigurationException e) { + return false; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + return true; + } + return false; + } + + /** + * + * @param message + * @param encryptionKey + * @param signatureKey + * @return + * @throws VirtualFireAlarmException + */ + public static String prepareSecurePayLoad(String message, Key encryptionKey, PrivateKey signatureKey) + throws VirtualFireAlarmException { + String encryptedMsg = SecurityManager.encryptMessage(message, encryptionKey); + String signedPayload = SecurityManager.signMessage(encryptedMsg, signatureKey); + + JSONObject jsonPayload = new JSONObject(); + jsonPayload.put(JSON_MESSAGE_KEY, encryptedMsg); + jsonPayload.put(JSON_SIGNATURE_KEY, signedPayload); + + return jsonPayload.toString(); + } + + /** + * + * @param message + * @param decryptionKey + * @param verifySignatureKey + * @return + * @throws VirtualFireAlarmException + */ + public static String extractMessageFromPayload(String message, Key decryptionKey, PublicKey verifySignatureKey) + throws VirtualFireAlarmException { + String actualMessage; + + JSONObject jsonPayload = new JSONObject(message); + Object encryptedMessage = jsonPayload.get(JSON_MESSAGE_KEY); + Object signedPayload = jsonPayload.get(JSON_SIGNATURE_KEY); + + if (encryptedMessage != null && signedPayload != null) { + if (SecurityManager.verifySignature( + encryptedMessage.toString(), signedPayload.toString(), verifySignatureKey)) { + actualMessage = SecurityManager.decryptMessage(encryptedMessage.toString(), decryptionKey); + } else { + String errorMsg = "The message was not signed by a valid client. Could not verify signature on payload"; + throw new VirtualFireAlarmException(errorMsg); + } + } else { + String errorMsg = "The received message is in an INVALID format. " + + "Need to be JSON - {\"Msg\":\"\", \"Sig\":\"\"}."; + throw new VirtualFireAlarmException(errorMsg); + } + + return actualMessage; + } + + /** + * + * @param deviceId + * @return + * @throws VirtualFireAlarmException + */ + public static PublicKey getDevicePublicKey(String deviceId) throws VirtualFireAlarmException { + PublicKey clientPublicKey; + String alias = ""; + + try { + alias += deviceId.hashCode(); + + CertificateManagementService certificateManagementService = + VirtualFireAlarmServiceUtils.getCertificateManagementService(); + X509Certificate clientCertificate = (X509Certificate) certificateManagementService.getCertificateByAlias( + alias); + clientPublicKey = clientCertificate.getPublicKey(); + + } catch (VirtualFireAlarmException e) { + String errorMsg = "Could not retrieve CertificateManagementService from the runtime."; + if(log.isDebugEnabled()){ + log.debug(errorMsg); + } + throw new VirtualFireAlarmException(errorMsg, e); + } catch (KeystoreException e) { + String errorMsg; + if (e.getMessage().contains("NULL_CERT")) { + errorMsg = "The Device-View page might have been accessed prior to the device being started."; + if(log.isDebugEnabled()){ + log.debug(errorMsg); + } + throw new VirtualFireAlarmException(errorMsg, e); + } else { + errorMsg = "An error occurred whilst trying to retrieve certificate for deviceId [" + deviceId + + "] with alias: [" + alias + "]"; + if(log.isDebugEnabled()){ + log.debug(errorMsg); + } + throw new VirtualFireAlarmException(errorMsg, e); + } + } + return clientPublicKey; + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/util/scep/ContentType.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/util/scep/ContentType.java new file mode 100644 index 000000000..879309c12 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/util/scep/ContentType.java @@ -0,0 +1,26 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.controller.service.impl.util.scep; + +public class ContentType { + public static final String X_PKI_MESSAGE = "application/x-pki-message"; + public static final String X_X509_CA_CERT = "application/x-x509-ca-cert"; + public static final String X_X509_CA_RA_CERT = "application/x-x509-ca-ra-cert"; +} + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/util/scep/SCEPOperation.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/util/scep/SCEPOperation.java new file mode 100644 index 000000000..f9f8f0898 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/controller/service/impl/util/scep/SCEPOperation.java @@ -0,0 +1,39 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.controller.service.impl.util.scep; + +public enum SCEPOperation { + GET_CA_CERT("GetCACert"), + GET_CA_CAPS("GetCACaps"), + PKI_OPERATION("PKIOperation"); + + private String value; + + private SCEPOperation(String value) { + this.setValue(value); + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/webapp/META-INF/webapp-classloading.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/webapp/META-INF/webapp-classloading.xml new file mode 100644 index 000000000..fa4461919 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/webapp/META-INF/webapp-classloading.xml @@ -0,0 +1,33 @@ + + + + + + + + + false + + + CXF,Carbon + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml new file mode 100644 index 000000000..fdb49fd8a --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/webapp/WEB-INF/web.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..0689ffaf0 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,65 @@ + + + WSO2 IoT Server + WSO2 IoT Server + + + CXFServlet + org.apache.cxf.transport.servlet.CXFServlet + 1 + + + + + CXFServlet + /* + + + + isAdminService + false + + + doAuthentication + false + + + + + managed-api-enabled + true + + + managed-api-owner + admin + + + managed-api-context-template + /virtual_firealarm/{version} + + + managed-api-application + virtual_firealarm + + + managed-api-isSecured + true + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/pom.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/pom.xml new file mode 100644 index 000000000..ed8eb5d9b --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/pom.xml @@ -0,0 +1,271 @@ + + + + + + + virtual-fire-alarm-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl + war + WSO2 Carbon - IoT Server VirtualFireAlarm API + WSO2 Carbon - Virtual FireAlarm Service Management API Implementation + http://wso2.org + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.core + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.analytics + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.certificate.mgt.core + provided + + + commons-codec.wso2 + commons-codec + + + + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + provided + + + org.apache.cxf + 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 + provided + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl + provided + + + + + org.codehaus.jackson + jackson-core-asl + + + org.codehaus.jackson + jackson-jaxrs + + + javax + javaee-web-api + provided + + + javax.ws.rs + jsr311-api + provided + + + commons-httpclient.wso2 + commons-httpclient + provided + + + + org.wso2.carbon + org.wso2.carbon.utils + provided + + + org.bouncycastle.wso2 + bcprov-jdk15on + + + org.wso2.carbon + org.wso2.carbon.user.api + + + org.wso2.carbon + org.wso2.carbon.queuing + + + org.wso2.carbon + org.wso2.carbon.base + + + org.apache.axis2.wso2 + axis2 + + + org.igniterealtime.smack.wso2 + smack + + + org.igniterealtime.smack.wso2 + smackx + + + jaxen + jaxen + + + commons-fileupload.wso2 + commons-fileupload + + + org.apache.ant.wso2 + ant + + + org.apache.ant.wso2 + ant + + + commons-httpclient.wso2 + commons-httpclient + + + org.eclipse.equinox + javax.servlet + + + org.wso2.carbon + org.wso2.carbon.registry.api + + + + + + commons-codec.wso2 + commons-codec + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.annotations + provided + + + + org.igniterealtime.smack.wso2 + smack + provided + + + org.igniterealtime.smack.wso2 + smackx + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.webapp.publisher + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.jwt.client.extension + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.application.extension + provided + + + + + + + + maven-compiler-plugin + + UTF-8 + ${wso2.maven.compiler.source} + ${wso2.maven.compiler.target} + + + + maven-war-plugin + + virtual_firealarm_mgt + + + + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/manager/service/impl/VirtualFireAlarmManagerService.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/manager/service/impl/VirtualFireAlarmManagerService.java new file mode 100644 index 000000000..50f1c1c19 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/manager/service/impl/VirtualFireAlarmManagerService.java @@ -0,0 +1,312 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.manager.service.impl; + +import org.apache.commons.io.FileUtils; +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.common.Device; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.EnrolmentInfo; +import org.wso2.carbon.device.mgt.iot.controlqueue.xmpp.XmppAccount; +import org.wso2.carbon.device.mgt.iot.controlqueue.xmpp.XmppConfig; +import org.wso2.carbon.device.mgt.iot.controlqueue.xmpp.XmppServerClient; +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.device.mgt.iot.virtualfirealarm.manager.service.impl.util.APIUtil; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.constants.VirtualFireAlarmConstants; +import org.wso2.carbon.device.mgt.jwt.client.extension.JWTClient; +import org.wso2.carbon.device.mgt.jwt.client.extension.JWTClientManager; +import org.wso2.carbon.device.mgt.jwt.client.extension.dto.AccessTokenInfo; +import org.wso2.carbon.device.mgt.jwt.client.extension.exception.JWTClientException; +import org.wso2.carbon.user.api.UserStoreException; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +public class VirtualFireAlarmManagerService { + @Context //injected response proxy supporting multiple thread + private HttpServletResponse response; + private static final String KEY_TYPE = "PRODUCTION"; + private static ApiApplicationKey apiApplicationKey; + + + @Path("manager/device/register") + @POST + public boolean register(@QueryParam("deviceId") String deviceId, + @QueryParam("name") String name) { + try { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(VirtualFireAlarmConstants.DEVICE_TYPE); + if (APIUtil.getDeviceManagementService().isEnrolled(deviceIdentifier)) { + response.setStatus(Response.Status.CONFLICT.getStatusCode()); + return false; + } + Device device = new Device(); + device.setDeviceIdentifier(deviceId); + EnrolmentInfo enrolmentInfo = new EnrolmentInfo(); + enrolmentInfo.setDateOfEnrolment(new Date().getTime()); + enrolmentInfo.setDateOfLastUpdate(new Date().getTime()); + enrolmentInfo.setStatus(EnrolmentInfo.Status.ACTIVE); + enrolmentInfo.setOwnership(EnrolmentInfo.OwnerShip.BYOD); + device.setName(name); + device.setType(VirtualFireAlarmConstants.DEVICE_TYPE); + enrolmentInfo.setOwner(APIUtil.getAuthenticatedUser()); + device.setEnrolmentInfo(enrolmentInfo); + boolean added = APIUtil.getDeviceManagementService().enrollDevice(device); + if (added) { + response.setStatus(Response.Status.OK.getStatusCode()); + } else { + response.setStatus(Response.Status.NOT_ACCEPTABLE.getStatusCode()); + } + return added; + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/device/{device_id}") + @DELETE + public void removeDevice(@PathParam("device_id") String deviceId, + @Context HttpServletResponse response) { + try { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(VirtualFireAlarmConstants.DEVICE_TYPE); + boolean removed = APIUtil.getDeviceManagementService().disenrollDevice( + deviceIdentifier); + if (removed) { + response.setStatus(Response.Status.OK.getStatusCode()); + } else { + response.setStatus(Response.Status.NOT_ACCEPTABLE.getStatusCode()); + } + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/device/{device_id}") + @PUT + public boolean updateDevice(@PathParam("device_id") String deviceId, + @QueryParam("name") String name, + @Context HttpServletResponse response) { + try { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(VirtualFireAlarmConstants.DEVICE_TYPE); + Device device = APIUtil.getDeviceManagementService().getDevice(deviceIdentifier); + device.setDeviceIdentifier(deviceId); + device.getEnrolmentInfo().setDateOfLastUpdate(new Date().getTime()); + device.setName(name); + device.setType(VirtualFireAlarmConstants.DEVICE_TYPE); + boolean updated = APIUtil.getDeviceManagementService().modifyEnrollment(device); + if (updated) { + response.setStatus(Response.Status.OK.getStatusCode()); + } else { + response.setStatus(Response.Status.NOT_ACCEPTABLE.getStatusCode()); + } + return updated; + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return false; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/device/{device_id}") + @GET + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Device getDevice(@PathParam("device_id") String deviceId) { + try { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(VirtualFireAlarmConstants.DEVICE_TYPE); + return APIUtil.getDeviceManagementService().getDevice(deviceIdentifier); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/devices") + @GET + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Device[] getFirealarmDevices() { + try { + List userDevices = + APIUtil.getDeviceManagementService().getDevicesOfUser(APIUtil.getAuthenticatedUser()); + ArrayList userDevicesforFirealarm = new ArrayList<>(); + for (Device device : userDevices) { + if (device.getType().equals(VirtualFireAlarmConstants.DEVICE_TYPE) && + device.getEnrolmentInfo().getStatus().equals(EnrolmentInfo.Status.ACTIVE)) { + userDevicesforFirealarm.add(device); + } + } + return userDevicesforFirealarm.toArray(new Device[]{}); + } catch (DeviceManagementException e) { + response.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + return null; + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/device/{sketch_type}/download") + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response downloadSketch(@QueryParam("deviceName") String deviceName, + @PathParam("sketch_type") String sketchType) { + try { + ZipArchive zipFile = createDownloadFile(APIUtil.getAuthenticatedUser(), deviceName, sketchType); + Response.ResponseBuilder response = Response.ok(FileUtils.readFileToByteArray(zipFile.getZipFile())); + response.type("application/zip"); + response.header("Content-Disposition", "attachment; filename=\"" + zipFile.getFileName() + "\""); + return response.build(); + } catch (IllegalArgumentException ex) { + return Response.status(400).entity(ex.getMessage()).build();//bad request + } catch (DeviceManagementException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (JWTClientException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (APIManagerException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + }catch (DeviceControllerException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (IOException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (UserStoreException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @Path("manager/device/{sketch_type}/generate_link") + @GET + public Response generateSketchLink(@QueryParam("deviceName") String deviceName, + @PathParam("sketch_type") String sketchType) { + try { + ZipArchive zipFile = createDownloadFile(APIUtil.getAuthenticatedUser(), deviceName, sketchType); + Response.ResponseBuilder rb = Response.ok(zipFile.getDeviceId()); + return rb.build(); + } catch (IllegalArgumentException ex) { + return Response.status(400).entity(ex.getMessage()).build();//bad request + } catch (DeviceManagementException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (JWTClientException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (APIManagerException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (DeviceControllerException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } catch (UserStoreException ex) { + return Response.status(500).entity(ex.getMessage()).build(); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + private ZipArchive createDownloadFile(String owner, String deviceName, String sketchType) + throws DeviceManagementException, APIManagerException, JWTClientException, DeviceControllerException, + UserStoreException { + //create new device id + String deviceId = shortUUID(); + if (apiApplicationKey == null) { + String applicationUsername = PrivilegedCarbonContext.getThreadLocalCarbonContext().getUserRealm().getRealmConfiguration() + .getAdminUserName(); + APIManagementProviderService apiManagementProviderService = APIUtil.getAPIManagementProviderService(); + String[] tags = {VirtualFireAlarmConstants.DEVICE_TYPE}; + apiApplicationKey = apiManagementProviderService.generateAndRetrieveApplicationKeys( + VirtualFireAlarmConstants.DEVICE_TYPE, tags, KEY_TYPE, applicationUsername, true); + } + JWTClient jwtClient = JWTClientManager.getInstance().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(); + //adding registering data + XmppAccount newXmppAccount = new XmppAccount(); + newXmppAccount.setAccountName(owner + "_" + deviceId); + newXmppAccount.setUsername(deviceId); + newXmppAccount.setPassword(accessToken); + newXmppAccount.setEmail(deviceId + "@" + APIUtil.getTenantDomainOftheUser()); + XmppServerClient xmppServerClient = new XmppServerClient(); + xmppServerClient.initControlQueue(); + boolean status; + if (XmppConfig.getInstance().isEnabled()) { + status = xmppServerClient.createXMPPAccount(newXmppAccount); + if (!status) { + String msg = + "XMPP Account was not created for device - " + deviceId + " of owner - " + owner + + ".XMPP might have been disabled in org.wso2.carbon.device.mgt.iot" + + ".common.config.server.configs"; + throw new DeviceManagementException(msg); + } + } + status = register(deviceId, deviceName); + if (!status) { + String msg = "Error occurred while registering the device with " + "id: " + deviceId + " owner:" + owner; + throw new DeviceManagementException(msg); + } + ZipUtil ziputil = new ZipUtil(); + ZipArchive zipFile = ziputil.createZipFile(owner, APIUtil.getTenantDomainOftheUser(), sketchType, deviceId, + deviceName, accessToken, refreshToken); + zipFile.setDeviceId(deviceId); + return zipFile; + } + + private static String shortUUID() { + UUID uuid = UUID.randomUUID(); + long l = ByteBuffer.wrap(uuid.toString().getBytes(StandardCharsets.UTF_8)).getLong(); + return Long.toString(l, Character.MAX_RADIX); + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/manager/service/impl/util/APIUtil.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/manager/service/impl/util/APIUtil.java new file mode 100644 index 000000000..dca274402 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/manager/service/impl/util/APIUtil.java @@ -0,0 +1,55 @@ +package org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl.util; + +import org.apache.commons.logging.Log; +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; + +/** + * This class provides utility functions used by REST-API. + */ +public class APIUtil { + + private static Log log = LogFactory.getLog(APIUtil.class); + + public static String getAuthenticatedUser() { + PrivilegedCarbonContext threadLocalCarbonContext = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + String username = threadLocalCarbonContext.getUsername(); + String tenantDomain = threadLocalCarbonContext.getTenantDomain(); + if (username.endsWith(tenantDomain)) { + return username.substring(0, username.lastIndexOf("@")); + } + 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 = + (DeviceManagementProviderService) ctx.getOSGiService(DeviceManagementProviderService.class, null); + if (deviceManagementProviderService == null) { + String msg = "Device Management service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return deviceManagementProviderService; + } + + 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; + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/webapp/META-INF/webapp-classloading.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/webapp/META-INF/webapp-classloading.xml new file mode 100644 index 000000000..fa4461919 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/webapp/META-INF/webapp-classloading.xml @@ -0,0 +1,33 @@ + + + + + + + + + false + + + CXF,Carbon + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml new file mode 100644 index 000000000..532cd8e97 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/webapp/WEB-INF/cxf-servlet.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/webapp/WEB-INF/web.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..0b6b913db --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,65 @@ + + + WSO2 IoT Server + WSO2 IoT Server + + + CXFServlet + org.apache.cxf.transport.servlet.CXFServlet + 1 + + + + + CXFServlet + /* + + + + isAdminService + false + + + doAuthentication + true + + + + + managed-api-enabled + false + + + managed-api-owner + admin + + + managed-api-context-template + /virtual_firealarm/{version} + + + managed-api-application + virtual_firealarm + + + managed-api-isSecured + true + + + + + + + + + + + + + + + diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/pom.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/pom.xml new file mode 100644 index 000000000..0090fcc36 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/pom.xml @@ -0,0 +1,124 @@ + + + + + + + + virtual-fire-alarm-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl + bundle + WSO2 Carbon - IoT Server VirtualFireAlarm Management Plugin + WSO2 Carbon - Virtual FireAlarm Management/Control Plugin Implementation + http://wso2.org + + + + + org.apache.felix + maven-scr-plugin + + + maven-compiler-plugin + + 1.7 + 1.7 + + 2.3.2 + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + ${project.artifactId} + ${project.artifactId} + ${carbon.devicemgt.plugins.version} + IoT Server Virtual Firealarm Impl Bundle + org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.internal + + org.osgi.framework, + org.osgi.service.component, + org.apache.commons.logging, + javax.xml.bind.*;resolution:=optional, + javax.naming;resolution:=optional, + javax.sql;resolution:=optional, + javax.xml.bind.annotation.*;resolution:=optional, + javax.xml.parsers;resolution:=optional, + javax.net;resolution:=optional, + javax.net.ssl;resolution:=optional, + org.w3c.dom;resolution:=optional, + org.wso2.carbon.device.mgt.common.*, + org.wso2.carbon.device.mgt.common, + org.wso2.carbon.context.*, + org.wso2.carbon.ndatasource.core, + org.wso2.carbon.device.mgt.iot.*, + org.wso2.carbon.device.mgt.extensions.feature.mgt.*, + org.wso2.carbon.utils.* + + + !org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.internal, + org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.* + + + + + + + + + + org.eclipse.osgi + org.eclipse.osgi + + + org.eclipse.osgi + org.eclipse.osgi.services + + + org.wso2.carbon + org.wso2.carbon.logging + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + + + org.wso2.carbon + org.wso2.carbon.ndatasource.core + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.extensions + + + org.wso2.carbon + org.wso2.carbon.utils + + + \ No newline at end of file diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/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.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/constants/VirtualFireAlarmConstants.java new file mode 100644 index 000000000..3b8b5e2a8 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/constants/VirtualFireAlarmConstants.java @@ -0,0 +1,40 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.plugin.constants; + +public class VirtualFireAlarmConstants { + public final static String DEVICE_TYPE = "virtual_firealarm"; + public final static String DEVICE_PLUGIN_DEVICE_NAME = "DEVICE_NAME"; + public final static String DEVICE_PLUGIN_DEVICE_ID = "VIRTUAL_FIREALARM_DEVICE_ID"; + public final static String STATE_ON = "ON"; + public final static String STATE_OFF = "OFF"; + + public static final String URL_PREFIX = "http://"; + public static final String BULB_CONTEXT = "/BULB/"; + public static final String HUMIDITY_CONTEXT = "/HUMIDITY/"; + public static final String TEMPERATURE_CONTEXT = "/TEMPERATURE/"; + + public static final String SENSOR_TEMP = "temperature"; + //sensor events sumerized table name for temperature + public static final String TEMPERATURE_EVENT_TABLE = "DEVICE_TEMPERATURE_SUMMARY"; + public static final String SENSOR_HUMIDITY = "humidity"; + //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"; +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/exception/VirtualFirealarmDeviceMgtPluginException.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/exception/VirtualFirealarmDeviceMgtPluginException.java new file mode 100644 index 000000000..150745ac8 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/exception/VirtualFirealarmDeviceMgtPluginException.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.device.mgt.iot.virtualfirealarm.plugin.exception; + + +public class VirtualFirealarmDeviceMgtPluginException extends Exception{ + + private String errorMessage; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public VirtualFirealarmDeviceMgtPluginException(String msg, Exception nestedEx) { + super(msg, nestedEx); + setErrorMessage(msg); + } + + public VirtualFirealarmDeviceMgtPluginException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public VirtualFirealarmDeviceMgtPluginException(String msg) { + super(msg); + setErrorMessage(msg); + } + + public VirtualFirealarmDeviceMgtPluginException() { + super(); + } + + public VirtualFirealarmDeviceMgtPluginException(Throwable cause) { + super(cause); + } + +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/VirtualFireAlarmManager.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/VirtualFireAlarmManager.java new file mode 100644 index 000000000..a90c15359 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/VirtualFireAlarmManager.java @@ -0,0 +1,256 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.plugin.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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.common.DeviceManager; +import org.wso2.carbon.device.mgt.common.EnrolmentInfo; +import org.wso2.carbon.device.mgt.common.FeatureManager; +import org.wso2.carbon.device.mgt.common.configuration.mgt.TenantConfiguration; +import org.wso2.carbon.device.mgt.common.license.mgt.License; +import org.wso2.carbon.device.mgt.common.license.mgt.LicenseManagementException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.exception.VirtualFirealarmDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl.dao.VirtualFireAlarmDAO; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl.feature.VirtualFirealarmFeatureManager; +import java.util.List; + + +/** + * This represents the FireAlarm implementation of DeviceManagerService. + */ +public class VirtualFireAlarmManager implements DeviceManager { + + private static final VirtualFireAlarmDAO virtualFireAlarmDAO = new VirtualFireAlarmDAO(); + private static final Log log = LogFactory.getLog(VirtualFireAlarmManager.class); + private FeatureManager virtualFirealarmFeatureManager = new VirtualFirealarmFeatureManager(); + + @Override + public FeatureManager getFeatureManager() { + return virtualFirealarmFeatureManager; + } + + @Override + public boolean saveConfiguration(TenantConfiguration tenantConfiguration) + throws DeviceManagementException { + return false; + } + + @Override + public TenantConfiguration getConfiguration() throws DeviceManagementException { + return null; + } + + @Override + public boolean enrollDevice(Device device) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Enrolling a new Virtual Firealarm device : " + device.getDeviceIdentifier()); + } + VirtualFireAlarmDAO.beginTransaction(); + status = virtualFireAlarmDAO.getDeviceDAO().addDevice(device); + VirtualFireAlarmDAO.commitTransaction(); + } catch (VirtualFirealarmDeviceMgtPluginException e) { + try { + VirtualFireAlarmDAO.rollbackTransaction(); + } catch (VirtualFirealarmDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the device enrol transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while enrolling the Virtual Firealarm device : " + device.getDeviceIdentifier(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean modifyEnrollment(Device device) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Modifying the Virtual Firealarm device enrollment data"); + } + VirtualFireAlarmDAO.beginTransaction(); + status = virtualFireAlarmDAO.getDeviceDAO().updateDevice(device); + VirtualFireAlarmDAO.commitTransaction(); + } catch (VirtualFirealarmDeviceMgtPluginException e) { + try { + VirtualFireAlarmDAO.rollbackTransaction(); + } catch (VirtualFirealarmDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the update device transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while updating the enrollment of the Virtual Firealarm device : " + + device.getDeviceIdentifier(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean disenrollDevice(DeviceIdentifier deviceId) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Dis-enrolling Virtual Firealarm device : " + deviceId); + } + VirtualFireAlarmDAO.beginTransaction(); + status = virtualFireAlarmDAO.getDeviceDAO().deleteDevice(deviceId.getId()); + VirtualFireAlarmDAO.commitTransaction(); + } catch (VirtualFirealarmDeviceMgtPluginException e) { + try { + VirtualFireAlarmDAO.rollbackTransaction(); + } catch (VirtualFirealarmDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the device dis enrol transaction :" + deviceId.toString(); + log.warn(msg, iotDAOEx); + } + String msg = "Error while removing the Virtual Firealarm device : " + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean isEnrolled(DeviceIdentifier deviceId) throws DeviceManagementException { + boolean isEnrolled = false; + try { + if (log.isDebugEnabled()) { + log.debug("Checking the enrollment of Virtual Firealarm device : " + deviceId.getId()); + } + Device device = virtualFireAlarmDAO.getDeviceDAO().getDevice(deviceId.getId()); + if (device != null) { + isEnrolled = true; + } + } catch (VirtualFirealarmDeviceMgtPluginException e) { + String msg = "Error while checking the enrollment status of Virtual Firealarm device : " + + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return isEnrolled; + } + + @Override + public boolean isActive(DeviceIdentifier deviceId) throws DeviceManagementException { + return true; + } + + @Override + public boolean setActive(DeviceIdentifier deviceId, boolean status) + throws DeviceManagementException { + return true; + } + + @Override + public Device getDevice(DeviceIdentifier deviceId) throws DeviceManagementException { + Device device; + try { + if (log.isDebugEnabled()) { + log.debug("Getting the details of Virtual Firealarm device : " + deviceId.getId()); + } + device = virtualFireAlarmDAO.getDeviceDAO().getDevice(deviceId.getId()); + } catch (VirtualFirealarmDeviceMgtPluginException e) { + String msg = "Error while fetching the Virtual Firealarm device : " + deviceId.getId(); + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return device; + } + + @Override + public boolean setOwnership(DeviceIdentifier deviceId, String ownershipType) + throws DeviceManagementException { + return true; + } + + public boolean isClaimable(DeviceIdentifier deviceIdentifier) throws DeviceManagementException { + return false; + } + + @Override + public boolean setStatus(DeviceIdentifier deviceId, String currentOwner, + EnrolmentInfo.Status status) throws DeviceManagementException { + return false; + } + + @Override + public License getLicense(String s) throws LicenseManagementException { + return null; + } + + @Override + public void addLicense(License license) throws LicenseManagementException { + + } + + @Override + public boolean requireDeviceAuthorization() { + return false; + } + + @Override + public boolean updateDeviceInfo(DeviceIdentifier deviceIdentifier, Device device) throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug( + "updating the details of Virtual Firealarm device : " + deviceIdentifier); + } + VirtualFireAlarmDAO.beginTransaction(); + status = virtualFireAlarmDAO.getDeviceDAO().updateDevice(device); + VirtualFireAlarmDAO.commitTransaction(); + } catch (VirtualFirealarmDeviceMgtPluginException e) { + try { + VirtualFireAlarmDAO.rollbackTransaction(); + } catch (VirtualFirealarmDeviceMgtPluginException iotDAOEx) { + String msg = "Error occurred while roll back the update device info transaction :" + device.toString(); + log.warn(msg, iotDAOEx); + } + String msg = + "Error while updating the Virtual Firealarm device : " + deviceIdentifier; + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public List getAllDevices() throws DeviceManagementException { + List devices; + try { + if (log.isDebugEnabled()) { + log.debug("Fetching the details of all Virtual Firealarm devices"); + } + devices = virtualFireAlarmDAO.getDeviceDAO().getAllDevices(); + } catch (VirtualFirealarmDeviceMgtPluginException e) { + String msg = "Error while fetching all Virtual Firealarm devices."; + log.error(msg, e); + throw new DeviceManagementException(msg, e); + } + return devices; + } + +} \ No newline at end of file diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/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.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/VirtualFireAlarmManagerService.java new file mode 100644 index 000000000..68c8ec94f --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/VirtualFireAlarmManagerService.java @@ -0,0 +1,107 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.plugin.impl; + +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.DeviceManager; +import org.wso2.carbon.device.mgt.common.app.mgt.Application; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManagementException; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManager; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.constants.VirtualFireAlarmConstants; + +import java.util.List; + +public class VirtualFireAlarmManagerService implements DeviceManagementService{ + private DeviceManager deviceManager; + @Override + public String getType() { + return VirtualFireAlarmConstants.DEVICE_TYPE; + } + + + @Override + public String getProviderTenantDomain() { + return "carbon.super"; + } + + @Override + public boolean isSharedWithAllTenants() { + return true; + } + + @Override + public void init() throws DeviceManagementException { + this.deviceManager=new VirtualFireAlarmManager(); + } + + @Override + public DeviceManager getDeviceManager() { + return deviceManager; + } + + @Override + public ApplicationManager getApplicationManager() { + return null; + } + + @Override + public void notifyOperationToDevices(Operation operation, List deviceIds) + throws DeviceManagementException { + + } + + @Override + public Application[] getApplications(String domain, int pageNumber, int size) + throws ApplicationManagementException { + return new Application[0]; + } + + @Override + public void updateApplicationStatus(DeviceIdentifier deviceId, Application application, + String status) throws ApplicationManagementException { + + } + + @Override + public String getApplicationStatus(DeviceIdentifier deviceId, Application application) + throws ApplicationManagementException { + return null; + } + + @Override + public void installApplicationForDevices(Operation operation, List deviceIdentifiers) + throws ApplicationManagementException { + + } + + @Override + public void installApplicationForUsers(Operation operation, List userNameList) + throws ApplicationManagementException { + + } + + @Override + public void installApplicationForUserRoles(Operation operation, List userRoleList) + throws ApplicationManagementException { + + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/dao/VirtualFireAlarmDAO.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/dao/VirtualFireAlarmDAO.java new file mode 100644 index 000000000..8064c524e --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/dao/VirtualFireAlarmDAO.java @@ -0,0 +1,125 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.plugin.impl.dao; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.constants.VirtualFireAlarmConstants; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.exception.VirtualFirealarmDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl.dao.impl.VirtualFireAlarmDeviceDAOImpl; +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +public class VirtualFireAlarmDAO { + + private static final Log log = LogFactory.getLog(VirtualFireAlarmDAO.class); + static DataSource dataSource; + private static ThreadLocal currentConnection = new ThreadLocal(); + + public VirtualFireAlarmDAO() { + initFireAlarmDAO(); + } + + public static void initFireAlarmDAO() { + try { + Context ctx = new InitialContext(); + dataSource = (DataSource) ctx.lookup(VirtualFireAlarmConstants.DATA_SOURCE_NAME); + } catch (NamingException e) { + log.error("Error while looking up the data source: " + VirtualFireAlarmConstants.DATA_SOURCE_NAME); + } + } + + public VirtualFireAlarmDeviceDAOImpl getDeviceDAO() { + return new VirtualFireAlarmDeviceDAOImpl(); + } + + public static void beginTransaction() throws VirtualFirealarmDeviceMgtPluginException { + try { + Connection conn = dataSource.getConnection(); + conn.setAutoCommit(false); + currentConnection.set(conn); + } catch (SQLException e) { + throw new VirtualFirealarmDeviceMgtPluginException("Error occurred while retrieving datasource connection", e); + } + } + + public static Connection getConnection() throws VirtualFirealarmDeviceMgtPluginException { + if (currentConnection.get() == null) { + try { + currentConnection.set(dataSource.getConnection()); + } catch (SQLException e) { + throw new VirtualFirealarmDeviceMgtPluginException("Error occurred while retrieving data source connection", e); + } + } + return currentConnection.get(); + } + + public static void commitTransaction() throws VirtualFirealarmDeviceMgtPluginException { + try { + Connection conn = currentConnection.get(); + if (conn != null) { + conn.commit(); + } else { + if (log.isDebugEnabled()) { + log.debug("Datasource connection associated with the current thread is null, hence commit " + + "has not been attempted"); + } + } + } catch (SQLException e) { + throw new VirtualFirealarmDeviceMgtPluginException("Error occurred while committing the transaction", e); + } finally { + closeConnection(); + } + } + + public static void closeConnection() throws VirtualFirealarmDeviceMgtPluginException { + + Connection con = currentConnection.get(); + if (con != null) { + try { + con.close(); + } catch (SQLException e) { + log.error("Error occurred while close the connection"); + } + } + currentConnection.remove(); + } + + public static void rollbackTransaction() throws VirtualFirealarmDeviceMgtPluginException { + try { + Connection conn = currentConnection.get(); + if (conn != null) { + conn.rollback(); + } else { + if (log.isDebugEnabled()) { + log.debug("Datasource connection associated with the current thread is null, hence rollback " + + "has not been attempted"); + } + } + } catch (SQLException e) { + throw new VirtualFirealarmDeviceMgtPluginException("Error occurred while rollback the transaction", e); + } finally { + closeConnection(); + } + } +} \ No newline at end of file diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/dao/impl/VirtualFireAlarmDeviceDAOImpl.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/dao/impl/VirtualFireAlarmDeviceDAOImpl.java new file mode 100644 index 000000000..c4896b0ab --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/dao/impl/VirtualFireAlarmDeviceDAOImpl.java @@ -0,0 +1,196 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.plugin.impl.dao.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.constants.VirtualFireAlarmConstants; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.exception.VirtualFirealarmDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl.dao.VirtualFireAlarmDAO; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl.util.VirtualFireAlarmUtils; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * Implements CRUD for virtual firealarm Devices. + */ +public class VirtualFireAlarmDeviceDAOImpl { + + private static final Log log = LogFactory.getLog(VirtualFireAlarmDeviceDAOImpl.class); + + public Device getDevice(String deviceId) throws VirtualFirealarmDeviceMgtPluginException { + Connection conn = null; + PreparedStatement stmt = null; + Device device = null; + ResultSet resultSet = null; + try { + conn = VirtualFireAlarmDAO.getConnection(); + String selectDBQuery = + "SELECT VIRTUAL_FIREALARM_DEVICE_ID, DEVICE_NAME" + + " FROM VIRTUAL_FIREALARM_DEVICE WHERE VIRTUAL_FIREALARM_DEVICE_ID = ?"; + stmt = conn.prepareStatement(selectDBQuery); + stmt.setString(1, deviceId); + resultSet = stmt.executeQuery(); + + if (resultSet.next()) { + device = new Device(); + device.setName(resultSet.getString(VirtualFireAlarmConstants.DEVICE_PLUGIN_DEVICE_NAME)); + if (log.isDebugEnabled()) { + log.debug("Virtual Firealarm device " + deviceId + " data has been fetched from " + + "Virtual Firealarm database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while fetching Virtual Firealarm device : '" + deviceId + "'"; + log.error(msg, e); + throw new VirtualFirealarmDeviceMgtPluginException(msg, e); + } finally { + VirtualFireAlarmUtils.cleanupResources(stmt, resultSet); + VirtualFireAlarmDAO.closeConnection(); + } + + return device; + } + + public boolean addDevice(Device device) throws VirtualFirealarmDeviceMgtPluginException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = VirtualFireAlarmDAO.getConnection(); + String createDBQuery = + "INSERT INTO VIRTUAL_FIREALARM_DEVICE(VIRTUAL_FIREALARM_DEVICE_ID, DEVICE_NAME) VALUES (?, ?)"; + + stmt = conn.prepareStatement(createDBQuery); + stmt.setString(1, device.getDeviceIdentifier()); + stmt.setString(2, device.getName()); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Virtual Firealarm device " + device.getDeviceIdentifier() + " data has been" + + " added to the Virtual Firealarm database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while adding the Virtual Firealarm device '" + + device.getDeviceIdentifier() + "' to the Virtual Firealarm db."; + log.error(msg, e); + throw new VirtualFirealarmDeviceMgtPluginException(msg, e); + } finally { + VirtualFireAlarmUtils.cleanupResources(stmt, null); + } + return status; + } + + public boolean updateDevice(Device device) throws VirtualFirealarmDeviceMgtPluginException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = VirtualFireAlarmDAO.getConnection(); + String updateDBQuery = + "UPDATE VIRTUAL_FIREALARM_DEVICE SET DEVICE_NAME = ? WHERE VIRTUAL_FIREALARM_DEVICE_ID = ?"; + + stmt = conn.prepareStatement(updateDBQuery); + stmt.setString(1, device.getName()); + stmt.setString(2, device.getDeviceIdentifier()); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Virtualm Firealarm device " + device.getDeviceIdentifier() + " data has been" + + " modified."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while modifying the Virtual Firealarm device '" + + device.getDeviceIdentifier() + "' data."; + log.error(msg, e); + throw new VirtualFirealarmDeviceMgtPluginException(msg, e); + } finally { + VirtualFireAlarmUtils.cleanupResources(stmt, null); + } + return status; + } + + public boolean deleteDevice(String iotDeviceId) throws VirtualFirealarmDeviceMgtPluginException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = VirtualFireAlarmDAO.getConnection(); + String deleteDBQuery = "DELETE FROM VIRTUAL_FIREALARM_DEVICE WHERE VIRTUAL_FIREALARM_DEVICE_ID = ?"; + stmt = conn.prepareStatement(deleteDBQuery); + stmt.setString(1, iotDeviceId); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Virtual Firealarm device " + iotDeviceId + " data has deleted" + + " from the Virtual Firealarm database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while deleting Virtual Firealarm device " + iotDeviceId; + log.error(msg, e); + throw new VirtualFirealarmDeviceMgtPluginException(msg, e); + } finally { + VirtualFireAlarmUtils.cleanupResources(stmt, null); + } + return status; + } + + public List getAllDevices() throws VirtualFirealarmDeviceMgtPluginException { + Connection conn; + PreparedStatement stmt = null; + ResultSet resultSet = null; + Device device; + List devices = new ArrayList<>(); + try { + conn = VirtualFireAlarmDAO.getConnection(); + String selectDBQuery = + "SELECT VIRTUAL_FIREALARM_DEVICE_ID, DEVICE_NAME FROM VIRTUAL_FIREALARM_DEVICE"; + stmt = conn.prepareStatement(selectDBQuery); + resultSet = stmt.executeQuery(); + while (resultSet.next()) { + device = new Device(); + device.setDeviceIdentifier(resultSet.getString(VirtualFireAlarmConstants.DEVICE_PLUGIN_DEVICE_ID)); + device.setName(resultSet.getString(VirtualFireAlarmConstants.DEVICE_PLUGIN_DEVICE_NAME)); + devices.add(device); + } + if (log.isDebugEnabled()) { + log.debug("All Virtual Firealarm device details have fetched from Firealarm database."); + } + return devices; + } catch (SQLException e) { + String msg = "Error occurred while fetching all Virtual Firealarm device data'"; + log.error(msg, e); + throw new VirtualFirealarmDeviceMgtPluginException(msg, e); + } finally { + VirtualFireAlarmUtils.cleanupResources(stmt, resultSet); + VirtualFireAlarmDAO.closeConnection(); + } + } +} \ No newline at end of file diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/feature/VirtualFirealarmFeatureManager.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/feature/VirtualFirealarmFeatureManager.java new file mode 100644 index 000000000..7290b6179 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/feature/VirtualFirealarmFeatureManager.java @@ -0,0 +1,58 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.plugin.impl.feature; + +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.Feature; +import org.wso2.carbon.device.mgt.common.FeatureManager; +import org.wso2.carbon.device.mgt.extensions.feature.mgt.GenericFeatureManager; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.constants.VirtualFireAlarmConstants; + +import java.util.List; + +public class VirtualFirealarmFeatureManager implements FeatureManager{ + @Override + public boolean addFeature(Feature feature) throws DeviceManagementException { + return false; + } + + @Override + public boolean addFeatures(List features) throws DeviceManagementException { + return false; + } + + @Override + public Feature getFeature(String name) throws DeviceManagementException { + GenericFeatureManager genericFeatureManager = GenericFeatureManager.getInstance(); + return genericFeatureManager.getFeature(VirtualFireAlarmConstants.DEVICE_TYPE, name); + } + + @Override + public List getFeatures() throws DeviceManagementException { + GenericFeatureManager genericFeatureManager = GenericFeatureManager.getInstance(); + return genericFeatureManager.getFeatures(VirtualFireAlarmConstants.DEVICE_TYPE); + } + + @Override + public boolean removeFeature(String name) throws DeviceManagementException { + return false; + } + + @Override + public boolean addSupportedFeaturesToDB() throws DeviceManagementException { + return false; + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/util/DeviceSchemaInitializer.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/util/DeviceSchemaInitializer.java new file mode 100644 index 000000000..450014502 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/util/DeviceSchemaInitializer.java @@ -0,0 +1,50 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.plugin.impl.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.utils.CarbonUtils; +import org.wso2.carbon.utils.dbcreator.DatabaseCreator; + +import javax.sql.DataSource; +import java.io.File; + +/** + * Provides methods for initializing the database script. + */ +public class DeviceSchemaInitializer extends DatabaseCreator{ + + private static final Log log = LogFactory.getLog(DeviceSchemaInitializer.class); + private static final String setupSQLScriptBaseLocation = CarbonUtils.getCarbonHome() + File.separator + "dbscripts" + + File.separator + "cdm" + File.separator + "plugins" + File.separator; + + public DeviceSchemaInitializer(DataSource dataSource) { + super(dataSource); + } + + @Override + protected String getDbScriptLocation(String databaseType) { + String scriptName = databaseType + ".sql"; + if (log.isDebugEnabled()) { + log.debug("Loading database script from :" + scriptName); + } + return setupSQLScriptBaseLocation.replaceFirst("DBTYPE", databaseType) + scriptName; + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/util/VirtualFireAlarmUtils.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/util/VirtualFireAlarmUtils.java new file mode 100644 index 000000000..216b41f24 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/impl/util/VirtualFireAlarmUtils.java @@ -0,0 +1,111 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.plugin.impl.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.constants.VirtualFireAlarmConstants; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.exception.VirtualFirealarmDeviceMgtPluginException; + +import javax.naming.Context; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; + +/** + * Contains utility methods used by FireAlarm plugin. + */ +public class VirtualFireAlarmUtils { + + private static Log log = LogFactory.getLog(VirtualFireAlarmUtils.class); + + public static String getDeviceProperty(List deviceProperties, String propertyKey) { + String deviceProperty = ""; + for(Device.Property property :deviceProperties){ + if(propertyKey.equals(property.getName())){ + deviceProperty = property.getValue(); + } + } + return deviceProperty; + } + + public static Device.Property getProperty(String property, String value) { + if (property != null) { + Device.Property prop = new Device.Property(); + prop.setName(property); + prop.setValue(value); + return prop; + } + return null; + } + + public static void cleanupResources(Connection conn, PreparedStatement stmt, ResultSet rs) { + if (rs != null) { + try { + rs.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing result set", e); + } + } + if (stmt != null) { + try { + stmt.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing prepared statement", e); + } + } + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing database connection", e); + } + } + } + + public static void cleanupResources(PreparedStatement stmt, ResultSet rs) { + cleanupResources(null, stmt, rs); + } + + /** + * Creates the device management schema. + */ + public static void setupDeviceManagementSchema() throws VirtualFirealarmDeviceMgtPluginException { + try { + Context ctx = new InitialContext(); + DataSource dataSource = (DataSource) ctx.lookup(VirtualFireAlarmConstants.DATA_SOURCE_NAME); + DeviceSchemaInitializer initializer = + new DeviceSchemaInitializer(dataSource); + log.info("Initializing device management repository database schema"); + initializer.createRegistryDatabase(); + } catch (NamingException e) { + log.error("Error while looking up the data source: " + VirtualFireAlarmConstants.DATA_SOURCE_NAME); + } catch (Exception e) { + throw new VirtualFirealarmDeviceMgtPluginException("Error occurred while initializing Iot Device " + + "Management database schema", e); + } + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/internal/VirtualFirealarmManagementServiceComponent.java b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/internal/VirtualFirealarmManagementServiceComponent.java new file mode 100644 index 000000000..bdc3d3a75 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl/src/main/java/org/wso2/carbon/device/mgt/iot/virtualfirealarm/plugin/internal/VirtualFirealarmManagementServiceComponent.java @@ -0,0 +1,84 @@ +/* + * 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.device.mgt.iot.virtualfirealarm.plugin.internal; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.ComponentContext; +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.exception.VirtualFirealarmDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl.VirtualFireAlarmManagerService; +import org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl.util.VirtualFireAlarmUtils; + +/** + * @scr.component name="org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.internal + * .VirtualFirealarmManagementServiceComponent" + * immediate="true" + */ +public class VirtualFirealarmManagementServiceComponent { + + private static final Log log = LogFactory.getLog(VirtualFirealarmManagementServiceComponent.class); + private ServiceRegistration firealarmServiceRegRef; + + protected void activate(ComponentContext ctx) { + if (log.isDebugEnabled()) { + log.debug("Activating Virtual Firealarm Device Management Service Component"); + } + try { + BundleContext bundleContext = ctx.getBundleContext(); + firealarmServiceRegRef = bundleContext.registerService(DeviceManagementService.class.getName(), + new VirtualFireAlarmManagerService(), null); + String setupOption = System.getProperty("setup"); + if (setupOption != null) { + if (log.isDebugEnabled()) { + log.debug("-Dsetup is enabled. Iot Device management repository schema initialization is about " + + "to begin"); + } + try { + VirtualFireAlarmUtils.setupDeviceManagementSchema(); + } catch (VirtualFirealarmDeviceMgtPluginException e) { + log.error("Exception occurred while initializing device management database schema", e); + } + } + if (log.isDebugEnabled()) { + log.debug("Virtual Firealarm Device Management Service Component has been successfully activated"); + } + } catch (Throwable e) { + log.error("Error occurred while activating Virtual Firealarm Device Management Service Component", e); + } + } + + protected void deactivate(ComponentContext ctx) { + if (log.isDebugEnabled()) { + log.debug("De-activating Virtual Firealarm Device Management Service Component"); + } + try { + if (firealarmServiceRegRef != null) { + firealarmServiceRegRef.unregister(); + } + if (log.isDebugEnabled()) { + log.debug("Virtual Firealarm Device Management Service Component has been successfully de-activated"); + } + } catch (Throwable e) { + log.error("Error occurred while de-activating Virtual Firealarm Device Management bundle", e); + } + } +} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/pom.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/pom.xml new file mode 100644 index 000000000..77cd6f2f7 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/pom.xml @@ -0,0 +1,62 @@ + + + + + + + + virtual-fire-alarm-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui + WSO2 Carbon - IoT Server Virtual Firealarm UI + pom + + + + + maven-assembly-plugin + 2.5.5 + + ${project.artifactId}-${carbon.device.mgt.version} + false + + src/assembly/src.xml + + + + + create-archive + package + + single + + + + + + + + \ No newline at end of file diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/assembly/src.xml b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/assembly/src.xml new file mode 100644 index 000000000..2797034e0 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/assembly/src.xml @@ -0,0 +1,36 @@ + + + + src + + zip + + false + ${basedir}/src + + + ${basedir}/src/main/resources/jaggeryapps/devicemgt + / + true + + + \ No newline at end of file diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.device-view/device-view.hbs b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.device-view/device-view.hbs new file mode 100644 index 000000000..c6d599d8e --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.device-view/device-view.hbs @@ -0,0 +1,105 @@ +{{#zone "topCss"}} + +{{/zone}} + +{{#zone "device-thumbnail"}} + +{{/zone}} + +{{#zone "device-opetations"}} +

      + Operations +
      +
      + {{unit "iot.unit.device.operation-bar" device=device}} +
      +{{/zone}} + +{{#zone "device-detail-properties"}} +
      + +
      +
      + +
      +
      Device Statistics
      + {{unit "iot.unit.device.stats" device=device}} +
      + +
      +
      Policies
      +
      + +
      +
      + No policies found +
      +
      +
      +
      + + + + + Add device specific policy +
      +
      +
      Operations Log
      +
      + +
      +
      + Not available yet +
      +
      +
      +
      +
      +
      +
      +
      +{{/zone}} + +{{#zone "bottomJs"}} + +{{/zone}} \ No newline at end of file diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.device-view/device-view.js b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.device-view/device-view.js new file mode 100644 index 000000000..dbec07bd6 --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.device-view/device-view.js @@ -0,0 +1,35 @@ +/* + * 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. + */ + +function onRequest(context) { + var log = new Log("device-view.js"); + var deviceType = context.uriParams.deviceType; + var deviceId = request.getParameter("id"); + + if (deviceType != null && deviceType != undefined && deviceId != null && deviceId != undefined) { + var deviceModule = require("/app/modules/device.js").deviceModule; + var device = deviceModule.viewDevice(deviceType, deviceId); + + if (device && device.status != "error") { + return {"device": device}; + } else { + response.sendError(404, "Device Id " + deviceId + " of type " + deviceType + " cannot be found!"); + exit(); + } + } +} \ No newline at end of file diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.device-view/device-view.json b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.device-view/device-view.json new file mode 100644 index 000000000..9eecd8f5b --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.device-view/device-view.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0" +} \ No newline at end of file diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.device-view/public/images/firealarm-icon.png b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.device-view/public/images/firealarm-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7c5808697e193b734c127fdf563f335ecd094bea GIT binary patch literal 28584 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

      Xbds!OMAZOyVX815vlcvCahpl6FV`n*q* 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

      r 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�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

      !IRVBX%L5p zV}Ri&{W62XFNPf;d2DWO<`opA$Y=1JqQrb@4QFuPUY*XW_nWehN%C#ri?h?eC0y7* zMYrb|gLxRxob#d3cUeDoAZ1Fka0s7w%cMK~80PX?i^r%z?f3Q#koj^T2ZjvNX{z}` zWINme1at7OIXg|A?H09ZjKE9G-Is`q8&jOkzH4(U`widHjNZ0j>&TZ}w*=dZQ_4#1 z5Oq5F^T7fWUx1ZQzG=!{zusFP5(7V@*+@-;9$>zu>t5p(6{VrhU2*eI`orc)zD6LJ z``t#QRjq+0pu=@PgKhEiQiWVIb7GUO5SbjNwRtk^(=c_#+XnWu@N^5O3QV|(13vl% zP+~rA?i66PldNj{ncQ*8%2VcMbtNO9luP>#QuYt2YT&y2& zRkiZjYw_8$tysOJ1v@XE8uG|Pp$#GEY@*doC6radKs*O9n{#PKq+E04t0t4!47u>f zw2CR5MchQKys)JuJGqV>PEE{@W#7`#YMX28Jm=+t3tt6Ch1?7L#KT{VfRGj9aVUts1)XM{$J?X0dCJX)+OU~Lbuxoz&fv?EsNlehg7S2#Y4>pVnkXzO}y>}={E#s#As8J4RD zJP;c@!KiFx62FyyaAIE#O<=%I7y5)^ z9Ute;SPvNrj1F}glpo+GE6bE=CoGfr7lec?W;(md#%YEU)|(bXCS49ub<6(f8f_l| zc~vMXgiKkF)}zq%!V@`S zKj=w@=k-P{e~e4hl#C;bNAA{ZAC&8RSBLM}2pIP%gbpaIz5oxdF|%OfxSsG=d+_RX zLIN1q4-#+|YoW~JdBJ{c`zNs#Mr_p7xNE>Z)C0ZbGbHNy^(PGUowWEL)*QFj> z+-cW9Vhz4E88mk$gw&#gcdJ|3$_0r%uAyCbMmtwq$2%HbsthS{lL^hQJ9kDjc8mLJ zycl+i^~XjFx)+^nP|-gfs`oMSi66qXs>hM#@pq?eP{@+03U$5FPKqWQgL2W0La_qM zPIXlL)H2=7*euVWt<>5SWoIXUz+!dlVtJstkVvMBkpHOcLCJxaK)bO^)nIXWSv}5& zWQeul{sY~n_S(c$q1UnT!6HJz5<;!UD4k8Tx$yW8oJWGkkBzYj=IG&8t$Orbw9OFO zrPxQeY{=~HSC<6`O6t_{^(MG$8?14)NYp}idvM)}ktLy^#pa3e^~%m~e<{qkS0|J# zmK_QQjk)FiG|F6%M^lF{I)n#{KH2xQXRyjJN=`}W+HL5bV05vLJW;ruR89Q?rBgk! zcA8kI?uK}3 zKP&vRKBNfL0*K6krlk)U(1u6_t=VPBuTU+|%_#%cq!v_E`P2*wZvms#1%SHUhS>K0 zYoi-oAM8xEA|NL$*v`9tHbNnjeopojVf=Xm3cqBi-|BZ?zOqf^L%_5LJD7TQeZ@4c zT-k%e)W}>bkNdER3D7P)Y6ut%2qz%DO5HSP@3fr^2dbsm`rW@PpK zhZtMtNS%`9V98sSsUOa|4KFt?&&3D2^IvszqLM1ovTpRQ5+(NcW!l}{1@6U+Ap;Nj z@$qp89JSYKfNdG*tsys)YP!|rGcrGmy??Xk`_EyGz7}tTaC2y*v@!7Jcr+L!Z6PP zzGwdGWay0Fq9yETYfJ=O6Y4m{d0C8(kcEdl(Go6&$`bHMfl45dbp?_JpZ1!Xo>=@#$u1N9dTie-d7SXR3Nk>Bs zH>NX3D_e&9&Vgb!?B||REvv_uDgT9VBi3f(nNRm_rYInE|si zSf0{CpRLCZ``ZA8jK%I#ayc8$h_6&%rsr>+ST+&GAzYPU2j{ zjGw@zJRkj%&P255F}F3xh874mZM257uIq1&eHd0;<1h3n#O+?4==f4&*y%$oR*f{| zw|W8i$51||9!n`BFj|j|SBV$P+;$ck!Qysn56A_|$0b|RkV5=u?25B4^%T~)c!b}- z`+}|C$PX>wMW*rk~Mj^6h#5_KJcSCZfbq5Xz8kXz(*m~Dvr|QBTy-P%EddMr)KNSTx_uORBP@vedye6)?hJB=EMKN3?ZgGm;|JaQ2Rha>RO5pP>Q(LCC#Lz|QNU57X?8!r!E zp9$tJ+W}hs1*!PO1<4}QRPrx-*W#%g6E-GSDwdq&(DF1T;T2+?aTnZAz~Vv$dyKx1s_5W!}&c>zZ?cBzd7_;In8XJr+wz3TgXL zLQ(=0eW>{Mr7{xR$d(`2cNoiq+L2)j(Yg_?5mwD{R*q3ynvxz=e$tsdyqnN-5kq!J zwz!p4j+pHm!T&Yx_S*~^yYL90;Y6zI1fq1GGFVkN|FTVF3-1hu5?9!{8k7HRVP`D& zZsx=fa*xETWY3_KY}HkUy?GDa87;6Ilwh-_(c)IWZ&VJKwl@=itO_x~cwkK$EzoA= zDkOkkk#V;w4EdL_E4m45l%0Go@hzT_jwK0LuK1>>xa~$Ws}4$$x&t1R5Ci{ceLmI< zoj?A=Oy4y3PQ5-&NnhoNyKZNvOO^MNQ!>wF*rv^W*4Fv%ckk~U zzwWvBHt^r;zt;Oc@AEwGyJm{LrDeM>WPd1k45+?Ho5{YH9+G(Z&FGc>+f{EJCq37V z-oUr-X=yM42ci1Dx-h)Kz2w#L&uRD8q*S_o(yUqyR>j6q)B66fHFpNhu%kg_rlMyw z?hk{FcWX*{haKB`!=JEk37@Ru*rJ}w>`R&3E^*v8tqFd2W!7n{*|4V}`IG8)`n zgs-Iay?dYd8sGbsN-C-`@rf#nxxqzyX3+JfZNo&N%aI)X6C!(%tp~mnJ-^PfjWmy+c_?D zPaW5p#w8?>A!;a&4UCLXlKrv7-Dknt{I?NFJw)s(Dk>@+f)g5=C-DtH@P*VM6GB8B zv>}Y(?)Xl_rpdrYmk$0M=2#twN2F4iR0DgmK(c~i#V`CHr8^w?)IbV|zN>K;lPPk5 z7=&(wcyjw7F<^nJhPvQ6QDoZPm(fIoJ!p6)&dm!vW~Z8}-@g5QrieNr2}Ma~6ucwu1zxE&03I^1C%Z<3tAGCf0f?=hXeMTHS%Bz$(5T1Di9GK}Jz-7YBLnLd>6uX$86d9drnx%gZS5C&WG22XMqKSBWPgtv) zLwWq_x3=vkHl*`(aGs4W$e1TF~IQ>aW8gIxy~ zaK@*l=>k7NcmhpM-VXU0)M@|O7Y2rgqy5=tTw(xy*smvJR(%?DlmYwN$g)jZ7~ zvL&xvbgh3p{L1ts$51)B=N+1SW$~Lhq!hkHjs?E_%}=~8^=vTr@OYMi+t77x|2^IP z=DLPQ3~u;L*|(3!^&c(n`8a-EDaX;Ug>jyERx9_Ps?5qxh~k8+gJ6ZB74U)0eT~b- zRa&~@%2Z-w!0na9(^SU16_Hl^9Kroeyi$C{<`4cMAJyl@bc;U_K>@>uKnke>-sZ`Ro_%8(@V|jY-Z1gzbJK- zTD|n7>7nzHj5-aszH_sVsr=cs?Zq=W69emPhBX^)v(4(K>x*eFvW3qkGMhE_8QQ3g zKW=b;cfK#Z&Zf~kH^Ztf)Vk2Iq#-FZqnitnJZIzvsu0H(Tm^HCr3ONUZy0-AlVmK@ z6O`SMn}~{uQHO+CW>R-M)=jY^W1X!`Pd=BKdri+nTJ7%iQW@K^+Pct{Zi;G*T`_&< z-D_;z`lK%QIV}6CA=oHt(mV3FM(fYIBZz=XueHStOr6ao5j`*8Y-rt4Otq!ob(Gxm z=HgeYOm%Th_2r`=@ALa$VwRWx8GNUHtp;Dcmca1# z!txt;3wzHuW>@d9$=Go^TiLeC-|=2yZ)=I0OZk%Zp>%tA3Y}OUk}!m>rs6OH=f> z5=aZjOZ}X7%BZo3Atc{*tJ$<Vi>iR-~|5rBCT0q0iM7KfWG2^SMW`opIi@l6P3M zs>3n3TItULzNjg0uGOnnRn*qTJ1fw;oc_qZZOCN?%y6>KxyI#_$qYHTAxpyH{J^E+Ag#m9S& z`epH%r{&Oxky6$#YTfZ}L+w@7K7 zeZUoj_8@e?eoy5yFNbH2CLTVgKBDX<NXhdk7)L&dv@ojnuMvd0q$@q|vOW zI@!wJUJo4VZQoe>@Mn4Qi1;&fcm+-VHhuxE22=Fq0t#`faZ@qOCLq{F->zNt!CE5A zjXWhMXFNQ8pd!cm>RcHOP8eSrZi#Q0{Q8G)R(3Xe{P16gj?etZ6G#p@yEDGA(F9ou z>JVbKOW5ksgpP2kiS6qlA2isoGAlalzTmF;5u8tp)2H|DTVW1;4O3>}I|--GWMYv~ zo>tger3a((h9vq@kTAd~{GP-2!JO_;uCA!K4}$^d-)Dr?1S&?_go26=r;6ebS?GZ1 z^NW_GkMBomPdr5n7cPWljl2fa>F=vq*D3X^YZ~{_e7yT97N|!*ck;pKFh$mr;-+8i zTvH>X3Q6k35s$$)96$|nm%e$KP?-*)d#)TrMumX`ZUxqAd=L&7d2|%@b%IWO=l23) zIl-Trx(|7i#Da{9OPLcVo<6+oH*~(G>Ad$Nwr|>z6p=8xrtPV2bB&&wr~4G0rtfG9 zC_TCG?qEez`BQX^+KFUag4ef@4+CCyeu%<&8i(mGY&qk-4<(vj#cq=o9;r>2 z&)Sv##65na@?hj*e&#|}B7tr8`v&5l{h3irotib)1xXkq&EmFs;$s5MF*;Nzn1EEe zY11YwFF4FQO0&~f@TBp{$yyN1CMQa$5@-diuFmKx-d_ARMEKP!-nzfl7}>-*P#7JdiU-___=ePUiO_UU^#Y7oG|cjS6&z6k~#I@Gm7>0y4>Gd6oUj>m|8;Yv_T6_WHSzYd zg{v+$zK(az!DFEt!yX3@kainJ!h@epOiTpaL;GfR=9P|-kK2*~VT>~`t76lBdvlE( zepn;PmN7JxhGiUE6xsr;BJr`YYT%~vet(*S$QGm1l^R~V=z@MPQJ#Gr<}r2(`Y>x7 z=!phI!2&@z2iT1(aA_>SokQD49GWtyvd! zr#v(J-v0Iok4ij%3If&R001#E5+B3xor-2ReW9vlBfM zdkD%Yu?ykb@vd3(#VR=`2GE&+G@MK@JrHwB6=d&yFh}#KUkh=+058L@pytO{3OgeF zI&im1!mzM?`xA1?0kN@IK{j=!qJwLAe598_Z-$h_LcH)C5_JH_YLh76*VlqD_Tq;C zm;%su_xBUtr**d|ZA{Vy05NMAZi%=$koE=xWnb{@MKuJyBpz$kz-4rD7Nhebp*g) ziA(D7Q|+0P4tQFA1?OW}4dM?ZF%EIstk946Ibr5JVLxq5WC>j~9;4&hkBUa-<$Sxr zBM`@+C5%D&`T3+v5zGu)^G1FYN%kO4lc)*X%H_<*Bot$t;5J(;N5>N|n@sjQ)#>PXa<#4dtt#rQn#BT8P*!?}g7XdUI#|b^OYbH;_h;O}QDz=%<=} z9zP1i3IFt=_IYaN?`Izx&iuv(clJKCsgZO3^OO%dtiYj|9I+tz4{F_{(`Zlj2_2X?(ZCam4ajb8Lq*F5jY2P{`fhg zoSd8-3L23(7=Xvn9}E8<`u?6i-7xoeATh!Hfb#|KQ&yy)>wGW%rTsx{4ufuT8fvnnzL>Y!{>qcsO2rA-qdidEzFHY(_5A zh8&{{pC7|WhMSnH(Zbv4HlWDt*3TuUt&u@r%`*%V6qbw!hvSclbcQ-Lmg0hULW4F< zgf_u$o&j4qab#fw!b)saokTJ&3b?YHD}~|vi(sHpnj91)+{&h=WNg6h)w$!|62@6k zdZN)Sh5iX39??I7qO<}U6^b_eD||uPe zW^2%nR*IJaR|UAoXNW|PVlvxJN(URY^S9qG=@=~!pV8A#HmM{TxGtAjr(Ie$$TX_1 zEV#9r1T!Sh&R|fDu}+OHYWSY;myymItl}s_S@#?saUIBEj0}g*8YT5~qZfsoPptYV zsydM5EF}|>7_>Og{Hv%{NJLb*vD^a1338L7N>|I$}`jk}ut}_!I4%6dy z#Df=9MAAaX=TM}86D#J+U1|Sw=cA+c({J}br3^+rp@`7O(09R~XaQ?~XKf3I!`NRN zLgjWw+*Z9|+8oF4P$+3*w#3Mlx7V!KAkl3votJ#H=nA$fLv}HNU za>93zeL82#BME~bNt{B`1+)(C{xoy})yR^*JQ;6g(g#!|j+P+T6KI4y_G1ay{|H;k z;H3ot7VqysDLwme$&uebG6cD2M;vn3ck1uT#i)h3P8f#JLgUjdL7Uolna5^K_;&1;7S6bpi=WIy6RyY zo4m9|z{Z>jm?i~1M()lD9-x$fF|NjSnx8ydKCA#dyG=idL>Fz)L%zlm$d+i)%W@rk zhM62_(t?8kC^zZWYDv*W^8gssCv|=)N%V6=V#qW?g9W-P@<%5Jzj`6~dW<*UAf9VD zq_31H#v;E%ScWSbS7gD5)=h4>k3JHFEDJ=sO&z}ADzcx?rPangvB1BnO(QbOyTZcJ zv~cFp>SWVuoMdbdQjg&L8ozedW=XxIbb!JFunU+sCo2({1G6yX8v+Z^VFMEYd66Vw z8QSJVqw@fCPXhKo33y;wNGRR0<0+Cns+CSc>VR<|tvPSryot*-$#FdnK;zn<*&&61 z3r|E>XK5tR2zh1%xu>uS;ukPeX(`O0TSplTbWlL;A$!B#US!oR(lmsAdD84=Wo1<{ zi>5|tAa_O+)s!W=>zlZ!0>WcX06s(M(Bi2j6a{q=if=O7!L_aA;!B^JD7v}pNh)2@ z6J9QOjOujbtAP$cGw)(~cw~?2rrI3&aNsB!RSzz79j(8qLgpkY(cO;(6$(p9haZpE z(QOzNTI;@Np_lU_C^;+shS02Y=#?CeZGwjew14kly!qFgoRTxjfF%U3z{m*H;jEz9 z!ZvR3f<;TR#y@0$RzoSu>L$JcdnWo!O40mqx(QA7*yuQZ9HkV6d5l|%A2DL|OY$wM z>yp<#7-PtEF^$G|1c8{*7{JZU+dt=@?iePAK?t1OTo(W`B{~8SYCqcXq0)pC z87Y-ODxmq&93NQK9gI<+vH%JyUQ~OimQVnp>+IYq(6TV3R^q&l_NJ8(j7<4<-A=lwSkxb)t;_(`73`S~)71T*z z<#K*G+_nL5NUA&LC$ z$-!DO`8Z|0!;6#mF*XLDXDsmWW|Wb}`LKe+IK9O8V%^-mqXLoMJ1s}WpSc4+&bhud2l5QBlHMNBm z!Rn>Eokap}KJ@w3n76y9tn*m7pP<~6lBCAcSd2UU+GnBhNnF(Qndsx9W1S2BJ|h3O z%2fMs_Htdv8lPW`Da$FQvY_k{>*rdv=emy(f92&90K!n$=^SH63rU8j!U#;?DPU#q{x4bc9%x+jTZvlN9bD=L4O;$XOpU$I^{NW$I zJA4Y(=I;+YbN5q9r`dSNNwFC67GdGa^72T)IW@CqXV%~h zz`-c$Wf2^M9)g*K>YhoQ1(4=(ATLM=L>*kBF#IP_?+5^=s`oZwVPW(Q5m+YMc zCHbz9bqUdcSOByPjG~FKiO61o*MabTw7foL90QEC?-n4iA{nBgloV~yaP9aI1xdXP zRKl^G4_M~dOWQLqK9YS!FdiTrX%LHv!C4u>lRZmJ6c?rOzI)P6GY=c~SsL;(!a%>Sz|@pC3e)ZZY-FI)+$1D0WA3mDklNfffWP9 zi(6rho+;e%rmC7hv(_ZU0Ug))A`vE}xVyVDfs|j^h=gB4rEG!M@)INj(!qmHAOF4- z1psmN78iFB(j9q`)vR0!3^&@HkbKbl&_H8O0yGf>(NIOV6W~qAnq6$jB3vD_aex^{ zwm;$s<{pW{KfY*Q4&?01d}`#?L}a0 zYiQx14~GuKm6{(Y*e^RdWJz&0+L7?K`Y;A@VEij4N>}xCXm!=c=WCx?T^^OUP~djh zXBo^Kk#f9r=z^brcYcX&XAtgPFKSsS`oQX01#fBnz5V|T%qSFhkCTDXP+cCysGAX@ z<7UBcD2g#HMQE;x^;96%EJNG~yE%owj)roWpQm-67jv=D1lP>e4@Jj)D?|pn19FGI zhxXcIL8=p<8nmwg`cx7q zyphSCtNPd^Sf1Z!v&poW8!6WZQTwxe;BxM1cg z4n$h~A~_JJ0^b&WMPL@-l8r>pu=VIMbZvW}Pu!f$0@G=4*t{KPrd!{52BS2|2ec{}^OcKy|+1KXlO-OnEysnk z&$Z~UqTP1g61|`BV|wqty}9%EvbvuCbGCj`bl=p`;sjm8J|EYg=9z$tLij>pI4O4U z8c8LKS)n+2ILYY5mKuO8!T_E8l6TzqR@$-Ohu4f?BJEm6=~qR=2&nVII+ zFE;V)W7(f&sjv5{Hw8T{Uh2ggFuhlFq2H?edKxsb2Fcx)r^w7euuIqnlT+9O7aN$E z{!cOr;k4kNz+OSDB4jHfBw7!}Va<$UGD(pyB`~5l%pclM1Ure4tHA1GhwCDlbk0-X?nQ+Ae3B zhhT9)Yry0|nlf+qWI+=`ZWHwhM(vY$qRqu**YD{^EMKI3~5z0HUaBshu$4x*qY zt3VzXLIsIKD1C`{t0}*ZA@h#zo~_{IR6a?(C^mJ*y}J@8T}c&bsSgo%dn; z<^=%*>Q|FPSIsOtwWRgqm)8N$<_G>t-_|lCe{r(wYE-~4y4rpUkF?popn~KveeE(F zl(JZ0+?AAKa3NDcmN6kC#g)*b7(0(CrB=mVZhdRD9>hMa4g~p)aaNU*36b>113Zm zV&XzeF{*wZjvR1o0ExuVG#Ub{l9tlM0epOXQix1s5DgRFu#$cZ7>XAyI{-;um}bmC2cB^8{>d6l_x%EkrRV*oKRBu9lV=z z0yC{;vd?T7K>dM)hxdZT#42`PP@Z~(5C*Y`bW?-j2EoyV(1t$$ui^O1;WHz#Fjbd> zkA%X7^p5|M&qahb99eJ$=;J{b)jH1{?$9mUDS2=O7Vb~W|gP65Q z%jT>2o-8?t21J6uFfXJ*fFL96gHq{r&#zLP%XUVb7S^g4+c1&UU*})7bkJW~w5Q?Ku4sRWrV2@AdPIrsQ4!Om zgDN|g8vEz|m&ti9r(vGQoMnmRw12QSdlXMIn`U#sC6RoL$_c0>!GR)ELN$W-cb z$fH}1hJ#lUoPL}Yxs%zJ?bF7lF_Fsi+$HSHnz`VNxffE!8ifUCcdq3z8Bx%p*u>hK~lC10e+Aq-u2-U~%K+T2D*5YAgOti71#R>x@ z&YjY}T5>6LkJ#86rB(B+R#~4T&WCb0Blh6kZliqg;bgEX?^A)`5vPlrbrCwYu=De(ldHC+FbjKPz5Kzz4CJM892GhWb${anp5s47|R9%ScpATS2B zz7ILuhxw>}q^``od1eh^yGTjP9H5#)0oF2J_P*_MX=eKE%JqxB-VDw&n68it6Tw(f zPKHxY^acN}(cxJ8$<99}v+5ok{;PnOO+bI>VdB$9iS+Y2yuE#8sE#;CO-c)_oTSoH zp6jG1UGmKPZ8ha5yY-g}nL+jw!Qoa2KTr^uE(Fsg7@axk6C+&d7+Krqlb6%d?2AJoc3TT&Fn-l=#9_v~ByCCAfs#eD6%f3066 zlT#?^9u^f>V}?^v-Gfeg(Lg@Bl7PROve?!J$v$D1|@Jsh!?*zo!XQFXE)) zs+p;S1;MyHfMus7IuJp>+e8n$U>=l)mR8aBW#mP3&3xm+XgqNYxtp2^8f!=uLpj9! z5tLg&98<7Y6xz{86zXWz&+Tr28gg99h}4O9?<$nZx3O(xTrvcrk_+_{;99Eg^3)KY*XmVfj#7A|{k>~)# zz>#Z$(|b|iVP_Fu3CJVse0IJP-~6j!PVnn!xsmR4noBMSl?slK4CP*@untuZX|acB zk6k}n+B&sVfpqy<4UfpUPAe05nO+3>9?B&&zERjKTxl2s-OKiY zG%Jp^{xfGv@e=m7sY94Iy%I_AhzV@a$WZIE)G_<|v@)tUEv=H60x}iZ5naAk=H?=z z_*VW2H|%6hPX2W%+ifcNmc5TESN?wQ9@BNJ#+ThX>C=;2yg(?wu)3uA?YtYs;TKMw z&}+40c6h6<3~3T}zl|xcZI{VhXq42b#|W*B!v^II`x;9X!MMMN})B zi;aGlW`h||T}-P7@+UnSdOfNKnhyGVFW9ILc(7*=!hvk)rb1XqFct+uVc~^HW?k>l zXrZNYGH1tVvl&D-GJ%wI&f3fI^W4uNumyf#sD1Di#&CvYZ0cnPBZGEIzzY0W7FbG2 zNs0Nx`bw@z(*@boQkre0e*P?)_k(SWs1nu=y-t_?=H<2c&3oe9doExxzp_-E?PKM5 z{gSXRLm0Ew+ZSM#w%tqEI>LMn2Ib-1+}GsSr8>oG^6^$lDo^YO8}jU;R&he(pny=M zQ1CzMut;(J&<$9zAN|^_soA?qh5_us1Y3s~((%$}Fi#fQ4TJIiYo4r5{Glw<@!L~I z6b11s&l;4RvIQLtW(Rtd&C*KhpXaroZy7p2(6Za_ViP?Pt3FpeV2)5xDlCTJGyxMr zA${jrC20nYPCzZ5a2vF0qJaS9WK z=e`D=LCFL8!oKu({ab}GLq%=ib-oYnuJ-*|BBP&yr8?i6E0MF3SpIP9*n&I?#UCTq z(HHQb_&fk8^7h8Nxbz#J8;}ofy&oRbA_fF)fNsAQg|VI**bae&M_oONY&h#$?Ebtm zXClF9El>*@FN$$0Fu03nNkjiQ>}a65LEj?<=XhqD&7?32e~uZ1gJ}02HiZWu5(;

      L-yz73Lyvs@BzC_&rR?ZRhZnU}0 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ĄnYm0knPmTM_5O*$L!#*D=oyhG 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%0AITlD{uj4`XhX{6njjl2UXwz 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

      v`WMUk~4RTnbu-PRw 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 z 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

      b?S-+*3^ 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�RZXZMY35zIYO*4Kr>@KD}5M9=Y< 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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/public/images/schematicsGuide.png b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/public/images/schematicsGuide.png new file mode 100644 index 0000000000000000000000000000000000000000..6933489b9edb613b2bd450717849eefcf7dcfc49 GIT binary patch literal 52343 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 literal 0 HcmV?d00001 diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/public/images/thumb.png b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/public/images/thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..806960498486782b91c90ee37aa12b1651d8c9ee GIT binary patch literal 31021 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 4) { + payload.deviceName = deviceName; + invokerUtil.post( + downloadDeviceAPI, + payload, + function (data, textStatus, jqxhr) { + doAction(data); + }, + function (data) { + doAction(data); + } + ); + } else if (deviceName) { + $('.controls').append(''); + $('.control-group').removeClass('success').addClass('error'); + } else { + $('.controls').append(''); + $('.control-group').removeClass('success').addClass('error'); + } + }); + + $("a#download-device-cancel-link").click(function () { + hideAgentDownloadPopup(); + }); + + }); +} + +function downloadAgent() { + var deviceName; + $('.new-device-name').each(function () { + if (this.value != "") { + deviceName = this.value; + } + }); + var deviceNameFormat = /^[^~?!#$:;%^*`+={}\[\]\\()|<>,'"]{1,30}$/; + if (deviceName && deviceNameFormat.test(deviceName)) { + $('#downloadForm').submit(); + hideAgentDownloadPopup(); + $(modalPopupContent).html($('#device-agent-downloading-content').html()); + showAgentDownloadPopup(); + setTimeout(function () { + hideAgentDownloadPopup(); + }, 1000); + } else { + $("#invalid-username-error-msg span").text("Invalid device name"); + $("#invalid-username-error-msg").removeClass("hidden"); + } +} + +function doAction(data) { + //if it is saml redirection response + if (data.status == null) { + document.write(data); + } + + if (data.status == "200") { + $(modalPopupContent).html($('#download-device-modal-content-links').html()); + $("input#download-device-url").val(data.responseText); + $("input#download-device-url").focus(function () { + $(this).select(); + }); + showAgentDownloadPopup(); + } else if (data.status == "401") { + $(modalPopupContent).html($('#device-401-content').html()); + $("#device-401-link").click(function () { + window.location = "/devicemgt/login"; + }); + showAgentDownloadPopup(); + } else if (data == "403") { + $(modalPopupContent).html($('#device-403-content').html()); + $("#device-403-link").click(function () { + window.location = "/devicemgt/login"; + }); + showAgentDownloadPopup(); + } else { + $(modalPopupContent).html($('#device-unexpected-error-content').html()); + $("a#device-unexpected-error-link").click(function () { + hideAgentDownloadPopup(); + }); + } +} \ No newline at end of file diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/public/js/jquery.validate.js b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/public/js/jquery.validate.js new file mode 100644 index 000000000..fe7ecf07a --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/public/js/jquery.validate.js @@ -0,0 +1,1227 @@ +/* + * 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. + */ + +(function($) { + +$.extend($.fn, { + // http://docs.jquery.com/Plugins/Validation/validate + validate: function( options ) { + + // if nothing is selected, return nothing; can't chain anyway + if ( !this.length ) { + if ( options && options.debug && window.console ) { + console.warn( "Nothing selected, can't validate, returning nothing." ); + } + return; + } + + // check if a validator for this form was already created + var validator = $.data( this[0], "validator" ); + if ( validator ) { + return validator; + } + + // Add novalidate tag if HTML5. + this.attr( "novalidate", "novalidate" ); + + validator = new $.validator( options, this[0] ); + $.data( this[0], "validator", validator ); + + if ( validator.settings.onsubmit ) { + + this.validateDelegate( ":submit", "click", function( event ) { + if ( validator.settings.submitHandler ) { + validator.submitButton = event.target; + } + // allow suppressing validation by adding a cancel class to the submit button + if ( $(event.target).hasClass("cancel") ) { + validator.cancelSubmit = true; + } + }); + + // validate the form on submit + this.submit( function( event ) { + if ( validator.settings.debug ) { + // prevent form submit to be able to see console output + event.preventDefault(); + } + function handle() { + var hidden; + if ( validator.settings.submitHandler ) { + if ( validator.submitButton ) { + // insert a hidden input as a replacement for the missing submit button + hidden = $("").attr("name", validator.submitButton.name).val(validator.submitButton.value).appendTo(validator.currentForm); + } + validator.settings.submitHandler.call( validator, validator.currentForm, event ); + if ( validator.submitButton ) { + // and clean up afterwards; thanks to no-block-scope, hidden can be referenced + hidden.remove(); + } + return false; + } + return true; + } + + // prevent submit for invalid forms or custom submit handlers + if ( validator.cancelSubmit ) { + validator.cancelSubmit = false; + return handle(); + } + if ( validator.form() ) { + if ( validator.pendingRequest ) { + validator.formSubmitted = true; + return false; + } + return handle(); + } else { + validator.focusInvalid(); + return false; + } + }); + } + + return validator; + }, + // http://docs.jquery.com/Plugins/Validation/valid + valid: function() { + if ( $(this[0]).is("form")) { + return this.validate().form(); + } else { + var valid = true; + var validator = $(this[0].form).validate(); + this.each(function() { + valid &= validator.element(this); + }); + return valid; + } + }, + // attributes: space seperated list of attributes to retrieve and remove + removeAttrs: function( attributes ) { + var result = {}, + $element = this; + $.each(attributes.split(/\s/), function( index, value ) { + result[value] = $element.attr(value); + $element.removeAttr(value); + }); + return result; + }, + // http://docs.jquery.com/Plugins/Validation/rules + rules: function( command, argument ) { + var element = this[0]; + + if ( command ) { + var settings = $.data(element.form, "validator").settings; + var staticRules = settings.rules; + var existingRules = $.validator.staticRules(element); + switch(command) { + case "add": + $.extend(existingRules, $.validator.normalizeRule(argument)); + staticRules[element.name] = existingRules; + if ( argument.messages ) { + settings.messages[element.name] = $.extend( settings.messages[element.name], argument.messages ); + } + break; + case "remove": + if ( !argument ) { + delete staticRules[element.name]; + return existingRules; + } + var filtered = {}; + $.each(argument.split(/\s/), function( index, method ) { + filtered[method] = existingRules[method]; + delete existingRules[method]; + }); + return filtered; + } + } + + var data = $.validator.normalizeRules( + $.extend( + {}, + $.validator.classRules(element), + $.validator.attributeRules(element), + $.validator.dataRules(element), + $.validator.staticRules(element) + ), element); + + // make sure required is at front + if ( data.required ) { + var param = data.required; + delete data.required; + data = $.extend({required: param}, data); + } + + return data; + } +}); + +// Custom selectors +$.extend($.expr[":"], { + // http://docs.jquery.com/Plugins/Validation/blank + blank: function( a ) { return !$.trim("" + a.value); }, + // http://docs.jquery.com/Plugins/Validation/filled + filled: function( a ) { return !!$.trim("" + a.value); }, + // http://docs.jquery.com/Plugins/Validation/unchecked + unchecked: function( a ) { return !a.checked; } +}); + +// constructor for validator +$.validator = function( options, form ) { + this.settings = $.extend( true, {}, $.validator.defaults, options ); + this.currentForm = form; + this.init(); +}; + +$.validator.format = function( source, params ) { + if ( arguments.length === 1 ) { + return function() { + var args = $.makeArray(arguments); + args.unshift(source); + return $.validator.format.apply( this, args ); + }; + } + if ( arguments.length > 2 && params.constructor !== Array ) { + params = $.makeArray(arguments).slice(1); + } + if ( params.constructor !== Array ) { + params = [ params ]; + } + $.each(params, function( i, n ) { + source = source.replace( new RegExp("\\{" + i + "\\}", "g"), function() { + return n; + }); + }); + return source; +}; + +$.extend($.validator, { + + defaults: { + messages: {}, + groups: {}, + rules: {}, + errorClass: "error", + validClass: "valid", + errorElement: "label", + focusInvalid: true, + errorContainer: $([]), + errorLabelContainer: $([]), + onsubmit: true, + ignore: ":hidden", + ignoreTitle: false, + onfocusin: function( element, event ) { + this.lastActive = element; + + // hide error label and remove error class on focus if enabled + if ( this.settings.focusCleanup && !this.blockFocusCleanup ) { + if ( this.settings.unhighlight ) { + this.settings.unhighlight.call( this, element, this.settings.errorClass, this.settings.validClass ); + } + this.addWrapper(this.errorsFor(element)).hide(); + } + }, + onfocusout: function( element, event ) { + if ( !this.checkable(element) && (element.name in this.submitted || !this.optional(element)) ) { + this.element(element); + } + }, + onkeyup: function( element, event ) { + if ( event.which === 9 && this.elementValue(element) === "" ) { + return; + } else if ( element.name in this.submitted || element === this.lastElement ) { + this.element(element); + } + }, + onclick: function( element, event ) { + // click on selects, radiobuttons and checkboxes + if ( element.name in this.submitted ) { + this.element(element); + } + // or option elements, check parent select in that case + else if ( element.parentNode.name in this.submitted ) { + this.element(element.parentNode); + } + }, + highlight: function( element, errorClass, validClass ) { + if ( element.type === "radio" ) { + this.findByName(element.name).addClass(errorClass).removeClass(validClass); + } else { + $(element).addClass(errorClass).removeClass(validClass); + } + }, + unhighlight: function( element, errorClass, validClass ) { + if ( element.type === "radio" ) { + this.findByName(element.name).removeClass(errorClass).addClass(validClass); + } else { + $(element).removeClass(errorClass).addClass(validClass); + } + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/setDefaults + setDefaults: function( settings ) { + $.extend( $.validator.defaults, settings ); + }, + + messages: { + required: "This field is required.", + remote: "Please fix this field.", + email: "Please enter a valid email address.", + url: "Please enter a valid URL.", + date: "Please enter a valid date.", + dateISO: "Please enter a valid date (ISO).", + number: "Please enter a valid number.", + digits: "Please enter only digits.", + creditcard: "Please enter a valid credit card number.", + equalTo: "Please enter the same value again.", + maxlength: $.validator.format("Please enter no more than {0} characters."), + minlength: $.validator.format("Please enter at least {0} characters."), + rangelength: $.validator.format("Please enter a value between {0} and {1} characters long."), + range: $.validator.format("Please enter a value between {0} and {1}."), + max: $.validator.format("Please enter a value less than or equal to {0}."), + min: $.validator.format("Please enter a value greater than or equal to {0}.") + }, + + autoCreateRanges: false, + + prototype: { + + init: function() { + this.labelContainer = $(this.settings.errorLabelContainer); + this.errorContext = this.labelContainer.length && this.labelContainer || $(this.currentForm); + this.containers = $(this.settings.errorContainer).add( this.settings.errorLabelContainer ); + this.submitted = {}; + this.valueCache = {}; + this.pendingRequest = 0; + this.pending = {}; + this.invalid = {}; + this.reset(); + + var groups = (this.groups = {}); + $.each(this.settings.groups, function( key, value ) { + if ( typeof value === "string" ) { + value = value.split(/\s/); + } + $.each(value, function( index, name ) { + groups[name] = key; + }); + }); + var rules = this.settings.rules; + $.each(rules, function( key, value ) { + rules[key] = $.validator.normalizeRule(value); + }); + + function delegate(event) { + var validator = $.data(this[0].form, "validator"), + eventType = "on" + event.type.replace(/^validate/, ""); + if ( validator.settings[eventType] ) { + validator.settings[eventType].call(validator, this[0], event); + } + } + $(this.currentForm) + .validateDelegate(":text, [type='password'], [type='file'], select, textarea, " + + "[type='number'], [type='search'] ,[type='tel'], [type='url'], " + + "[type='email'], [type='datetime'], [type='date'], [type='month'], " + + "[type='week'], [type='time'], [type='datetime-local'], " + + "[type='range'], [type='color'] ", + "focusin focusout keyup", delegate) + .validateDelegate("[type='radio'], [type='checkbox'], select, option", "click", delegate); + + if ( this.settings.invalidHandler ) { + $(this.currentForm).bind("invalid-form.validate", this.settings.invalidHandler); + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/form + form: function() { + this.checkForm(); + $.extend(this.submitted, this.errorMap); + this.invalid = $.extend({}, this.errorMap); + if ( !this.valid() ) { + $(this.currentForm).triggerHandler("invalid-form", [this]); + } + this.showErrors(); + return this.valid(); + }, + + checkForm: function() { + this.prepareForm(); + for ( var i = 0, elements = (this.currentElements = this.elements()); elements[i]; i++ ) { + this.check( elements[i] ); + } + return this.valid(); + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/element + element: function( element ) { + element = this.validationTargetFor( this.clean( element ) ); + this.lastElement = element; + this.prepareElement( element ); + this.currentElements = $(element); + var result = this.check( element ) !== false; + if ( result ) { + delete this.invalid[element.name]; + } else { + this.invalid[element.name] = true; + } + if ( !this.numberOfInvalids() ) { + // Hide error containers on last error + this.toHide = this.toHide.add( this.containers ); + } + this.showErrors(); + return result; + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/showErrors + showErrors: function( errors ) { + if ( errors ) { + // add items to error list and map + $.extend( this.errorMap, errors ); + this.errorList = []; + for ( var name in errors ) { + this.errorList.push({ + message: errors[name], + element: this.findByName(name)[0] + }); + } + // remove items from success list + this.successList = $.grep( this.successList, function( element ) { + return !(element.name in errors); + }); + } + if ( this.settings.showErrors ) { + this.settings.showErrors.call( this, this.errorMap, this.errorList ); + } else { + this.defaultShowErrors(); + } + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/resetForm + resetForm: function() { + if ( $.fn.resetForm ) { + $(this.currentForm).resetForm(); + } + this.submitted = {}; + this.lastElement = null; + this.prepareForm(); + this.hideErrors(); + this.elements().removeClass( this.settings.errorClass ).removeData( "previousValue" ); + }, + + numberOfInvalids: function() { + return this.objectLength(this.invalid); + }, + + objectLength: function( obj ) { + var count = 0; + for ( var i in obj ) { + count++; + } + return count; + }, + + hideErrors: function() { + this.addWrapper( this.toHide ).hide(); + }, + + valid: function() { + return this.size() === 0; + }, + + size: function() { + return this.errorList.length; + }, + + focusInvalid: function() { + if ( this.settings.focusInvalid ) { + try { + $(this.findLastActive() || this.errorList.length && this.errorList[0].element || []) + .filter(":visible") + .focus() + // manually trigger focusin event; without it, focusin handler isn't called, findLastActive won't have anything to find + .trigger("focusin"); + } catch(e) { + // ignore IE throwing errors when focusing hidden elements + } + } + }, + + findLastActive: function() { + var lastActive = this.lastActive; + return lastActive && $.grep(this.errorList, function( n ) { + return n.element.name === lastActive.name; + }).length === 1 && lastActive; + }, + + elements: function() { + var validator = this, + rulesCache = {}; + + // select all valid inputs inside the form (no submit or reset buttons) + return $(this.currentForm) + .find("input, select, textarea") + .not(":submit, :reset, :image, [disabled]") + .not( this.settings.ignore ) + .filter(function() { + if ( !this.name ) { + if ( window.console ) { + console.error( "%o has no name assigned", this ); + } + throw new Error( "Failed to validate, found an element with no name assigned. See console for element reference." ); + } + + // select only the first element for each name, and only those with rules specified + if ( this.name in rulesCache || !validator.objectLength($(this).rules()) ) { + return false; + } + + rulesCache[this.name] = true; + return true; + }); + }, + + clean: function( selector ) { + return $(selector)[0]; + }, + + errors: function() { + var errorClass = this.settings.errorClass.replace(" ", "."); + return $(this.settings.errorElement + "." + errorClass, this.errorContext); + }, + + reset: function() { + this.successList = []; + this.errorList = []; + this.errorMap = {}; + this.toShow = $([]); + this.toHide = $([]); + this.currentElements = $([]); + }, + + prepareForm: function() { + this.reset(); + this.toHide = this.errors().add( this.containers ); + }, + + prepareElement: function( element ) { + this.reset(); + this.toHide = this.errorsFor(element); + }, + + elementValue: function( element ) { + var type = $(element).attr("type"), + val = $(element).val(); + + if ( type === "radio" || type === "checkbox" ) { + return $("input[name='" + $(element).attr("name") + "']:checked").val(); + } + + if ( typeof val === "string" ) { + return val.replace(/\r/g, ""); + } + return val; + }, + + check: function( element ) { + element = this.validationTargetFor( this.clean( element ) ); + + var rules = $(element).rules(); + var dependencyMismatch = false; + var val = this.elementValue(element); + var result; + + for (var method in rules ) { + var rule = { method: method, parameters: rules[method] }; + try { + + result = $.validator.methods[method].call( this, val, element, rule.parameters ); + + // if a method indicates that the field is optional and therefore valid, + // don't mark it as valid when there are no other rules + if ( result === "dependency-mismatch" ) { + dependencyMismatch = true; + continue; + } + dependencyMismatch = false; + + if ( result === "pending" ) { + this.toHide = this.toHide.not( this.errorsFor(element) ); + return; + } + + if ( !result ) { + this.formatAndAdd( element, rule ); + return false; + } + } catch(e) { + if ( this.settings.debug && window.console ) { + console.log( "Exception occured when checking element " + element.id + ", check the '" + rule.method + "' method.", e ); + } + throw e; + } + } + if ( dependencyMismatch ) { + return; + } + if ( this.objectLength(rules) ) { + this.successList.push(element); + } + return true; + }, + + // return the custom message for the given element and validation method + // specified in the element's HTML5 data attribute + customDataMessage: function( element, method ) { + return $(element).data("msg-" + method.toLowerCase()) || (element.attributes && $(element).attr("data-msg-" + method.toLowerCase())); + }, + + // return the custom message for the given element name and validation method + customMessage: function( name, method ) { + var m = this.settings.messages[name]; + return m && (m.constructor === String ? m : m[method]); + }, + + // return the first defined argument, allowing empty strings + findDefined: function() { + for(var i = 0; i < arguments.length; i++) { + if ( arguments[i] !== undefined ) { + return arguments[i]; + } + } + return undefined; + }, + + defaultMessage: function( element, method ) { + return this.findDefined( + this.customMessage( element.name, method ), + this.customDataMessage( element, method ), + // title is never undefined, so handle empty string as undefined + !this.settings.ignoreTitle && element.title || undefined, + $.validator.messages[method], + "Warning: No message defined for " + element.name + "" + ); + }, + + formatAndAdd: function( element, rule ) { + var message = this.defaultMessage( element, rule.method ), + theregex = /\$?\{(\d+)\}/g; + if ( typeof message === "function" ) { + message = message.call(this, rule.parameters, element); + } else if (theregex.test(message)) { + message = $.validator.format(message.replace(theregex, "{$1}"), rule.parameters); + } + this.errorList.push({ + message: message, + element: element + }); + + this.errorMap[element.name] = message; + this.submitted[element.name] = message; + }, + + addWrapper: function( toToggle ) { + if ( this.settings.wrapper ) { + toToggle = toToggle.add( toToggle.parent( this.settings.wrapper ) ); + } + return toToggle; + }, + + defaultShowErrors: function() { + var i, elements; + for ( i = 0; this.errorList[i]; i++ ) { + var error = this.errorList[i]; + if ( this.settings.highlight ) { + this.settings.highlight.call( this, error.element, this.settings.errorClass, this.settings.validClass ); + } + this.showLabel( error.element, error.message ); + } + if ( this.errorList.length ) { + this.toShow = this.toShow.add( this.containers ); + } + if ( this.settings.success ) { + for ( i = 0; this.successList[i]; i++ ) { + this.showLabel( this.successList[i] ); + } + } + if ( this.settings.unhighlight ) { + for ( i = 0, elements = this.validElements(); elements[i]; i++ ) { + this.settings.unhighlight.call( this, elements[i], this.settings.errorClass, this.settings.validClass ); + } + } + this.toHide = this.toHide.not( this.toShow ); + this.hideErrors(); + this.addWrapper( this.toShow ).show(); + }, + + validElements: function() { + return this.currentElements.not(this.invalidElements()); + }, + + invalidElements: function() { + return $(this.errorList).map(function() { + return this.element; + }); + }, + + showLabel: function( element, message ) { + var label = this.errorsFor( element ); + if ( label.length ) { + // refresh error/success class + label.removeClass( this.settings.validClass ).addClass( this.settings.errorClass ); + + // check if we have a generated label, replace the message then + if ( label.attr("generated") ) { + label.html(message); + } + } else { + // create label + label = $("<" + this.settings.errorElement + "/>") + .attr({"for": this.idOrName(element), generated: true}) + .addClass(this.settings.errorClass) + .html(message || ""); + if ( this.settings.wrapper ) { + // make sure the element is visible, even in IE + // actually showing the wrapped element is handled elsewhere + label = label.hide().show().wrap("<" + this.settings.wrapper + "/>").parent(); + } + if ( !this.labelContainer.append(label).length ) { + if ( this.settings.errorPlacement ) { + this.settings.errorPlacement(label, $(element) ); + } else { + label.insertAfter(element); + } + } + } + if ( !message && this.settings.success ) { + label.text(""); + if ( typeof this.settings.success === "string" ) { + label.addClass( this.settings.success ); + } else { + this.settings.success( label, element ); + } + } + this.toShow = this.toShow.add(label); + }, + + errorsFor: function( element ) { + var name = this.idOrName(element); + return this.errors().filter(function() { + return $(this).attr("for") === name; + }); + }, + + idOrName: function( element ) { + return this.groups[element.name] || (this.checkable(element) ? element.name : element.id || element.name); + }, + + validationTargetFor: function( element ) { + // if radio/checkbox, validate first element in group instead + if ( this.checkable(element) ) { + element = this.findByName( element.name ).not(this.settings.ignore)[0]; + } + return element; + }, + + checkable: function( element ) { + return (/radio|checkbox/i).test(element.type); + }, + + findByName: function( name ) { + return $(this.currentForm).find("[name='" + name + "']"); + }, + + getLength: function( value, element ) { + switch( element.nodeName.toLowerCase() ) { + case "select": + return $("option:selected", element).length; + case "input": + if ( this.checkable( element) ) { + return this.findByName(element.name).filter(":checked").length; + } + } + return value.length; + }, + + depend: function( param, element ) { + return this.dependTypes[typeof param] ? this.dependTypes[typeof param](param, element) : true; + }, + + dependTypes: { + "boolean": function( param, element ) { + return param; + }, + "string": function( param, element ) { + return !!$(param, element.form).length; + }, + "function": function( param, element ) { + return param(element); + } + }, + + optional: function( element ) { + var val = this.elementValue(element); + return !$.validator.methods.required.call(this, val, element) && "dependency-mismatch"; + }, + + startRequest: function( element ) { + if ( !this.pending[element.name] ) { + this.pendingRequest++; + this.pending[element.name] = true; + } + }, + + stopRequest: function( element, valid ) { + this.pendingRequest--; + // sometimes synchronization fails, make sure pendingRequest is never < 0 + if ( this.pendingRequest < 0 ) { + this.pendingRequest = 0; + } + delete this.pending[element.name]; + if ( valid && this.pendingRequest === 0 && this.formSubmitted && this.form() ) { + $(this.currentForm).submit(); + this.formSubmitted = false; + } else if (!valid && this.pendingRequest === 0 && this.formSubmitted) { + $(this.currentForm).triggerHandler("invalid-form", [this]); + this.formSubmitted = false; + } + }, + + previousValue: function( element ) { + return $.data(element, "previousValue") || $.data(element, "previousValue", { + old: null, + valid: true, + message: this.defaultMessage( element, "remote" ) + }); + } + + }, + + classRuleSettings: { + required: {required: true}, + email: {email: true}, + url: {url: true}, + date: {date: true}, + dateISO: {dateISO: true}, + number: {number: true}, + digits: {digits: true}, + creditcard: {creditcard: true} + }, + + addClassRules: function( className, rules ) { + if ( className.constructor === String ) { + this.classRuleSettings[className] = rules; + } else { + $.extend(this.classRuleSettings, className); + } + }, + + classRules: function( element ) { + var rules = {}; + var classes = $(element).attr("class"); + if ( classes ) { + $.each(classes.split(" "), function() { + if ( this in $.validator.classRuleSettings ) { + $.extend(rules, $.validator.classRuleSettings[this]); + } + }); + } + return rules; + }, + + attributeRules: function( element ) { + var rules = {}; + var $element = $(element); + + for (var method in $.validator.methods) { + var value; + + // support for in both html5 and older browsers + if ( method === "required" ) { + value = $element.get(0).getAttribute(method); + // Some browsers return an empty string for the required attribute + // and non-HTML5 browsers might have required="" markup + if ( value === "" ) { + value = true; + } + // force non-HTML5 browsers to return bool + value = !!value; + } else { + value = $element.attr(method); + } + + if ( value ) { + rules[method] = value; + } else if ( $element[0].getAttribute("type") === method ) { + rules[method] = true; + } + } + + // maxlength may be returned as -1, 2147483647 (IE) and 524288 (safari) for text inputs + if ( rules.maxlength && /-1|2147483647|524288/.test(rules.maxlength) ) { + delete rules.maxlength; + } + + return rules; + }, + + dataRules: function( element ) { + var method, value, + rules = {}, $element = $(element); + for (method in $.validator.methods) { + value = $element.data("rule-" + method.toLowerCase()); + if ( value !== undefined ) { + rules[method] = value; + } + } + return rules; + }, + + staticRules: function( element ) { + var rules = {}; + var validator = $.data(element.form, "validator"); + if ( validator.settings.rules ) { + rules = $.validator.normalizeRule(validator.settings.rules[element.name]) || {}; + } + return rules; + }, + + normalizeRules: function( rules, element ) { + // handle dependency check + $.each(rules, function( prop, val ) { + // ignore rule when param is explicitly false, eg. required:false + if ( val === false ) { + delete rules[prop]; + return; + } + if ( val.param || val.depends ) { + var keepRule = true; + switch (typeof val.depends) { + case "string": + keepRule = !!$(val.depends, element.form).length; + break; + case "function": + keepRule = val.depends.call(element, element); + break; + } + if ( keepRule ) { + rules[prop] = val.param !== undefined ? val.param : true; + } else { + delete rules[prop]; + } + } + }); + + // evaluate parameters + $.each(rules, function( rule, parameter ) { + rules[rule] = $.isFunction(parameter) ? parameter(element) : parameter; + }); + + // clean number parameters + $.each(["minlength", "maxlength", "min", "max"], function() { + if ( rules[this] ) { + rules[this] = Number(rules[this]); + } + }); + $.each(["rangelength", "range"], function() { + var parts; + if ( rules[this] ) { + if ( $.isArray(rules[this]) ) { + rules[this] = [Number(rules[this][0]), Number(rules[this][1])]; + } else if ( typeof rules[this] === "string" ) { + parts = rules[this].split(/[\s,]+/); + rules[this] = [Number(parts[0]), Number(parts[1])]; + } + } + }); + + if ( $.validator.autoCreateRanges ) { + // auto-create ranges + if ( rules.min && rules.max ) { + rules.range = [rules.min, rules.max]; + delete rules.min; + delete rules.max; + } + if ( rules.minlength && rules.maxlength ) { + rules.rangelength = [rules.minlength, rules.maxlength]; + delete rules.minlength; + delete rules.maxlength; + } + } + + return rules; + }, + + // Converts a simple string to a {string: true} rule, e.g., "required" to {required:true} + normalizeRule: function( data ) { + if ( typeof data === "string" ) { + var transformed = {}; + $.each(data.split(/\s/), function() { + transformed[this] = true; + }); + data = transformed; + } + return data; + }, + + // http://docs.jquery.com/Plugins/Validation/Validator/addMethod + addMethod: function( name, method, message ) { + $.validator.methods[name] = method; + $.validator.messages[name] = message !== undefined ? message : $.validator.messages[name]; + if ( method.length < 3 ) { + $.validator.addClassRules(name, $.validator.normalizeRule(name)); + } + }, + + methods: { + + // http://docs.jquery.com/Plugins/Validation/Methods/required + required: function( value, element, param ) { + // check if dependency is met + if ( !this.depend(param, element) ) { + return "dependency-mismatch"; + } + if ( element.nodeName.toLowerCase() === "select" ) { + // could be an array for select-multiple or a string, both are fine this way + var val = $(element).val(); + return val && val.length > 0; + } + if ( this.checkable(element) ) { + return this.getLength(value, element) > 0; + } + return $.trim(value).length > 0; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/remote + remote: function( value, element, param ) { + if ( this.optional(element) ) { + return "dependency-mismatch"; + } + + var previous = this.previousValue(element); + if (!this.settings.messages[element.name] ) { + this.settings.messages[element.name] = {}; + } + previous.originalMessage = this.settings.messages[element.name].remote; + this.settings.messages[element.name].remote = previous.message; + + param = typeof param === "string" && {url:param} || param; + + if ( previous.old === value ) { + return previous.valid; + } + + previous.old = value; + var validator = this; + this.startRequest(element); + var data = {}; + data[element.name] = value; + $.ajax($.extend(true, { + url: param, + mode: "abort", + port: "validate" + element.name, + dataType: "json", + data: data, + success: function( response ) { + validator.settings.messages[element.name].remote = previous.originalMessage; + var valid = response === true || response === "true"; + if ( valid ) { + var submitted = validator.formSubmitted; + validator.prepareElement(element); + validator.formSubmitted = submitted; + validator.successList.push(element); + delete validator.invalid[element.name]; + validator.showErrors(); + } else { + var errors = {}; + var message = response || validator.defaultMessage( element, "remote" ); + errors[element.name] = previous.message = $.isFunction(message) ? message(value) : message; + validator.invalid[element.name] = true; + validator.showErrors(errors); + } + previous.valid = valid; + validator.stopRequest(element, valid); + } + }, param)); + return "pending"; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/minlength + minlength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || length >= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/maxlength + maxlength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || length <= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/rangelength + rangelength: function( value, element, param ) { + var length = $.isArray( value ) ? value.length : this.getLength($.trim(value), element); + return this.optional(element) || ( length >= param[0] && length <= param[1] ); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/min + min: function( value, element, param ) { + return this.optional(element) || value >= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/max + max: function( value, element, param ) { + return this.optional(element) || value <= param; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/range + range: function( value, element, param ) { + return this.optional(element) || ( value >= param[0] && value <= param[1] ); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/email + email: function( value, element ) { + // contributed by Scott Gonzalez: http://projects.scottsplayground.com/email_address_validation/ + return this.optional(element) || /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/url + url: function( value, element ) { + // contributed by Scott Gonzalez: http://projects.scottsplayground.com/iri/ + return this.optional(element) || /^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/date + date: function( value, element ) { + return this.optional(element) || !/Invalid|NaN/.test(new Date(value).toString()); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/dateISO + dateISO: function( value, element ) { + return this.optional(element) || /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/number + number: function( value, element ) { + return this.optional(element) || /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/digits + digits: function( value, element ) { + return this.optional(element) || /^\d+$/.test(value); + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/creditcard + // based on http://en.wikipedia.org/wiki/Luhn + creditcard: function( value, element ) { + if ( this.optional(element) ) { + return "dependency-mismatch"; + } + // accept only spaces, digits and dashes + if ( /[^0-9 \-]+/.test(value) ) { + return false; + } + var nCheck = 0, + nDigit = 0, + bEven = false; + + value = value.replace(/\D/g, ""); + + for (var n = value.length - 1; n >= 0; n--) { + var cDigit = value.charAt(n); + nDigit = parseInt(cDigit, 10); + if ( bEven ) { + if ( (nDigit *= 2) > 9 ) { + nDigit -= 9; + } + } + nCheck += nDigit; + bEven = !bEven; + } + + return (nCheck % 10) === 0; + }, + + // http://docs.jquery.com/Plugins/Validation/Methods/equalTo + equalTo: function( value, element, param ) { + // bind to the blur event of the target in order to revalidate whenever the target field is updated + // TODO find a way to bind the event just once, avoiding the unbind-rebind overhead + var target = $(param); + if ( this.settings.onfocusout ) { + target.unbind(".validate-equalTo").bind("blur.validate-equalTo", function() { + $(element).valid(); + }); + } + return value === target.val(); + } + + } + +}); + +// deprecated, use $.validator.format instead +$.format = $.validator.format; + +}(jQuery)); + +// ajax mode: abort +// usage: $.ajax({ mode: "abort"[, port: "uniqueport"]}); +// if mode:"abort" is used, the previous request on that port (port can be undefined) is aborted via XMLHttpRequest.abort() +(function($) { + var pendingRequests = {}; + // Use a prefilter if available (1.5+) + if ( $.ajaxPrefilter ) { + $.ajaxPrefilter(function( settings, _, xhr ) { + var port = settings.port; + if ( settings.mode === "abort" ) { + if ( pendingRequests[port] ) { + pendingRequests[port].abort(); + } + pendingRequests[port] = xhr; + } + }); + } else { + // Proxy ajax + var ajax = $.ajax; + $.ajax = function( settings ) { + var mode = ( "mode" in settings ? settings : $.ajaxSettings ).mode, + port = ( "port" in settings ? settings : $.ajaxSettings ).port; + if ( mode === "abort" ) { + if ( pendingRequests[port] ) { + pendingRequests[port].abort(); + } + return (pendingRequests[port] = ajax.apply(this, arguments)); + } + return ajax.apply(this, arguments); + }; + } +}(jQuery)); + +// provides delegate(type: String, delegate: Selector, handler: Callback) plugin for easier event delegation +// handler is only called when $(event.target).is(delegate), in the scope of the jquery-object for event.target +(function($) { + $.extend($.fn, { + validateDelegate: function( delegate, type, handler ) { + return this.bind(type, function( event ) { + var target = $(event.target); + if ( target.is(delegate) ) { + return handler.apply(target, arguments); + } + }); + } + }); +}(jQuery)); diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/type-view.hbs b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/type-view.hbs new file mode 100644 index 000000000..a27f5547a --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/type-view.hbs @@ -0,0 +1,297 @@ +

      +
      + +
      +
      + +
      +
      +

      What it Does

      +
      +

      A Virtual Device that mimics the functionality of a real Firealarm. + Once run, the Virtual Firealarm will connect to WSO2 IoTServer and + push Temperature readings.

      +

      The device supports MQTT and XMPP Communications. It is configured to use MQTT by + default.

      +
      +

      What You Need

      +
      +
        +
      • + STEP 01 +    Go ahead and [Download] the Device. +
      • +
      • + STEP 02 +    Proceed to [Prepare] section. +
      • +
      • + STEP 03 +    Read [Try Out] section to further experiment with the device. +
      • +
      +
      + View API
        + + + Download Agent +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +

      +
      +
      +

      Prepare

      +
      +
        +
      • + 01 +    Download your VirtualFireAlarm using [Download Agent] button above. +
      • +
      • + 02 +    Unzip the downloaded Agent. +
      • +
      • + 03 +    Move into the unzipped Agent folder in the terminal. +
      • +
      • + 04 +    Unzip the downloaded Agent and start terminal to run this command: [sh + start-device.sh] +
      • +
      +
      +
      +
      +

      The Virtual-Fire-Alarm Device

      +
      +

      Click on the image to zoom

      +
      + + + +
      +
      +
      +
      +

      Try Out

      +
      +
        +
      • + 01 +    You can view all your connected devices at + [Device Management] page. +
      • +
      • + 02 +    Select one of connected devices and check for available control + operations and monitor Real-Time data. +
      • +
      • + 03 +    You can also view analytics of the data published to IoT-Server by + navigating to Device Analytics page. +
      • +
      +
      +

      Click on the image to zoom

      +
      + + + +
      +
      + +{{#zone "topCss"}} + +{{/zone}} + +{{#zone "bottomJs"}} + {{js "/js/download.js"}} + {{js "/js/jquery.validate.js"}} +{{/zone}} diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/type-view.json b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/type-view.json new file mode 100644 index 000000000..9eecd8f5b --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui/src/main/resources/jaggeryapps/devicemgt/app/units/cdmf.unit.device.type.virtual_firealarm.type-view/type-view.json @@ -0,0 +1,3 @@ +{ + "version": "1.0.0" +} \ No newline at end of file diff --git a/components/iot-plugins/virtual-fire-alarm-plugin/pom.xml b/components/iot-plugins/virtual-fire-alarm-plugin/pom.xml new file mode 100644 index 000000000..7bac29dcd --- /dev/null +++ b/components/iot-plugins/virtual-fire-alarm-plugin/pom.xml @@ -0,0 +1,64 @@ + + + + + + + org.wso2.carbon.devicemgt-plugins + iot-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + virtual-fire-alarm-plugin + pom + WSO2 Carbon - Arduino Plugin + http://wso2.org + + + org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl + org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl + org.wso2.carbon.device.mgt.iot.virtualfirealarm.ui + org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl + org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl + org.wso2.carbon.device.mgt.iot.virtualfirealarm.analytics + org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl + + + + + + + org.apache.felix + maven-scr-plugin + 1.7.2 + + + generate-scr-scrdescriptor + + scr + + + + + + + + diff --git a/components/key-mgt/org.wso2.carbon.key.mgt.handler.valve/pom.xml b/components/key-mgt/org.wso2.carbon.key.mgt.handler.valve/pom.xml deleted file mode 100644 index 2f548409a..000000000 --- a/components/key-mgt/org.wso2.carbon.key.mgt.handler.valve/pom.xml +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - org.wso2.carbon.devicemgt-plugins - key-mgt - 1.9.0-SNAPSHOT - ../pom.xml - - - 4.0.0 - org.wso2.carbon.devicemgt-plugins - org.wso2.carbon.key.mgt.handler.valve - 1.9.0-SNAPSHOT - bundle - WSO2 Carbon - Key Management Handler Valve - WSO2 Carbon - Key Management Handler Valve - http://wso2.org - - - - - org.apache.felix - maven-bundle-plugin - true - - - ${project.artifactId} - ${project.artifactId} - org.wso2.carbon.tomcat.patch - - org.wso2.carbon.key.mgt.handler.valve.* - - tomcat - - - - - - - - - org.apache.tomcat.wso2 - tomcat - - - org.wso2.carbon - org.wso2.carbon.apimgt.core - - - org.wso2.carbon - org.wso2.carbon.apimgt.impl - - - org.wso2.carbon - org.wso2.carbon.logging - - - org.wso2.carbon - org.wso2.carbon.tomcat.ext - - - - - diff --git a/components/key-mgt/org.wso2.carbon.key.mgt.handler.valve/src/main/java/org/wso2/carbon/key/mgt/handler/valve/HandlerUtil.java b/components/key-mgt/org.wso2.carbon.key.mgt.handler.valve/src/main/java/org/wso2/carbon/key/mgt/handler/valve/HandlerUtil.java deleted file mode 100644 index 69a430380..000000000 --- a/components/key-mgt/org.wso2.carbon.key.mgt.handler.valve/src/main/java/org/wso2/carbon/key/mgt/handler/valve/HandlerUtil.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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.key.mgt.handler.valve; - -import org.apache.axiom.om.OMAbstractFactory; -import org.apache.axiom.om.OMElement; -import org.apache.axiom.om.OMFactory; -import org.apache.axiom.om.OMNamespace; -import org.apache.catalina.connector.Response; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.wso2.carbon.apimgt.api.APIManagementException; -import org.wso2.carbon.apimgt.core.APIManagerErrorConstants; -import org.wso2.carbon.apimgt.core.authenticate.APITokenValidator; -import org.wso2.carbon.apimgt.impl.APIConstants; -import org.wso2.carbon.apimgt.impl.dto.APIKeyValidationInfoDTO; -import org.wso2.carbon.context.PrivilegedCarbonContext; -import org.wso2.carbon.identity.base.IdentityException; -import org.wso2.carbon.identity.core.util.IdentityUtil; - -import javax.servlet.http.HttpServletRequest; -import java.io.IOException; - -public class HandlerUtil { - - private static APIKeyValidationInfoDTO apiKeyValidationDTO; - private static final Log log = LogFactory.getLog(HandlerUtil.class); - - /** - * Retrieve bearer token form the HTTP header - * @param bearerToken Bearer Token extracted out of the corresponding HTTP header - */ - public static String getAccessToken(String bearerToken) { - String accessToken = null; - String[] token = bearerToken.split(HandlerConstants.TOKEN_NAME_BEARER); - if (token.length > 1 && token[1] != null) { - accessToken = token[1].trim(); - } - return accessToken; - } - - public static String getAPIVersion(HttpServletRequest request) { - int contextStartsIndex = (request.getRequestURI()).indexOf(request.getContextPath()) + 1; - int length = request.getContextPath().length(); - String afterContext = (request.getRequestURI()).substring(contextStartsIndex + length); - int SlashIndex = afterContext.indexOf(("/")); - - if (SlashIndex != -1) { - return afterContext.substring(0, SlashIndex); - } else { - return afterContext; - } - } - - public static void handleNoMatchAuthSchemeCallForRestService(Response response,String httpVerb, String reqUri, - String version, String context ) { - String errMsg = "Resource is not matched for HTTP Verb " + httpVerb + ". API context " + context + - ",version " + version + ", request " + reqUri; - APIFaultException e = new APIFaultException( APIManagerErrorConstants.API_AUTH_INCORRECT_API_RESOURCE, errMsg); - String faultPayload = getFaultPayload(e, APIManagerErrorConstants.API_SECURITY_NS, - APIManagerErrorConstants.API_SECURITY_NS_PREFIX).toString(); - handleRestFailure(response, faultPayload); - } - - public static boolean doAuthenticate(String context, String version, String accessToken, - String requiredAuthenticationLevel, String clientDomain) - throws APIManagementException, - APIFaultException { - - if (APIConstants.AUTH_NO_AUTHENTICATION.equals(requiredAuthenticationLevel)) { - return true; - } - APITokenValidator tokenValidator = new APITokenValidator(); - apiKeyValidationDTO = tokenValidator.validateKey(context, version, accessToken, - requiredAuthenticationLevel, clientDomain); - if (apiKeyValidationDTO.isAuthorized()) { - String userName = apiKeyValidationDTO.getEndUserName(); - PrivilegedCarbonContext.getThreadLocalCarbonContext() - .setUsername(apiKeyValidationDTO.getEndUserName()); - try { - PrivilegedCarbonContext.getThreadLocalCarbonContext() - .setTenantId(IdentityUtil.getTenantIdOFUser(userName)); - } catch (IdentityException e) { - log.error("Error while retrieving Tenant Id", e); - return false; - } - return true; - } else { - throw new APIFaultException(apiKeyValidationDTO.getValidationStatus(), - "Access failure for API: " + context + ", version: " + - version + " with key: " + accessToken); - } - } - - public static void handleRestFailure(Response response, String payload) { - response.setStatus(403); - response.setContentType("application/xml"); - response.setCharacterEncoding("UTF-8"); - try { - response.getWriter().write(payload); - } catch (IOException e) { - log.error("Error in sending fault response", e); - } - } - - public static OMElement getFaultPayload(APIFaultException exception, String FaultNS, - String FaultNSPrefix) { - OMFactory fac = OMAbstractFactory.getOMFactory(); - OMNamespace ns = fac.createOMNamespace(FaultNS, FaultNSPrefix); - OMElement payload = fac.createOMElement("fault", ns); - - OMElement errorCode = fac.createOMElement("code", ns); - errorCode.setText(String.valueOf(exception.getErrorCode())); - OMElement errorMessage = fac.createOMElement("message", ns); - errorMessage.setText(APIManagerErrorConstants.getFailureMessage(exception.getErrorCode())); - OMElement errorDetail = fac.createOMElement("description", ns); - errorDetail.setText(exception.getMessage()); - - payload.addChild(errorCode); - payload.addChild(errorMessage); - payload.addChild(errorDetail); - return payload; - } - -} diff --git a/components/key-mgt/org.wso2.carbon.key.mgt.handler.valve/src/main/java/org/wso2/carbon/key/mgt/handler/valve/OAuthTokenValidatorValve.java b/components/key-mgt/org.wso2.carbon.key.mgt.handler.valve/src/main/java/org/wso2/carbon/key/mgt/handler/valve/OAuthTokenValidatorValve.java deleted file mode 100644 index 23fd9a8f9..000000000 --- a/components/key-mgt/org.wso2.carbon.key.mgt.handler.valve/src/main/java/org/wso2/carbon/key/mgt/handler/valve/OAuthTokenValidatorValve.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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.key.mgt.handler.valve; - -import org.apache.catalina.connector.Request; -import org.apache.catalina.connector.Response; -import org.apache.catalina.valves.ValveBase; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.wso2.carbon.apimgt.api.APIManagementException; -import org.wso2.carbon.apimgt.core.authenticate.APITokenValidator; -import org.wso2.carbon.apimgt.core.gateway.APITokenAuthenticator; -import org.wso2.carbon.apimgt.impl.APIConstants; -import org.wso2.carbon.apimgt.impl.dao.ApiMgtDAO; -import org.wso2.carbon.apimgt.impl.utils.APIUtil; - -import javax.servlet.ServletException; -import java.io.IOException; -import java.util.Enumeration; - -public class OAuthTokenValidatorValve extends ValveBase { - - private static final Log log = LogFactory.getLog(OAuthTokenValidatorValve.class); - - APITokenAuthenticator authenticator; - - public OAuthTokenValidatorValve() { - authenticator = new APITokenAuthenticator(); - } - - @Override - public void invoke(Request request, Response response) throws java.io.IOException, javax.servlet.ServletException { - String context = request.getContextPath(); - if (context == null || context.equals("")) { - //Invoke the next valve in handler chain. - getNext().invoke(request, response); - return; - } - - boolean contextExist; - Boolean contextValueInCache = null; - if (APIUtil.getAPIContextCache().get(context) != null) { - contextValueInCache = Boolean.parseBoolean(APIUtil.getAPIContextCache().get(context).toString()); - } - - if (contextValueInCache != null) { - contextExist = contextValueInCache; - } else { - contextExist = ApiMgtDAO.isContextExist(context); - APIUtil.getAPIContextCache().put(context, contextExist); - } - - if (!contextExist) { - getNext().invoke(request, response); - return; - } - - try { - handleWSDLGetRequest(request, response, context); - } catch (IOException e) { - e.printStackTrace(); - } catch (ServletException e) { - e.printStackTrace(); - } - - String authHeader = request.getHeader(APIConstants.OperationParameter.AUTH_PARAM_NAME); - String accessToken = null; - - /* Authenticate*/ - try { - if (authHeader != null) { - accessToken = HandlerUtil.getAccessToken(authHeader); - } else { - // There can be some API published with None Auth Type - /* - * throw new - * APIFaultException(APIConstants.KeyValidationStatus - * .API_AUTH_INVALID_CREDENTIALS, - * "Invalid format for Authorization header. Expected 'Bearer '" - * ); - */ - } - - String apiVersion = HandlerUtil.getAPIVersion(request); - String domain = request.getHeader(APITokenValidator.getAPIManagerClientDomainHeader()); - String authLevel = authenticator.getResourceAuthenticationScheme(context, - apiVersion, - request.getRequestURI(), - request.getMethod()); - if (HandlerConstants.NO_MATCHING_AUTH_SCHEME.equals(authLevel)) { - HandlerUtil.handleNoMatchAuthSchemeCallForRestService(response, - request.getMethod(), request.getRequestURI(), - apiVersion, context); - return; - } else { - HandlerUtil.doAuthenticate(context, apiVersion, accessToken, authLevel, domain); - } - } catch (APIManagementException e) { - //ignore - } catch (APIFaultException e) { - log.error("Error occurred while key validation", e); - return; - } - - getNext().invoke(request, response); - } - - private void handleWSDLGetRequest(Request request, Response response, - String context) throws IOException, ServletException { - if (request.getMethod().equals("GET")) { - // TODO:Need to get these paths from a config file. - if (request.getRequestURI().matches(context + "/[^/]*/services")) { - getNext().invoke(request, response); - return; - } - Enumeration params = request.getParameterNames(); - String paramName; - while (params.hasMoreElements()) { - paramName = params.nextElement(); - if (paramName.endsWith("wsdl") || paramName.endsWith("wadl")) { - getNext().invoke(request, response); - return; - } - } - } - } - -} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/pom.xml b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/pom.xml new file mode 100644 index 000000000..0da2c6395 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/pom.xml @@ -0,0 +1,180 @@ + + + + + + android-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.mobile.android.agent + WSO2 Carbon - Android JAX-RS API + Android JAX-RS API + war + + + + + maven-compiler-plugin + + 1.7 + 1.7 + + 2.3.2 + + + maven-war-plugin + 2.2 + + WEB-INF/lib/*cxf*.jar + ${project.artifactId} + + + + + + + + deploy + + compile + + + org.apache.maven.plugins + maven-antrun-plugin + 1.7 + + + compile + + run + + + + + + + + + + + + + + + + + + + client + + test + + + org.codehaus.mojo + exec-maven-plugin + 1.2.1 + + + test + + java + + + + + + + + + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + + + org.apache.cxf + cxf-rt-frontend-jaxrs + + + org.apache.cxf + cxf-rt-transports-http + + + javax.ws.rs + jsr311-api + provided + + + org.wso2.carbon + org.wso2.carbon.utils + provided + + + org.wso2.carbon + org.wso2.carbon.logging + provided + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.core + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.policy.mgt.common + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.policy.mgt.core + provided + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.mobile.android + provided + + + + commons-httpclient.wso2 + commons-httpclient + provided + + + + com.google.code.gson + gson + + + diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/ConfigurationMgtService.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/ConfigurationMgtService.java new file mode 100644 index 000000000..565393e49 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/ConfigurationMgtService.java @@ -0,0 +1,161 @@ +/* + * 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.mdm.services.android; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.DeviceManagementConstants; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.configuration.mgt.ConfigurationEntry; +import org.wso2.carbon.device.mgt.common.configuration.mgt.TenantConfiguration; +import org.wso2.carbon.device.mgt.common.license.mgt.License; +import org.wso2.carbon.mdm.services.android.exception.AndroidAgentException; +import org.wso2.carbon.mdm.services.android.util.AndroidAPIUtils; +import org.wso2.carbon.mdm.services.android.util.AndroidConstants; +import org.wso2.carbon.mdm.services.android.util.Message; + +import javax.jws.WebService; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.List; + +/** + * Android Platform Configuration REST-API implementation. + * All end points supports JSON, XMl with content negotiation. + */ +@WebService +@Produces({ "application/json", "application/xml" }) +@Consumes({ "application/json", "application/xml" }) +public class ConfigurationMgtService { + + private static Log log = LogFactory.getLog(ConfigurationMgtService.class); + + @POST + public Message configureSettings(TenantConfiguration configuration) + throws AndroidAgentException { + + Message responseMsg = new Message(); + String msg; + ConfigurationEntry licenseEntry = null; + try { + configuration.setType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID); + List configs = configuration.getConfiguration(); + for(ConfigurationEntry entry : configs){ + if(AndroidConstants.TenantConfigProperties.LICENSE_KEY.equals(entry.getName())){ + License license = new License(); + license.setName(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID); + license.setLanguage(AndroidConstants.TenantConfigProperties.LANGUAGE_US); + license.setVersion("1.0.0"); + license.setText(entry.getValue().toString()); + AndroidAPIUtils.getDeviceManagementService().addLicense(DeviceManagementConstants. + MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID, license); + licenseEntry = entry; + break; + } + } + + if(licenseEntry != null) { + configs.remove(licenseEntry); + } + configuration.setConfiguration(configs); + AndroidAPIUtils.getDeviceManagementService().saveConfiguration(configuration); + Response.status(Response.Status.CREATED); + responseMsg.setResponseMessage("Android platform configuration saved successfully."); + responseMsg.setResponseCode(Response.Status.CREATED.toString()); + } catch (DeviceManagementException e) { + msg = "Error occurred while configuring the android platform"; + log.error(msg, e); + throw new AndroidAgentException(msg, e); + } + return responseMsg; + } + + @GET + public TenantConfiguration getConfiguration() throws AndroidAgentException { + String msg; + TenantConfiguration tenantConfiguration; + List configs; + try { + tenantConfiguration = AndroidAPIUtils.getDeviceManagementService(). + getConfiguration(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID); + if(tenantConfiguration != null) { + configs = tenantConfiguration.getConfiguration(); + } else { + tenantConfiguration = new TenantConfiguration(); + configs = new ArrayList(); + } + + ConfigurationEntry entry = new ConfigurationEntry(); + License license = AndroidAPIUtils.getDeviceManagementService().getLicense( + DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID, AndroidConstants. + TenantConfigProperties.LANGUAGE_US); + + if(license != null && configs != null) { + entry.setContentType(AndroidConstants.TenantConfigProperties.CONTENT_TYPE_TEXT); + entry.setName(AndroidConstants.TenantConfigProperties.LICENSE_KEY); + entry.setValue(license.getText()); + configs.add(entry); + tenantConfiguration.setConfiguration(configs); + } + } catch (DeviceManagementException e) { + msg = "Error occurred while retrieving the Android tenant configuration"; + log.error(msg, e); + throw new AndroidAgentException(msg, e); + } + return tenantConfiguration; + } + + @PUT + public Message updateConfiguration(TenantConfiguration configuration) throws AndroidAgentException { + String msg; + Message responseMsg = new Message(); + ConfigurationEntry licenseEntry = null; + try { + configuration.setType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID); + List configs = configuration.getConfiguration(); + for(ConfigurationEntry entry : configs){ + if(AndroidConstants.TenantConfigProperties.LICENSE_KEY.equals(entry.getName())){ + License license = new License(); + license.setName(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID); + license.setLanguage(AndroidConstants.TenantConfigProperties.LANGUAGE_US); + license.setVersion("1.0.0"); + license.setText(entry.getValue().toString()); + AndroidAPIUtils.getDeviceManagementService().addLicense(DeviceManagementConstants. + MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID, license); + licenseEntry = entry; + } + } + + if(licenseEntry != null) { + configs.remove(licenseEntry); + } + configuration.setConfiguration(configs); + AndroidAPIUtils.getDeviceManagementService().saveConfiguration(configuration); + Response.status(Response.Status.ACCEPTED); + responseMsg.setResponseMessage("Android platform configuration has updated successfully."); + responseMsg.setResponseCode(Response.Status.ACCEPTED.toString()); + } catch (DeviceManagementException e) { + msg = "Error occurred while modifying configuration settings of Android platform"; + log.error(msg, e); + throw new AndroidAgentException(msg, e); + } + return responseMsg; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/DeviceManagementService.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/DeviceManagementService.java new file mode 100644 index 000000000..a6be42cdc --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/DeviceManagementService.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 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. + */ +package org.wso2.carbon.mdm.services.android; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementConstants; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.app.mgt.Application; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManagementException; +import org.wso2.carbon.device.mgt.common.license.mgt.License; +import org.wso2.carbon.mdm.services.android.exception.AndroidAgentException; +import org.wso2.carbon.mdm.services.android.util.AndroidAPIUtils; +import org.wso2.carbon.mdm.services.android.util.Message; + +import javax.jws.WebService; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import java.util.List; + +/** + * Android Device Management REST-API implementation. + * All end points supports JSON, XMl with content negotiation. + */ +@WebService +@Produces({ "application/json", "application/xml" }) +@Consumes({ "application/json", "application/xml" }) +public class DeviceManagementService { + + private static Log log = LogFactory.getLog(DeviceManagementService.class); + + /** + * Get all devices.Returns list of Android devices registered in MDM. + * + * @return Device List + * @throws org.wso2.carbon.mdm.services.android.exception.AndroidAgentException + */ + @GET + public List getAllDevices() + throws AndroidAgentException { + String msg; + List devices; + + try { + devices = AndroidAPIUtils.getDeviceManagementService(). + getAllDevices(DeviceManagementConstants.MobileDeviceTypes. + MOBILE_DEVICE_TYPE_ANDROID); + } catch (DeviceManagementException e) { + msg = "Error occurred while fetching the device list."; + log.error(msg, e); + throw new AndroidAgentException(msg, e); + } + return devices; + } + + /** + * Fetch Android device details of a given device Id. + * + * @param id Device Id + * @return Device + * @throws org.wso2.carbon.mdm.services.android.exception.AndroidAgentException + */ + @GET + @Path("{id}") + public org.wso2.carbon.device.mgt.common.Device getDevice(@PathParam("id") String id) + throws AndroidAgentException { + + String msg; + org.wso2.carbon.device.mgt.common.Device device; + + try { + DeviceIdentifier deviceIdentifier = AndroidAPIUtils.convertToDeviceIdentifierObject(id); + device = AndroidAPIUtils.getDeviceManagementService().getDevice(deviceIdentifier); + if (device == null) { + Response.status(Response.Status.NOT_FOUND); + } + } catch (DeviceManagementException deviceMgtEx) { + msg = "Error occurred while fetching the device information."; + log.error(msg, deviceMgtEx); + throw new AndroidAgentException(msg, deviceMgtEx); + } + return device; + } + + /** + * Update Android device details of given device id. + * + * @param id Device Id + * @param device Device Details + * @return Message + * @throws AndroidAgentException + */ + @PUT + @Path("{id}") + public Message updateDevice(@PathParam("id") String id, Device device) + throws AndroidAgentException { + String msg; + Message responseMessage = new Message(); + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(id); + deviceIdentifier + .setType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID); + boolean result; + try { + device.setType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID); + result = AndroidAPIUtils.getDeviceManagementService() + .updateDeviceInfo(deviceIdentifier, device); + if (result) { + Response.status(Response.Status.ACCEPTED); + responseMessage.setResponseMessage("Device information has modified successfully."); + } else { + Response.status(Response.Status.NOT_MODIFIED); + responseMessage.setResponseMessage("Device not found for the update."); + } + } catch (DeviceManagementException e) { + msg = "Error occurred while modifying the device information."; + log.error(msg, e); + throw new AndroidAgentException(msg, e); + } + return responseMessage; + } + + @POST + @Path("appList/{id}") + public Message updateApplicationList(@PathParam("id") String id, List applications) + throws + AndroidAgentException { + + Message responseMessage = new Message(); + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(id); + deviceIdentifier.setType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID); + try { + AndroidAPIUtils.getApplicationManagerService(). + updateApplicationListInstalledInDevice(deviceIdentifier, applications); + Response.status(Response.Status.ACCEPTED); + responseMessage.setResponseMessage("Device information has modified successfully."); + + } catch (ApplicationManagementException e) { + String msg = "Error occurred while modifying the application list."; + log.error(msg, e); + throw new AndroidAgentException(msg, e); + } + return responseMessage; + } + + @GET + @Path("license") + @Produces("text/html") + public String getLicense() throws AndroidAgentException { + License license; + + try { + license = + AndroidAPIUtils.getDeviceManagementService().getLicense( + DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID, + DeviceManagementConstants.LanguageCodes.LANGUAGE_CODE_ENGLISH_US); + } catch (DeviceManagementException e) { + String msg = "Error occurred while retrieving the license configured for Android device enrolment"; + log.error(msg, e); + throw new AndroidAgentException(msg, e); + } + return (license == null) ? null : license.getText(); + } + +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/EnrollmentService.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/EnrollmentService.java new file mode 100644 index 000000000..5ecf373e7 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/EnrollmentService.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 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. + */ + +package org.wso2.carbon.mdm.services.android; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementConstants; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.mdm.services.android.exception.AndroidAgentException; +import org.wso2.carbon.mdm.services.android.util.AndroidAPIUtils; +import org.wso2.carbon.mdm.services.android.util.Message; + +import javax.jws.WebService; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; + +/** + * Android Device Enrollment REST-API implementation. + * All end points supports JSON, XMl with content negotiation. + */ +@WebService +@Produces({ "application/json", "application/xml" }) +@Consumes({ "application/json", "application/xml" }) +public class EnrollmentService { + + private static Log log = LogFactory.getLog(EnrollmentService.class); + + @POST + public Message enrollDevice(org.wso2.carbon.device.mgt.common.Device device) + throws AndroidAgentException { + + Message responseMsg = new Message(); + String msg; + try { + device.setType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID); + device.getEnrolmentInfo().setOwner(AndroidAPIUtils.getAuthenticatedUser()); + boolean status = AndroidAPIUtils.getDeviceManagementService().enrollDevice(device); + if (status) { + Response.status(Response.Status.CREATED); + responseMsg.setResponseMessage("Device enrollment succeeded."); + responseMsg.setResponseCode(Response.Status.CREATED.toString()); + } else { + Response.status(Response.Status.INTERNAL_SERVER_ERROR); + responseMsg.setResponseMessage("Device enrollment failed."); + responseMsg.setResponseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()); + } + } catch (DeviceManagementException e) { + msg = "Error occurred while enrolling the device"; + log.error(msg, e); + throw new AndroidAgentException(msg, e); + } + return responseMsg; + } + + @GET + @Path("{deviceId}") + public Message isEnrolled(@PathParam("deviceId") String id) throws AndroidAgentException { + String msg; + boolean result; + Message responseMsg = new Message(); + DeviceIdentifier deviceIdentifier = AndroidAPIUtils.convertToDeviceIdentifierObject(id); + + try { + result = AndroidAPIUtils.getDeviceManagementService().isEnrolled(deviceIdentifier); + if (result) { + responseMsg.setResponseMessage("Device has already enrolled"); + responseMsg.setResponseCode(Response.Status.ACCEPTED.toString()); + Response.status(Response.Status.ACCEPTED); + } else { + responseMsg.setResponseMessage("Device not found"); + responseMsg.setResponseCode(Response.Status.NOT_FOUND.toString()); + Response.status(Response.Status.NOT_FOUND); + } + } catch (DeviceManagementException e) { + msg = "Error occurred while enrollment of the device."; + log.error(msg, e); + throw new AndroidAgentException(msg, e); + } + return responseMsg; + } + + @PUT + @Path("{deviceId}") + public Message modifyEnrollment(@PathParam("deviceId") String id, + org.wso2.carbon.device.mgt.common.Device device) + throws AndroidAgentException { + String msg; + boolean result; + Message responseMsg = new Message(); + try { + device.setType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID); + result = AndroidAPIUtils.getDeviceManagementService().modifyEnrollment(device); + if (result) { + responseMsg.setResponseMessage("Device enrollment has updated successfully"); + responseMsg.setResponseCode(Response.Status.ACCEPTED.toString()); + Response.status(Response.Status.ACCEPTED); + } else { + responseMsg.setResponseMessage("Device not found for enrollment"); + responseMsg.setResponseCode(Response.Status.NOT_MODIFIED.toString()); + Response.status(Response.Status.NOT_MODIFIED); + } + } catch (DeviceManagementException e) { + msg = "Error occurred while modifying enrollment of the device"; + log.error(msg, e); + throw new AndroidAgentException(msg, e); + } + return responseMsg; + } + + @DELETE + @Path("{deviceId}") + public Message disEnrollDevice(@PathParam("deviceId") String id) throws AndroidAgentException { + Message responseMsg = new Message(); + boolean result; + String msg; + DeviceIdentifier deviceIdentifier = AndroidAPIUtils.convertToDeviceIdentifierObject(id); + + try { + result = AndroidAPIUtils.getDeviceManagementService().disenrollDevice(deviceIdentifier); + if (result) { + responseMsg.setResponseMessage("Device has removed successfully"); + responseMsg.setResponseCode(Response.Status.ACCEPTED.toString()); + Response.status(Response.Status.ACCEPTED); + } else { + responseMsg.setResponseMessage("Device not found"); + responseMsg.setResponseCode(Response.Status.NOT_FOUND.toString()); + Response.status(Response.Status.NOT_FOUND); + } + } catch (DeviceManagementException e) { + msg = "Error occurred while dis enrolling the device"; + log.error(msg, e); + throw new AndroidAgentException(msg, e); + } + return responseMsg; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/OperationMgtService.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/OperationMgtService.java new file mode 100644 index 000000000..91329cc94 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/OperationMgtService.java @@ -0,0 +1,931 @@ +/* + * 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.mdm.services.android; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManagementException; +import org.wso2.carbon.device.mgt.common.notification.mgt.*; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.common.operation.mgt.OperationManagementException; +import org.wso2.carbon.device.mgt.core.operation.mgt.CommandOperation; +import org.wso2.carbon.device.mgt.core.operation.mgt.ProfileOperation; +import org.wso2.carbon.mdm.services.android.bean.*; +import org.wso2.carbon.mdm.services.android.bean.Notification; +import org.wso2.carbon.mdm.services.android.bean.wrapper.*; +import org.wso2.carbon.mdm.services.android.exception.AndroidOperationException; +import org.wso2.carbon.mdm.services.android.util.AndroidAPIUtils; +import org.wso2.carbon.mdm.services.android.util.AndroidConstants; +import org.wso2.carbon.mdm.services.android.util.AndroidDeviceUtils; +import org.wso2.carbon.mdm.services.android.util.Message; +import org.wso2.carbon.policy.mgt.common.monitor.PolicyComplianceException; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; + +/** + * Android Device Operation REST-API implementation. + */ +public class OperationMgtService { + + private static Log log = LogFactory.getLog(OperationMgtService.class); + private static final String ACCEPT = "Accept"; + private static final String OPERATION_ERROR_STATUS = "ERROR"; + private static final String DEVICE_TYPE_ANDROID = "android"; + + @PUT + @Path("{id}") + public List getPendingOperations + (@HeaderParam(ACCEPT) String acceptHeader, @PathParam("id") String id, + List resultOperations) { + Message message; + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + + if (id == null || id.isEmpty()) { + String errorMessage = "Device identifier is null or empty, hence returning device not found"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.BAD_REQUEST.toString()).build(); + log.error(errorMessage); + throw new AndroidOperationException(message, responseMediaType); + } + + DeviceIdentifier deviceIdentifier = AndroidAPIUtils.convertToDeviceIdentifierObject(id); + try { + if (!AndroidDeviceUtils.isValidDeviceIdentifier(deviceIdentifier)) { + String errorMessage = "Device not found for identifier '" + id + "'"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.BAD_REQUEST.toString()).build(); + log.error(errorMessage); + throw new AndroidOperationException(message, responseMediaType); + } + if (log.isDebugEnabled()) { + log.debug("Invoking Android pending operations:" + id); + } + if (resultOperations != null && !resultOperations.isEmpty()) { + updateOperations(id, resultOperations); + } + } catch (OperationManagementException e) { + log.error("Issue in retrieving operation management service instance", e); + } catch (PolicyComplianceException e) { + log.error("Issue in updating Monitoring operation"); + } catch (DeviceManagementException e) { + log.error("Issue in retrieving device management service instance", e); + } catch (ApplicationManagementException e) { + log.error("Issue in retrieving application management service instance", e); + } catch (NotificationManagementException e) { + log.error("Issue in retrieving Notification management service instance", e); + } + + List pendingOperations; + try { + pendingOperations = AndroidAPIUtils.getPendingOperations(deviceIdentifier); + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + return pendingOperations; + } + + @POST + @Path("lock") + public Response configureDeviceLock(@HeaderParam(ACCEPT) String acceptHeader, List deviceIDs) { + + if (log.isDebugEnabled()) { + log.debug("Invoking Android device lock operation"); + } + + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + Response response; + + try { + CommandOperation operation = new CommandOperation(); + operation.setCode(AndroidConstants.OperationCodes.DEVICE_LOCK); + operation.setType(Operation.Type.COMMAND); + operation.setEnabled(true); + response = AndroidAPIUtils.getOperationResponse(deviceIDs, operation, message, responseMediaType); + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + return response; + } + + @POST + @Path("location") + public Response getDeviceLocation(@HeaderParam(ACCEPT) String acceptHeader, + List deviceIDs) { + if (log.isDebugEnabled()) { + log.debug("Invoking Android device location operation"); + } + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + + try { + CommandOperation operation = new CommandOperation(); + operation.setCode(AndroidConstants.OperationCodes.DEVICE_LOCATION); + operation.setType(Operation.Type.COMMAND); + return AndroidAPIUtils.getOperationResponse(deviceIDs, operation, + message, responseMediaType); + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + } + + @POST + @Path("clear-password") + public Response removePassword(@HeaderParam(ACCEPT) String acceptHeader, + List deviceIDs) { + if (log.isDebugEnabled()) { + log.debug("Invoking Android clear password operation"); + } + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + + try { + CommandOperation operation = new CommandOperation(); + operation.setCode(AndroidConstants.OperationCodes.CLEAR_PASSWORD); + operation.setType(Operation.Type.COMMAND); + + return AndroidAPIUtils.getOperationResponse(deviceIDs, operation, + message, responseMediaType); + + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + } + + @POST + @Path("camera") + public Response configureCamera(@HeaderParam(ACCEPT) String acceptHeader, + CameraBeanWrapper cameraBeanWrapper) { + + if (log.isDebugEnabled()) { + log.debug("Invoking Android Camera operation"); + } + + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + + try { + Camera camera = cameraBeanWrapper.getOperation(); + + if (camera == null) { + if (log.isDebugEnabled()) { + log.debug("The payload of the configure camera operation is incorrect"); + } + throw new OperationManagementException("Issue in creating a new camera instance"); + } + + CommandOperation operation = new CommandOperation(); + operation.setCode(AndroidConstants.OperationCodes.CAMERA); + operation.setType(Operation.Type.COMMAND); + operation.setEnabled(camera.isEnabled()); + + return AndroidAPIUtils.getOperationResponse(cameraBeanWrapper.getDeviceIDs(), operation, message, + responseMediaType); + + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + } + + @POST + @Path("device-info") + public Response getDeviceInformation(@HeaderParam(ACCEPT) String acceptHeader, + List deviceIDs) { + + if (log.isDebugEnabled()) { + log.debug("Invoking get Android device information operation"); + } + + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + + try { + CommandOperation operation = new CommandOperation(); + operation.setCode(AndroidConstants.OperationCodes.DEVICE_INFO); + operation.setType(Operation.Type.COMMAND); + getApplications(acceptHeader, deviceIDs); + return AndroidAPIUtils.getOperationResponse(deviceIDs, operation, message, + responseMediaType); + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + } + + @POST + @Path("enterprise-wipe") + public Response wipeDevice(@HeaderParam(ACCEPT) String acceptHeader, + List deviceIDs) { + + if (log.isDebugEnabled()) { + log.debug("Invoking enterprise-wipe device operation"); + } + + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + + try { + CommandOperation operation = new CommandOperation(); + operation.setCode(AndroidConstants.OperationCodes.ENTERPRISE_WIPE); + operation.setType(Operation.Type.COMMAND); + + return AndroidAPIUtils.getOperationResponse(deviceIDs, operation, message, + responseMediaType); + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + } + + @POST + @Path("wipe-data") + public Response wipeData(@HeaderParam(ACCEPT) String acceptHeader, + WipeDataBeanWrapper wipeDataBeanWrapper) { + + if (log.isDebugEnabled()) { + log.debug("Invoking Android wipe-data device operation"); + } + + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + + try { + WipeData wipeData = wipeDataBeanWrapper.getOperation(); + + if (wipeData == null) { + throw new OperationManagementException("WipeData bean is empty"); + } + + ProfileOperation operation = new ProfileOperation(); + operation.setCode(AndroidConstants.OperationCodes.WIPE_DATA); + operation.setType(Operation.Type.PROFILE); + operation.setPayLoad(wipeData.toJSON()); + + return AndroidAPIUtils.getOperationResponse(wipeDataBeanWrapper.getDeviceIDs(), operation, message, + responseMediaType); + + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + } + + @POST + @Path("application-list") + public Response getApplications(@HeaderParam(ACCEPT) String acceptHeader, + List deviceIDs) { + + if (log.isDebugEnabled()) { + log.debug("Invoking Android getApplicationList device operation"); + } + + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + + try { + CommandOperation operation = new CommandOperation(); + operation.setCode(AndroidConstants.OperationCodes.APPLICATION_LIST); + operation.setType(Operation.Type.COMMAND); + + return AndroidAPIUtils.getOperationResponse(deviceIDs, operation, message, + responseMediaType); + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + } + + @POST + @Path("ring-device") + public Response ringDevice(@HeaderParam(ACCEPT) String acceptHeader, + List deviceIDs) { + + if (log.isDebugEnabled()) { + log.debug("Invoking Android ring-device device operation"); + } + + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + + try { + CommandOperation operation = new CommandOperation(); + operation.setCode(AndroidConstants.OperationCodes.DEVICE_RING); + operation.setType(Operation.Type.COMMAND); + return AndroidAPIUtils.getOperationResponse(deviceIDs, operation, message, + responseMediaType); + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + } + + @POST + @Path("mute") + public Response muteDevice(@HeaderParam(ACCEPT) String acceptHeader, + List deviceIDs) { + + if (log.isDebugEnabled()) { + log.debug("Invoking mute device operation"); + } + + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + + try { + CommandOperation operation = new CommandOperation(); + operation.setCode(AndroidConstants.OperationCodes.DEVICE_MUTE); + operation.setType(Operation.Type.COMMAND); + operation.setEnabled(true); + return AndroidAPIUtils.getOperationResponse(deviceIDs, operation, message, + responseMediaType); + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + } + + @POST + @Path("install-application") + public Response installApplication(@HeaderParam(ACCEPT) String acceptHeader, + ApplicationInstallationBeanWrapper applicationInstallationBeanWrapper) { + + if (log.isDebugEnabled()) { + log.debug("Invoking 'InstallApplication' operation"); + } + + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + + try { + ApplicationInstallation applicationInstallation = applicationInstallationBeanWrapper.getOperation(); + + if (applicationInstallation == null) { + if (log.isDebugEnabled()) { + log.debug("The payload of the application installing operation is incorrect"); + } + throw new OperationManagementException("Issue in creating a new application installation instance"); + } + + ProfileOperation operation = new ProfileOperation(); + operation.setCode(AndroidConstants.OperationCodes.INSTALL_APPLICATION); + operation.setType(Operation.Type.PROFILE); + operation.setPayLoad(applicationInstallation.toJSON()); + + return AndroidAPIUtils.getOperationResponse(applicationInstallationBeanWrapper.getDeviceIDs(), + operation, message, responseMediaType); + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + } + + @POST + @Path("uninstall-application") + public Response uninstallApplication(@HeaderParam(ACCEPT) String acceptHeader, + ApplicationUninstallationBeanWrapper applicationUninstallationBeanWrapper) { + if (log.isDebugEnabled()) { + log.debug("Invoking 'UninstallApplication' operation"); + } + + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + + try { + ApplicationUninstallation applicationUninstallation = applicationUninstallationBeanWrapper.getOperation(); + + if (applicationUninstallation == null) { + if (log.isDebugEnabled()) { + log.debug("The payload of the application uninstalling operation is incorrect"); + } + throw new OperationManagementException("Issue in creating a new application uninstallation instance"); + } + + ProfileOperation operation = new ProfileOperation(); + operation.setCode(AndroidConstants.OperationCodes.UNINSTALL_APPLICATION); + operation.setType(Operation.Type.PROFILE); + operation.setPayLoad(applicationUninstallation.toJSON()); + + return AndroidAPIUtils.getOperationResponse(applicationUninstallationBeanWrapper.getDeviceIDs(), + operation, message, responseMediaType); + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + } + + @POST + @Path("blacklist-applications") + public Response blacklistApplications(@HeaderParam(ACCEPT) String acceptHeader, + BlacklistApplicationsBeanWrapper blacklistApplicationsBeanWrapper) { + if (log.isDebugEnabled()) { + log.debug("Invoking 'Blacklist-Applications' operation"); + } + + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + + try { + BlacklistApplications blacklistApplications = blacklistApplicationsBeanWrapper.getOperation(); + + if (blacklistApplications == null) { + if (log.isDebugEnabled()) { + log.debug("The payload of the blacklisting apps operation is incorrect"); + } + throw new OperationManagementException("Issue in creating a new blacklist applications instance"); + } + + ProfileOperation operation = new ProfileOperation(); + operation.setCode(AndroidConstants.OperationCodes.BLACKLIST_APPLICATIONS); + operation.setType(Operation.Type.PROFILE); + operation.setPayLoad(blacklistApplications.toJSON()); + + return AndroidAPIUtils.getOperationResponse(blacklistApplicationsBeanWrapper.getDeviceIDs(), + operation, message, responseMediaType); + + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + } + + @POST + @Path("notification") + public Response sendNotification(@HeaderParam(ACCEPT) String acceptHeader, + NotificationBeanWrapper notificationBeanWrapper) { + if (log.isDebugEnabled()) { + log.debug("Invoking 'notification' operation"); + } + + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + + try { + Notification notification = notificationBeanWrapper.getOperation(); + + if (notification == null) { + if (log.isDebugEnabled()) { + log.debug("The payload of the notification operation is incorrect"); + } + throw new OperationManagementException("Issue in creating a new notification instance"); + } + + ProfileOperation operation = new ProfileOperation(); + operation.setCode(AndroidConstants.OperationCodes.NOTIFICATION); + operation.setType(Operation.Type.PROFILE); + operation.setPayLoad(notification.toJSON()); + + return AndroidAPIUtils.getOperationResponse(notificationBeanWrapper.getDeviceIDs(), + operation, message, responseMediaType); + + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + } + + @POST + @Path("wifi") + public Response configureWifi(@HeaderParam(ACCEPT) String acceptHeader, + WifiBeanWrapper wifiBeanWrapper) { + if (log.isDebugEnabled()) { + log.debug("Invoking 'configure wifi' operation"); + } + + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + + try { + Wifi wifi = wifiBeanWrapper.getOperation(); + + if (wifi == null) { + if (log.isDebugEnabled()) { + log.debug("The payload of the wifi operation is incorrect"); + } + throw new OperationManagementException("Issue in creating a new Wifi instance"); + } + + ProfileOperation operation = new ProfileOperation(); + operation.setCode(AndroidConstants.OperationCodes.WIFI); + operation.setType(Operation.Type.PROFILE); + operation.setPayLoad(wifi.toJSON()); + + return AndroidAPIUtils.getOperationResponse(wifiBeanWrapper.getDeviceIDs(), + operation, message, responseMediaType); + + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + } + + @POST + @Path("encrypt") + public Response encryptStorage(@HeaderParam(ACCEPT) String acceptHeader, + EncryptionBeanWrapper encryptionBeanWrapper) { + if (log.isDebugEnabled()) { + log.debug("Invoking 'encrypt' operation"); + } + + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + + try { + DeviceEncryption deviceEncryption = encryptionBeanWrapper.getOperation(); + + if (deviceEncryption == null) { + if (log.isDebugEnabled()) { + log.debug("The payload of the device encryption operation is incorrect"); + } + throw new OperationManagementException("Issue in creating a new encryption instance"); + } + + CommandOperation operation = new CommandOperation(); + operation.setCode(AndroidConstants.OperationCodes.ENCRYPT_STORAGE); + operation.setType(Operation.Type.COMMAND); + operation.setEnabled(deviceEncryption.isEncrypted()); + + return AndroidAPIUtils.getOperationResponse(encryptionBeanWrapper.getDeviceIDs(), + operation, message, responseMediaType); + + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + } + + @POST + @Path("change-lock-code") + public Response changeLockCode(@HeaderParam(ACCEPT) String acceptHeader, + LockCodeBeanWrapper lockCodeBeanWrapper) { + if (log.isDebugEnabled()) { + log.debug("Invoking 'change lock code' operation"); + } + + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + + try { + LockCode lockCode = lockCodeBeanWrapper.getOperation(); + + if (lockCode == null) { + if (log.isDebugEnabled()) { + log.debug("The payload of the change lock code operation is incorrect"); + } + throw new OperationManagementException("Issue in retrieving a new lock-code instance"); + } + + ProfileOperation operation = new ProfileOperation(); + operation.setCode(AndroidConstants.OperationCodes.CHANGE_LOCK_CODE); + operation.setType(Operation.Type.PROFILE); + operation.setPayLoad(lockCode.toJSON()); + + return AndroidAPIUtils.getOperationResponse(lockCodeBeanWrapper.getDeviceIDs(), + operation, message, responseMediaType); + + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + } + + @POST + @Path("password-policy") + public Response setPasswordPolicy(@HeaderParam(ACCEPT) String acceptHeader, + PasswordPolicyBeanWrapper passwordPolicyBeanWrapper) { + if (log.isDebugEnabled()) { + log.debug("Invoking 'password policy' operation"); + } + + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + + try { + PasscodePolicy passcodePolicy = passwordPolicyBeanWrapper.getOperation(); + + if (passcodePolicy == null) { + if (log.isDebugEnabled()) { + log.debug("The payload of the change password policy operation is incorrect"); + } + throw new OperationManagementException("Issue in creating a new Password policy instance"); + } + + ProfileOperation operation = new ProfileOperation(); + operation.setCode(AndroidConstants.OperationCodes.PASSCODE_POLICY); + operation.setType(Operation.Type.PROFILE); + operation.setPayLoad(passcodePolicy.toJSON()); + + return AndroidAPIUtils.getOperationResponse(passwordPolicyBeanWrapper.getDeviceIDs(), + operation, message, responseMediaType); + + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + } + + @POST + @Path("webclip") + public Response setWebClip(@HeaderParam(ACCEPT) String acceptHeader, + WebClipBeanWrapper webClipBeanWrapper) { + if (log.isDebugEnabled()) { + log.debug("Invoking 'webclip' operation"); + } + + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + + try { + WebClip webClip = webClipBeanWrapper.getOperation(); + + if (webClip == null) { + if (log.isDebugEnabled()) { + log.debug("The payload of the add webclip operation is incorrect"); + } + throw new OperationManagementException("Issue in creating a new web clip instance"); + } + + ProfileOperation operation = new ProfileOperation(); + operation.setCode(AndroidConstants.OperationCodes.WEBCLIP); + operation.setType(Operation.Type.PROFILE); + operation.setPayLoad(webClip.toJSON()); + + return AndroidAPIUtils.getOperationResponse(webClipBeanWrapper.getDeviceIDs(), + operation, message, responseMediaType); + + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + } + + @POST + @Path("disenroll") + public Response setDisenrollment(@HeaderParam(ACCEPT) String acceptHeader, + DisenrollmentBeanWrapper disenrollmentBeanWrapper) { + + if (log.isDebugEnabled()) { + log.debug("Invoking Android device disenrollment operation"); + } + + MediaType responseMediaType = AndroidAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + + try { + Disenrollment disenrollment = disenrollmentBeanWrapper.getOperation(); + + if (disenrollment == null) { + if (log.isDebugEnabled()) { + log.debug("The payload of the device disenrollment operation is incorrect"); + } + throw new OperationManagementException("Issue in creating a new disenrollment instance"); + } + + CommandOperation operation = new CommandOperation(); + operation.setCode(AndroidConstants.OperationCodes.DISENROLL); + operation.setType(Operation.Type.COMMAND); + operation.setEnabled(disenrollment.isEnabled()); + + return AndroidAPIUtils.getOperationResponse(disenrollmentBeanWrapper.getDeviceIDs(), operation, + message, responseMediaType); + + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message = Message.responseMessage(errorMessage). + responseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()).build(); + log.error(errorMessage, e); + throw new AndroidOperationException(message, responseMediaType); + } + } + + private void updateOperations(String deviceId, List operations) + throws OperationManagementException, PolicyComplianceException, + ApplicationManagementException, NotificationManagementException, DeviceManagementException { + for (org.wso2.carbon.device.mgt.common.operation.mgt.Operation operation : operations) { + AndroidAPIUtils.updateOperation(deviceId, operation); + if(operation.getStatus().equals(OPERATION_ERROR_STATUS)){ + org.wso2.carbon.device.mgt.common.notification.mgt.Notification notification = new + org.wso2.carbon.device.mgt.common.notification.mgt.Notification(); + DeviceIdentifier id = new DeviceIdentifier(); + id.setId(deviceId); + id.setType(DEVICE_TYPE_ANDROID); + String deviceName = AndroidAPIUtils.getDeviceManagementService().getDevice(id).getName(); + notification.setOperationId(operation.getId()); + notification.setStatus(org.wso2.carbon.device.mgt.common.notification.mgt.Notification. + Status.NEW.toString()); + notification.setDeviceIdentifier(id); + notification.setDescription("Operation " + operation.getCode() + " failed to execute on device "+ + deviceName+". Device ID : " + deviceId); + AndroidAPIUtils.getNotificationManagementService().addNotification(notification); + } + if (log.isDebugEnabled()) { + log.debug("Updating operation '" + operation.toString() + "'"); + } + } + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/PolicyMgtService.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/PolicyMgtService.java new file mode 100644 index 000000000..e2e9d2cb8 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/PolicyMgtService.java @@ -0,0 +1,92 @@ +/* + * 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.mdm.services.android; + + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.mdm.services.android.exception.AndroidAgentException; +import org.wso2.carbon.mdm.services.android.util.AndroidAPIUtils; +import org.wso2.carbon.mdm.services.android.util.Message; +import org.wso2.carbon.policy.mgt.common.FeatureManagementException; +import org.wso2.carbon.policy.mgt.common.Policy; +import org.wso2.carbon.policy.mgt.common.PolicyManagementException; +import org.wso2.carbon.policy.mgt.common.ProfileFeature; +import org.wso2.carbon.policy.mgt.core.PolicyManagerService; + +import javax.jws.WebService; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import java.util.List; + +@WebService +@Produces({"application/json", "application/xml"}) +@Consumes({"application/json", "application/xml"}) +public class PolicyMgtService { + + private static Log log = LogFactory.getLog(PolicyMgtService.class); + + @GET + @Path("{deviceId}") + public Message getEffectivePolicy(@HeaderParam("Accept") String acceptHeader, + @PathParam("deviceId") String id) throws AndroidAgentException { + + DeviceIdentifier deviceIdentifier = AndroidAPIUtils.convertToDeviceIdentifierObject(id); + Message responseMessage = new Message(); + Policy policy; + try { + PolicyManagerService policyManagerService = AndroidAPIUtils.getPolicyManagerService(); + policy = policyManagerService.getEffectivePolicy(deviceIdentifier); + if (policy == null) { + responseMessage = Message.responseMessage("No effective policy found"). + responseCode(Response.Status.NO_CONTENT.toString()).build(); + } else { + responseMessage = Message.responseMessage("Effective policy added to operation"). + responseCode(Response.Status.OK.toString()).build(); + } + } catch (PolicyManagementException e) { + String msg = "Error occurred while getting the policy."; + log.error(msg, e); + throw new AndroidAgentException(msg, e); + } + return responseMessage; + } + + @GET + @Path("/features/{deviceId}") + public List getEffectiveFeatures(@HeaderParam("Accept") String acceptHeader, + @PathParam("deviceId") String id) throws AndroidAgentException { + List profileFeatures; + DeviceIdentifier deviceIdentifier = AndroidAPIUtils.convertToDeviceIdentifierObject(id); + try { + PolicyManagerService policyManagerService = AndroidAPIUtils.getPolicyManagerService(); + profileFeatures = policyManagerService.getEffectiveFeatures(deviceIdentifier); + if (profileFeatures == null) { + Response.status(Response.Status.NOT_FOUND); + } + } catch (FeatureManagementException e) { + String msg = "Error occurred while getting the features."; + log.error(msg, e); + throw new AndroidAgentException(msg, e); + } + return profileFeatures; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/AndroidOperation.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/AndroidOperation.java new file mode 100644 index 000000000..297a5df75 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/AndroidOperation.java @@ -0,0 +1,36 @@ +/* + * 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.mdm.services.android.bean; + +import com.google.gson.Gson; +/* +* This abstract class is used for extending generic functions with regard to operation. +*/ +public abstract class AndroidOperation { + + /* + * This method is used to convert operation object to a json format. + * + * @return json formatted String. + */ + public String toJSON() { + Gson gson = new Gson(); + return gson.toJson(this); + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/ApplicationInstallation.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/ApplicationInstallation.java new file mode 100644 index 000000000..e545c5bab --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/ApplicationInstallation.java @@ -0,0 +1,55 @@ +/* + * 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.mdm.services.android.bean; + +import java.io.Serializable; + +/** + * This class represents the information of install application operation. + */ +public class ApplicationInstallation extends AndroidOperation implements Serializable { + + private String appIdentifier; + private String type; + private String url; + + public String getAppIdentifier() { + return appIdentifier; + } + + public void setAppIdentifier(String appIdentifier) { + this.appIdentifier = appIdentifier; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/ApplicationUninstallation.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/ApplicationUninstallation.java new file mode 100644 index 000000000..5e0b2909f --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/ApplicationUninstallation.java @@ -0,0 +1,64 @@ +/* + * 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.mdm.services.android.bean; + +import java.io.Serializable; + +/** + * This class represents the information of uninstall application operation. + */ +public class ApplicationUninstallation extends AndroidOperation implements Serializable { + + private String appIdentifier; + private String type; + private String url; + private String name; + + public String getAppIdentifier() { + return appIdentifier; + } + + public void setAppIdentifier(String appIdentifier) { + this.appIdentifier = appIdentifier; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/BlacklistApplications.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/BlacklistApplications.java new file mode 100644 index 000000000..159ddfec9 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/BlacklistApplications.java @@ -0,0 +1,39 @@ +/* + * 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.mdm.services.android.bean; + +import java.io.Serializable; +import java.util.List; + +/** + * This class represents the blacklist applications information. + */ +public class BlacklistApplications extends AndroidOperation implements Serializable { + + private List appIdentifiers; + + public List getAppIdentifier() { + return appIdentifiers; + } + + public void setAppIdentifier(List appIdentifiers) { + this.appIdentifiers = appIdentifiers; + } + +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/Camera.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/Camera.java new file mode 100644 index 000000000..2b392a19c --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/Camera.java @@ -0,0 +1,37 @@ +/* + * 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.mdm.services.android.bean; + +import java.io.Serializable; + +/** + * This class represents the information of camera operation. + */ +public class Camera extends AndroidOperation implements Serializable { + + private boolean enabled; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/DeviceEncryption.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/DeviceEncryption.java new file mode 100644 index 000000000..de81c94a5 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/DeviceEncryption.java @@ -0,0 +1,37 @@ +/* + * 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.mdm.services.android.bean; + +import java.io.Serializable; + +/** + * This class represents the information of encrypt operation. + */ +public class DeviceEncryption extends AndroidOperation implements Serializable { + + private boolean encrypted; + + public boolean isEncrypted() { + return encrypted; + } + + public void setEncrypted(boolean encrypted) { + this.encrypted = encrypted; + } +} \ No newline at end of file diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/Disenrollment.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/Disenrollment.java new file mode 100644 index 000000000..8a1c2f8ac --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/Disenrollment.java @@ -0,0 +1,35 @@ +/* + * 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.mdm.services.android.bean; + +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import java.io.Serializable; + +public class Disenrollment extends AndroidOperation implements Serializable { + + private boolean enabled; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/LockCode.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/LockCode.java new file mode 100644 index 000000000..a68ec2253 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/LockCode.java @@ -0,0 +1,37 @@ +/* + * 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.mdm.services.android.bean; + +import java.io.Serializable; + +/** + * This class represents the information of changing lock code operation. + */ +public class LockCode extends AndroidOperation implements Serializable { + + private String lockCode; + + public String getLockCode() { + return lockCode; + } + + public void setLockCode(String lockCode) { + this.lockCode = lockCode; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/Notification.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/Notification.java new file mode 100644 index 000000000..95e6b56f1 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/Notification.java @@ -0,0 +1,37 @@ +/* + * 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.mdm.services.android.bean; + +import java.io.Serializable; + +/** + * This class represents the information of sending notification operation. + */ +public class Notification extends AndroidOperation implements Serializable { + + private String message; + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/PasscodePolicy.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/PasscodePolicy.java new file mode 100644 index 000000000..5142f9e63 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/PasscodePolicy.java @@ -0,0 +1,91 @@ +/* + * 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.mdm.services.android.bean; + +import java.io.Serializable; + +/** + * This class represents the information of setting up password policy. + */ +public class PasscodePolicy extends AndroidOperation implements Serializable { + + private int maxFailedAttempts; + private int minLength; + private int pinHistory; + private int minComplexChars; + private int maxPINAgeInDays; + private boolean requireAlphanumeric; + private boolean allowSimple; + + public int getMaxFailedAttempts() { + return maxFailedAttempts; + } + + public void setMaxFailedAttempts(int maxFailedAttempts) { + this.maxFailedAttempts = maxFailedAttempts; + } + + public int getMinLength() { + return minLength; + } + + public void setMinLength(int minLength) { + this.minLength = minLength; + } + + public int getPinHistory() { + return pinHistory; + } + + public void setPinHistory(int pinHistory) { + this.pinHistory = pinHistory; + } + + public int getMinComplexChars() { + return minComplexChars; + } + + public void setMinComplexChars(int minComplexChars) { + this.minComplexChars = minComplexChars; + } + + public int getMaxPINAgeInDays() { + return maxPINAgeInDays; + } + + public void setMaxPINAgeInDays(int maxPINAgeInDays) { + this.maxPINAgeInDays = maxPINAgeInDays; + } + + public boolean isRequireAlphanumeric() { + return requireAlphanumeric; + } + + public void setRequireAlphanumeric(boolean requireAlphanumeric) { + this.requireAlphanumeric = requireAlphanumeric; + } + + public boolean isAllowSimple() { + return allowSimple; + } + + public void setAllowSimple(boolean allowSimple) { + this.allowSimple = allowSimple; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/WebClip.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/WebClip.java new file mode 100644 index 000000000..77fa8e18d --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/WebClip.java @@ -0,0 +1,55 @@ +/* + * 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.mdm.services.android.bean; + +import java.io.Serializable; + +/** + * This class represents the information of setting up webclip. + */ +public class WebClip extends AndroidOperation implements Serializable { + + private String identity; + private String title; + private String type; + + public String getIdentity() { + return identity; + } + + public void setIdentity(String identity) { + this.identity = identity; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/Wifi.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/Wifi.java new file mode 100644 index 000000000..97cee8634 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/Wifi.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.mdm.services.android.bean; + +import java.io.Serializable; + +/** + * This class represents the information of configuring wifi operation. + */ +public class Wifi extends AndroidOperation implements Serializable { + + private String ssid; + private String password; + + public String getSsid() { + return ssid; + } + + public void setSsid(String ssid) { + this.ssid = ssid; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/WipeData.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/WipeData.java new file mode 100644 index 000000000..e3ba747cd --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/WipeData.java @@ -0,0 +1,37 @@ +/* + * 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.mdm.services.android.bean; + +import java.io.Serializable; + +/** + * This class represents the information of wipedata operation. + */ +public class WipeData extends AndroidOperation implements Serializable { + + private String pin; + + public String getPin() { + return pin; + } + + public void setPin(String pin) { + this.pin = pin; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/ApplicationInstallationBeanWrapper.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/ApplicationInstallationBeanWrapper.java new file mode 100644 index 000000000..ae809b8ee --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/ApplicationInstallationBeanWrapper.java @@ -0,0 +1,47 @@ +/* + * 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.mdm.services.android.bean.wrapper; + +import org.wso2.carbon.mdm.services.android.bean.ApplicationInstallation; + +import java.util.List; + +/** + * This class is used to wrap the InstallApplication bean with devices. + */ +public class ApplicationInstallationBeanWrapper { + + private List deviceIDs; + private ApplicationInstallation operation; + + public List getDeviceIDs() { + return deviceIDs; + } + + public void setDeviceIDs(List deviceIDs) { + this.deviceIDs = deviceIDs; + } + + public ApplicationInstallation getOperation() { + return operation; + } + + public void setOperation(ApplicationInstallation operation) { + this.operation = operation; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/ApplicationUninstallationBeanWrapper.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/ApplicationUninstallationBeanWrapper.java new file mode 100644 index 000000000..ef34d97dc --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/ApplicationUninstallationBeanWrapper.java @@ -0,0 +1,47 @@ +/* + * 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.mdm.services.android.bean.wrapper; + +import org.wso2.carbon.mdm.services.android.bean.ApplicationUninstallation; + +import java.util.List; + +/** + * This class is used to wrap the UninstallApplication bean with devices. + */ +public class ApplicationUninstallationBeanWrapper { + + private List deviceIDs; + private ApplicationUninstallation operation; + + public List getDeviceIDs() { + return deviceIDs; + } + + public void setDeviceIDs(List deviceIDs) { + this.deviceIDs = deviceIDs; + } + + public ApplicationUninstallation getOperation() { + return operation; + } + + public void setOperation(ApplicationUninstallation operation) { + this.operation = operation; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/BlacklistApplicationsBeanWrapper.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/BlacklistApplicationsBeanWrapper.java new file mode 100644 index 000000000..f74b0e863 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/BlacklistApplicationsBeanWrapper.java @@ -0,0 +1,47 @@ +/* + * 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.mdm.services.android.bean.wrapper; + +import org.wso2.carbon.mdm.services.android.bean.BlacklistApplications; + +import java.util.List; + +/** + * This class is used to wrap the BlacklistApplications bean with devices. + */ +public class BlacklistApplicationsBeanWrapper { + + private BlacklistApplications operation; + private List deviceIDs; + + public BlacklistApplications getOperation() { + return operation; + } + + public void setOperation(BlacklistApplications operation) { + this.operation = operation; + } + + public List getDeviceIDs() { + return deviceIDs; + } + + public void setDeviceIDs(List deviceIDs) { + this.deviceIDs = deviceIDs; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/CameraBeanWrapper.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/CameraBeanWrapper.java new file mode 100644 index 000000000..4bbde3ac4 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/CameraBeanWrapper.java @@ -0,0 +1,47 @@ +/* + * 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.mdm.services.android.bean.wrapper; + +import org.wso2.carbon.mdm.services.android.bean.Camera; + +import java.util.List; + +/** + * This class is used to wrap the Camera bean with devices. + */ +public class CameraBeanWrapper { + + private Camera operation; + private List deviceIDs; + + public Camera getOperation() { + return operation; + } + + public void setOperation(Camera operation) { + this.operation = operation; + } + + public List getDeviceIDs() { + return deviceIDs; + } + + public void setDeviceIDs(List deviceIDs) { + this.deviceIDs = deviceIDs; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/DisenrollmentBeanWrapper.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/DisenrollmentBeanWrapper.java new file mode 100644 index 000000000..c4d22061e --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/DisenrollmentBeanWrapper.java @@ -0,0 +1,44 @@ +/* + * 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.mdm.services.android.bean.wrapper; + +import org.wso2.carbon.mdm.services.android.bean.Disenrollment; + +import java.util.List; + +public class DisenrollmentBeanWrapper { + + private Disenrollment operation; + private List deviceIDs; + + public Disenrollment getOperation() { + return operation; + } + + public void setOperation(Disenrollment operation) { + this.operation = operation; + } + + public List getDeviceIDs() { + return deviceIDs; + } + + public void setDeviceIDs(List deviceIDs) { + this.deviceIDs = deviceIDs; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/EncryptionBeanWrapper.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/EncryptionBeanWrapper.java new file mode 100644 index 000000000..462b1137f --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/EncryptionBeanWrapper.java @@ -0,0 +1,47 @@ +/* + * 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.mdm.services.android.bean.wrapper; + +import org.wso2.carbon.mdm.services.android.bean.DeviceEncryption; + +import java.util.List; + +/** + * This class is used to wrap the Encrypt bean with devices. + */ +public class EncryptionBeanWrapper { + + private DeviceEncryption operation; + private List deviceIDs; + + public DeviceEncryption getOperation() { + return operation; + } + + public void setOperation(DeviceEncryption operation) { + this.operation = operation; + } + + public List getDeviceIDs() { + return deviceIDs; + } + + public void setDeviceIDs(List deviceIDs) { + this.deviceIDs = deviceIDs; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/LockCodeBeanWrapper.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/LockCodeBeanWrapper.java new file mode 100644 index 000000000..41594c280 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/LockCodeBeanWrapper.java @@ -0,0 +1,47 @@ +/* + * 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.mdm.services.android.bean.wrapper; + +import org.wso2.carbon.mdm.services.android.bean.LockCode; + +import java.util.List; + +/** + * This class is used to wrap the LockCode bean with devices. + */ +public class LockCodeBeanWrapper { + + private LockCode operation; + private List deviceIDs; + + public LockCode getOperation() { + return operation; + } + + public void setOperation(LockCode operation) { + this.operation = operation; + } + + public List getDeviceIDs() { + return deviceIDs; + } + + public void setDeviceIDs(List deviceIDs) { + this.deviceIDs = deviceIDs; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/NotificationBeanWrapper.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/NotificationBeanWrapper.java new file mode 100644 index 000000000..e1092861a --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/NotificationBeanWrapper.java @@ -0,0 +1,47 @@ +/* + * 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.mdm.services.android.bean.wrapper; + +import org.wso2.carbon.mdm.services.android.bean.Notification; + +import java.util.List; + +/** + * This class is used to wrap the Notification bean with devices. + */ +public class NotificationBeanWrapper { + + private List deviceIDs; + private Notification operation; + + public List getDeviceIDs() { + return deviceIDs; + } + + public void setDeviceIDs(List deviceIDs) { + this.deviceIDs = deviceIDs; + } + + public Notification getOperation() { + return operation; + } + + public void setOperation(Notification operation) { + this.operation = operation; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/PasswordPolicyBeanWrapper.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/PasswordPolicyBeanWrapper.java new file mode 100644 index 000000000..cf54147e0 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/PasswordPolicyBeanWrapper.java @@ -0,0 +1,47 @@ +/* + * 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.mdm.services.android.bean.wrapper; + +import org.wso2.carbon.mdm.services.android.bean.PasscodePolicy; + +import java.util.List; + +/** + * This class is used to wrap the PasswordPolicy bean with devices. + */ +public class PasswordPolicyBeanWrapper { + + private PasscodePolicy operation; + private List deviceIDs; + + public PasscodePolicy getOperation() { + return operation; + } + + public void setOperation(PasscodePolicy operation) { + this.operation = operation; + } + + public List getDeviceIDs() { + return deviceIDs; + } + + public void setDeviceIDs(List deviceIDs) { + this.deviceIDs = deviceIDs; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/WebClipBeanWrapper.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/WebClipBeanWrapper.java new file mode 100644 index 000000000..7fbbe8559 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/WebClipBeanWrapper.java @@ -0,0 +1,47 @@ +/* + * 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.mdm.services.android.bean.wrapper; + +import org.wso2.carbon.mdm.services.android.bean.WebClip; + +import java.util.List; + +/** + * This class is used to wrap the WebClip bean with devices. + */ +public class WebClipBeanWrapper { + + private WebClip operation; + private List deviceIDs; + + public WebClip getOperation() { + return operation; + } + + public void setOperation(WebClip operation) { + this.operation = operation; + } + + public List getDeviceIDs() { + return deviceIDs; + } + + public void setDeviceIDs(List deviceIDs) { + this.deviceIDs = deviceIDs; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/WifiBeanWrapper.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/WifiBeanWrapper.java new file mode 100644 index 000000000..b19e01726 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/WifiBeanWrapper.java @@ -0,0 +1,47 @@ +/* + * 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.mdm.services.android.bean.wrapper; + +import org.wso2.carbon.mdm.services.android.bean.Wifi; + +import java.util.List; + +/** + * This class is used to wrap the Wifi bean with devices. + */ +public class WifiBeanWrapper { + + private Wifi operation; + private List deviceIDs; + + public Wifi getOperation() { + return operation; + } + + public void setOperation(Wifi operation) { + this.operation = operation; + } + + public List getDeviceIDs() { + return deviceIDs; + } + + public void setDeviceIDs(List deviceIDs) { + this.deviceIDs = deviceIDs; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/WipeDataBeanWrapper.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/WipeDataBeanWrapper.java new file mode 100644 index 000000000..ebf05d1cd --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/bean/wrapper/WipeDataBeanWrapper.java @@ -0,0 +1,47 @@ +/* + * 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.mdm.services.android.bean.wrapper; + +import org.wso2.carbon.mdm.services.android.bean.WipeData; + +import java.util.List; + +/** + * This class is used to wrap the WipeData bean with devices. + */ +public class WipeDataBeanWrapper { + + private WipeData operation; + private List deviceIDs; + + public WipeData getOperation() { + return operation; + } + + public void setOperation(WipeData operation) { + this.operation = operation; + } + + public List getDeviceIDs() { + return deviceIDs; + } + + public void setDeviceIDs(List deviceIDs) { + this.deviceIDs = deviceIDs; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/common/ErrorHandler.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/common/ErrorHandler.java new file mode 100644 index 000000000..6d5e0d1c4 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/common/ErrorHandler.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 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. + */ +package org.wso2.carbon.mdm.services.android.common; + +import org.wso2.carbon.mdm.services.android.exception.AndroidAgentException; + +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + + +@Produces({ "application/json", "application/xml" }) +public class ErrorHandler implements ExceptionMapper { + + public Response toResponse(AndroidAgentException exception) { + ErrorMessage errorMessage = new ErrorMessage(); + errorMessage.setErrorMessage(exception.getErrorMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(errorMessage).build(); + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/common/ErrorMessage.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/common/ErrorMessage.java new file mode 100644 index 000000000..86be36f36 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/common/ErrorMessage.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 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. + */ +package org.wso2.carbon.mdm.services.android.common; + + +public class ErrorMessage { + + private String errorMessage; + private String errorCode; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public String getErrorCode() { + return errorCode; + } + + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/common/GsonMessageBodyHandler.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/common/GsonMessageBodyHandler.java new file mode 100644 index 000000000..193eda1bc --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/common/GsonMessageBodyHandler.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 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. + */ + +package org.wso2.carbon.mdm.services.android.common; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; + +import java.io.*; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +@Provider +@Produces(APPLICATION_JSON) +@Consumes(APPLICATION_JSON) +public class GsonMessageBodyHandler implements MessageBodyWriter, MessageBodyReader { + + private Gson gson; + private static final String UTF_8 = "UTF-8"; + + public boolean isReadable(Class aClass, Type type, Annotation[] annotations, MediaType mediaType) { + return true; + } + + private Gson getGson() { + if (gson == null) { + final GsonBuilder gsonBuilder = new GsonBuilder(); + gson = gsonBuilder.create(); + } + return gson; + } + + public Object readFrom(Class objectClass, Type type, Annotation[] annotations, MediaType mediaType, + MultivaluedMap stringStringMultivaluedMap, InputStream entityStream) + throws IOException, WebApplicationException { + + InputStreamReader reader = new InputStreamReader(entityStream, "UTF-8"); + + try { + return getGson().fromJson(reader, type); + } finally { + reader.close(); + } + } + + public boolean isWriteable(Class aClass, Type type, Annotation[] annotations, MediaType mediaType) { + return true; + } + + public long getSize(Object o, Class aClass, Type type, Annotation[] annotations, MediaType mediaType) { + return -1; + } + + public void writeTo(Object object, Class aClass, Type type, Annotation[] annotations, MediaType mediaType, + MultivaluedMap stringObjectMultivaluedMap, OutputStream entityStream) + throws IOException, WebApplicationException { + + OutputStreamWriter writer = new OutputStreamWriter(entityStream, UTF_8); + try { + Type jsonType; + if (type.equals(type)) { + jsonType = type; + } else { + jsonType = type; + } + getGson().toJson(object, jsonType, writer); + } finally { + writer.close(); + } + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/exception/AndroidAgentException.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/exception/AndroidAgentException.java new file mode 100644 index 000000000..8698dda76 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/exception/AndroidAgentException.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 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. + */ +package org.wso2.carbon.mdm.services.android.exception; + +/** + * + * Custom exception class for Android API related exceptions. + * + */ +public class AndroidAgentException extends Exception{ + + private static final long serialVersionUID = 7950151650447893900L; + private String errorMessage; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public AndroidAgentException(String msg, Exception nestedEx) { + super(msg, nestedEx); + setErrorMessage(msg); + } + + public AndroidAgentException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public AndroidAgentException(String msg) { + super(msg); + setErrorMessage(msg); + } + + public AndroidAgentException() { + super(); + } + + public AndroidAgentException(Throwable cause) { + super(cause); + } + + + +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/exception/AndroidOperationException.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/exception/AndroidOperationException.java new file mode 100644 index 000000000..d2938a115 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/exception/AndroidOperationException.java @@ -0,0 +1,37 @@ +/* + * 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.mdm.services.android.exception; + +import org.wso2.carbon.mdm.services.android.util.Message; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +/** + * Custom exception class for wrapping Android Operation related exceptions. + */ +public class AndroidOperationException extends WebApplicationException { + + public AndroidOperationException(Message message, MediaType mediaType) { + super(Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(message). + type(mediaType).build()); + } +} + diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/exception/BadRequestException.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/exception/BadRequestException.java new file mode 100644 index 000000000..dcf69575e --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/exception/BadRequestException.java @@ -0,0 +1,36 @@ +/* + * 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.mdm.services.android.exception; + +import org.wso2.carbon.mdm.services.android.util.Message; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +/** + * Custom exception class for wrapping BadRequest related exceptions. + */ +public class BadRequestException extends WebApplicationException { + + public BadRequestException(Message message, MediaType mediaType) { + super(Response.status(Response.Status.BAD_REQUEST).entity(message). + type(mediaType).build()); + } +} \ No newline at end of file diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/exception/OperationConfigurationException.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/exception/OperationConfigurationException.java new file mode 100644 index 000000000..f0437f9d9 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/exception/OperationConfigurationException.java @@ -0,0 +1,37 @@ +/* + * 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.mdm.services.android.exception; + +/** + * + * Custom exception class for operation configuration related exceptions. + * + */ +public class OperationConfigurationException extends Exception { + + private static final long serialVersionUID = 7435543643747623629L; + + public OperationConfigurationException(String errorMessage) { + super(errorMessage); + } + + public OperationConfigurationException(String errorMessage, Throwable throwable) { + super(errorMessage, throwable); + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/AndroidAPIUtils.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/AndroidAPIUtils.java new file mode 100644 index 000000000..2292a3ea2 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/AndroidAPIUtils.java @@ -0,0 +1,215 @@ +/* + * 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.mdm.services.android.util; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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.DeviceManagementConstants; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.app.mgt.Application; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManagementException; +import org.wso2.carbon.device.mgt.common.notification.mgt.NotificationManagementService; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.common.operation.mgt.OperationManagementException; +import org.wso2.carbon.device.mgt.core.app.mgt.ApplicationManagementProviderService; +import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; +import org.wso2.carbon.device.mgt.mobile.android.impl.gcm.GCMService; +import org.wso2.carbon.policy.mgt.common.monitor.PolicyComplianceException; +import org.wso2.carbon.policy.mgt.core.PolicyManagerService; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.List; + +/** + * AndroidAPIUtil class provides utility functions used by Android REST-API classes. + */ +public class AndroidAPIUtils { + + private static Log log = LogFactory.getLog(AndroidAPIUtils.class); + + public static DeviceIdentifier convertToDeviceIdentifierObject(String deviceId) { + DeviceIdentifier identifier = new DeviceIdentifier(); + identifier.setId(deviceId); + identifier.setType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID); + return identifier; + } + + public static String getAuthenticatedUser() { + PrivilegedCarbonContext threadLocalCarbonContext = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + String username = threadLocalCarbonContext.getUsername(); + String tenantDomain = threadLocalCarbonContext.getTenantDomain(); + if (username.endsWith(tenantDomain)) { + return username.substring(0, username.lastIndexOf("@")); + } + return username; + } + + public static DeviceManagementProviderService getDeviceManagementService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + DeviceManagementProviderService deviceManagementProviderService = + (DeviceManagementProviderService) ctx.getOSGiService(DeviceManagementProviderService.class, null); + if (deviceManagementProviderService == null) { + String msg = "Device Management service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return deviceManagementProviderService; + } + + public static GCMService getGCMService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + GCMService gcmService = (GCMService) ctx.getOSGiService(GCMService.class, null); + if (gcmService == null) { + String msg = "GCM service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return gcmService; + } + + public static MediaType getResponseMediaType(String acceptHeader) { + MediaType responseMediaType; + if (MediaType.WILDCARD.equals(acceptHeader)) { + responseMediaType = MediaType.APPLICATION_JSON_TYPE; + } else { + responseMediaType = MediaType.valueOf(acceptHeader); + } + return responseMediaType; + } + + public static Response getOperationResponse(List deviceIDs, Operation operation, + Message message, MediaType responseMediaType) + throws DeviceManagementException, OperationManagementException { + + AndroidDeviceUtils deviceUtils = new AndroidDeviceUtils(); + DeviceIDHolder deviceIDHolder = deviceUtils.validateDeviceIdentifiers(deviceIDs, + message, responseMediaType); + int status = getDeviceManagementService().addOperation(operation, deviceIDHolder.getValidDeviceIDList()); + if (status > 0) { + GCMService gcmService = getGCMService(); + if (gcmService.isGCMEnabled()) { + List devices = new ArrayList(); + for (DeviceIdentifier deviceIdentifier : deviceIDHolder.getValidDeviceIDList()) { + devices.add(getDeviceManagementService().getDevice(deviceIdentifier)); + } + getGCMService().sendNotification(operation.getCode(), devices); + } + } + if (!deviceIDHolder.getErrorDeviceIdList().isEmpty()) { + return javax.ws.rs.core.Response.status(AndroidConstants.StatusCodes. + MULTI_STATUS_HTTP_CODE).type( + responseMediaType).entity(deviceUtils. + convertErrorMapIntoErrorMessage(deviceIDHolder.getErrorDeviceIdList())).build(); + } + return javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.CREATED). + type(responseMediaType).build(); + } + + + public static PolicyManagerService getPolicyManagerService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + PolicyManagerService policyManagerService = (PolicyManagerService) ctx.getOSGiService( + PolicyManagerService.class, null); + if (policyManagerService == null) { + String msg = "Policy Manager service has not initialized"; + log.error(msg); + throw new IllegalStateException(msg); + } + return policyManagerService; + } + + public static ApplicationManagementProviderService getApplicationManagerService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + ApplicationManagementProviderService applicationManagementProviderService = + (ApplicationManagementProviderService) ctx.getOSGiService(ApplicationManagementProviderService.class, null); + if (applicationManagementProviderService == null) { + String msg = "Application Management provider service has not initialized"; + log.error(msg); + throw new IllegalStateException(msg); + } + return applicationManagementProviderService; + } + + public static NotificationManagementService getNotificationManagementService() { + NotificationManagementService notificationManagementService; + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + notificationManagementService = (NotificationManagementService) ctx.getOSGiService( + NotificationManagementService.class, null); + if (notificationManagementService == null) { + String msg = "Notification Management service not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return notificationManagementService; + } + + public static void updateOperation(String deviceId, Operation operation) + throws OperationManagementException, PolicyComplianceException, ApplicationManagementException { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID); + if (AndroidConstants.OperationCodes.MONITOR.equals(operation.getCode())) { + if (log.isDebugEnabled()) { + log.info("Received compliance status from MONITOR operation ID: " + operation.getId()); + } + getPolicyManagerService().checkPolicyCompliance(deviceIdentifier, operation.getOperationResponse()); + } else if (AndroidConstants.OperationCodes.APPLICATION_LIST.equals(operation.getCode())) { + if (log.isDebugEnabled()) { + log.info("Received applications list from device '" + deviceId + "'"); + } + updateApplicationList(operation, deviceIdentifier); + } + getDeviceManagementService().updateOperation(deviceIdentifier, operation); + } + + public static List getPendingOperations + (DeviceIdentifier deviceIdentifier) throws OperationManagementException { + + List operations; + operations = getDeviceManagementService().getPendingOperations(deviceIdentifier); + return operations; + } + + private static void updateApplicationList(Operation operation, DeviceIdentifier deviceIdentifier) + throws ApplicationManagementException { + List applications = new ArrayList(); + // Parsing json string to get applications list. + JsonElement jsonElement = new JsonParser().parse(operation.getOperationResponse()); + JsonArray jsonArray = jsonElement.getAsJsonArray(); + Application app; + for (JsonElement element : jsonArray) { + app = new Application(); + app.setName(element.getAsJsonObject(). + get(AndroidConstants.ApplicationProperties.NAME).getAsString()); + app.setApplicationIdentifier(element.getAsJsonObject(). + get(AndroidConstants.ApplicationProperties.IDENTIFIER).getAsString()); + app.setPlatform(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID); + applications.add(app); + } + getApplicationManagerService().updateApplicationListInstalledInDevice(deviceIdentifier, applications); + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/AndroidConstants.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/AndroidConstants.java new file mode 100644 index 000000000..875560de7 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/AndroidConstants.java @@ -0,0 +1,114 @@ +/* + * 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.mdm.services.android.util; + +/** + * Defines constants used in Android-REST API bundle. + */ +public final class AndroidConstants { + + public final class DeviceProperties{ + private DeviceProperties() { + throw new AssertionError(); + } + public static final String PROPERTY_USER_KEY = "username"; + public static final String PROPERTY_DEVICE_KEY = "device"; + } + + public final class DeviceFeatures{ + private DeviceFeatures() { + throw new AssertionError(); + } + } + + public final class DeviceConstants{ + private DeviceConstants() { + throw new AssertionError(); + } + public static final String DEVICE_MAC_KEY = "mac"; + public static final String DEVICE_DESCRIPTION_KEY = "description"; + public static final String DEVICE_OWNERSHIP_KEY = "ownership"; + public static final String DEVICE_PROPERTIES_KEY = "properties"; + public static final String DEVICE_FEATURES_KEY = "features"; + public static final String DEVICE_DATA = "data"; + public static final String DEVICE_ID_NOT_FOUND = "Device Id not found for device found at %s"; + public static final String DEVICE_ID_SERVICE_NOT_FOUND = + "Issue in retrieving device management service instance for device found at %s"; + } + + public final class Messages{ + private Messages(){ + throw new AssertionError(); + } + public static final String DEVICE_MANAGER_SERVICE_NOT_AVAILABLE = + "Device Manager service not available"; + } + + public final class OperationCodes{ + private OperationCodes(){ + throw new AssertionError(); + } + public static final String DEVICE_LOCK = "DEVICE_LOCK"; + public static final String DEVICE_LOCATION = "DEVICE_LOCATION"; + public static final String WIFI = "WIFI"; + public static final String CAMERA = "CAMERA"; + public static final String DEVICE_MUTE = "DEVICE_MUTE"; + public static final String PASSCODE_POLICY = "PASSCODE_POLICY"; + public static final String DEVICE_INFO = "DEVICE_INFO"; + public static final String ENTERPRISE_WIPE = "ENTERPRISE_WIPE"; + public static final String CLEAR_PASSWORD = "CLEAR_PASSWORD"; + public static final String WIPE_DATA = "WIPE_DATA"; + public static final String APPLICATION_LIST = "APPLICATION_LIST"; + public static final String CHANGE_LOCK_CODE = "CHANGE_LOCK_CODE"; + public static final String INSTALL_APPLICATION = "INSTALL_APPLICATION"; + public static final String UNINSTALL_APPLICATION = "UNINSTALL_APPLICATION"; + public static final String BLACKLIST_APPLICATIONS = "BLACKLIST_APPLICATIONS"; + public static final String ENCRYPT_STORAGE = "ENCRYPT_STORAGE"; + public static final String DEVICE_RING = "DEVICE_RING"; + public static final String NOTIFICATION = "NOTIFICATION"; + public static final String WEBCLIP = "WEBCLIP"; + public static final String DISENROLL = "DISENROLL"; + public static final String MONITOR = "MONITOR"; + } + + public final class StatusCodes{ + private StatusCodes(){ + throw new AssertionError(); + } + public static final int MULTI_STATUS_HTTP_CODE = 207; + } + + public final class TenantConfigProperties{ + private TenantConfigProperties(){ + throw new AssertionError(); + } + public static final String LICENSE_KEY = "androidEula"; + public static final String LANGUAGE_US = "en_US"; + public static final String CONTENT_TYPE_TEXT = "text"; + } + + public final class ApplicationProperties { + private ApplicationProperties(){ + throw new AssertionError(); + } + public static final String NAME = "name"; + public static final String IDENTIFIER = "package"; + public static final String ICON = "icon"; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/AndroidDeviceUtils.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/AndroidDeviceUtils.java new file mode 100644 index 000000000..b41fbd392 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/AndroidDeviceUtils.java @@ -0,0 +1,98 @@ +/* + * 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.mdm.services.android.util; + +import org.apache.commons.lang.StringUtils; +import org.wso2.carbon.device.mgt.common.*; +import org.wso2.carbon.mdm.services.android.exception.BadRequestException; + +import javax.ws.rs.core.MediaType; +import java.util.ArrayList; +import java.util.List; + +/** + * Util class for holding Android device related util methods. + */ +public class AndroidDeviceUtils { + + private static final String COMMA_SEPARATION_PATTERN = ", "; + + public DeviceIDHolder validateDeviceIdentifiers(List deviceIDs, + Message message, MediaType responseMediaType) { + + if (deviceIDs == null || deviceIDs.isEmpty()) { + message.setResponseMessage("Device identifier list is empty"); + throw new BadRequestException(message, responseMediaType); + } + + List errorDeviceIdList = new ArrayList(); + List validDeviceIDList = new ArrayList(); + + int deviceIDCounter = 0; + for (String deviceID : deviceIDs) { + + deviceIDCounter++; + + if (deviceID == null || deviceID.isEmpty()) { + errorDeviceIdList.add(String.format(AndroidConstants.DeviceConstants.DEVICE_ID_NOT_FOUND, + deviceIDCounter)); + continue; + } + + try { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceID); + deviceIdentifier.setType(DeviceManagementConstants.MobileDeviceTypes. + MOBILE_DEVICE_TYPE_ANDROID); + + if (isValidDeviceIdentifier(deviceIdentifier)) { + validDeviceIDList.add(deviceIdentifier); + } else { + errorDeviceIdList.add(String.format(AndroidConstants.DeviceConstants.DEVICE_ID_NOT_FOUND, + deviceIDCounter)); + } + } catch (DeviceManagementException e) { + errorDeviceIdList.add(String.format(AndroidConstants.DeviceConstants.DEVICE_ID_SERVICE_NOT_FOUND, + deviceIDCounter)); + } + } + + DeviceIDHolder deviceIDHolder = new DeviceIDHolder(); + deviceIDHolder.setValidDeviceIDList(validDeviceIDList); + deviceIDHolder.setErrorDeviceIdList(errorDeviceIdList); + + return deviceIDHolder; + } + + public String convertErrorMapIntoErrorMessage(List errorDeviceIdList) { + return StringUtils.join(errorDeviceIdList.iterator(), COMMA_SEPARATION_PATTERN); + } + + public static boolean isValidDeviceIdentifier(DeviceIdentifier deviceIdentifier) throws DeviceManagementException { + Device device = AndroidAPIUtils.getDeviceManagementService(). + getDevice(deviceIdentifier); + if (device == null || device.getDeviceIdentifier() == null || + device.getDeviceIdentifier().isEmpty() || device.getEnrolmentInfo() == null) { + return false; + } else if (EnrolmentInfo.Status.REMOVED.equals(device.getEnrolmentInfo().getStatus())) { + return false; + } + return true; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/DeviceIDHolder.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/DeviceIDHolder.java new file mode 100644 index 000000000..db8565730 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/DeviceIDHolder.java @@ -0,0 +1,48 @@ +/* + * 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.mdm.services.android.util; + +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; + +import java.util.List; + +/** + * Holder class for storing valid & invalid device-ids. + */ +public class DeviceIDHolder { + + private List errorDeviceIdList; + private List validDeviceIDList; + + public List getErrorDeviceIdList() { + return errorDeviceIdList; + } + + public void setErrorDeviceIdList(List errorDeviceIdList) { + this.errorDeviceIdList = errorDeviceIdList; + } + + public List getValidDeviceIDList() { + return validDeviceIDList; + } + + public void setValidDeviceIDList(List validDeviceIDList) { + this.validDeviceIDList = validDeviceIDList; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/Message.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/Message.java new file mode 100644 index 000000000..d75631001 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/java/org/wso2/carbon/mdm/services/android/util/Message.java @@ -0,0 +1,88 @@ +/* + * 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.mdm.services.android.util; + +import javax.ws.rs.core.MediaType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * This class contains the information of response message. + */ +@XmlRootElement +public class Message { + + private String responseCode; + private String responseMessage; + + @XmlElement + public String getResponseMessage() { + return responseMessage; + } + + public void setResponseMessage(String responseMessage) { + this.responseMessage = responseMessage; + } + + @XmlElement + public String getResponseCode() { + return responseCode; + } + + public void setResponseCode(String responseCode) { + this.responseCode = responseCode; + } + + private Message.MessageBuilder getBuilder() { + return new Message.MessageBuilder(); + } + + public static Message.MessageBuilder responseCode(String responseCode) { + Message message = new Message(); + return message.getBuilder().responseCode(responseCode); + } + + public static Message.MessageBuilder responseMessage(String responseMessage) { + Message message = new Message(); + return message.getBuilder().responseMessage(responseMessage); + } + + public class MessageBuilder { + + private String responseCode; + private String responseMessage; + + public MessageBuilder responseCode(String responseCode) { + this.responseCode = responseCode; + return this; + } + + public MessageBuilder responseMessage(String responseMessage) { + this.responseMessage = responseMessage; + return this; + } + + public Message build() { + Message message = new Message(); + message.setResponseCode(responseCode); + message.setResponseMessage(responseMessage); + return message; + } + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/META-INF/permissions.xml b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/META-INF/permissions.xml new file mode 100644 index 000000000..42478f021 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/META-INF/permissions.xml @@ -0,0 +1,315 @@ + + + + + + + + + Get Pending Operation + /device-mgt/android/operations/poll + /operation/* + PUT + emm_android_agent + + + + Lock + /device-mgt/android/operations/lock + /operation/lock + POST + emm_admin,emm_user + + + + Mute + /device-mgt/android/operations/mute + /operation/mute + POST + emm_admin,emm_user + + + + Location + /device-mgt/android/operations/location + /operation/location + POST + emm_admin,emm_user + + + + Clear Passcode + /operation/clear-password/ + POST + emm_admin + + + + Camera + /device-mgt/android/operations/camera + /operation/camera + POST + emm_admin,emm_user + + + + Device Info + /device-mgt/android/operations/device-info + /operation/device-info + POST + emm_admin,emm_user + + + + Enterprise Wipe + /device-mgt/android/operations/enterprise-wipe + /operation/enterprise-wipe + POST + emm_admin + + + + Wipe Data + /device-mgt/android/operations/wipe-data + /operation/wipe-data + POST + emm_admin + + + + Application List + /device-mgt/android/operations/application-list + /operation/application-list + POST + emm_admin,emm_user + + + + Ring + /device-mgt/android/operations/ring-device + /operation/ring-device + POST + emm_admin,emm_user + + + + Install Application + /device-mgt/android/operations/install-application + /operation/install-application + POST + emm_admin,emm_user + + + + Uninstall Application + /device-mgt/android/operations/uninstall-application + /operation/uninstall-application + POST + emm_admin,emm_user + + + + Blacklist Applications + /device-mgt/android/operations/blacklist-applications + /operation/blacklist-applications + POST + emm_admin + + + + Notification + /device-mgt/android/operations/notification + /operation/notification + POST + emm_admin,emm_user + + + + Wifi + /device-mgt/android/operations/wifi + /operation/wifi + POST + emm_admin + + + + Encryption + /device-mgt/android/operations/encrypt + /operation/encrypt + POST + emm_admin + + + + Change lock code + /device-mgt/android/operations/change-lock-code + /operation/change-lock-code + POST + emm_admin + + + + Password Policy + /device-mgt/android/operations/password-policy + /operation/password-policy + POST + emm_admin + + + + Webclip + /device-mgt/android/operations/webclip + /operation/webclip + POST + emm_admin + + + + Disenroll + /device-mgt/android/operations/disenroll + /operation/disenroll + POST + emm_android_agent + + + + + View devices + /device-mgt/android/devices/list + /device + GET + emm_admin + + + + View device + /device-mgt/android/devices/view + /device/* + GET + emm_admin,emm_user + + + + Update device + /device-mgt/android/devices/update + /device/* + PUT + emm_admin,emm_user + + + + Update application list + /device-mgt/android/devices/update-app + /device/appList/* + POST + emm_admin + + + + View license + /device-mgt/android/license/view + /device/license + GET + emm_android_agent + + + + + Enroll device + /device-mgt/android/devices/enroll + /enrollment + POST + emm_android_agent + + + + Get Enrollment Status + /device-mgt/android/devices/enroll + /enrollment/* + GET + emm_android_agent + + + + Update Enrollment + /device-mgt/android/devices/enroll + /enrollment/* + PUT + emm_user,emm_admin + + + + Disenroll device + /device-mgt/android/devices/enroll + /enrollment/* + DELETE + emm_user,emm_admin + + + + + View Policies + /device-mgt/android/policies/view + /policy/* + GET + emm_admin + + + + View Policy Features + /device-mgt/android/policies/view + /policy/features/* + GET + emm_admin + + + + + View Tenant configuration + /device-mgt/android/tenant/configuration + /configuration + GET + emm_admin + + + + Add Tenant configuration + /device-mgt/android/tenant/configuration + /configuration + POST + emm_admin + + + + Update Tenant configuration + /device-mgt/android/tenant/configuration + /configuration + PUT + emm_admin + + \ No newline at end of file diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/META-INF/webapp-classloading.xml b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/META-INF/webapp-classloading.xml new file mode 100644 index 000000000..46c211bb2 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/META-INF/webapp-classloading.xml @@ -0,0 +1,35 @@ + + + + + + + + + false + + + CXF,Carbon + diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/WEB-INF/cxf-servlet.xml b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/WEB-INF/cxf-servlet.xml new file mode 100644 index 000000000..cfe04fb73 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/WEB-INF/cxf-servlet.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/WEB-INF/web.xml b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..9e336958f --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,77 @@ + + + + Android-Agent-Webapp + + JAX-WS/JAX-RS MDM Android Endpoint + JAX-WS/JAX-RS Servlet + CXFServlet + + org.apache.cxf.transport.servlet.CXFServlet + + 1 + + + CXFServlet + /* + + + 60 + + + + isAdminService + false + + + managed-api-enabled + false + + + managed-api-owner + admin + + + doAuthentication + true + + + + + + + + + + + + + + diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/servicelist.css b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/servicelist.css new file mode 100644 index 000000000..85bc3f613 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android.agent/src/main/webapp/servicelist.css @@ -0,0 +1,117 @@ +@CHARSET "ISO-8859-1"; + +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} + +html { + background: #efefef; +} + +body { + line-height: 1; + width:960px; + margin:auto; + background:white; + padding:10px; + box-shadow:0px 0px 5px #CCC; + font-family:"Lucida Grande","Lucida Sans","Microsoft Sans Serif", "Lucida Sans Unicode","Verdana","Sans-serif","trebuchet ms" !important; + +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; + width:960px; + border:solid 1px #ccc; +} + +table a { + font-size:12px; + color:#1e90ff; + padding:7px; +float:left; +; +} + +.heading { + font-size: 18px; + margin-top: 20px; + float:left; + color:#0067B1; + margin-bottom:20px; + padding-top:20px; +} + +.field { + font-weight: normal; + width:120px; + font-size:12px; + float:left; + padding:7px; + clear:left; +} +.value { + font-weight: bold; + font-size:12px; + float:left; + padding:7px; + clear:right; +} +.porttypename { + font-weight: bold; + font-size:14px; +} +UL { + margin-top: 0; +} +LI { + font-weight: normal; + font-size:12px; + margin-top:10px; +} + +TD { + border:1px solid #ccc; + vertical-align: text-top; + padding: 5px; +} + + diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/pom.xml b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/pom.xml new file mode 100644 index 000000000..15f5e0f91 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/pom.xml @@ -0,0 +1,176 @@ + + + + + + + android-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.mobile.android + bundle + WSO2 Carbon - Mobile Device Management Android Impl + WSO2 Carbon - Mobile Device Management Android Implementation + http://wso2.org + + + + + org.apache.felix + maven-scr-plugin + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + ${project.artifactId} + ${project.artifactId} + ${carbon.devicemgt.plugins.version} + Device Management Mobile Android Impl Bundle + org.wso2.carbon.device.mgt.mobile.android.internal + + org.osgi.framework, + org.osgi.service.component, + org.apache.commons.logging, + javax.xml.bind.*, + javax.naming, + javax.sql, + javax.xml.bind.annotation.*, + javax.xml.parsers.*;resolution:=optional, + org.w3c.dom, + org.wso2.carbon.core, + org.wso2.carbon.context, + org.wso2.carbon.utils.*, + org.wso2.carbon.device.mgt.common.*, + org.wso2.carbon.device.mgt.mobile.*, + org.wso2.carbon.ndatasource.core, + org.wso2.carbon.policy.mgt.common.*, + org.wso2.carbon.policy.mgt.core.*, + org.wso2.carbon.registry.core, + org.wso2.carbon.registry.core.exceptions, + org.wso2.carbon.registry.core.service, + org.wso2.carbon.registry.core.session, + org.wso2.carbon.registry.api, + org.wso2.carbon.device.mgt.extensions.license.mgt.registry, + com.google.gson.* + + + !org.wso2.carbon.device.mgt.mobile.android.internal, + org.wso2.carbon.device.mgt.mobile.android.* + + + + + + + + + + + + + + + + + + + + + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.mobile + + + org.eclipse.osgi + org.eclipse.osgi + + + org.eclipse.osgi + org.eclipse.osgi.services + + + org.wso2.carbon + org.wso2.carbon.core + + + org.wso2.carbon + org.wso2.carbon.logging + + + org.wso2.carbon + org.wso2.carbon.utils + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.extensions + + + org.wso2.carbon + org.wso2.carbon.ndatasource.core + + + org.wso2.carbon.devicemgt + org.wso2.carbon.policy.mgt.common + + + org.wso2.carbon.devicemgt + org.wso2.carbon.policy.mgt.core + + + org.wso2.carbon + org.wso2.carbon.registry.api + + + org.wso2.carbon + org.wso2.carbon.registry.core + + + org.testng + testng + + + org.apache.tomcat.wso2 + jdbc-pool + + + com.h2database.wso2 + h2-database-engine + test + + + com.google.code.gson + gson + + + diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/AndroidDeviceManagementService.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/AndroidDeviceManagementService.java new file mode 100644 index 000000000..60c29fec9 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/AndroidDeviceManagementService.java @@ -0,0 +1,109 @@ +/* + * 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.device.mgt.mobile.android.impl; + +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.DeviceManager; +import org.wso2.carbon.device.mgt.common.app.mgt.Application; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManagementException; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManager; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; + +import java.util.List; + +/** + * This represents the Android implementation of DeviceManagerService. + */ +public class AndroidDeviceManagementService implements DeviceManagementService { + + private DeviceManager deviceManager; + public static final String DEVICE_TYPE_ANDROID = "android"; + public static final String DEVICE_TYPE_TENANT = "carbon.super"; + + @Override + public String getType() { + return AndroidDeviceManagementService.DEVICE_TYPE_ANDROID; + } + + @Override + public String getProviderTenantDomain() { + return DEVICE_TYPE_TENANT; + } + + @Override + public boolean isSharedWithAllTenants() { + return true; + } + + @Override + public void init() throws DeviceManagementException { + this.deviceManager = new AndroidDeviceManager(); + } + + @Override + public DeviceManager getDeviceManager() { + return deviceManager; + } + + @Override + public ApplicationManager getApplicationManager() { + return null; + } + + @Override + public void notifyOperationToDevices(Operation operation, List deviceIdentifiers) + throws DeviceManagementException { + + } + + @Override + public Application[] getApplications(String s, int i, int i2) throws ApplicationManagementException { + return new Application[0]; + } + + @Override + public void updateApplicationStatus(DeviceIdentifier deviceIdentifier, Application application, + String s) throws ApplicationManagementException { + + } + + @Override + public String getApplicationStatus(DeviceIdentifier deviceIdentifier, + Application application) throws ApplicationManagementException { + return null; + } + + @Override public void installApplicationForDevices(Operation operation, List deviceIdentifiers) + throws ApplicationManagementException { + + } + + @Override public void installApplicationForUsers(Operation operation, List strings) + throws ApplicationManagementException { + + } + + @Override public void installApplicationForUserRoles(Operation operation, List strings) + throws ApplicationManagementException { + + } + +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/AndroidDeviceManager.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/AndroidDeviceManager.java new file mode 100644 index 000000000..e4996c0e4 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/AndroidDeviceManager.java @@ -0,0 +1,356 @@ +/* + * 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.device.mgt.mobile.android.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementConstants; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.DeviceManager; +import org.wso2.carbon.device.mgt.common.EnrolmentInfo; +import org.wso2.carbon.device.mgt.common.FeatureManager; +import org.wso2.carbon.device.mgt.common.configuration.mgt.TenantConfiguration; +import org.wso2.carbon.device.mgt.common.license.mgt.License; +import org.wso2.carbon.device.mgt.common.license.mgt.LicenseManagementException; +import org.wso2.carbon.device.mgt.common.license.mgt.LicenseManager; +import org.wso2.carbon.device.mgt.extensions.license.mgt.registry.RegistryBasedLicenseManager; +import org.wso2.carbon.device.mgt.mobile.android.impl.dao.AndroidDAOFactory; +import org.wso2.carbon.device.mgt.mobile.android.impl.util.AndroidPluginUtils; +import org.wso2.carbon.device.mgt.mobile.common.MobileDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.mobile.common.MobilePluginConstants; +import org.wso2.carbon.device.mgt.mobile.dao.AbstractMobileDeviceManagementDAOFactory; +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceManagementDAOException; +import org.wso2.carbon.device.mgt.mobile.dto.MobileDevice; +import org.wso2.carbon.device.mgt.mobile.util.MobileDeviceManagementUtil; +import org.wso2.carbon.registry.api.RegistryException; +import org.wso2.carbon.registry.api.Resource; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +public class AndroidDeviceManager implements DeviceManager { + + private AbstractMobileDeviceManagementDAOFactory daoFactory; + private static final Log log = LogFactory.getLog(AndroidDeviceManagementService.class); + private FeatureManager featureManager = new AndroidFeatureManager(); + private LicenseManager licenseManager; + + public AndroidDeviceManager() { + this.daoFactory = new AndroidDAOFactory(); + this.licenseManager = new RegistryBasedLicenseManager(); + License defaultLicense; + + try { + if (licenseManager.getLicense(AndroidDeviceManagementService.DEVICE_TYPE_ANDROID, + MobilePluginConstants.LANGUAGE_CODE_ENGLISH_US) == null) { + defaultLicense = AndroidPluginUtils.getDefaultLicense(); + licenseManager.addLicense(AndroidDeviceManagementService.DEVICE_TYPE_ANDROID, defaultLicense); + } + featureManager.addSupportedFeaturesToDB(); + } catch (LicenseManagementException e) { + log.error("Error occurred while adding default license for Android devices", e); + } catch (DeviceManagementException e) { + log.error("Error occurred while adding supported device features for Android platform", e); + } + } + + @Override + public FeatureManager getFeatureManager() { + return featureManager; + } + + @Override + public boolean saveConfiguration(TenantConfiguration tenantConfiguration) + throws DeviceManagementException { + boolean status; + try { + if (log.isDebugEnabled()) { + log.debug("Persisting android configurations in Registry"); + } + String resourcePath = MobileDeviceManagementUtil.getPlatformConfigPath( + DeviceManagementConstants. + MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID); + StringWriter writer = new StringWriter(); + JAXBContext context = JAXBContext.newInstance(TenantConfiguration.class); + Marshaller marshaller = context.createMarshaller(); + marshaller.marshal(tenantConfiguration, writer); + + Resource resource = MobileDeviceManagementUtil.getConfigurationRegistry().newResource(); + resource.setContent(writer.toString()); + resource.setMediaType(MobilePluginConstants.MEDIA_TYPE_XML); + MobileDeviceManagementUtil.putRegistryResource(resourcePath, resource); + status = true; + } catch (MobileDeviceMgtPluginException e) { + throw new DeviceManagementException( + "Error occurred while retrieving the Registry instance : " + e.getMessage(), e); + } catch (RegistryException e) { + throw new DeviceManagementException( + "Error occurred while persisting the Registry resource of Android Configuration : " + e.getMessage(), e); + } catch (JAXBException e) { + throw new DeviceManagementException( + "Error occurred while parsing the Android configuration : " + e.getMessage(), e); + } + return status; + } + + @Override + public TenantConfiguration getConfiguration() throws DeviceManagementException { + Resource resource; + try { + String androidRegPath = + MobileDeviceManagementUtil.getPlatformConfigPath(DeviceManagementConstants. + MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID); + resource = MobileDeviceManagementUtil.getRegistryResource(androidRegPath); + if (resource != null) { + JAXBContext context = JAXBContext.newInstance(TenantConfiguration.class); + Unmarshaller unmarshaller = context.createUnmarshaller(); + return (TenantConfiguration) unmarshaller.unmarshal( + new StringReader(new String((byte[]) resource.getContent(), Charset. + forName(MobilePluginConstants.CHARSET_UTF8)))); + } + return null; + } catch (MobileDeviceMgtPluginException e) { + throw new DeviceManagementException( + "Error occurred while retrieving the Registry instance : " + e.getMessage(), e); + } catch (JAXBException e) { + throw new DeviceManagementException( + "Error occurred while parsing the Android configuration : " + e.getMessage(), e); + } catch (RegistryException e) { + throw new DeviceManagementException( + "Error occurred while retrieving the Registry resource of Android Configuration : " + e.getMessage(), e); + } + } + + @Override + public boolean enrollDevice(Device device) throws DeviceManagementException { + boolean status = false; + MobileDevice mobileDevice = MobileDeviceManagementUtil.convertToMobileDevice(device); + try { + if (log.isDebugEnabled()) { + log.debug("Enrolling a new Android device : " + device.getDeviceIdentifier()); + } + boolean isEnrolled = this.isEnrolled( + new DeviceIdentifier(device.getDeviceIdentifier(), device.getType())); + if (isEnrolled) { + this.modifyEnrollment(device); + } else { + AndroidDAOFactory.beginTransaction(); + status = daoFactory.getMobileDeviceDAO().addMobileDevice(mobileDevice); + AndroidDAOFactory.commitTransaction(); + } + } catch (MobileDeviceManagementDAOException e) { + try { + AndroidDAOFactory.rollbackTransaction(); + } catch (MobileDeviceManagementDAOException mobileDAOEx) { + String msg = "Error occurred while roll back the device enrol transaction :" + + device.toString(); + log.warn(msg, mobileDAOEx); + } + String msg = + "Error while enrolling the Android device : " + device.getDeviceIdentifier(); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean modifyEnrollment(Device device) throws DeviceManagementException { + boolean status; + MobileDevice mobileDevice = MobileDeviceManagementUtil.convertToMobileDevice(device); + try { + if (log.isDebugEnabled()) { + log.debug("Modifying the Android device enrollment data"); + } + AndroidDAOFactory.beginTransaction(); + status = daoFactory.getMobileDeviceDAO().updateMobileDevice(mobileDevice); + AndroidDAOFactory.commitTransaction(); + } catch (MobileDeviceManagementDAOException e) { + try { + AndroidDAOFactory.rollbackTransaction(); + } catch (MobileDeviceManagementDAOException mobileDAOEx) { + String msg = "Error occurred while roll back the update device transaction :" + + device.toString(); + log.warn(msg, mobileDAOEx); + } + String msg = "Error while updating the enrollment of the Android device : " + + device.getDeviceIdentifier(); + throw new DeviceManagementException(msg, e); + } + return status; + } + + @Override + public boolean disenrollDevice(DeviceIdentifier deviceId) throws DeviceManagementException { + //Here we don't have anything specific to do. Hence returning. + return true; + } + + @Override + public boolean isEnrolled(DeviceIdentifier deviceId) throws DeviceManagementException { + boolean isEnrolled = false; + try { + if (log.isDebugEnabled()) { + log.debug("Checking the enrollment of Android device : " + deviceId.getId()); + } + MobileDevice mobileDevice = + daoFactory.getMobileDeviceDAO().getMobileDevice(deviceId.getId()); + if (mobileDevice != null) { + isEnrolled = true; + } + } catch (MobileDeviceManagementDAOException e) { + String msg = "Error while checking the enrollment status of Android device : " + + deviceId.getId(); + throw new DeviceManagementException(msg, e); + } + return isEnrolled; + } + + @Override + public boolean isActive(DeviceIdentifier deviceId) throws DeviceManagementException { + return true; + } + + @Override + public boolean setActive(DeviceIdentifier deviceId, boolean status) + throws DeviceManagementException { + return true; + } + + @Override + public Device getDevice(DeviceIdentifier deviceId) throws DeviceManagementException { + Device device; + try { + if (log.isDebugEnabled()) { + log.debug("Getting the details of Android device : '" + deviceId.getId() + "'"); + } + MobileDevice mobileDevice = daoFactory.getMobileDeviceDAO(). + getMobileDevice(deviceId.getId()); + device = MobileDeviceManagementUtil.convertToDevice(mobileDevice); + } catch (MobileDeviceManagementDAOException e) { + throw new DeviceManagementException( + "Error occurred while fetching the Android device: '" + + deviceId.getId() + "'", e); + } + return device; + } + + @Override + public boolean setOwnership(DeviceIdentifier deviceId, String ownershipType) + throws DeviceManagementException { + return true; + } + + @Override + public boolean isClaimable(DeviceIdentifier deviceIdentifier) throws DeviceManagementException { + return false; + } + + @Override + public boolean setStatus(DeviceIdentifier deviceIdentifier, String currentUser, + EnrolmentInfo.Status status) throws DeviceManagementException { + return false; + } + + @Override + public License getLicense(String languageCode) throws LicenseManagementException { + return licenseManager. + getLicense(AndroidDeviceManagementService.DEVICE_TYPE_ANDROID, languageCode); + } + + @Override + public void addLicense(License license) throws LicenseManagementException { + licenseManager.addLicense(AndroidDeviceManagementService.DEVICE_TYPE_ANDROID, license); + } + + @Override + public boolean requireDeviceAuthorization() { + return true; + } + + @Override + public boolean updateDeviceInfo(DeviceIdentifier deviceIdentifier, Device device) + throws DeviceManagementException { + boolean status; + Device existingDevice = this.getDevice(deviceIdentifier); + // This object holds the current persisted device object + MobileDevice existingMobileDevice = + MobileDeviceManagementUtil.convertToMobileDevice(existingDevice); + + // This object holds the newly received device object from response + MobileDevice mobileDevice = MobileDeviceManagementUtil.convertToMobileDevice(device); + + // Updating current object features using newer ones + existingMobileDevice.setLatitude(mobileDevice.getLatitude()); + existingMobileDevice.setLongitude(mobileDevice.getLongitude()); + existingMobileDevice.setDeviceProperties(mobileDevice.getDeviceProperties()); + + try { + if (log.isDebugEnabled()) { + log.debug( + "updating the details of Android device : " + device.getDeviceIdentifier()); + } + AndroidDAOFactory.beginTransaction(); + status = daoFactory.getMobileDeviceDAO().updateMobileDevice(existingMobileDevice); + AndroidDAOFactory.commitTransaction(); + } catch (MobileDeviceManagementDAOException e) { + try { + AndroidDAOFactory.rollbackTransaction(); + } catch (MobileDeviceManagementDAOException e1) { + log.warn("Error occurred while roll back the update device info transaction : '" + + device.toString() + "'", e1); + } + throw new DeviceManagementException( + "Error occurred while updating the Android device: '" + + device.getDeviceIdentifier() + "'", e); + } + return status; + } + + @Override + public List getAllDevices() throws DeviceManagementException { + List devices = null; + try { + if (log.isDebugEnabled()) { + log.debug("Fetching the details of all Android devices"); + } + List mobileDevices = + daoFactory.getMobileDeviceDAO().getAllMobileDevices(); + if (mobileDevices != null) { + devices = new ArrayList<>(mobileDevices.size()); + for (MobileDevice mobileDevice : mobileDevices) { + devices.add(MobileDeviceManagementUtil.convertToDevice(mobileDevice)); + } + } + } catch (MobileDeviceManagementDAOException e) { + throw new DeviceManagementException("Error occurred while fetching all Android devices", + e); + } + return devices; + } + +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/AndroidFeatureManager.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/AndroidFeatureManager.java new file mode 100644 index 000000000..4649cc63b --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/AndroidFeatureManager.java @@ -0,0 +1,244 @@ +/* + * 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.device.mgt.mobile.android.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.Feature; +import org.wso2.carbon.device.mgt.common.FeatureManager; +import org.wso2.carbon.device.mgt.mobile.android.impl.dao.AndroidDAOFactory; +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceManagementDAOException; +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceManagementDAOFactory; +import org.wso2.carbon.device.mgt.mobile.dao.MobileFeatureDAO; +import org.wso2.carbon.device.mgt.mobile.dto.MobileFeature; +import org.wso2.carbon.device.mgt.mobile.util.MobileDeviceManagementUtil; + +import java.util.ArrayList; +import java.util.List; + +public class AndroidFeatureManager implements FeatureManager { + + private MobileFeatureDAO featureDAO; + private static final Log log = LogFactory.getLog(AndroidFeatureManager.class); + + public AndroidFeatureManager() { + MobileDeviceManagementDAOFactory daoFactory = new AndroidDAOFactory(); + this.featureDAO = daoFactory.getMobileFeatureDAO(); + } + + @Override + public boolean addFeature(Feature feature) throws DeviceManagementException { + try { + AndroidDAOFactory.beginTransaction(); + MobileFeature mobileFeature = MobileDeviceManagementUtil.convertToMobileFeature(feature); + featureDAO.addFeature(mobileFeature); + AndroidDAOFactory.commitTransaction(); + return true; + } catch (MobileDeviceManagementDAOException e) { + try { + AndroidDAOFactory.rollbackTransaction(); + } catch (MobileDeviceManagementDAOException e1) { + log.warn("Error occurred while roll-backing the transaction", e); + } + throw new DeviceManagementException("Error occurred while adding the feature", e); + } + } + + @Override + public boolean addFeatures(List features) throws DeviceManagementException { + List mobileFeatures = new ArrayList(features.size()); + for (Feature feature : features) { + mobileFeatures.add(MobileDeviceManagementUtil.convertToMobileFeature(feature)); + } + try { + AndroidDAOFactory.beginTransaction(); + featureDAO.addFeatures(mobileFeatures); + AndroidDAOFactory.commitTransaction(); + return true; + } catch (MobileDeviceManagementDAOException e) { + try { + AndroidDAOFactory.rollbackTransaction(); + } catch (MobileDeviceManagementDAOException e1) { + log.warn("Error occurred while roll-backing the transaction", e); + } + throw new DeviceManagementException("Error occurred while adding the features", e); + } + } + + @Override + public Feature getFeature(String name) throws DeviceManagementException { + try { + MobileFeature mobileFeature = featureDAO.getFeatureByCode(name); + Feature feature = MobileDeviceManagementUtil.convertToFeature(mobileFeature); + return feature; + } catch (MobileDeviceManagementDAOException e) { + throw new DeviceManagementException("Error occurred while retrieving the feature", e); + } + } + + @Override + public List getFeatures() throws DeviceManagementException { + try { + List mobileFeatures = featureDAO.getAllFeatures(); + List featureList = new ArrayList(mobileFeatures.size()); + for (MobileFeature mobileFeature : mobileFeatures) { + featureList.add(MobileDeviceManagementUtil.convertToFeature(mobileFeature)); + } + return featureList; + } catch (MobileDeviceManagementDAOException e) { + throw new DeviceManagementException("Error occurred while retrieving the list of features registered for " + + "Android platform", e); + } + } + + @Override + public boolean removeFeature(String code) throws DeviceManagementException { + boolean status; + try { + AndroidDAOFactory.beginTransaction(); + featureDAO.deleteFeatureByCode(code); + AndroidDAOFactory.commitTransaction(); + status = true; + } catch (MobileDeviceManagementDAOException e) { + try { + AndroidDAOFactory.rollbackTransaction(); + } catch (MobileDeviceManagementDAOException e1) { + log.warn("Error occurred while roll-backing the transaction", e); + } + throw new DeviceManagementException("Error occurred while removing the feature", e); + } + return status; + } + + @Override + public boolean addSupportedFeaturesToDB() throws DeviceManagementException { + synchronized (this) { + List supportedFeatures = getSupportedFeatures(); + List existingFeatures = this.getFeatures(); + List missingFeatures = MobileDeviceManagementUtil. + getMissingFeatures(supportedFeatures, existingFeatures); + if (missingFeatures.size() > 0) { + return this.addFeatures(missingFeatures); + } + return true; + } + } + + //Get the supported feature list. + private static List getSupportedFeatures() { + List supportedFeatures = new ArrayList(); + Feature feature = new Feature(); + feature.setCode("DEVICE_LOCK"); + feature.setName("Device Lock"); + feature.setDescription("Lock the device"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("DEVICE_LOCATION"); + feature.setName("Location"); + feature.setDescription("Request coordinates of device location"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("WIFI"); + feature.setName("wifi"); + feature.setDescription("Setting up wifi configuration"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("CAMERA"); + feature.setName("camera"); + feature.setDescription("Enable or disable camera"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("EMAIL"); + feature.setName("Email"); + feature.setDescription("Configure email settings"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("DEVICE_MUTE"); + feature.setName("mute"); + feature.setDescription("Enable mute in the device"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("DEVICE_INFO"); + feature.setName("Device info"); + feature.setDescription("Request device information"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("ENTERPRISE_WIPE"); + feature.setName("Enterprise Wipe"); + feature.setDescription("Remove enterprise applications"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("CLEAR_PASSWORD"); + feature.setName("Clear Password"); + feature.setDescription("Clear current password"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("WIPE_DATA"); + feature.setName("Wipe Data"); + feature.setDescription("Factory reset the device"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("APPLICATION_LIST"); + feature.setName("Application List"); + feature.setDescription("Request list of current installed applications"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("CHANGE_LOCK_CODE"); + feature.setName("Change Lock-code"); + feature.setDescription("Change current lock code"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("INSTALL_APPLICATION"); + feature.setName("Install App"); + feature.setDescription("Install Enterprise or Market application"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("UNINSTALL_APPLICATION"); + feature.setName("Uninstall App"); + feature.setDescription("Uninstall application"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("BLACKLIST_APPLICATIONS"); + feature.setName("Blacklist app"); + feature.setDescription("Blacklist applications"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("ENCRYPT_STORAGE"); + feature.setName("Encrypt storage"); + feature.setDescription("Encrypt storage"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("DEVICE_RING"); + feature.setName("Ring"); + feature.setDescription("Ring the device"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("PASSCODE_POLICY"); + feature.setName("Password Policy"); + feature.setDescription("Set passcode policy"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("NOTIFICATION"); + feature.setName("Message"); + feature.setDescription("Send message"); + supportedFeatures.add(feature); + return supportedFeatures; + } +} \ No newline at end of file diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/AndroidPolicyMonitoringService.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/AndroidPolicyMonitoringService.java new file mode 100644 index 000000000..35c28bd2f --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/AndroidPolicyMonitoringService.java @@ -0,0 +1,100 @@ +/* + * 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.device.mgt.mobile.android.impl; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementConstants; +import org.wso2.carbon.device.mgt.mobile.android.impl.gcm.GCMService; +import org.wso2.carbon.device.mgt.mobile.android.internal.AndroidDeviceManagementDataHolder; +import org.wso2.carbon.policy.mgt.common.Policy; +import org.wso2.carbon.policy.mgt.common.monitor.ComplianceData; +import org.wso2.carbon.policy.mgt.common.monitor.ComplianceFeature; +import org.wso2.carbon.policy.mgt.common.monitor.PolicyComplianceException; +import org.wso2.carbon.policy.mgt.common.spi.PolicyMonitoringService; + +import java.util.ArrayList; +import java.util.List; + +public class AndroidPolicyMonitoringService implements PolicyMonitoringService { + + private static Log log = LogFactory.getLog(AndroidPolicyMonitoringService.class); + + @Override + public void notifyDevices(List list) throws PolicyComplianceException { + GCMService gcmService = AndroidDeviceManagementDataHolder.getInstance().getGCMService(); + if (gcmService.isGCMEnabled() && !list.isEmpty()) { + gcmService.sendNotification("POLICY_BUNDLE", list); + } + } + + @Override + public ComplianceData checkPolicyCompliance(DeviceIdentifier deviceIdentifier, Policy policy, + Object compliancePayload) throws PolicyComplianceException { + if (log.isDebugEnabled()) { + log.debug("Checking policy compliance status of device '" + deviceIdentifier.getId() + "'"); + } + ComplianceData complianceData = new ComplianceData(); + if (compliancePayload == null || policy == null) { + return complianceData; + } + String compliancePayloadString = new Gson().toJson(compliancePayload); + // Parsing json string to get compliance features. + JsonElement jsonElement; + if (compliancePayloadString instanceof String) { + jsonElement = new JsonParser().parse(compliancePayloadString); + } else { + throw new PolicyComplianceException("Invalid policy compliance payload"); + } + + JsonArray jsonArray = jsonElement.getAsJsonArray(); + Gson gson = new Gson(); + ComplianceFeature complianceFeature; + List complianceFeatures = new ArrayList(jsonArray.size()); + List nonComplianceFeatures = new ArrayList<>(); + + for (JsonElement element : jsonArray) { + complianceFeature = gson.fromJson(element, ComplianceFeature.class); + complianceFeatures.add(complianceFeature); + } + + for (ComplianceFeature cf : complianceFeatures) { + if (!cf.isCompliant()) { + complianceData.setStatus(false); + nonComplianceFeatures.add(cf); + break; + } + } + + complianceData.setComplianceFeatures(nonComplianceFeatures); + return complianceData; + } + + @Override + public String getType() { + return DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID; + } +} \ No newline at end of file diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/dao/AndroidDAOFactory.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/dao/AndroidDAOFactory.java new file mode 100644 index 000000000..a2e676acc --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/dao/AndroidDAOFactory.java @@ -0,0 +1,120 @@ +/* + * 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.device.mgt.mobile.android.impl.dao; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.DeviceManagementConstants; +import org.wso2.carbon.device.mgt.mobile.android.impl.dao.impl.AndroidDeviceDAOImpl; +import org.wso2.carbon.device.mgt.mobile.android.impl.dao.impl.AndroidFeatureDAOImpl; +import org.wso2.carbon.device.mgt.mobile.dao.AbstractMobileDeviceManagementDAOFactory; +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceDAO; +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceManagementDAOException; +import org.wso2.carbon.device.mgt.mobile.dao.MobileFeatureDAO; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +public class AndroidDAOFactory extends AbstractMobileDeviceManagementDAOFactory { + + private static final Log log = LogFactory.getLog(AndroidDAOFactory.class); + protected static DataSource dataSource; + private static ThreadLocal currentConnection = new ThreadLocal<>(); + + public AndroidDAOFactory() { + this.dataSource = getDataSourceMap().get(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID); + } + + @Override + public MobileDeviceDAO getMobileDeviceDAO() { + return new AndroidDeviceDAOImpl(); + } + + public MobileFeatureDAO getMobileFeatureDAO() { + return new AndroidFeatureDAOImpl(); + } + + public static void beginTransaction() throws MobileDeviceManagementDAOException { + try { + Connection conn = dataSource.getConnection(); + conn.setAutoCommit(false); + currentConnection.set(conn); + } catch (SQLException e) { + throw new MobileDeviceManagementDAOException("Error occurred while retrieving datasource connection", e); + } + } + + public static Connection getConnection() throws MobileDeviceManagementDAOException { + if (currentConnection.get() == null) { + try { + currentConnection.set(dataSource.getConnection()); + } catch (SQLException e) { + throw new MobileDeviceManagementDAOException("Error occurred while retrieving data source connection", + e); + } + } + return currentConnection.get(); + } + + public static void commitTransaction() throws MobileDeviceManagementDAOException { + try { + Connection conn = currentConnection.get(); + if (conn != null) { + conn.commit(); + } else { + if (log.isDebugEnabled()) { + log.debug("Datasource connection associated with the current thread is null, hence commit " + + "has not been attempted"); + } + } + } catch (SQLException e) { + throw new MobileDeviceManagementDAOException("Error occurred while committing the transaction", e); + } + } + + public static void closeConnection() throws MobileDeviceManagementDAOException { + Connection conn = currentConnection.get(); + try { + if (conn != null) { + conn.close(); + } + } catch (SQLException e) { + log.error("Error occurred while close the connection"); + } + currentConnection.remove(); + } + + public static void rollbackTransaction() throws MobileDeviceManagementDAOException { + try { + Connection conn = currentConnection.get(); + if (conn != null) { + conn.rollback(); + } else { + if (log.isDebugEnabled()) { + log.debug("Datasource connection associated with the current thread is null, hence rollback " + + "has not been attempted"); + } + } + } catch (SQLException e) { + throw new MobileDeviceManagementDAOException("Error occurred while rollback the transaction", e); + } + } + +} \ No newline at end of file diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/dao/AndroidFeatureManagementDAOException.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/dao/AndroidFeatureManagementDAOException.java new file mode 100644 index 000000000..db218d6df --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/dao/AndroidFeatureManagementDAOException.java @@ -0,0 +1,79 @@ +/* + * 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.device.mgt.mobile.android.impl.dao; + +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceManagementDAOException; + +public class AndroidFeatureManagementDAOException extends MobileDeviceManagementDAOException { + + private String message; + private static final long serialVersionUID = 2021891706072918865L; + + /** + * Constructs a new MobileDeviceManagementDAOException with the specified detail message and + * nested exception. + * + * @param message error message + * @param nestedException exception + */ + public AndroidFeatureManagementDAOException(String message, Exception nestedException) { + super(message, nestedException); + setErrorMessage(message); + } + + /** + * Constructs a new MobileDeviceManagementDAOException with the specified detail message + * and cause. + * + * @param message the detail message. + * @param cause the cause of this exception. + */ + public AndroidFeatureManagementDAOException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + /** + * Constructs a new MobileDeviceManagementDAOException with the specified detail message. + * + * @param message the detail message. + */ + public AndroidFeatureManagementDAOException(String message) { + super(message); + setErrorMessage(message); + } + + /** + * Constructs a new MobileDeviceManagementDAOException with the specified and cause. + * + * @param cause the cause of this exception. + */ + public AndroidFeatureManagementDAOException(Throwable cause) { + super(cause); + } + + public String getMessage() { + return message; + } + + public void setErrorMessage(String errorMessage) { + this.message = errorMessage; + } + +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/dao/impl/AndroidDeviceDAOImpl.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/dao/impl/AndroidDeviceDAOImpl.java new file mode 100644 index 000000000..0d831901d --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/dao/impl/AndroidDeviceDAOImpl.java @@ -0,0 +1,265 @@ +/* + * 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.device.mgt.mobile.android.impl.dao.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.mobile.android.impl.dao.AndroidDAOFactory; +import org.wso2.carbon.device.mgt.mobile.android.impl.util.AndroidPluginConstants; +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceDAO; +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceManagementDAOException; +import org.wso2.carbon.device.mgt.mobile.dao.util.MobileDeviceManagementDAOUtil; +import org.wso2.carbon.device.mgt.mobile.dto.MobileDevice; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Implements MobileDeviceDAO for Android Devices. + */ +public class AndroidDeviceDAOImpl implements MobileDeviceDAO{ + + private static final Log log = LogFactory.getLog(AndroidDeviceDAOImpl.class); + + @Override + public MobileDevice getMobileDevice(String mblDeviceId) throws MobileDeviceManagementDAOException { + Connection conn; + PreparedStatement stmt = null; + MobileDevice mobileDevice = null; + ResultSet rs = null; + try { + conn = AndroidDAOFactory.getConnection(); + String selectDBQuery = + "SELECT DEVICE_ID, GCM_TOKEN, DEVICE_INFO, DEVICE_MODEL, SERIAL, " + + "VENDOR, MAC_ADDRESS, DEVICE_NAME, LATITUDE, LONGITUDE, IMEI, IMSI, OS_VERSION" + + " FROM AD_DEVICE WHERE DEVICE_ID = ?"; + stmt = conn.prepareStatement(selectDBQuery); + stmt.setString(1, mblDeviceId); + rs = stmt.executeQuery(); + + if (rs.next()) { + mobileDevice = new MobileDevice(); + mobileDevice.setMobileDeviceId(rs.getString(AndroidPluginConstants.DEVICE_ID)); + mobileDevice.setModel(rs.getString(AndroidPluginConstants.DEVICE_MODEL)); + mobileDevice.setSerial(rs.getString(AndroidPluginConstants.SERIAL)); + mobileDevice.setVendor(rs.getString(AndroidPluginConstants.VENDOR)); + mobileDevice.setLatitude(rs.getString(AndroidPluginConstants.LATITUDE)); + mobileDevice.setLongitude(rs.getString(AndroidPluginConstants.LONGITUDE)); + mobileDevice.setImei(rs.getString(AndroidPluginConstants.IMEI)); + mobileDevice.setImsi(rs.getString(AndroidPluginConstants.IMSI)); + mobileDevice.setOsVersion(rs.getString(AndroidPluginConstants.OS_VERSION)); + + Map propertyMap = new HashMap(); + propertyMap.put(AndroidPluginConstants.GCM_TOKEN, rs.getString(AndroidPluginConstants.GCM_TOKEN)); + propertyMap.put(AndroidPluginConstants.DEVICE_INFO, rs.getString(AndroidPluginConstants.DEVICE_INFO)); + propertyMap.put(AndroidPluginConstants.DEVICE_NAME, rs.getString(AndroidPluginConstants.DEVICE_NAME)); + mobileDevice.setDeviceProperties(propertyMap); + + if (log.isDebugEnabled()) { + log.debug("Android device " + mblDeviceId + " data has been fetched from " + + "Android database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while fetching Android device : '" + mblDeviceId + "'"; + log.error(msg, e); + throw new MobileDeviceManagementDAOException(msg, e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, rs); + AndroidDAOFactory.closeConnection(); + } + + return mobileDevice; + } + + @Override + public boolean addMobileDevice(MobileDevice mobileDevice) throws MobileDeviceManagementDAOException { + boolean status = false; + Connection conn; + PreparedStatement stmt = null; + try { + conn = AndroidDAOFactory.getConnection(); + String createDBQuery = + "INSERT INTO AD_DEVICE(DEVICE_ID, GCM_TOKEN, DEVICE_INFO, SERIAL, " + + "VENDOR, MAC_ADDRESS, DEVICE_NAME, LATITUDE, LONGITUDE, IMEI, IMSI, " + + "OS_VERSION, DEVICE_MODEL) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + stmt = conn.prepareStatement(createDBQuery); + stmt.setString(1, mobileDevice.getMobileDeviceId()); + + Map properties = mobileDevice.getDeviceProperties(); + stmt.setString(2, properties.get(AndroidPluginConstants.GCM_TOKEN)); + stmt.setString(3, properties.get(AndroidPluginConstants.DEVICE_INFO)); + stmt.setString(4, mobileDevice.getSerial()); + stmt.setString(5, mobileDevice.getVendor()); + stmt.setString(6, mobileDevice.getMobileDeviceId()); + stmt.setString(7, properties.get(AndroidPluginConstants.DEVICE_NAME)); + stmt.setString(8, mobileDevice.getLongitude()); + stmt.setString(9, mobileDevice.getLongitude()); + stmt.setString(10, mobileDevice.getImei()); + stmt.setString(11, mobileDevice.getImsi()); + stmt.setString(12, mobileDevice.getOsVersion()); + stmt.setString(13, mobileDevice.getModel()); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Android device " + mobileDevice.getMobileDeviceId() + " data has been" + + " added to the Android database."); + } + } + } catch (SQLException e) { + throw new MobileDeviceManagementDAOException("Error occurred while adding the Android device '" + + mobileDevice.getMobileDeviceId() + "' information to the Android plugin data store.", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, null); + } + return status; + } + + @Override + public boolean updateMobileDevice(MobileDevice mobileDevice) throws MobileDeviceManagementDAOException { + boolean status = false; + Connection conn; + PreparedStatement stmt = null; + try { + conn = AndroidDAOFactory.getConnection(); + String updateDBQuery = + "UPDATE AD_DEVICE SET GCM_TOKEN = ?, DEVICE_INFO = ?, SERIAL = ?, VENDOR = ?, " + + "MAC_ADDRESS = ?, DEVICE_NAME = ?, LATITUDE = ?, LONGITUDE = ?, IMEI = ?, " + + "IMSI = ?, OS_VERSION = ?, DEVICE_MODEL = ? WHERE DEVICE_ID = ?"; + stmt = conn.prepareStatement(updateDBQuery); + + Map properties = mobileDevice.getDeviceProperties(); + stmt.setString(1, properties.get(AndroidPluginConstants.GCM_TOKEN)); + stmt.setString(2, properties.get(AndroidPluginConstants.DEVICE_INFO)); + stmt.setString(3, mobileDevice.getSerial()); + stmt.setString(4, mobileDevice.getVendor()); + stmt.setString(5, mobileDevice.getMobileDeviceId()); + stmt.setString(6, properties.get(AndroidPluginConstants.DEVICE_NAME)); + stmt.setString(7, mobileDevice.getLatitude()); + stmt.setString(8, mobileDevice.getLongitude()); + stmt.setString(9, mobileDevice.getImei()); + stmt.setString(10, mobileDevice.getImsi()); + stmt.setString(11, mobileDevice.getOsVersion()); + stmt.setString(12, mobileDevice.getModel()); + stmt.setString(13, mobileDevice.getMobileDeviceId()); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Android device " + mobileDevice.getMobileDeviceId() + " data has been" + + " modified."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while modifying the Android device '" + + mobileDevice.getMobileDeviceId() + "' data."; + log.error(msg, e); + throw new MobileDeviceManagementDAOException(msg, e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, null); + } + return status; + } + + @Override + public boolean deleteMobileDevice(String mblDeviceId) + throws MobileDeviceManagementDAOException { + boolean status = false; + Connection conn; + PreparedStatement stmt = null; + try { + conn = AndroidDAOFactory.getConnection(); + String deleteDBQuery = + "DELETE FROM AD_DEVICE WHERE DEVICE_ID = ?"; + stmt = conn.prepareStatement(deleteDBQuery); + stmt.setString(1, mblDeviceId); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Android device " + mblDeviceId + " data has deleted" + + " from the Android database."); + } + } + } catch (SQLException e) { + throw new MobileDeviceManagementDAOException("Error occurred while deleting android device '" + + mblDeviceId + "'", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, null); + } + return status; + } + + @Override + public List getAllMobileDevices() throws MobileDeviceManagementDAOException { + Connection conn; + PreparedStatement stmt = null; + ResultSet rs = null; + MobileDevice mobileDevice; + List mobileDevices = new ArrayList(); + try { + conn = AndroidDAOFactory.getConnection(); + String selectDBQuery = + "SELECT DEVICE_ID, GCM_TOKEN, DEVICE_INFO, DEVICE_MODEL, SERIAL, " + + "VENDOR, MAC_ADDRESS, DEVICE_NAME, LATITUDE, LONGITUDE, IMEI, IMSI, OS_VERSION " + + "FROM AD_DEVICE"; + stmt = conn.prepareStatement(selectDBQuery); + rs = stmt.executeQuery(); + + while (rs.next()) { + mobileDevice = new MobileDevice(); + mobileDevice.setMobileDeviceId(rs.getString(AndroidPluginConstants.DEVICE_ID)); + mobileDevice.setModel(rs.getString(AndroidPluginConstants.DEVICE_MODEL)); + mobileDevice.setSerial(rs.getString(AndroidPluginConstants.SERIAL)); + mobileDevice.setVendor(rs.getString(AndroidPluginConstants.VENDOR)); + mobileDevice.setLatitude(rs.getString(AndroidPluginConstants.LATITUDE)); + mobileDevice.setLongitude(rs.getString(AndroidPluginConstants.LONGITUDE)); + mobileDevice.setImei(rs.getString(AndroidPluginConstants.IMEI)); + mobileDevice.setImsi(rs.getString(AndroidPluginConstants.IMSI)); + mobileDevice.setOsVersion(rs.getString(AndroidPluginConstants.OS_VERSION)); + + Map propertyMap = new HashMap<>(); + propertyMap.put(AndroidPluginConstants.GCM_TOKEN, rs.getString(AndroidPluginConstants.GCM_TOKEN)); + propertyMap.put(AndroidPluginConstants.DEVICE_INFO, rs.getString(AndroidPluginConstants.DEVICE_INFO)); + propertyMap.put(AndroidPluginConstants.DEVICE_NAME, rs.getString(AndroidPluginConstants.DEVICE_NAME)); + mobileDevice.setDeviceProperties(propertyMap); + + mobileDevices.add(mobileDevice); + } + if (log.isDebugEnabled()) { + log.debug("All Android device details have fetched from Android database."); + } + return mobileDevices; + } catch (SQLException e) { + throw new MobileDeviceManagementDAOException("Error occurred while fetching all Android device data", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, rs); + AndroidDAOFactory.closeConnection(); + } + } + +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/dao/impl/AndroidFeatureDAOImpl.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/dao/impl/AndroidFeatureDAOImpl.java new file mode 100644 index 000000000..072a54c9e --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/dao/impl/AndroidFeatureDAOImpl.java @@ -0,0 +1,285 @@ +/* + * 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.device.mgt.mobile.android.impl.dao.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.DeviceManagementConstants; +import org.wso2.carbon.device.mgt.mobile.android.impl.dao.AndroidDAOFactory; +import org.wso2.carbon.device.mgt.mobile.android.impl.dao.AndroidFeatureManagementDAOException; +import org.wso2.carbon.device.mgt.mobile.android.impl.util.AndroidPluginConstants; +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceManagementDAOException; +import org.wso2.carbon.device.mgt.mobile.dao.MobileFeatureDAO; +import org.wso2.carbon.device.mgt.mobile.dao.util.MobileDeviceManagementDAOUtil; +import org.wso2.carbon.device.mgt.mobile.dto.MobileFeature; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +public class AndroidFeatureDAOImpl implements MobileFeatureDAO { + + private static final Log log = LogFactory.getLog(AndroidFeatureDAOImpl.class); + + public AndroidFeatureDAOImpl() { + + } + + @Override + public boolean addFeature(MobileFeature mobileFeature) throws MobileDeviceManagementDAOException { + PreparedStatement stmt = null; + boolean status = false; + Connection conn; + try { + conn = AndroidDAOFactory.getConnection(); + String sql = "INSERT INTO AD_FEATURE(CODE, NAME, DESCRIPTION) VALUES (?, ?, ?)"; + stmt = conn.prepareStatement(sql); + stmt.setString(1, mobileFeature.getCode()); + stmt.setString(2, mobileFeature.getName()); + stmt.setString(3, mobileFeature.getDescription()); + stmt.executeUpdate(); + status = true; + } catch (SQLException e) { + throw new AndroidFeatureManagementDAOException( + "Error occurred while adding android feature '" + + mobileFeature.getName() + "' into the metadata repository", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, null); + } + return status; + } + + @Override + public boolean addFeatures(List mobileFeatures) throws MobileDeviceManagementDAOException { + PreparedStatement stmt = null; + MobileFeature mobileFeature; + boolean status = false; + Connection conn; + try { + conn = AndroidDAOFactory.getConnection(); + stmt = conn.prepareStatement("INSERT INTO AD_FEATURE(CODE, NAME, DESCRIPTION) VALUES (?, ?, ?)"); + for (int i = 0; i < mobileFeatures.size(); i++) { + mobileFeature = mobileFeatures.get(i); + stmt.setString(1, mobileFeature.getCode()); + stmt.setString(2, mobileFeature.getName()); + stmt.setString(3, mobileFeature.getDescription()); + stmt.addBatch(); + } + stmt.executeBatch(); + status = true; + } catch (SQLException e) { + throw new AndroidFeatureManagementDAOException( + "Error occurred while adding android features into the metadata repository", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, null); + } + return status; + } + + @Override + public boolean updateFeature(MobileFeature mobileFeature) throws MobileDeviceManagementDAOException { + boolean status = false; + Connection conn; + PreparedStatement stmt = null; + try { + conn = AndroidDAOFactory.getConnection(); + String updateDBQuery = + "UPDATE AD_FEATURE SET NAME = ?, DESCRIPTION = ?" + + "WHERE CODE = ?"; + + stmt = conn.prepareStatement(updateDBQuery); + stmt.setString(1, mobileFeature.getName()); + stmt.setString(2, mobileFeature.getDescription()); + stmt.setString(3, mobileFeature.getCode()); + + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Android Feature " + mobileFeature.getCode() + " data has been " + + "modified."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while updating the Android Feature '" + + mobileFeature.getCode() + "' to the Android db."; + log.error(msg, e); + throw new AndroidFeatureManagementDAOException(msg, e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, null); + } + return status; + } + + @Override + public boolean deleteFeatureById(int mblFeatureId) throws MobileDeviceManagementDAOException { + + PreparedStatement stmt = null; + boolean status = false; + Connection conn; + try { + conn = AndroidDAOFactory.getConnection(); + String sql = "DELETE FROM AD_FEATURE WHERE ID = ?"; + stmt = conn.prepareStatement(sql); + stmt.setInt(1, mblFeatureId); + stmt.execute(); + status = true; + } catch (SQLException e) { + throw new AndroidFeatureManagementDAOException( + "Error occurred while deleting android feature '" + + mblFeatureId + "' from Android database.", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, null); + } + return status; + } + + @Override + public boolean deleteFeatureByCode(String mblFeatureCode) throws MobileDeviceManagementDAOException { + PreparedStatement stmt = null; + boolean status = false; + Connection conn; + try { + conn = AndroidDAOFactory.getConnection(); + String sql = "DELETE FROM AD_FEATURE WHERE CODE = ?"; + stmt = conn.prepareStatement(sql); + stmt.setString(1, mblFeatureCode); + stmt.execute(); + status = true; + } catch (SQLException e) { + throw new AndroidFeatureManagementDAOException( + "Error occurred while deleting android feature '" + + mblFeatureCode + "' from Android database.", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, null); + } + return status; + } + + @Override + public MobileFeature getFeatureById(int mblFeatureId) throws MobileDeviceManagementDAOException { + PreparedStatement stmt = null; + ResultSet rs = null; + Connection conn; + try { + conn = AndroidDAOFactory.getConnection(); + String sql = "SELECT ID, CODE, NAME, DESCRIPTION FROM AD_FEATURE WHERE ID = ?"; + stmt = conn.prepareStatement(sql); + stmt.setInt(1, mblFeatureId); + rs = stmt.executeQuery(); + + MobileFeature mobileFeature = null; + if (rs.next()) { + mobileFeature = new MobileFeature(); + mobileFeature.setId(rs.getInt(AndroidPluginConstants.ANDROID_FEATURE_ID)); + mobileFeature.setCode(rs.getString(AndroidPluginConstants.ANDROID_FEATURE_CODE)); + mobileFeature.setName(rs.getString(AndroidPluginConstants.ANDROID_FEATURE_NAME)); + mobileFeature.setDescription(rs.getString(AndroidPluginConstants. + ANDROID_FEATURE_DESCRIPTION)); + mobileFeature.setDeviceType( + DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID); + } + return mobileFeature; + } catch (SQLException e) { + throw new AndroidFeatureManagementDAOException( + "Error occurred while retrieving android feature '" + + mblFeatureId + "' from the Android database.", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, rs); + AndroidDAOFactory.closeConnection(); + } + } + + @Override + public MobileFeature getFeatureByCode(String mblFeatureCode) throws MobileDeviceManagementDAOException { + PreparedStatement stmt = null; + ResultSet rs = null; + Connection conn; + + try { + conn = AndroidDAOFactory.getConnection(); + String sql = "SELECT ID, CODE, NAME, DESCRIPTION FROM AD_FEATURE WHERE CODE = ?"; + stmt = conn.prepareStatement(sql); + stmt.setString(1, mblFeatureCode); + rs = stmt.executeQuery(); + + MobileFeature mobileFeature = null; + if (rs.next()) { + mobileFeature = new MobileFeature(); + mobileFeature.setId(rs.getInt(AndroidPluginConstants.ANDROID_FEATURE_ID)); + mobileFeature.setCode(rs.getString(AndroidPluginConstants.ANDROID_FEATURE_CODE)); + mobileFeature.setName(rs.getString(AndroidPluginConstants.ANDROID_FEATURE_NAME)); + mobileFeature.setDescription(rs.getString(AndroidPluginConstants. + ANDROID_FEATURE_DESCRIPTION)); + mobileFeature.setDeviceType( + DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID); + } + return mobileFeature; + } catch (SQLException e) { + throw new AndroidFeatureManagementDAOException( + "Error occurred while retrieving android feature '" + + mblFeatureCode + "' from the Android database.", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, rs); + AndroidDAOFactory.closeConnection(); + } + } + + @Override + public List getFeatureByDeviceType(String deviceType) + throws MobileDeviceManagementDAOException { + return this.getAllFeatures(); + } + + @Override + public List getAllFeatures() throws MobileDeviceManagementDAOException { + PreparedStatement stmt = null; + ResultSet rs = null; + Connection conn = null; + List features = new ArrayList<>(); + try { + conn = AndroidDAOFactory.getConnection(); + String sql = "SELECT ID, CODE, NAME, DESCRIPTION FROM AD_FEATURE"; + stmt = conn.prepareStatement(sql); + rs = stmt.executeQuery(); + MobileFeature mobileFeature = null; + + while (rs.next()) { + mobileFeature = new MobileFeature(); + mobileFeature.setId(rs.getInt(AndroidPluginConstants.ANDROID_FEATURE_ID)); + mobileFeature.setCode(rs.getString(AndroidPluginConstants.ANDROID_FEATURE_CODE)); + mobileFeature.setName(rs.getString(AndroidPluginConstants.ANDROID_FEATURE_NAME)); + mobileFeature.setDescription(rs.getString(AndroidPluginConstants.ANDROID_FEATURE_DESCRIPTION)); + mobileFeature.setDeviceType( + DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID); + features.add(mobileFeature); + } + return features; + } catch (SQLException e) { + throw new AndroidFeatureManagementDAOException("Error occurred while retrieving all " + + "android features from the android database.", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, rs); + AndroidDAOFactory.closeConnection(); + } + } +} \ No newline at end of file diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/gcm/GCMResult.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/gcm/GCMResult.java new file mode 100644 index 000000000..f9c770358 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/gcm/GCMResult.java @@ -0,0 +1,53 @@ +/* + * 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.device.mgt.mobile.android.impl.gcm; + +/** + * Represents model object for holding GCM response data. + */ +public class GCMResult { + + private String errorMsg; + private String msg; + private int statusCode; + + public String getErrorMsg() { + return errorMsg; + } + + public void setErrorMsg(String errorMsg) { + this.errorMsg = errorMsg; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public int getStatusCode() { + return statusCode; + } + + public void setStatusCode(int statusCode) { + this.statusCode = statusCode; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/gcm/GCMService.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/gcm/GCMService.java new file mode 100644 index 000000000..bfa1685fa --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/gcm/GCMService.java @@ -0,0 +1,64 @@ +/* + * 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.device.mgt.mobile.android.impl.gcm; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.Device; + +import java.util.ArrayList; +import java.util.List; + +/** + * GCM notification service implementation for Android platform. + */ +public class GCMService { + + private static final Log log = LogFactory.getLog(GCMService.class); + private static final String NOTIFIER_TYPE = "notifierType"; + private static final String GCM_NOTIFIER_CODE = "2"; + + public boolean isGCMEnabled() { + String notifierType = GCMUtil.getConfigurationProperty(NOTIFIER_TYPE); + if (GCM_NOTIFIER_CODE.equals(notifierType)) { + return true; + } + return false; + } + + public void sendNotification(String messageData, Device device) { + List devices = new ArrayList<>(1); + devices.add(device); + GCMResult result = GCMUtil.sendWakeUpCall(messageData, devices); + if (result.getStatusCode() != 200) { + log.error("Exception occurred while sending the GCM notification : " + result.getErrorMsg()); + } + } + + public void sendNotification(String messageData, List devices) { + GCMResult result = GCMUtil.sendWakeUpCall(messageData, devices); + if (result.getStatusCode() != 200) { + log.error("Exception occurred while sending the GCM notification : " + result.getErrorMsg()); + } + } + + public void resetTenantConfigCache() { + GCMUtil.resetTenantConfigCache(); + } +} \ No newline at end of file diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/gcm/GCMUtil.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/gcm/GCMUtil.java new file mode 100644 index 000000000..ba41698e8 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/gcm/GCMUtil.java @@ -0,0 +1,204 @@ +/* + * 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.device.mgt.mobile.android.impl.gcm; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.context.CarbonContext; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.configuration.mgt.ConfigurationEntry; +import org.wso2.carbon.device.mgt.common.configuration.mgt.TenantConfiguration; +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; +import org.wso2.carbon.device.mgt.mobile.android.impl.util.AndroidPluginConstants; +import org.wso2.carbon.device.mgt.mobile.android.internal.AndroidDeviceManagementDataHolder; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.ProtocolException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * Implements utility methods used by GCMService. + */ +public class GCMUtil { + + private static final Log log = LogFactory.getLog(GCMService.class); + + private final static String GCM_ENDPOINT = "https://gcm-http.googleapis.com/gcm/send"; + private static final String GCM_API_KEY = "gcmAPIKey"; + private static final int TIME_TO_LIVE = 60; + private static final int HTTP_STATUS_CODE_OK = 200; + + private static HashMap tenantConfigurationCache = new HashMap<>(); + + public static GCMResult sendWakeUpCall(String message, List devices) { + GCMResult result = new GCMResult(); + + byte[] bytes = getGCMRequest(message, getGCMTokens(devices)).getBytes(); + HttpURLConnection conn; + try { + conn = (HttpURLConnection) (new URL(GCM_ENDPOINT)).openConnection(); + conn.setDoOutput(true); + conn.setUseCaches(false); + conn.setFixedLengthStreamingMode(bytes.length); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + conn.setRequestProperty("Authorization", "key=" + getConfigurationProperty(GCM_API_KEY)); + + OutputStream out = conn.getOutputStream(); + out.write(bytes); + out.close(); + + int status = conn.getResponseCode(); + result.setStatusCode(status); + if (status != HTTP_STATUS_CODE_OK) { + result.setErrorMsg(getString(conn.getErrorStream())); + } else { + result.setMsg(getString(conn.getInputStream())); + } + } catch (ProtocolException e) { + log.error("Exception occurred while setting the HTTP protocol.", e); + } catch (IOException ex) { + log.error("Exception occurred while sending the GCM request.", ex); + } + + return result; + } + + private static String getString(InputStream stream) throws IOException { + if (stream != null) { + BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + StringBuilder content = new StringBuilder(); + + String newLine; + do { + newLine = reader.readLine(); + if (newLine != null) { + content.append(newLine).append('\n'); + } + } while (newLine != null); + + if (content.length() > 0) { + content.setLength(content.length() - 1); + } + + return content.toString(); + } + return null; + } + + private static String getGCMRequest(String message, List registrationIds) { + JsonObject gcmRequest = new JsonObject(); + gcmRequest.addProperty("delay_while_idle", false); + gcmRequest.addProperty("time_to_live", TIME_TO_LIVE); + + //Add message to GCM request + JsonObject data = new JsonObject(); + if (message != null && !message.isEmpty()) { + data.addProperty("data", message); + gcmRequest.add("data", data); + } + + //Set device reg-ids + JsonArray regIds = new JsonArray(); + for (String regId : registrationIds) { + if (regId == null || regId.isEmpty()) { + continue; + } + regIds.add(new JsonPrimitive(regId)); + } + + gcmRequest.add("registration_ids", regIds); + return gcmRequest.toString(); + } + + private static List getGCMTokens(List devices) { + List tokens = new ArrayList<>(devices.size()); + for (Device device : devices) { + tokens.add(getGCMToken(device.getProperties())); + } + return tokens; + } + + private static String getGCMToken(List properties) { + String gcmToken = null; + for (Device.Property property : properties) { + if (AndroidPluginConstants.GCM_TOKEN.equals(property.getName())) { + gcmToken = property.getValue(); + break; + } + } + return gcmToken; + } + + public static String getConfigurationProperty(String property) { + DeviceManagementService androidDMService = AndroidDeviceManagementDataHolder.getInstance(). + getAndroidDeviceManagementService(); + try { + //Get the TenantConfiguration from cache if not we'll get it from DM service + TenantConfiguration tenantConfiguration = getTenantConfigurationFromCache(); + if (tenantConfiguration == null) { + tenantConfiguration = androidDMService.getDeviceManager().getConfiguration(); + if (tenantConfiguration != null) { + addTenantConfigurationToCache(tenantConfiguration); + } + } + + if (tenantConfiguration != null) { + List configs = tenantConfiguration.getConfiguration(); + for (ConfigurationEntry entry : configs) { + if (property.equals(entry.getName())) { + return (String) entry.getValue(); + } + } + } + return ""; + } catch (DeviceManagementException e) { + log.error("Exception occurred while fetching the tenant-config.",e); + } + return null; + } + + public static void resetTenantConfigCache() { + tenantConfigurationCache.remove(getTenantId()); + } + + private static void addTenantConfigurationToCache(TenantConfiguration tenantConfiguration) { + tenantConfigurationCache.put(getTenantId(), tenantConfiguration); + } + + private static TenantConfiguration getTenantConfigurationFromCache() { + return tenantConfigurationCache.get(getTenantId()); + } + + private static int getTenantId() { + return CarbonContext.getThreadLocalCarbonContext().getTenantId(); + } +} \ No newline at end of file diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/util/AndroidPluginConstants.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/util/AndroidPluginConstants.java new file mode 100644 index 000000000..7f6386210 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/util/AndroidPluginConstants.java @@ -0,0 +1,47 @@ +/* + * 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.device.mgt.mobile.android.impl.util; + +/** + * Defines constants used by android plugin. + */ +public class AndroidPluginConstants { + + //Properties related to AD_DEVICE table + public static final String DEVICE_ID = "DEVICE_ID"; + public static final String GCM_TOKEN = "GCM_TOKEN"; + public static final String DEVICE_INFO = "DEVICE_INFO"; + public static final String SERIAL = "SERIAL"; + public static final String DEVICE_MODEL = "DEVICE_MODEL"; + public static final String DEVICE_NAME = "DEVICE_NAME"; + public static final String LATITUDE = "LATITUDE"; + public static final String LONGITUDE = "LONGITUDE"; + public static final String IMEI = "IMEI"; + public static final String IMSI = "IMSI"; + public static final String VENDOR = "VENDOR"; + public static final String OS_VERSION = "OS_VERSION"; + public static final String MAC_ADDRESS = "MAC_ADDRESS"; + + //Properties related to AD_FEATURE table + public static final String ANDROID_FEATURE_ID = "ID"; + public static final String ANDROID_FEATURE_CODE = "CODE"; + public static final String ANDROID_FEATURE_NAME = "NAME"; + public static final String ANDROID_FEATURE_DESCRIPTION = "DESCRIPTION"; + +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/util/AndroidPluginUtils.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/util/AndroidPluginUtils.java new file mode 100644 index 000000000..40ea3c18b --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/util/AndroidPluginUtils.java @@ -0,0 +1,58 @@ +/* + * 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.device.mgt.mobile.android.impl.util; + +import org.wso2.carbon.device.mgt.common.license.mgt.License; +import org.wso2.carbon.device.mgt.mobile.android.impl.AndroidDeviceManagementService; + +/** + * Contains utility methods used by Android plugin. + */ +public class AndroidPluginUtils { + + public static License getDefaultLicense() { + License license = new License(); + license.setName(AndroidDeviceManagementService.DEVICE_TYPE_ANDROID); + license.setLanguage("en_US"); + license.setVersion("1.0.0"); + license.setText("This End User License Agreement (\"Agreement\") is a legal agreement between you (\"You\") " + + "and WSO2, Inc., regarding the enrollment of Your personal mobile device (\"Device\") in SoR's " + + "mobile device management program, and the loading to and removal from Your Device and Your use " + + "of certain applications and any associated software and user documentation, whether provided in " + + "\"online\" or electronic format, used in connection with the operation of or provision of services " + + "to WSO2, Inc., BY SELECTING \"I ACCEPT\" DURING INSTALLATION, YOU ARE ENROLLING YOUR DEVICE, AND " + + "THEREBY AUTHORIZING SOR OR ITS AGENTS TO INSTALL, UPDATE AND REMOVE THE APPS FROM YOUR DEVICE AS " + + "DESCRIBED IN THIS AGREEMENT. YOU ARE ALSO EXPLICITLY ACKNOWLEDGING AND AGREEING THAT (1) THIS IS " + + "A BINDING CONTRACT AND (2) YOU HAVE READ AND AGREE TO THE TERMS OF THIS AGREEMENT.\n" + + "\n" + + "IF YOU DO NOT ACCEPT THESE TERMS, DO NOT ENROLL YOUR DEVICE AND DO NOT PROCEED ANY FURTHER.\n" + + "\n" + + "You agree that: (1) You understand and agree to be bound by the terms and conditions contained " + + "in this Agreement, and (2) You are at least 21 years old and have the legal capacity to enter " + + "into this Agreement as defined by the laws of Your jurisdiction. SoR shall have the right, " + + "without prior notice, to terminate or suspend (i) this Agreement, (ii) the enrollment of Your " + + "Device, or (iii) the functioning of the Apps in the event of a violation of this Agreement or " + + "the cessation of Your relationship with SoR (including termination of Your employment if You are " + + "an employee or expiration or termination of Your applicable franchise or supply agreement if You " + + "are a franchisee of or supplier to the WSO2 WSO2, Inc., system). SoR expressly reserves all " + + "rights not expressly granted herein."); + return license; + } + +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/util/AndroidUtils.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/util/AndroidUtils.java new file mode 100644 index 000000000..583a6cef0 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/impl/util/AndroidUtils.java @@ -0,0 +1,31 @@ +/* + * 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.device.mgt.mobile.android.impl.util; + +import java.util.Map; + +/** + * Contains utility methods used by Android plugin. + */ +public class AndroidUtils { + + public static String getDeviceProperty(Map deviceProperties, String property) { + return deviceProperties.get(property); + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/internal/AndroidDeviceManagementDataHolder.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/internal/AndroidDeviceManagementDataHolder.java new file mode 100644 index 000000000..ee4a7c021 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/internal/AndroidDeviceManagementDataHolder.java @@ -0,0 +1,67 @@ +/* + * 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.device.mgt.mobile.android.internal; + +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; +import org.wso2.carbon.device.mgt.mobile.android.impl.gcm.GCMService; +import org.wso2.carbon.registry.core.service.RegistryService; + +/** + * DataHolder class of Mobile plugins component. + */ +public class AndroidDeviceManagementDataHolder { + + private RegistryService registryService; + private DeviceManagementService androidDeviceManagementService; + private GCMService gcmService; + + private static AndroidDeviceManagementDataHolder thisInstance = new AndroidDeviceManagementDataHolder(); + + private AndroidDeviceManagementDataHolder() { + } + + public static AndroidDeviceManagementDataHolder getInstance() { + return thisInstance; + } + + public RegistryService getRegistryService() { + return registryService; + } + + public void setRegistryService(RegistryService registryService) { + this.registryService = registryService; + } + + public DeviceManagementService getAndroidDeviceManagementService() { + return androidDeviceManagementService; + } + + public void setAndroidDeviceManagementService( + DeviceManagementService androidDeviceManagementService) { + this.androidDeviceManagementService = androidDeviceManagementService; + } + + public GCMService getGCMService() { + return gcmService; + } + + public void setGCMService(GCMService gcmService) { + this.gcmService = gcmService; + } +} diff --git a/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/internal/AndroidDeviceManagementServiceComponent.java b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/internal/AndroidDeviceManagementServiceComponent.java new file mode 100644 index 000000000..7eea7f814 --- /dev/null +++ b/components/mobile-plugins/android-plugin/org.wso2.carbon.device.mgt.mobile.android/src/main/java/org/wso2/carbon/device/mgt/mobile/android/internal/AndroidDeviceManagementServiceComponent.java @@ -0,0 +1,137 @@ +/* + * 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.device.mgt.mobile.android.internal; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.ComponentContext; +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; +import org.wso2.carbon.device.mgt.mobile.android.impl.AndroidDeviceManagementService; +import org.wso2.carbon.device.mgt.mobile.android.impl.AndroidPolicyMonitoringService; +import org.wso2.carbon.device.mgt.mobile.android.impl.gcm.GCMService; +import org.wso2.carbon.device.mgt.mobile.internal.MobileDeviceManagementDataHolder; +import org.wso2.carbon.ndatasource.core.DataSourceService; +import org.wso2.carbon.policy.mgt.common.spi.PolicyMonitoringService; +import org.wso2.carbon.registry.core.service.RegistryService; + +/** + * @scr.component name="org.wso2.carbon.device.mgt.mobile.android.impl.internal.AndroidDeviceManagementServiceComponent" + * immediate="true" + * @scr.reference name="org.wso2.carbon.ndatasource" + * interface="org.wso2.carbon.ndatasource.core.DataSourceService" + * cardinality="1..1" + * policy="dynamic" + * bind="setDataSourceService" + * unbind="unsetDataSourceService" + * @scr.reference name="registry.service" + * interface="org.wso2.carbon.registry.core.service.RegistryService" cardinality="0..1" + * policy="dynamic" bind="setRegistryService" unbind="unsetRegistryService" + *

      + * Adding reference to API Manager Configuration service is an unavoidable hack to get rid of NPEs thrown while + * initializing APIMgtDAOs attempting to register APIs programmatically. APIMgtDAO needs to be proper cleaned up + * to avoid as an ideal fix + */ +public class AndroidDeviceManagementServiceComponent { + + private static final Log log = LogFactory.getLog(AndroidDeviceManagementServiceComponent.class); + private ServiceRegistration androidServiceRegRef; + private ServiceRegistration gcmServiceRegRef; + + protected void activate(ComponentContext ctx) { + + if (log.isDebugEnabled()) { + log.debug("Activating Android Mobile Device Management Service Component"); + } + try { + BundleContext bundleContext = ctx.getBundleContext(); + + DeviceManagementService androidDeviceManagementService = new AndroidDeviceManagementService(); + GCMService gcmService = new GCMService(); + + androidServiceRegRef = + bundleContext.registerService(DeviceManagementService.class.getName(), + androidDeviceManagementService, null); + + gcmServiceRegRef = + bundleContext.registerService(GCMService.class.getName(), gcmService, null); + + + // Policy management service + + bundleContext.registerService(PolicyMonitoringService.class, + new AndroidPolicyMonitoringService(), null); + + AndroidDeviceManagementDataHolder.getInstance().setAndroidDeviceManagementService( + androidDeviceManagementService); + AndroidDeviceManagementDataHolder.getInstance().setGCMService(gcmService); + + if (log.isDebugEnabled()) { + log.debug("Android Mobile Device Management Service Component has been successfully activated"); + } + } catch (Throwable e) { + log.error("Error occurred while activating Android Mobile Device Management Service Component", e); + } + } + + protected void deactivate(ComponentContext ctx) { + if (log.isDebugEnabled()) { + log.debug("De-activating Android Mobile Device Management Service Component"); + } + try { + if (androidServiceRegRef != null) { + androidServiceRegRef.unregister(); + } + if (gcmServiceRegRef != null) { + gcmServiceRegRef.unregister(); + } + if (log.isDebugEnabled()) { + log.debug( + "Android Mobile Device Management Service Component has been successfully de-activated"); + } + } catch (Throwable e) { + log.error("Error occurred while de-activating Android Mobile Device Management bundle", e); + } + } + + protected void setDataSourceService(DataSourceService dataSourceService) { + /* This is to avoid mobile device management component getting initialized before the underlying datasources + are registered */ + if (log.isDebugEnabled()) { + log.debug("Data source service set to android mobile service component"); + } + } + + protected void unsetDataSourceService(DataSourceService dataSourceService) { + //do nothing + } + + protected void setRegistryService(RegistryService registryService) { + if (log.isDebugEnabled()) { + log.debug("RegistryService acquired"); + } + AndroidDeviceManagementDataHolder.getInstance().setRegistryService(registryService); + } + + protected void unsetRegistryService(RegistryService registryService) { + AndroidDeviceManagementDataHolder.getInstance().setRegistryService(null); + } + +} diff --git a/components/mobile-plugins/android-plugin/pom.xml b/components/mobile-plugins/android-plugin/pom.xml new file mode 100644 index 000000000..0dbcdff77 --- /dev/null +++ b/components/mobile-plugins/android-plugin/pom.xml @@ -0,0 +1,59 @@ + + + + + + + org.wso2.carbon.devicemgt-plugins + mobile-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + android-plugin + pom + WSO2 Carbon - Mobile Plugins + http://wso2.org + + + org.wso2.carbon.device.mgt.mobile.android + org.wso2.carbon.device.mgt.mobile.android.agent + + + + + + + org.apache.felix + maven-scr-plugin + 1.7.2 + + + generate-scr-scrdescriptor + + scr + + + + + + + + diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/pom.xml b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/pom.xml new file mode 100644 index 000000000..bd997683e --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/pom.xml @@ -0,0 +1,205 @@ + + + + + + + mobile-base-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.mobile.api + war + WSO2 Carbon - Mobile Device Management API + WSO2 Carbon - Mobile Device Management API + http://wso2.org + + + + + maven-compiler-plugin + + 1.7 + 1.7 + + 2.3.2 + + + maven-war-plugin + 2.2 + + WEB-INF/lib/*cxf*.jar + ${project.artifactId} + + + + + + + + deploy + + compile + + + org.apache.maven.plugins + maven-antrun-plugin + 1.7 + + + compile + + run + + + + + + + + + + + + + + + + + + + client + + test + + + org.codehaus.mojo + exec-maven-plugin + 1.2.1 + + + test + + java + + + + + + + + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + + + org.apache.cxf + cxf-rt-frontend-jaxrs + + + org.apache.cxf + cxf-rt-transports-http + + + junit + junit + test + + + commons-httpclient.wso2 + commons-httpclient + provided + + + javax.ws.rs + jsr311-api + provided + + + org.wso2.carbon + org.wso2.carbon.utils + provided + + + org.wso2.carbon.commons + org.wso2.carbon.user.mgt + provided + + + org.wso2.carbon + org.wso2.carbon.logging + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.core + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.policy.mgt.core + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + org.wso2.carbon.identity + org.wso2.carbon.identity.oauth.stub + provided + + + org.apache.axis2.wso2 + axis2-client + + + + + com.google.code.gson + gson + + + commons-codec.wso2 + commons-codec + provided + + + + diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Authentication.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Authentication.java new file mode 100644 index 000000000..42257b794 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Authentication.java @@ -0,0 +1,36 @@ +/* + * 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.mdm.api; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; + +/** + * Authentication related REST-API implementation. + */ +@Produces({ "application/json", "application/xml" }) +@Consumes({ "application/json", "application/xml" }) +public class Authentication { + + private static Log log = LogFactory.getLog(Authentication.class); +} + diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Configuration.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Configuration.java new file mode 100644 index 000000000..8acf4efe9 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Configuration.java @@ -0,0 +1,98 @@ +/* + * 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.mdm.api; + +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.configuration.mgt.ConfigurationManagementException; +import org.wso2.carbon.device.mgt.common.configuration.mgt.TenantConfiguration; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import org.wso2.carbon.mdm.api.util.MDMAPIUtils; +import org.wso2.carbon.mdm.api.util.MDMAppConstants; +import org.wso2.carbon.mdm.api.util.ResponsePayload; + +import javax.jws.WebService; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; + +/** + * General Tenant Configuration REST-API implementation. + * All end points support JSON, XMl with content negotiation. + */ +@WebService +@Produces({ "application/json", "application/xml" }) +@Consumes({ "application/json", "application/xml" }) +public class Configuration { + + private static Log log = LogFactory.getLog(Configuration.class); + + @POST + public ResponsePayload saveTenantConfiguration(TenantConfiguration configuration) + throws MDMAPIException { + ResponsePayload responseMsg = new ResponsePayload(); + try { + MDMAPIUtils.getTenantConfigurationManagementService().saveConfiguration(configuration, + MDMAppConstants.RegistryConstants.GENERAL_CONFIG_RESOURCE_PATH); + //Schedule the task service + MDMAPIUtils.scheduleTaskService(MDMAPIUtils.getNotifierFrequency(configuration)); + Response.status(HttpStatus.SC_CREATED); + responseMsg.setMessageFromServer("Tenant configuration saved successfully."); + responseMsg.setStatusCode(HttpStatus.SC_CREATED); + return responseMsg; + } catch (ConfigurationManagementException e) { + String msg = "Error occurred while saving the tenant configuration."; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + } + + @GET + public TenantConfiguration getConfiguration() throws MDMAPIException { + String msg; + try { + return MDMAPIUtils.getTenantConfigurationManagementService().getConfiguration(MDMAppConstants. + RegistryConstants.GENERAL_CONFIG_RESOURCE_PATH); + } catch (ConfigurationManagementException e) { + msg = "Error occurred while retrieving the tenant configuration."; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + } + + @PUT + public ResponsePayload updateConfiguration(TenantConfiguration configuration) throws MDMAPIException { + ResponsePayload responseMsg = new ResponsePayload(); + try { + MDMAPIUtils.getTenantConfigurationManagementService().saveConfiguration(configuration, + MDMAppConstants.RegistryConstants.GENERAL_CONFIG_RESOURCE_PATH); + //Schedule the task service + MDMAPIUtils.scheduleTaskService(MDMAPIUtils.getNotifierFrequency(configuration)); + Response.status(HttpStatus.SC_CREATED); + responseMsg.setMessageFromServer("Tenant configuration updated successfully."); + responseMsg.setStatusCode(HttpStatus.SC_CREATED); + return responseMsg; + } catch (ConfigurationManagementException e) { + String msg = "Error occurred while updating the tenant configuration."; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/DeviceNotification.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/DeviceNotification.java new file mode 100644 index 000000000..9a8567572 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/DeviceNotification.java @@ -0,0 +1,108 @@ +/* + * 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.mdm.api; + +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.notification.mgt.NotificationManagementException; +import org.wso2.carbon.device.mgt.common.notification.mgt.Notification; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import org.wso2.carbon.mdm.api.util.MDMAPIUtils; +import org.wso2.carbon.mdm.api.util.ResponsePayload; + +import javax.jws.WebService; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import java.util.List; + +/** + * DeviceNotification management REST-API implementation. + * All end points support JSON, XMl with content negotiation. + */ +@WebService +@Produces({ "application/json", "application/xml" }) +@Consumes({ "application/json", "application/xml" }) +public class DeviceNotification { + + private static Log log = LogFactory.getLog(Configuration.class); + + @GET + public List getNotifications() throws MDMAPIException { + String msg; + try { + return MDMAPIUtils.getNotificationManagementService().getAllNotifications(); + } catch (NotificationManagementException e) { + msg = "Error occurred while retrieving the notification list."; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + } + + @GET + @Path("{status}") + public List getNotificationsByStatus(@PathParam("status") Notification.Status status) + throws MDMAPIException { + String msg; + try { + return MDMAPIUtils.getNotificationManagementService().getNotificationsByStatus(status); + } catch (NotificationManagementException e) { + msg = "Error occurred while retrieving the notification list."; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + } + + @PUT + @Path("{id}/{status}") + public ResponsePayload updateNotificationStatus(@PathParam("id") int id, + @PathParam("status") Notification.Status status) + throws MDMAPIException{ + ResponsePayload responseMsg = new ResponsePayload(); + try { + MDMAPIUtils.getNotificationManagementService().updateNotificationStatus(id, status); + Response.status(HttpStatus.SC_ACCEPTED); + responseMsg.setMessageFromServer("Notification status updated successfully."); + responseMsg.setStatusCode(HttpStatus.SC_ACCEPTED); + return responseMsg; + } catch (NotificationManagementException e) { + String msg = "Error occurred while updating notification status."; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + } + + @POST + public ResponsePayload addNotification(Notification notification) + throws MDMAPIException{ + ResponsePayload responseMsg = new ResponsePayload(); + try { + MDMAPIUtils.getNotificationManagementService().addNotification(notification); + Response.status(HttpStatus.SC_CREATED); + responseMsg.setMessageFromServer("Notification has added successfully."); + responseMsg.setStatusCode(HttpStatus.SC_CREATED); + return responseMsg; + } catch (NotificationManagementException e) { + String msg = "Error occurred while updating notification status."; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Feature.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Feature.java new file mode 100644 index 000000000..1cd357b94 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Feature.java @@ -0,0 +1,62 @@ +/* + * 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.mdm.api; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import org.wso2.carbon.mdm.api.util.MDMAPIUtils; + +import javax.ws.rs.*; +import java.util.List; + +/** + * Features + */ +@Produces({"application/json", "application/xml"}) +@Consumes({"application/json", "application/xml"}) +public class Feature { + private static Log log = LogFactory.getLog(Feature.class); + + /** + * Get all features for Mobile Device Type + * + * @return Feature + * @throws org.wso2.carbon.mdm.api.common.MDMAPIException + * + */ + @GET + @Path("/{type}") + public List getFeatures(@PathParam("type") String type) + throws MDMAPIException { + List features; + DeviceManagementProviderService dmService; + try { + dmService = MDMAPIUtils.getDeviceManagementService(); + features = dmService.getFeatureManager(type).getFeatures(); + } catch (DeviceManagementException e) { + String msg = "Error occurred while retrieving the list of features"; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + return features; + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/License.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/License.java new file mode 100644 index 000000000..1b1e6c3d0 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/License.java @@ -0,0 +1,99 @@ +/* + * 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.mdm.api; + +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import org.wso2.carbon.mdm.api.util.MDMAPIUtils; +import org.wso2.carbon.mdm.api.util.ResponsePayload; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +/** + * This class represents license related operations. + */ +public class License { + + private static Log log = LogFactory.getLog(License.class); + + /** + * This method returns the license text related to a given device type and language code. + * + * @param deviceType Device type, ex: android, ios + * @param languageCode Language code, ex: en_US + * @return Returns the license text + * @throws MDMAPIException If the device type or language code arguments are not available or invalid. + */ + @GET + @Path ("{deviceType}/{languageCode}") + @Produces ({MediaType.APPLICATION_JSON}) + public Response getLicense(@PathParam ("deviceType") String deviceType, + @PathParam ("languageCode") String languageCode) throws MDMAPIException { + + org.wso2.carbon.device.mgt.common.license.mgt.License license; + ResponsePayload responsePayload = new ResponsePayload(); + try { + license = MDMAPIUtils.getDeviceManagementService().getLicense(deviceType, languageCode); + if (license == null) { + return Response.status(HttpStatus.SC_NOT_FOUND).build(); + } + responsePayload = ResponsePayload.statusCode(HttpStatus.SC_OK). + messageFromServer("License for '" + deviceType + "' was retrieved successfully"). + responseContent(license.getText()). + build(); + } catch (DeviceManagementException e) { + String msg = "Error occurred while retrieving the license configured for '" + deviceType + "' device type"; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } + + /** + * This method is used to add license to a specific device type. + * + * @param deviceType Device type, ex: android, ios + * @param license License object + * @return Returns the acknowledgement for the action + * @throws MDMAPIException + */ + @POST + @Path ("{deviceType}") + public Response addLicense(@PathParam ("deviceType") String deviceType, + org.wso2.carbon.device.mgt.common.license.mgt.License license) throws MDMAPIException { + + ResponsePayload responsePayload; + try { + MDMAPIUtils.getDeviceManagementService().addLicense(deviceType, license); + responsePayload = ResponsePayload.statusCode(HttpStatus.SC_OK). + messageFromServer("License added successfully for '" + deviceType + "' device type"). + build(); + } catch (DeviceManagementException e) { + String msg = "Error occurred while adding license for '" + deviceType + "' device type"; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/MobileDevice.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/MobileDevice.java new file mode 100644 index 000000000..be7c9bda3 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/MobileDevice.java @@ -0,0 +1,218 @@ +/* + * 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.mdm.api; + +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.*; +import org.wso2.carbon.device.mgt.core.dto.DeviceType; +import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import org.wso2.carbon.mdm.api.util.MDMAPIUtils; +import org.wso2.carbon.mdm.api.util.ResponsePayload; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; + +/** + * Device related operations + */ +public class MobileDevice { + private static Log log = LogFactory.getLog(MobileDevice.class); + + /** + * Get all devices. We have to use accept all the necessary query parameters sent by datatable. + * Hence had to put lot of query params here. + * + * @return Device List + * @throws MDMAPIException + */ + @GET + public Object getAllDevices(@QueryParam("type") String type, @QueryParam("user") String user, + @QueryParam("role") String role, @QueryParam("status") EnrolmentInfo.Status status, + @QueryParam("start") int startIdx, @QueryParam("length") int length, + @QueryParam("device-name") String deviceName, + @QueryParam("ownership") EnrolmentInfo.OwnerShip ownership + ) throws MDMAPIException { + try { + DeviceManagementProviderService service = MDMAPIUtils.getDeviceManagementService(); + //Length > 0 means this is a pagination request. + if (length > 0) { + PaginationRequest paginationRequest = new PaginationRequest(startIdx, length); + paginationRequest.setDeviceName(deviceName); + paginationRequest.setOwner(user); + if (ownership != null) { + paginationRequest.setOwnership(ownership.toString()); + } + if (status != null) { + paginationRequest.setStatus(status.toString()); + } + paginationRequest.setDeviceType(type); + return service.getAllDevices(paginationRequest); + } + + List allDevices = null; + if ((type != null) && !type.isEmpty()) { + allDevices = service.getAllDevices(type); + } else if ((user != null) && !user.isEmpty()) { + allDevices = service.getDevicesOfUser(user); + } else if ((role != null) && !role.isEmpty()) { + allDevices = service.getAllDevicesOfRole(role); + } else if (status != null) { + allDevices = service.getDevicesByStatus(status); + } else if (deviceName != null) { + allDevices = service.getDevicesByName(deviceName); + } else { + allDevices = service.getAllDevices(); + } + return allDevices; + } catch (DeviceManagementException e) { + String msg = "Error occurred while fetching the device list."; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + } + + /** + * Fetch device details for a given device type and device Id. + * + * @return Device wrapped inside Response + * @throws MDMAPIException + */ + @GET + @Path("view") + @Produces({MediaType.APPLICATION_JSON}) + public Response getDevice(@QueryParam("type") String type, + @QueryParam("id") String id) throws MDMAPIException { + DeviceIdentifier deviceIdentifier = MDMAPIUtils.instantiateDeviceIdentifier(type, id); + DeviceManagementProviderService deviceManagementProviderService = MDMAPIUtils.getDeviceManagementService(); + Device device; + try { + device = deviceManagementProviderService.getDevice(deviceIdentifier); + } catch (DeviceManagementException e) { + String error = "Error occurred while fetching the device information."; + log.error(error, e); + throw new MDMAPIException(error, e); + } + ResponsePayload responsePayload = new ResponsePayload(); + if (device == null) { + responsePayload.setStatusCode(HttpStatus.SC_NOT_FOUND); + responsePayload.setMessageFromServer("Requested device by type: " + + type + " and id: " + id + " does not exist."); + return Response.status(HttpStatus.SC_NOT_FOUND).entity(responsePayload).build(); + } else { + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer("Sending Requested device by type: " + type + " and id: " + id + "."); + responsePayload.setResponseContent(device); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } + } + + /** + * Fetch Android device details of a given user. + * + * @param user User Name + * @param tenantDomain tenant domain + * @return Device + * @throws org.wso2.carbon.mdm.api.common.MDMAPIException + */ + @GET + @Path("user/{user}/{tenantDomain}") + public List getDeviceByUser(@PathParam("user") String user, + @PathParam("tenantDomain") String tenantDomain) throws MDMAPIException { + List devices; + try { + devices = MDMAPIUtils.getDeviceManagementService().getDevicesOfUser(user); + if (devices == null) { + Response.status(Response.Status.NOT_FOUND); + } + return devices; + } catch (DeviceManagementException e) { + String msg = "Error occurred while fetching the devices list of given user."; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + } + + /** + * Get current device count + * + * @return device count + * @throws MDMAPIException + */ + @GET + @Path("count") + public int getDeviceCount() throws MDMAPIException { + try { + return MDMAPIUtils.getDeviceManagementService().getDeviceCount(); + } catch (DeviceManagementException e) { + String msg = "Error occurred while fetching the device count."; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + } + + /** + * Get the list of devices that matches with the given name. + * + * @param deviceName Device name + * @param tenantDomain Callee tenant domain + * @return list of devices. + * @throws MDMAPIException If some unusual behaviour is observed while fetching the device list + */ + @GET + @Path("name/{name}/{tenantDomain}") + public List getDevicesByName(@PathParam("name") String deviceName, + @PathParam("tenantDomain") String tenantDomain) throws MDMAPIException { + + List devices; + try { + devices = MDMAPIUtils.getDeviceManagementService().getDevicesByName(deviceName); + } catch (DeviceManagementException e) { + String msg = "Error occurred while fetching the devices list of device name."; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + return devices; + } + + /** + * Get the list of available device types. + * + * @return list of device types. + * @throws MDMAPIException If some unusual behaviour is observed while fetching the device list + */ + @GET + @Path("types") + public List getDeviceTypes() throws MDMAPIException { + + List deviceTypes; + try { + deviceTypes = MDMAPIUtils.getDeviceManagementService().getAvailableDeviceTypes(); + } catch (DeviceManagementException e) { + String msg = "Error occurred while fetching the list of device types."; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + return deviceTypes; + } +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Operation.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Operation.java new file mode 100644 index 000000000..a775318dd --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Operation.java @@ -0,0 +1,225 @@ +/* + * 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.mdm.api; + +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.PaginationRequest; +import org.wso2.carbon.device.mgt.common.PaginationResult; +import org.wso2.carbon.device.mgt.common.Platform; +import org.wso2.carbon.device.mgt.common.app.mgt.Application; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManagementException; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManager; +import org.wso2.carbon.device.mgt.common.operation.mgt.OperationManagementException; +import org.wso2.carbon.device.mgt.core.app.mgt.ApplicationManagementProviderService; +import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import org.wso2.carbon.mdm.api.context.DeviceOperationContext; +import org.wso2.carbon.mdm.api.util.MDMAPIUtils; +import org.wso2.carbon.mdm.api.util.MDMAndroidOperationUtil; +import org.wso2.carbon.mdm.api.util.MDMIOSOperationUtil; +import org.wso2.carbon.mdm.api.util.ResponsePayload; +import org.wso2.carbon.mdm.beans.ApplicationWrapper; +import org.wso2.carbon.mdm.beans.MobileApp; +import org.wso2.carbon.utils.multitenancy.MultitenantConstants; + +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.List; + +/** + * Operation related REST-API implementation. + */ +@Produces({"application/json", "application/xml"}) +@Consumes({"application/json", "application/xml"}) +public class Operation { + + private static Log log = LogFactory.getLog(Operation.class); + + @GET + public List getAllOperations() + throws MDMAPIException { + List operations; + DeviceManagementProviderService dmService; + try { + dmService = MDMAPIUtils.getDeviceManagementService(); + operations = dmService.getOperations(null); + } catch (OperationManagementException e) { + String msg = "Error occurred while fetching the operations for the device."; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + return operations; + } + + @GET + @Path("paginate/{type}/{id}") + public PaginationResult getDeviceOperations( + @PathParam("type") String type, @PathParam("id") String id, @QueryParam("start") int startIdx, + @QueryParam("length") int length, @QueryParam("search") String search) + throws MDMAPIException { + PaginationResult operations; + DeviceManagementProviderService dmService; + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + PaginationRequest paginationRequest = new PaginationRequest(startIdx, length); + try { + deviceIdentifier.setType(type); + deviceIdentifier.setId(id); + dmService = MDMAPIUtils.getDeviceManagementService(); + operations = dmService.getOperations(deviceIdentifier, paginationRequest); + } catch (OperationManagementException e) { + String msg = "Error occurred while fetching the operations for the device."; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + return operations; + } + + @GET + @Path("{type}/{id}") + public List getDeviceOperations( + @PathParam("type") String type, @PathParam("id") String id) + throws MDMAPIException { + List operations; + DeviceManagementProviderService dmService; + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + try { + deviceIdentifier.setType(type); + deviceIdentifier.setId(id); + dmService = MDMAPIUtils.getDeviceManagementService(); + operations = dmService.getOperations(deviceIdentifier); + } catch (OperationManagementException e) { + String msg = "Error occurred while fetching the operations for the device."; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + return operations; + } + + @POST + public ResponsePayload addOperation(DeviceOperationContext operationContext) throws MDMAPIException { + DeviceManagementProviderService dmService; + ResponsePayload responseMsg = new ResponsePayload(); + try { + dmService = MDMAPIUtils.getDeviceManagementService(); + int operationId = dmService.addOperation(operationContext.getOperation(), + operationContext.getDevices()); + if (operationId>0) { + Response.status(HttpStatus.SC_CREATED); + responseMsg.setMessageFromServer("Operation has added successfully."); + } + return responseMsg; + } catch (OperationManagementException e) { + String msg = "Error occurred while saving the operation"; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + } + + @GET + @Path("{type}/{id}/apps") + public List getInstalledApps( + @PathParam("type") String type, + @PathParam("id") String id) + throws MDMAPIException { + List applications; + ApplicationManagementProviderService appManagerConnector; + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + try { + deviceIdentifier.setType(type); + deviceIdentifier.setId(id); + appManagerConnector = MDMAPIUtils.getAppManagementService(); + applications = appManagerConnector.getApplicationListForDevice(deviceIdentifier); + } catch (ApplicationManagementException e) { + String msg = "Error occurred while fetching the apps of the device."; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + return applications; + } + + @POST + @Path("installApp/{tenantDomain}") + public ResponsePayload installApplication(ApplicationWrapper applicationWrapper, + @PathParam("tenantDomain") String tenantDomain) throws MDMAPIException { + ResponsePayload responseMsg = new ResponsePayload(); + ApplicationManager appManagerConnector; + org.wso2.carbon.device.mgt.common.operation.mgt.Operation operation = null; + ArrayList deviceIdentifiers; + try { + appManagerConnector = MDMAPIUtils.getAppManagementService(); + MobileApp mobileApp = applicationWrapper.getApplication(); + + if (applicationWrapper.getDeviceIdentifiers() != null) { + for (DeviceIdentifier deviceIdentifier : applicationWrapper.getDeviceIdentifiers()) { + deviceIdentifiers = new ArrayList(); + + if (deviceIdentifier.getType().equals(Platform.android.toString())) { + operation = MDMAndroidOperationUtil.createInstallAppOperation(mobileApp); + } else if (deviceIdentifier.getType().equals(Platform.ios.toString())) { + operation = MDMIOSOperationUtil.createInstallAppOperation(mobileApp); + } + deviceIdentifiers.add(deviceIdentifier); + } + appManagerConnector.installApplicationForDevices(operation, applicationWrapper.getDeviceIdentifiers()); + } + return responseMsg; + } catch (ApplicationManagementException e) { + String msg = "Error occurred while saving the operation"; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + } + + @POST + @Path("uninstallApp/{tenantDomain}") + public ResponsePayload uninstallApplication(ApplicationWrapper applicationWrapper, + @PathParam("tenantDomain") String tenantDomain) throws MDMAPIException { + ResponsePayload responseMsg = new ResponsePayload(); + ApplicationManager appManagerConnector; + org.wso2.carbon.device.mgt.common.operation.mgt.Operation operation = null; + ArrayList deviceIdentifiers; + try { + appManagerConnector = MDMAPIUtils.getAppManagementService(); + MobileApp mobileApp = applicationWrapper.getApplication(); + + if (applicationWrapper.getDeviceIdentifiers() != null) { + for (DeviceIdentifier deviceIdentifier : applicationWrapper.getDeviceIdentifiers()) { + deviceIdentifiers = new ArrayList(); + + if (deviceIdentifier.getType().equals(Platform.android.toString())) { + operation = MDMAndroidOperationUtil.createAppUninstallOperation(mobileApp); + } else if (deviceIdentifier.getType().equals(Platform.ios.toString())) { + operation = MDMIOSOperationUtil.createAppUninstallOperation(mobileApp); + } + deviceIdentifiers.add(deviceIdentifier); + } + appManagerConnector.installApplicationForDevices(operation, applicationWrapper.getDeviceIdentifiers()); + } + return responseMsg; + } catch (ApplicationManagementException e) { + String msg = "Error occurred while saving the operation"; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + } +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Policy.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Policy.java new file mode 100644 index 000000000..dea04d31b --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Policy.java @@ -0,0 +1,475 @@ +/* + * 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.mdm.api; + +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import org.wso2.carbon.mdm.api.util.MDMAPIUtils; +import org.wso2.carbon.mdm.api.util.ResponsePayload; +import org.wso2.carbon.mdm.beans.PolicyWrapper; +import org.wso2.carbon.mdm.beans.PriorityUpdatedPolicyWrapper; +import org.wso2.carbon.mdm.util.MDMUtil; +import org.wso2.carbon.policy.mgt.common.PolicyAdministratorPoint; +import org.wso2.carbon.policy.mgt.common.PolicyManagementException; +import org.wso2.carbon.policy.mgt.common.PolicyMonitoringTaskException; +import org.wso2.carbon.policy.mgt.common.monitor.ComplianceData; +import org.wso2.carbon.policy.mgt.common.monitor.PolicyComplianceException; +import org.wso2.carbon.policy.mgt.core.PolicyManagerService; +import org.wso2.carbon.policy.mgt.core.task.TaskScheduleService; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.List; + +public class Policy { + private static Log log = LogFactory.getLog(Policy.class); + + @POST + @Path("inactive-policy") + public ResponsePayload addPolicy(PolicyWrapper policyWrapper) throws MDMAPIException { + + PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); + ResponsePayload responseMsg = new ResponsePayload(); + org.wso2.carbon.policy.mgt.common.Policy policy = new org.wso2.carbon.policy.mgt.common.Policy(); + policy.setPolicyName(policyWrapper.getPolicyName()); + policy.setProfileId(policyWrapper.getProfileId()); + policy.setDescription(policyWrapper.getDescription()); + policy.setProfile(MDMUtil.convertProfile(policyWrapper.getProfile())); + policy.setOwnershipType(policyWrapper.getOwnershipType()); + policy.setRoles(policyWrapper.getRoles()); + policy.setUsers(policyWrapper.getUsers()); + policy.setTenantId(policyWrapper.getTenantId()); + policy.setCompliance(policyWrapper.getCompliance()); + + try { + PolicyAdministratorPoint pap = policyManagementService.getPAP(); + pap.addPolicy(policy); + Response.status(HttpStatus.SC_CREATED); + responseMsg.setStatusCode(HttpStatus.SC_CREATED); + responseMsg.setMessageFromServer("Policy has been added successfully."); + return responseMsg; + } catch (PolicyManagementException e) { + String error = "Policy Management related exception"; + log.error(error, e); + throw new MDMAPIException(error, e); + } + } + + @POST + @Path("active-policy") + public ResponsePayload addActivePolicy(PolicyWrapper policyWrapper) throws MDMAPIException { + + PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); + ResponsePayload responseMsg = new ResponsePayload(); + org.wso2.carbon.policy.mgt.common.Policy policy = new org.wso2.carbon.policy.mgt.common.Policy(); + policy.setPolicyName(policyWrapper.getPolicyName()); + policy.setProfileId(policyWrapper.getProfileId()); + policy.setDescription(policyWrapper.getDescription()); + policy.setProfile(MDMUtil.convertProfile(policyWrapper.getProfile())); + policy.setOwnershipType(policyWrapper.getOwnershipType()); + policy.setRoles(policyWrapper.getRoles()); + policy.setUsers(policyWrapper.getUsers()); + policy.setTenantId(policyWrapper.getTenantId()); + policy.setCompliance(policyWrapper.getCompliance()); + policy.setActive(true); + + try { + PolicyAdministratorPoint pap = policyManagementService.getPAP(); + pap.addPolicy(policy); + Response.status(HttpStatus.SC_CREATED); + responseMsg.setStatusCode(HttpStatus.SC_CREATED); + responseMsg.setMessageFromServer("Policy has been added successfully."); + return responseMsg; + } catch (PolicyManagementException e) { + String error = "Policy Management related exception"; + log.error(error, e); + throw new MDMAPIException(error, e); + } + } + + @GET + @Produces({MediaType.APPLICATION_JSON}) + public Response getAllPolicies() throws MDMAPIException { + PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); + List policies; + try { + PolicyAdministratorPoint policyAdministratorPoint = policyManagementService.getPAP(); + policies = policyAdministratorPoint.getPolicies(); + } catch (PolicyManagementException e) { + String error = "Policy Management related exception"; + log.error(error, e); + throw new MDMAPIException(error, e); + } + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer("Sending all retrieved device policies."); + responsePayload.setResponseContent(policies); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } + + @GET + @Produces({MediaType.APPLICATION_JSON}) + @Path("{id}") + public Response getPolicy(@PathParam("id") int policyId) throws MDMAPIException { + PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); + final org.wso2.carbon.policy.mgt.common.Policy policy; + try { + PolicyAdministratorPoint policyAdministratorPoint = policyManagementService.getPAP(); + policy = policyAdministratorPoint.getPolicy(policyId); + } catch (PolicyManagementException e) { + String error = "Policy Management related exception"; + log.error(error, e); + throw new MDMAPIException(error, e); + } + if (policy == null){ + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_NOT_FOUND); + responsePayload.setMessageFromServer("Policy for ID " + policyId + " not found."); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer("Sending all retrieved device policies."); + responsePayload.setResponseContent(policy); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } + + @GET + @Path("count") + public int getPolicyCount() throws MDMAPIException { + PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); + try { + PolicyAdministratorPoint policyAdministratorPoint = policyManagementService.getPAP(); + return policyAdministratorPoint.getPolicyCount(); + } catch (PolicyManagementException e) { + String error = "Policy Management related exception"; + log.error(error, e); + throw new MDMAPIException(error, e); + } + } + + @PUT + @Path("{id}") + public ResponsePayload updatePolicy(PolicyWrapper policyWrapper, @PathParam("id") int policyId) + throws MDMAPIException { +// PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); +// ResponsePayload responseMsg = new ResponsePayload(); +// try { +// PolicyAdministratorPoint pap = policyManagementService.getPAP(); +// org.wso2.carbon.policy.mgt.common.Policy previousPolicy = pap.getPolicy(policyId); +// policy.setProfile(pap.getProfile(previousPolicy.getProfileId())); +// policy.setId(previousPolicy.getId()); +// pap.updatePolicy(policy); +// Response.status(HttpStatus.SC_OK); +// responseMsg.setMessageFromServer("Policy has been updated successfully."); +// return responseMsg; +// } catch (PolicyManagementException e) { +// String error = "Policy Management related exception"; +// log.error(error, e); +// throw new MDMAPIException(error, e); +// } + + + + PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); + ResponsePayload responseMsg = new ResponsePayload(); + org.wso2.carbon.policy.mgt.common.Policy policy = new org.wso2.carbon.policy.mgt.common.Policy(); + policy.setPolicyName(policyWrapper.getPolicyName()); + policy.setId(policyId); + policy.setProfileId(policyWrapper.getProfileId()); + policy.setDescription(policyWrapper.getDescription()); + policy.setProfile(MDMUtil.convertProfile(policyWrapper.getProfile())); + policy.setOwnershipType(policyWrapper.getOwnershipType()); + policy.setRoles(policyWrapper.getRoles()); + policy.setUsers(policyWrapper.getUsers()); + policy.setTenantId(policyWrapper.getTenantId()); + policy.setCompliance(policyWrapper.getCompliance()); + // policy.setActive(true); + + try { + PolicyAdministratorPoint pap = policyManagementService.getPAP(); +// pap.addPolicy(policy); + pap.updatePolicy(policy); +// Response.status(HttpStatus.SC_CREATED); + Response.status(HttpStatus.SC_OK); + responseMsg.setStatusCode(HttpStatus.SC_CREATED); +// responseMsg.setMessageFromServer("Policy has been added successfully."); + responseMsg.setMessageFromServer("Policy has been updated successfully."); + return responseMsg; + } catch (PolicyManagementException e) { + String error = "Policy Management related exception in policy update."; + log.error(error, e); + throw new MDMAPIException(error, e); + } + } + + @PUT + @Path("priorities") + @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + public Response updatePolicyPriorities(List priorityUpdatedPolicies) + throws MDMAPIException { + PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); + List policiesToUpdate = + new ArrayList(priorityUpdatedPolicies.size()); + int i; + for (i = 0; i < priorityUpdatedPolicies.size(); i++) { + org.wso2.carbon.policy.mgt.common.Policy policyObj = new org.wso2.carbon.policy.mgt.common.Policy(); + policyObj.setId(priorityUpdatedPolicies.get(i).getId()); + policyObj.setPriorityId(priorityUpdatedPolicies.get(i).getPriority()); + policiesToUpdate.add(policyObj); + } + boolean policiesUpdated; + try { + PolicyAdministratorPoint pap = policyManagementService.getPAP(); + policiesUpdated = pap.updatePolicyPriorities(policiesToUpdate); + } catch (PolicyManagementException e) { + String error = "Exception in updating policy priorities."; + log.error(error, e); + throw new MDMAPIException(error, e); + } + ResponsePayload responsePayload = new ResponsePayload(); + if (policiesUpdated) { + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer("Policy Priorities successfully updated."); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } else { + responsePayload.setStatusCode(HttpStatus.SC_BAD_REQUEST); + responsePayload.setMessageFromServer("Policy priorities did not update. Bad Request."); + return Response.status(HttpStatus.SC_BAD_REQUEST).entity(responsePayload).build(); + } + } + + @POST + @Path("bulk-remove") + @Consumes("application/json") + @Produces("application/json") + public Response bulkRemovePolicy(List policyIds) throws MDMAPIException { + PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); + boolean policyDeleted = true; + try { + PolicyAdministratorPoint pap = policyManagementService.getPAP(); + for(int i : policyIds) { + org.wso2.carbon.policy.mgt.common.Policy policy = pap.getPolicy(i); + if(!pap.deletePolicy(policy)){ + policyDeleted = false; + } + } + } catch (PolicyManagementException e) { + String error = "Exception in deleting policies."; + log.error(error, e); + throw new MDMAPIException(error, e); + } + ResponsePayload responsePayload = new ResponsePayload(); + if (policyDeleted) { + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer("Policies have been successfully deleted."); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } else { + responsePayload.setStatusCode(HttpStatus.SC_BAD_REQUEST); + responsePayload.setMessageFromServer("Policy does not exist."); + return Response.status(HttpStatus.SC_BAD_REQUEST).entity(responsePayload).build(); + } + } + +// @GET +// @Path("task/{mf}") +// public int taskService(@PathParam("mf") int monitoringFrequency) throws MDMAPIException { +// int policyCount = 0; +// PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); +// try { +// TaskScheduleService taskScheduleService = policyManagementService.getTaskScheduleService(); +// taskScheduleService.startTask(monitoringFrequency); +// return policyCount; +// } catch (PolicyMonitoringTaskException e) { +// String error = "Policy Management related exception"; +// log.error(error, e); +// throw new MDMAPIException(error, e); +// } +// } + + + @PUT + @Produces("application/json") + @Path("activate") + public Response activatePolicy(List policyIds) throws MDMAPIException { + try { + PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); + PolicyAdministratorPoint pap = policyManagementService.getPAP(); + for(int i : policyIds) { + pap.activatePolicy(i); + } + } catch (PolicyManagementException e) { + String error = "Exception in activating policies."; + log.error(error, e); + throw new MDMAPIException(error, e); + } + + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer("Selected policies have been successfully activated."); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + + } + + + @PUT + @Produces("application/json") + @Path("inactivate") + public Response inactivatePolicy(List policyIds) throws MDMAPIException { + + try { + PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); + PolicyAdministratorPoint pap = policyManagementService.getPAP(); + for(int i : policyIds) { + pap.inactivatePolicy(i); + } + } catch (PolicyManagementException e) { + String error = "Exception in inactivating policies."; + log.error(error, e); + throw new MDMAPIException(error, e); + } + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer("Selected policies have been successfully inactivated."); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } + + + @PUT + @Produces("application/json") + @Path("apply-changes") + public Response applyChanges() throws MDMAPIException { + + try { + PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); + PolicyAdministratorPoint pap = policyManagementService.getPAP(); + pap.publishChanges(); + + + } catch (PolicyManagementException e) { + String error = "Exception in applying changes."; + log.error(error, e); + throw new MDMAPIException(error, e); + } + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer("Changes have been successfully updated."); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } + + @GET + @Path("start-task/{milliseconds}") + public Response startTaskService(@PathParam("milliseconds") int monitoringFrequency) throws MDMAPIException { + + PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); + try { + TaskScheduleService taskScheduleService = policyManagementService.getTaskScheduleService(); + taskScheduleService.startTask(monitoringFrequency); + + + } catch (PolicyMonitoringTaskException e) { + String error = "Policy Management related exception."; + log.error(error, e); + throw new MDMAPIException(error, e); + } + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer("Policy monitoring service started successfully."); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } + + + @GET + @Path("update-task/{milliseconds}") + public Response updateTaskService(@PathParam("milliseconds") int monitoringFrequency) throws MDMAPIException { + + PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); + try { + TaskScheduleService taskScheduleService = policyManagementService.getTaskScheduleService(); + taskScheduleService.updateTask(monitoringFrequency); + + } catch (PolicyMonitoringTaskException e) { + String error = "Policy Management related exception."; + log.error(error, e); + throw new MDMAPIException(error, e); + } + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer("Policy monitoring service updated successfully."); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } + + + @GET + @Path("stop-task") + public Response stopTaskService() throws MDMAPIException { + + PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); + try { + TaskScheduleService taskScheduleService = policyManagementService.getTaskScheduleService(); + taskScheduleService.stopTask(); + + } catch (PolicyMonitoringTaskException e) { + String error = "Policy Management related exception."; + log.error(error, e); + throw new MDMAPIException(error, e); + } + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer("Policy monitoring service stopped successfully."); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } + + + @GET + @Path("{type}/{id}") + public ComplianceData getComplianceDataOfDevice(@PathParam("type") String type, @PathParam("id") String id) throws + MDMAPIException { + try { + DeviceIdentifier deviceIdentifier = MDMAPIUtils.instantiateDeviceIdentifier(type, id); + PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); + return policyManagementService.getDeviceCompliance(deviceIdentifier); + } catch (PolicyComplianceException e) { + String error = "Error occurred while getting the compliance data."; + log.error(error, e); + throw new MDMAPIException(error, e); + } + } + + @GET + @Path("{type}/{id}/active-policy") + public org.wso2.carbon.policy.mgt.common.Policy getDeviceActivePolicy(@PathParam("type") String type, + @PathParam("id") String id) throws MDMAPIException { + try { + DeviceIdentifier deviceIdentifier = MDMAPIUtils.instantiateDeviceIdentifier(type, id); + PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); + return policyManagementService.getAppliedPolicyToDevice(deviceIdentifier); + } catch (PolicyManagementException e) { + String error = "Error occurred while getting the current policy."; + log.error(error, e); + throw new MDMAPIException(error, e); + } + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Profile.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Profile.java new file mode 100644 index 000000000..4cf216ec6 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Profile.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.mdm.api; + +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import org.wso2.carbon.mdm.api.util.MDMAPIUtils; +import org.wso2.carbon.mdm.api.util.ResponsePayload; +import org.wso2.carbon.policy.mgt.common.PolicyAdministratorPoint; +import org.wso2.carbon.policy.mgt.common.PolicyManagementException; +import org.wso2.carbon.policy.mgt.core.PolicyManagerService; + +import javax.ws.rs.DELETE; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.core.Response; + +public class Profile { + private static Log log = LogFactory.getLog(Profile.class); + + @POST + public org.wso2.carbon.policy.mgt.common.Profile addProfile(org.wso2.carbon.policy.mgt.common.Profile profile) throws MDMAPIException { + PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); + try { + PolicyAdministratorPoint pap = policyManagementService.getPAP(); + profile = pap.addProfile(profile); + Response.status(HttpStatus.SC_CREATED); + return profile; + } catch (PolicyManagementException e) { + String error = "Policy Management related exception"; + log.error(error, e); + throw new MDMAPIException(error, e); + } + } + @POST + @Path("{id}") + public ResponsePayload updateProfile(org.wso2.carbon.policy.mgt.common.Profile profile, @PathParam("id") String profileId) + throws MDMAPIException { + PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); + ResponsePayload responseMsg = new ResponsePayload(); + try { + PolicyAdministratorPoint pap = policyManagementService.getPAP(); + pap.updateProfile(profile); + Response.status(HttpStatus.SC_OK); + responseMsg.setMessageFromServer("Profile has been updated successfully."); + return responseMsg; + } catch (PolicyManagementException e) { + String error = "Policy Management related exception"; + log.error(error, e); + throw new MDMAPIException(error, e); + } + } + @DELETE + @Path("{id}") + public ResponsePayload deleteProfile(@PathParam("id") int profileId) throws MDMAPIException { + PolicyManagerService policyManagementService = MDMAPIUtils.getPolicyManagementService(); + ResponsePayload responseMsg = new ResponsePayload(); + try { + PolicyAdministratorPoint pap = policyManagementService.getPAP(); + org.wso2.carbon.policy.mgt.common.Profile profile = pap.getProfile(profileId); + pap.deleteProfile(profile); + Response.status(HttpStatus.SC_OK); + responseMsg.setMessageFromServer("Profile has been deleted successfully."); + return responseMsg; + } catch (PolicyManagementException e) { + String error = "Policy Management related exception"; + log.error(error, e); + throw new MDMAPIException(error, e); + } + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Role.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Role.java new file mode 100644 index 000000000..a0adeed6a --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/Role.java @@ -0,0 +1,422 @@ +/* + * 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.mdm.api; + +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.CarbonConstants; +import org.wso2.carbon.base.MultitenantConstants; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import org.wso2.carbon.mdm.api.util.MDMAPIUtils; +import org.wso2.carbon.mdm.api.util.ResponsePayload; +import org.wso2.carbon.mdm.beans.RoleWrapper; +import org.wso2.carbon.mdm.util.SetReferenceTransformer; +import org.wso2.carbon.user.api.*; +import org.wso2.carbon.user.core.common.AbstractUserStoreManager; +import org.wso2.carbon.user.mgt.UserRealmProxy; +import org.wso2.carbon.user.mgt.common.UIPermissionNode; +import org.wso2.carbon.user.mgt.common.UserAdminException; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class Role { + + private static Log log = LogFactory.getLog(Role.class); + + /** + * Get user roles (except all internal roles) from system. + * + * @return A list of users + * @throws org.wso2.carbon.mdm.api.common.MDMAPIException + */ + @GET + @Produces ({MediaType.APPLICATION_JSON}) + public Response getRoles() throws MDMAPIException { + UserStoreManager userStoreManager = MDMAPIUtils.getUserStoreManager(); + String[] roles; + try { + if (log.isDebugEnabled()) { + log.debug("Getting the list of user roles"); + } + roles = userStoreManager.getRoleNames(); + + } catch (UserStoreException e) { + String msg = "Error occurred while retrieving the list of user roles."; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + // removing all internal roles and roles created for Service-providers + List filteredRoles = new ArrayList(); + for (String role : roles) { + if (!(role.startsWith("Internal/") || role.startsWith("Application/"))) { + filteredRoles.add(role); + } + } + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer("All user roles were successfully retrieved."); + responsePayload.setResponseContent(filteredRoles); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } + + /** + * Get user roles by user store(except all internal roles) from system. + * + * @return A list of users + * @throws org.wso2.carbon.mdm.api.common.MDMAPIException + */ + @GET + @Path ("{userStore}") + @Produces ({MediaType.APPLICATION_JSON}) + public Response getRoles(@PathParam ("userStore") String userStore) throws MDMAPIException { + AbstractUserStoreManager abstractUserStoreManager = (AbstractUserStoreManager) MDMAPIUtils.getUserStoreManager(); + String[] roles; + try { + if (log.isDebugEnabled()) { + log.debug("Getting the list of user roles"); + } + roles = abstractUserStoreManager.getRoleNames(userStore+"/*", -1, false, true, true); + + } catch (UserStoreException e) { + String msg = "Error occurred while retrieving the list of user roles."; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + // removing all internal roles and roles created for Service-providers + List filteredRoles = new ArrayList(); + for (String role : roles) { + if (!(role.startsWith("Internal/") || role.startsWith("Application/"))) { + filteredRoles.add(role); + } + } + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer("All user roles were successfully retrieved."); + responsePayload.setResponseContent(filteredRoles); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } + + /** + * Get user roles by providing a filtering criteria(except all internal roles & system roles) from system. + * + * @return A list of users + * @throws org.wso2.carbon.mdm.api.common.MDMAPIException + */ + @GET + @Path ("search") + @Produces ({MediaType.APPLICATION_JSON}) + public Response getMatchingRoles(@QueryParam ("filter") String filter) throws MDMAPIException { + AbstractUserStoreManager abstractUserStoreManager = (AbstractUserStoreManager) MDMAPIUtils.getUserStoreManager(); + String[] roles; + try { + if (log.isDebugEnabled()) { + log.debug("Getting the list of user roles using filter : " + filter); + } + roles = abstractUserStoreManager.getRoleNames("*" + filter + "*", -1, true, true, true); + + } catch (UserStoreException e) { + String msg = "Error occurred while retrieving the list of user roles using the filter : " + filter; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + // removing all internal roles and roles created for Service-providers + List filteredRoles = new ArrayList(); + for (String role : roles) { + if (!(role.startsWith("Internal/") || role.startsWith("Application/"))) { + filteredRoles.add(role); + } + } + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer("All matching user roles were successfully retrieved."); + responsePayload.setResponseContent(filteredRoles); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } + + /** + * Get role permissions. + * + * @return list of permissions + * @throws MDMAPIException + */ + @GET + @Path ("permissions") + @Produces ({MediaType.APPLICATION_JSON}) + public ResponsePayload getPermissions(@QueryParam ("rolename") String roleName) throws MDMAPIException { + final UserRealm userRealm = MDMAPIUtils.getUserRealm(); + org.wso2.carbon.user.core.UserRealm userRealmCore = null; + final UIPermissionNode rolePermissions; + if (userRealm instanceof org.wso2.carbon.user.core.UserRealm) { + userRealmCore = (org.wso2.carbon.user.core.UserRealm) userRealm; + } + + try { + final UserRealmProxy userRealmProxy = new UserRealmProxy(userRealmCore); + rolePermissions = userRealmProxy.getRolePermissions(roleName, MultitenantConstants.SUPER_TENANT_ID); + UIPermissionNode[] deviceMgtPermissions = new UIPermissionNode[2]; + + for (UIPermissionNode permissionNode : rolePermissions.getNodeList()) { + if (permissionNode.getResourcePath().equals("/permission/admin")) { + for (UIPermissionNode node : permissionNode.getNodeList()) { + if (node.getResourcePath().equals("/permission/admin/device-mgt")) { + deviceMgtPermissions[0] = node; + } else if (node.getResourcePath().equals("/permission/admin/login")) { + deviceMgtPermissions[1] = node; + } + } + } + } + rolePermissions.setNodeList(deviceMgtPermissions); + } catch (UserAdminException e) { + String msg = "Error occurred while retrieving the user role"; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer("All permissions retrieved"); + responsePayload.setResponseContent(rolePermissions); + return responsePayload; + } + + /** + * Get user role of the system + * + * @return user role + * @throws org.wso2.carbon.mdm.api.common.MDMAPIException + */ + @GET + @Path("role") + @Produces ({MediaType.APPLICATION_JSON}) + public ResponsePayload getRole(@QueryParam ("rolename") String roleName) throws MDMAPIException { + final UserStoreManager userStoreManager = MDMAPIUtils.getUserStoreManager(); + final UserRealm userRealm = MDMAPIUtils.getUserRealm(); + org.wso2.carbon.user.core.UserRealm userRealmCore = null; + if (userRealm instanceof org.wso2.carbon.user.core.UserRealm) { + userRealmCore = (org.wso2.carbon.user.core.UserRealm) userRealm; + } + + RoleWrapper roleWrapper = new RoleWrapper(); + try { + final UserRealmProxy userRealmProxy = new UserRealmProxy(userRealmCore); + if (log.isDebugEnabled()) { + log.debug("Getting the list of user roles"); + } + if (userStoreManager.isExistingRole(roleName)) { + roleWrapper.setRoleName(roleName); + roleWrapper.setUsers(userStoreManager.getUserListOfRole(roleName)); + // Get the permission nodes and hand picking only device management and login perms + final UIPermissionNode rolePermissions = + userRealmProxy.getRolePermissions(roleName, MultitenantConstants.SUPER_TENANT_ID); + UIPermissionNode[] deviceMgtPermissions = new UIPermissionNode[2]; + + for (UIPermissionNode permissionNode : rolePermissions.getNodeList()) { + if (permissionNode.getResourcePath().equals("/permission/admin")) { + for (UIPermissionNode node : permissionNode.getNodeList()) { + if (node.getResourcePath().equals("/permission/admin/device-mgt")) { + deviceMgtPermissions[0] = node; + } else if (node.getResourcePath().equals("/permission/admin/login")) { + deviceMgtPermissions[1] = node; + } + } + } + } + rolePermissions.setNodeList(deviceMgtPermissions); + ArrayList permList = new ArrayList(); + iteratePermissions(rolePermissions, permList); + roleWrapper.setPermissionList(rolePermissions); + String[] permListAr = new String[permList.size()]; + roleWrapper.setPermissions(permList.toArray(permListAr)); + } + } catch (UserStoreException e) { + String msg = "Error occurred while retrieving the user role"; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } catch (UserAdminException e) { + String msg = "Error occurred while retrieving the user role"; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer("All user roles were successfully retrieved."); + responsePayload.setResponseContent(roleWrapper); + return responsePayload; + } + + /** + * API is used to persist a new Role + * + * @param roleWrapper + * @return + * @throws MDMAPIException + */ + @POST + @Produces ({MediaType.APPLICATION_JSON}) + public Response addRole(RoleWrapper roleWrapper) throws MDMAPIException { + UserStoreManager userStoreManager = MDMAPIUtils.getUserStoreManager(); + try { + if (log.isDebugEnabled()) { + log.debug("Persisting the role to user store"); + } + Permission[] permissions = null; + if (roleWrapper.getPermissions() != null && roleWrapper.getPermissions().length > 0) { + permissions = new Permission[roleWrapper.getPermissions().length]; + + for (int i = 0; i < permissions.length; i++) { + String permission = roleWrapper.getPermissions()[i]; + permissions[i] = new Permission(permission, CarbonConstants.UI_PERMISSION_ACTION); + } + } + userStoreManager.addRole(roleWrapper.getRoleName(), roleWrapper.getUsers(), permissions); + } catch (UserStoreException e) { + String msg = e.getMessage(); + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + return Response.status(HttpStatus.SC_CREATED).build(); + } + + /** + * API is used to update a role Role + * + * @param roleWrapper + * @return + * @throws MDMAPIException + */ + @PUT + @Produces ({MediaType.APPLICATION_JSON}) + public Response updateRole(@QueryParam ("rolename") String roleName, RoleWrapper roleWrapper) throws + MDMAPIException { + final UserStoreManager userStoreManager = MDMAPIUtils.getUserStoreManager(); + final AuthorizationManager authorizationManager = MDMAPIUtils.getAuthorizationManager(); + String newRoleName = roleWrapper.getRoleName(); + try { + if (log.isDebugEnabled()) { + log.debug("Updating the role to user store"); + } + if (newRoleName != null && !roleName.equals(newRoleName)) { + userStoreManager.updateRoleName(roleName, newRoleName); + } + if (roleWrapper.getUsers() != null) { + SetReferenceTransformer transformer = new SetReferenceTransformer(); + transformer.transform(Arrays.asList(userStoreManager.getUserListOfRole(newRoleName)), + Arrays.asList(roleWrapper.getUsers())); + final String[] usersToAdd = (String[]) + transformer.getObjectsToAdd().toArray(new String[transformer.getObjectsToAdd().size()]); + final String[] usersToDelete = (String[]) + transformer.getObjectsToRemove().toArray(new String[transformer.getObjectsToRemove().size()]); + userStoreManager.updateUserListOfRole(newRoleName, usersToDelete, usersToAdd); + } + if (roleWrapper.getPermissions() != null) { + // Delete all authorizations for the current role before authorizing the permission tree + authorizationManager.clearRoleAuthorization(roleName); + if (roleWrapper.getPermissions().length > 0) { + for (int i = 0; i < roleWrapper.getPermissions().length; i++) { + String permission = roleWrapper.getPermissions()[i]; + authorizationManager.authorizeRole(roleName, permission, CarbonConstants.UI_PERMISSION_ACTION); + } + } + } + } catch (UserStoreException e) { + String msg = e.getMessage(); + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + return Response.status(HttpStatus.SC_OK).build(); + } + + /** + * API is used to delete a role and authorizations + * + * @param roleName + * @return + * @throws MDMAPIException + */ + @DELETE + @Produces ({MediaType.APPLICATION_JSON}) + public Response deleteRole(@QueryParam ("rolename") String roleName) throws MDMAPIException { + final UserStoreManager userStoreManager = MDMAPIUtils.getUserStoreManager(); + final AuthorizationManager authorizationManager = MDMAPIUtils.getAuthorizationManager(); + try { + if (log.isDebugEnabled()) { + log.debug("Deleting the role in user store"); + } + userStoreManager.deleteRole(roleName); + // Delete all authorizations for the current role before deleting + authorizationManager.clearRoleAuthorization(roleName); + } catch (UserStoreException e) { + String msg = "Error occurred while deleting the role: " + roleName; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + return Response.status(HttpStatus.SC_OK).build(); + } + + /** + * API is used to update users of a role + * + * @param roleName + * @param userList + * @return + * @throws MDMAPIException + */ + @PUT + @Path ("users") + @Produces ({MediaType.APPLICATION_JSON}) + public Response updateUsers(@QueryParam ("rolename") String roleName, List userList) + throws MDMAPIException { + final UserStoreManager userStoreManager = MDMAPIUtils.getUserStoreManager(); + try { + if (log.isDebugEnabled()) { + log.debug("Updating the users of a role"); + } + SetReferenceTransformer transformer = new SetReferenceTransformer(); + transformer.transform(Arrays.asList(userStoreManager.getUserListOfRole(roleName)), + userList); + final String[] usersToAdd = (String[]) + transformer.getObjectsToAdd().toArray(new String[transformer.getObjectsToAdd().size()]); + final String[] usersToDelete = (String[]) + transformer.getObjectsToRemove().toArray(new String[transformer.getObjectsToRemove().size()]); + + userStoreManager.updateUserListOfRole(roleName, usersToDelete, usersToAdd); + } catch (UserStoreException e) { + String msg = "Error occurred while saving the users of the role: " + roleName; + log.error(msg, e); + throw new MDMAPIException(e.getMessage(), e); + } + return Response.status(HttpStatus.SC_OK).build(); + } + + public ArrayList iteratePermissions(UIPermissionNode uiPermissionNode, ArrayList list) { + for (UIPermissionNode permissionNode : uiPermissionNode.getNodeList()) { + list.add(permissionNode.getResourcePath()); + if (permissionNode.getNodeList() != null && permissionNode.getNodeList().length > 0) { + iteratePermissions(permissionNode, list); + } + } + return list; + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/User.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/User.java new file mode 100644 index 000000000..16c9ee7ca --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/User.java @@ -0,0 +1,786 @@ +/* + * 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.mdm.api; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.context.CarbonContext; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.EmailMessageProperties; +import org.wso2.carbon.device.mgt.common.PaginationRequest; +import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import org.wso2.carbon.mdm.api.util.MDMAPIUtils; +import org.wso2.carbon.mdm.api.util.ResponsePayload; +import org.wso2.carbon.mdm.beans.UserCredentialWrapper; +import org.wso2.carbon.mdm.beans.UserWrapper; +import org.wso2.carbon.mdm.util.Constants; +import org.wso2.carbon.mdm.util.SetReferenceTransformer; +import org.wso2.carbon.user.api.UserStoreException; +import org.wso2.carbon.user.api.UserStoreManager; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.TreeSet; + +/** + * This class represents the JAX-RS services of User related functionality. + */ +public class User { + + private static Log log = LogFactory.getLog(User.class); + private String ROLE_EVERYONE = "Internal/everyone"; + + /** + * Method to add user to emm-user-store. + * + * @param userWrapper Wrapper object representing input json payload + * @return {Response} Status of the request wrapped inside Response object + * @throws MDMAPIException + */ + @POST + @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + public Response addUser(UserWrapper userWrapper) throws MDMAPIException { + UserStoreManager userStoreManager = MDMAPIUtils.getUserStoreManager(); + ResponsePayload responsePayload = new ResponsePayload(); + try { + if (userStoreManager.isExistingUser(userWrapper.getUsername())) { + // if user already exists + if (log.isDebugEnabled()) { + log.debug("User by username: " + userWrapper.getUsername() + + " already exists. Therefore, request made to add user was refused."); + } + // returning response with bad request state + responsePayload.setStatusCode(HttpStatus.SC_CONFLICT); + responsePayload. + setMessageFromServer("User by username: " + userWrapper.getUsername() + + " already exists. Therefore, request made to add user was refused."); + return Response.status(HttpStatus.SC_CONFLICT).entity(responsePayload).build(); + } else { + String initialUserPassword = generateInitialUserPassword(); + Map defaultUserClaims = + buildDefaultUserClaims(userWrapper.getFirstname(), userWrapper.getLastname(), + userWrapper.getEmailAddress()); + // calling addUser method of carbon user api + userStoreManager.addUser(userWrapper.getUsername(), initialUserPassword, + userWrapper.getRoles(), defaultUserClaims, null); + // invite newly added user to enroll device + inviteNewlyAddedUserToEnrollDevice(userWrapper.getUsername(), initialUserPassword); + // Outputting debug message upon successful addition of user + if (log.isDebugEnabled()) { + log.debug("User by username: " + userWrapper.getUsername() + " was successfully added."); + } + // returning response with success state + responsePayload.setStatusCode(HttpStatus.SC_CREATED); + responsePayload.setMessageFromServer("User by username: " + userWrapper.getUsername() + + " was successfully added."); + return Response.status(HttpStatus.SC_CREATED).entity(responsePayload).build(); + } + } catch (UserStoreException e) { + String errorMsg = "Exception in trying to add user by username: " + userWrapper.getUsername(); + log.error(errorMsg, e); + throw new MDMAPIException(errorMsg, e); + } catch (DeviceManagementException e) { + String errorMsg = "Exception in trying to add user by username: " + userWrapper.getUsername(); + log.error(errorMsg, e); + throw new MDMAPIException(errorMsg, e); + } + } + + /** + * Method to get user information from emm-user-store. + * + * @param username User-name of the user + * @return {Response} Status of the request wrapped inside Response object + * @throws MDMAPIException + */ + @GET + @Path("view") + @Produces({MediaType.APPLICATION_JSON}) + public Response getUser(@QueryParam("username") String username) throws MDMAPIException { + UserStoreManager userStoreManager = MDMAPIUtils.getUserStoreManager(); + ResponsePayload responsePayload = new ResponsePayload(); + try { + if (userStoreManager.isExistingUser(username)) { + UserWrapper user = new UserWrapper(); + user.setUsername(username); + user.setEmailAddress(getClaimValue(username, Constants.USER_CLAIM_EMAIL_ADDRESS)); + user.setFirstname(getClaimValue(username, Constants.USER_CLAIM_FIRST_NAME)); + user.setLastname(getClaimValue(username, Constants.USER_CLAIM_LAST_NAME)); + // Outputting debug message upon successful retrieval of user + if (log.isDebugEnabled()) { + log.debug("User by username: " + username + " was found."); + } + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer("User information was retrieved successfully."); + responsePayload.setResponseContent(user); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } else { + // Outputting debug message upon trying to remove non-existing user + if (log.isDebugEnabled()) { + log.debug("User by username: " + username + " does not exist."); + } + // returning response with bad request state + responsePayload.setStatusCode(HttpStatus.SC_BAD_REQUEST); + responsePayload.setMessageFromServer( + "User by username: " + username + " does not exist."); + return Response.status(HttpStatus.SC_NOT_FOUND).entity(responsePayload).build(); + } + } catch (UserStoreException e) { + String errorMsg = "Exception in trying to retrieve user by username: " + username; + log.error(errorMsg, e); + throw new MDMAPIException(errorMsg, e); + } + } + + /** + * Update user in user store + * + * @param userWrapper Wrapper object representing input json payload + * @return {Response} Status of the request wrapped inside Response object + * @throws MDMAPIException + */ + @PUT + @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + public Response updateUser(UserWrapper userWrapper, @QueryParam("username") String username) + throws MDMAPIException { + UserStoreManager userStoreManager = MDMAPIUtils.getUserStoreManager(); + ResponsePayload responsePayload = new ResponsePayload(); + try { + if (userStoreManager.isExistingUser(userWrapper.getUsername())) { + Map defaultUserClaims = + buildDefaultUserClaims(userWrapper.getFirstname(), userWrapper.getLastname(), + userWrapper.getEmailAddress()); + if (StringUtils.isNotEmpty(userWrapper.getPassword())) { + // Decoding Base64 encoded password + byte[] decodedBytes = Base64.decodeBase64(userWrapper.getPassword()); + userStoreManager.updateCredentialByAdmin(userWrapper.getUsername(), + new String(decodedBytes, "UTF-8")); + log.debug("User credential of username: " + userWrapper.getUsername() + " has been changed"); + } + List listofFilteredRoles = getFilteredRoles(userStoreManager, userWrapper.getUsername()); + final String[] existingRoles = listofFilteredRoles.toArray(new String[listofFilteredRoles.size()]); + + /* + Use the Set theory to find the roles to delete and roles to add + The difference of roles in existingRolesSet and newRolesSet needed to be deleted + new roles to add = newRolesSet - The intersection of roles in existingRolesSet and newRolesSet + */ + final TreeSet existingRolesSet = new TreeSet(); + Collections.addAll(existingRolesSet, existingRoles); + final TreeSet newRolesSet = new TreeSet(); + Collections.addAll(newRolesSet, userWrapper.getRoles()); + existingRolesSet.removeAll(newRolesSet); + // Now we have the roles to delete + String[] rolesToDelete = existingRolesSet.toArray(new String[existingRolesSet.size()]); + List roles = new ArrayList(Arrays.asList(rolesToDelete)); + roles.remove(ROLE_EVERYONE); + rolesToDelete = roles.toArray(new String[0]); + // Clearing and re-initializing the set + existingRolesSet.clear(); + Collections.addAll(existingRolesSet, existingRoles); + newRolesSet.removeAll(existingRolesSet); + // Now we have the roles to add + String[] rolesToAdd = newRolesSet.toArray(new String[newRolesSet.size()]); + userStoreManager.updateRoleListOfUser(userWrapper.getUsername(), rolesToDelete, rolesToAdd); + userStoreManager.setUserClaimValues(userWrapper.getUsername(), defaultUserClaims, null); + // Outputting debug message upon successful addition of user + if (log.isDebugEnabled()) { + log.debug("User by username: " + userWrapper.getUsername() + " was successfully updated."); + } + // returning response with success state + responsePayload.setStatusCode(HttpStatus.SC_CREATED); + responsePayload.setMessageFromServer("User by username: " + userWrapper.getUsername() + + " was successfully updated."); + return Response.status(HttpStatus.SC_CREATED).entity(responsePayload).build(); + } else { + if (log.isDebugEnabled()) { + log.debug("User by username: " + userWrapper.getUsername() + + " doesn't exists. Therefore, request made to update user was refused."); + } + // returning response with bad request state + responsePayload.setStatusCode(HttpStatus.SC_CONFLICT); + responsePayload. + setMessageFromServer("User by username: " + userWrapper.getUsername() + + " doesn't exists. Therefore, request made to update user was refused."); + return Response.status(HttpStatus.SC_CONFLICT).entity(responsePayload).build(); + } + } catch (UserStoreException e) { + String errorMsg = "Exception in trying to update user by username: " + userWrapper.getUsername(); + log.error(errorMsg, e); + throw new MDMAPIException(errorMsg, e); + } catch (UnsupportedEncodingException e) { + String errorMsg = "Exception in trying to update user by username: " + userWrapper.getUsername(); + log.error(errorMsg, e); + throw new MDMAPIException(errorMsg, e); + } + } + + /** + * Private method to be used by addUser() to + * generate an initial user password for a user. + * This will be the password used by a user for his initial login to the system. + * + * @return {string} Initial User Password + */ + private String generateInitialUserPassword() { + int passwordLength = 6; + //defining the pool of characters to be used for initial password generation + String lowerCaseCharset = "abcdefghijklmnopqrstuvwxyz"; + String upperCaseCharset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + String numericCharset = "0123456789"; + Random randomGenerator = new Random(); + String totalCharset = lowerCaseCharset + upperCaseCharset + numericCharset; + int totalCharsetLength = totalCharset.length(); + StringBuffer initialUserPassword = new StringBuffer(); + for (int i = 0; i < passwordLength; i++) { + initialUserPassword + .append(totalCharset.charAt(randomGenerator.nextInt(totalCharsetLength))); + } + if (log.isDebugEnabled()) { + log.debug("Initial user password is created for new user: " + initialUserPassword); + } + return initialUserPassword.toString(); + } + + /** + * Method to build default user claims. + * + * @param firstname First name of the user + * @param lastname Last name of the user + * @param emailAddress Email address of the user + * @return {Object} Default user claims to be provided + */ + private Map buildDefaultUserClaims(String firstname, String lastname, String emailAddress) { + Map defaultUserClaims = new HashMap(); + defaultUserClaims.put(Constants.USER_CLAIM_FIRST_NAME, firstname); + defaultUserClaims.put(Constants.USER_CLAIM_LAST_NAME, lastname); + defaultUserClaims.put(Constants.USER_CLAIM_EMAIL_ADDRESS, emailAddress); + if (log.isDebugEnabled()) { + log.debug("Default claim map is created for new user: " + defaultUserClaims.toString()); + } + return defaultUserClaims; + } + + /** + * Method to remove user from emm-user-store. + * + * @param username Username of the user + * @return {Response} Status of the request wrapped inside Response object + * @throws MDMAPIException + */ + @DELETE + @Produces({MediaType.APPLICATION_JSON}) + public Response removeUser(@QueryParam("username") String username) throws MDMAPIException { + UserStoreManager userStoreManager = MDMAPIUtils.getUserStoreManager(); + ResponsePayload responsePayload = new ResponsePayload(); + try { + if (userStoreManager.isExistingUser(username)) { + // if user already exists, trying to remove user + userStoreManager.deleteUser(username); + // Outputting debug message upon successful removal of user + if (log.isDebugEnabled()) { + log.debug("User by username: " + username + " was successfully removed."); + } + // returning response with success state + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer( + "User by username: " + username + " was successfully removed."); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } else { + // Outputting debug message upon trying to remove non-existing user + if (log.isDebugEnabled()) { + log.debug("User by username: " + username + " does not exist for removal."); + } + // returning response with bad request state + responsePayload.setStatusCode(HttpStatus.SC_BAD_REQUEST); + responsePayload.setMessageFromServer( + "User by username: " + username + " does not exist for removal."); + return Response.status(HttpStatus.SC_BAD_REQUEST).entity(responsePayload).build(); + } + } catch (UserStoreException e) { + String errorMsg = "Exception in trying to remove user by username: " + username; + log.error(errorMsg, e); + throw new MDMAPIException(errorMsg, e); + } + } + + /** + * get all the roles except for the internal/xxx and application/xxx + * + * @param userStoreManager + * @param username + * @return the list of filtered roles + * @throws UserStoreException + */ + private List getFilteredRoles(UserStoreManager userStoreManager, String username) + throws UserStoreException { + String[] roleListOfUser = userStoreManager.getRoleListOfUser(username); + List filteredRoles = new ArrayList(); + for (String role : roleListOfUser) { + if (!(role.startsWith("Internal/") || role.startsWith("Application/"))) { + filteredRoles.add(role); + } + } + return filteredRoles; + } + + /** + * Get user's roles by username + * + * @param username Username of the user + * @return {Response} Status of the request wrapped inside Response object + * @throws MDMAPIException + */ + @GET + @Path("roles") + @Produces({MediaType.APPLICATION_JSON}) + public Response getRoles(@QueryParam("username") String username) throws MDMAPIException { + UserStoreManager userStoreManager = MDMAPIUtils.getUserStoreManager(); + ResponsePayload responsePayload = new ResponsePayload(); + try { + if (userStoreManager.isExistingUser(username)) { + responsePayload.setResponseContent(Arrays.asList(getFilteredRoles(userStoreManager, username))); + // Outputting debug message upon successful removal of user + if (log.isDebugEnabled()) { + log.debug("User by username: " + username + " was successfully removed."); + } + // returning response with success state + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer( + "User roles obtained for user " + username); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } else { + // Outputting debug message upon trying to remove non-existing user + if (log.isDebugEnabled()) { + log.debug("User by username: " + username + " does not exist for role retrieval."); + } + // returning response with bad request state + responsePayload.setStatusCode(HttpStatus.SC_BAD_REQUEST); + responsePayload.setMessageFromServer( + "User by username: " + username + " does not exist for role retrieval."); + return Response.status(HttpStatus.SC_BAD_REQUEST).entity(responsePayload).build(); + } + } catch (UserStoreException e) { + String errorMsg = "Exception in trying to retrieve roles for user by username: " + username; + log.error(errorMsg, e); + throw new MDMAPIException(errorMsg, e); + } + } + + /** + * Get the list of all users with all user-related info. + * + * @return A list of users + * @throws MDMAPIException + */ + @GET + @Produces({MediaType.APPLICATION_JSON}) + public Response getAllUsers() throws MDMAPIException { + if (log.isDebugEnabled()) { + log.debug("Getting the list of users with all user-related information"); + } + UserStoreManager userStoreManager = MDMAPIUtils.getUserStoreManager(); + ArrayList userList; + try { + String[] users = userStoreManager.listUsers("*", -1); + userList = new ArrayList(users.length); + UserWrapper user; + for (String username : users) { + user = new UserWrapper(); + user.setUsername(username); + user.setEmailAddress(getClaimValue(username, Constants.USER_CLAIM_EMAIL_ADDRESS)); + user.setFirstname(getClaimValue(username, Constants.USER_CLAIM_FIRST_NAME)); + user.setLastname(getClaimValue(username, Constants.USER_CLAIM_LAST_NAME)); + userList.add(user); + } + } catch (UserStoreException e) { + String msg = "Error occurred while retrieving the list of users"; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_OK); + int count = 0; + if (userList != null) { + count = userList.size(); + } + responsePayload.setMessageFromServer("All users were successfully retrieved. " + + "Obtained user count: " + count); + responsePayload.setResponseContent(userList); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } + + /** + * Get the list of all users with all user-related info. + * + * @return A list of users + * @throws MDMAPIException + */ + @GET + @Path("{filter}") + @Produces({MediaType.APPLICATION_JSON}) + public Response getMatchingUsers(@PathParam("filter") String filter) throws MDMAPIException { + if (log.isDebugEnabled()) { + log.debug("Getting the list of users with all user-related information using the filter : " + filter); + } + UserStoreManager userStoreManager = MDMAPIUtils.getUserStoreManager(); + ArrayList userList; + try { + String[] users = userStoreManager.listUsers(filter + "*", -1); + userList = new ArrayList(users.length); + UserWrapper user; + for (String username : users) { + user = new UserWrapper(); + user.setUsername(username); + user.setEmailAddress(getClaimValue(username, Constants.USER_CLAIM_EMAIL_ADDRESS)); + user.setFirstname(getClaimValue(username, Constants.USER_CLAIM_FIRST_NAME)); + user.setLastname(getClaimValue(username, Constants.USER_CLAIM_LAST_NAME)); + userList.add(user); + } + } catch (UserStoreException e) { + String msg = "Error occurred while retrieving the list of users using the filter : " + filter; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_OK); + int count = 0; + if (userList != null) { + count = userList.size(); + } + responsePayload.setMessageFromServer("All users were successfully retrieved. " + + "Obtained user count: " + count); + responsePayload.setResponseContent(userList); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } + + /** + * Get the list of user names in the system. + * + * @return A list of user names. + * @throws MDMAPIException + */ + @GET + @Path("view-users") + public Response getAllUsersByUsername(@QueryParam("username") String userName) throws MDMAPIException { + if (log.isDebugEnabled()) { + log.debug("Getting the list of users by name"); + } + UserStoreManager userStoreManager = MDMAPIUtils.getUserStoreManager(); + ArrayList userList; + try { + String[] users = userStoreManager.listUsers("*" + userName + "*", -1); + userList = new ArrayList(users.length); + UserWrapper user; + for (String username : users) { + user = new UserWrapper(); + user.setUsername(username); + user.setEmailAddress(getClaimValue(username, Constants.USER_CLAIM_EMAIL_ADDRESS)); + user.setFirstname(getClaimValue(username, Constants.USER_CLAIM_FIRST_NAME)); + user.setLastname(getClaimValue(username, Constants.USER_CLAIM_LAST_NAME)); + userList.add(user); + } + } catch (UserStoreException e) { + String msg = "Error occurred while retrieving the list of users"; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_OK); + int count = 0; + if (userList != null) { + count = userList.size(); + } + responsePayload.setMessageFromServer("All users by username were successfully retrieved. " + + "Obtained user count: " + count); + responsePayload.setResponseContent(userList); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } + + /** + * Get the list of user names in the system. + * + * @return A list of user names. + * @throws MDMAPIException + */ + @GET + @Path("users-by-username") + public Response getAllUserNamesByUsername(@QueryParam("username") String userName) throws MDMAPIException { + if (log.isDebugEnabled()) { + log.debug("Getting the list of users by name"); + } + UserStoreManager userStoreManager = MDMAPIUtils.getUserStoreManager(); + ArrayList userList; + try { + String[] users = userStoreManager.listUsers("*" + userName + "*", -1); + userList = new ArrayList(users.length); + UserWrapper user; + for (String username : users) { + userList.add(username); + } + } catch (UserStoreException e) { + String msg = "Error occurred while retrieving the list of users"; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_OK); + int count = 0; + if (userList != null) { + count = userList.size(); + } + responsePayload.setMessageFromServer("All users by username were successfully retrieved. " + + "Obtained user count: " + count); + responsePayload.setResponseContent(userList); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } + + /** + * Gets a claim-value from user-store. + * + * @param username Username of the user + * @param claimUri required ClaimUri + * @return A list of usernames + * @throws MDMAPIException, UserStoreException + */ + private String getClaimValue(String username, String claimUri) throws MDMAPIException, UserStoreException { + UserStoreManager userStoreManager = MDMAPIUtils.getUserStoreManager(); + return userStoreManager.getUserClaimValue(username, claimUri, null); + } + + /** + * Method used to send an invitation email to a new user to enroll a device. + * + * @param username Username of the user + * @throws MDMAPIException, UserStoreException, DeviceManagementException + */ + private void inviteNewlyAddedUserToEnrollDevice(String username, String password) throws + MDMAPIException, + UserStoreException, + DeviceManagementException { + if (log.isDebugEnabled()) { + log.debug("Sending invitation mail to user by username: " + username); + } + String tennentDomain = CarbonContext.getThreadLocalCarbonContext().getTenantDomain(); + if (tennentDomain.equalsIgnoreCase("carbon.super")) { + tennentDomain = ""; + } + if (!username.contains("/")) { + username = "/" + username; + } + String[] usernameBits = username.split("/"); + DeviceManagementProviderService deviceManagementProviderService = MDMAPIUtils.getDeviceManagementService(); + EmailMessageProperties emailMessageProperties = new EmailMessageProperties(); + emailMessageProperties.setUserName(usernameBits[1]); + emailMessageProperties.setDomainName(tennentDomain); + emailMessageProperties.setFirstName(getClaimValue(username, Constants.USER_CLAIM_FIRST_NAME)); + emailMessageProperties.setPassword(password); + String[] mailAddress = new String[1]; + mailAddress[0] = getClaimValue(username, Constants.USER_CLAIM_EMAIL_ADDRESS); + emailMessageProperties.setMailTo(mailAddress); + deviceManagementProviderService.sendRegistrationEmail(emailMessageProperties); + } + + /** + * Method used to send an invitation email to a existing user to enroll a device. + * + * @param usernames Username list of the users to be invited + * @throws MDMAPIException + */ + @POST + @Path("email-invitation") + @Produces({MediaType.APPLICATION_JSON}) + public Response inviteExistingUsersToEnrollDevice(List usernames) throws MDMAPIException { + if (log.isDebugEnabled()) { + log.debug("Sending enrollment invitation mail to existing user."); + } + DeviceManagementProviderService deviceManagementProviderService = MDMAPIUtils.getDeviceManagementService(); + try { + for (int i = 0; i < usernames.size(); i++) { + EmailMessageProperties emailMessageProperties = new EmailMessageProperties(); + emailMessageProperties + .setFirstName(getClaimValue(usernames.get(i), Constants.USER_CLAIM_FIRST_NAME)); + emailMessageProperties.setUserName(usernames.get(i)); + String[] mailAddress = new String[1]; + mailAddress[0] = getClaimValue(usernames.get(i), Constants.USER_CLAIM_EMAIL_ADDRESS); + if (StringUtils.isNotEmpty(mailAddress[0])) { + emailMessageProperties.setMailTo(mailAddress); + deviceManagementProviderService.sendEnrolmentInvitation(emailMessageProperties); + } + } + } catch (UserStoreException e) { + String errorMsg = "Exception in trying to invite user."; + log.error(errorMsg, e); + throw new MDMAPIException(errorMsg, e); + } catch (DeviceManagementException e) { + String errorMsg = "Exception in trying to invite user."; + log.error(errorMsg, e); + throw new MDMAPIException(errorMsg, e); + } + ResponsePayload responsePayload = new ResponsePayload(); + responsePayload.setStatusCode(HttpStatus.SC_OK); + responsePayload.setMessageFromServer("Email invitation was successfully sent to user."); + return Response.status(HttpStatus.SC_OK).entity(responsePayload).build(); + } + + /** + * Get a list of devices based on the username. + * + * @param username Username of the device owner + * @return A list of devices + * @throws MDMAPIException + */ + @GET + @Produces({MediaType.APPLICATION_JSON}) + @Path("devices") + public Object getAllDeviceOfUser(@QueryParam("username") String username, @QueryParam("start") int startIdx, + @QueryParam("length") int length) + throws MDMAPIException { + DeviceManagementProviderService dmService; + try { + dmService = MDMAPIUtils.getDeviceManagementService(); + if (length > 0) { + PaginationRequest request = new PaginationRequest(startIdx, length); + request.setOwner(username); + return dmService.getDevicesOfUser(request); + } + return dmService.getDevicesOfUser(username); + } catch (DeviceManagementException e) { + String errorMsg = "Device management error"; + log.error(errorMsg, e); + throw new MDMAPIException(errorMsg, e); + } + } + + /** + * This method is used to retrieve the user count of the system. + * + * @return returns the count. + * @throws MDMAPIException + */ + @GET + @Path("count") + public int getUserCount() throws MDMAPIException { + try { + String[] users = MDMAPIUtils.getUserStoreManager().listUsers("*", -1); + if (users == null) { + return 0; + } + return users.length; + } catch (UserStoreException e) { + String msg = + "Error occurred while retrieving the list of users that exist within the current tenant"; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + } + + /** + * API is used to update roles of a user + * + * @param username + * @param userList + * @return + * @throws MDMAPIException + */ + @PUT + @Path("{roleName}/users") + @Produces({MediaType.APPLICATION_JSON}) + public Response updateRoles(@PathParam("username") String username, List userList) + throws MDMAPIException { + final UserStoreManager userStoreManager = MDMAPIUtils.getUserStoreManager(); + try { + if (log.isDebugEnabled()) { + log.debug("Updating the roles of a user"); + } + SetReferenceTransformer transformer = new SetReferenceTransformer(); + transformer.transform(Arrays.asList(userStoreManager.getRoleListOfUser(username)), + userList); + final String[] rolesToAdd = (String[]) + transformer.getObjectsToAdd().toArray(new String[transformer.getObjectsToAdd().size()]); + final String[] rolesToDelete = (String[]) + transformer.getObjectsToRemove().toArray(new String[transformer.getObjectsToRemove().size()]); + + userStoreManager.updateRoleListOfUser(username, rolesToDelete, rolesToAdd); + } catch (UserStoreException e) { + String msg = "Error occurred while saving the roles for user: " + username; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + return Response.status(HttpStatus.SC_OK).build(); + } + + /** + * Method to change the user password. + * + * @param credentials Wrapper object representing user credentials. + * @return {Response} Status of the request wrapped inside Response object. + * @throws MDMAPIException + */ + @POST + @Path("reset-password") + @Consumes({MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_JSON}) + public Response resetPassword(UserCredentialWrapper credentials) throws MDMAPIException { + UserStoreManager userStoreManager = MDMAPIUtils.getUserStoreManager(); + ResponsePayload responsePayload = new ResponsePayload(); + try { + byte[] decodedNewPassword = Base64.decodeBase64(credentials.getNewPassword()); + userStoreManager.updateCredentialByAdmin(credentials.getUsername(), new String( + decodedNewPassword, "UTF-8")); + responsePayload.setStatusCode(HttpStatus.SC_CREATED); + responsePayload.setMessageFromServer("User password by username: " + credentials.getUsername() + + " was successfully changed."); + return Response.status(HttpStatus.SC_CREATED).entity(responsePayload).build(); + } catch (UserStoreException e) { + String errorMsg = "Exception in trying to change the password by username: " + credentials.getUsername(); + log.error(errorMsg, e); + responsePayload.setStatusCode(HttpStatus.SC_BAD_REQUEST); + responsePayload.setMessageFromServer("Old password does not match."); + return Response.status(HttpStatus.SC_BAD_REQUEST).entity(responsePayload).build(); + } catch (UnsupportedEncodingException e) { + String errorMsg = "Exception in trying to change the password by username: " + credentials.getUsername(); + log.error(errorMsg, e); + throw new MDMAPIException(errorMsg, e); + } + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/common/ErrorHandler.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/common/ErrorHandler.java new file mode 100644 index 000000000..da8825b17 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/common/ErrorHandler.java @@ -0,0 +1,32 @@ +/* + * 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.mdm.api.common; + +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +@Produces({ "application/json", "application/xml" }) +public class ErrorHandler implements ExceptionMapper { + + public Response toResponse(MDMAPIException exception) { + ErrorMessage errorMessage = new ErrorMessage(); + errorMessage.setErrorMessage(exception.getErrorMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(errorMessage).build(); + } +} diff --git a/components/key-mgt/org.wso2.carbon.key.mgt.handler.valve/src/main/java/org/wso2/carbon/key/mgt/handler/valve/APIFaultException.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/common/ErrorMessage.java similarity index 63% rename from components/key-mgt/org.wso2.carbon.key.mgt.handler.valve/src/main/java/org/wso2/carbon/key/mgt/handler/valve/APIFaultException.java rename to components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/common/ErrorMessage.java index f04a64f99..7dbe39553 100644 --- a/components/key-mgt/org.wso2.carbon.key.mgt.handler.valve/src/main/java/org/wso2/carbon/key/mgt/handler/valve/APIFaultException.java +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/common/ErrorMessage.java @@ -15,26 +15,27 @@ * specific language governing permissions and limitations * under the License. */ +package org.wso2.carbon.mdm.api.common; -package org.wso2.carbon.key.mgt.handler.valve; -public class APIFaultException extends Exception { +public class ErrorMessage { - private static final long serialVersionUID = 1L; - private int errorCode; + private String errorMessage; + private String errorCode; - public APIFaultException(int errorCode, String message) { - super(message); - this.errorCode = errorCode; + public String getErrorMessage() { + return errorMessage; } - public APIFaultException(int errorCode, String message, Throwable cause) { - super(message, cause); - this.errorCode = errorCode; + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; } - public int getErrorCode() { + public String getErrorCode() { return errorCode; } + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } } diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/common/MDMAPIException.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/common/MDMAPIException.java new file mode 100644 index 000000000..4139e56f4 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/common/MDMAPIException.java @@ -0,0 +1,59 @@ +/* + * 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.mdm.api.common; + +/** + * Custom exception class for handling CDM API related exceptions. + */ +public class MDMAPIException extends Exception { + + private static final long serialVersionUID = 7950151650447893900L; + private String errorMessage; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public MDMAPIException(String msg, Exception e) { + super(msg, e); + setErrorMessage(msg); + } + + public MDMAPIException(String msg, Throwable cause) { + super(msg, cause); + setErrorMessage(msg); + } + + public MDMAPIException(String msg) { + super(msg); + setErrorMessage(msg); + } + + public MDMAPIException() { + super(); + } + + public MDMAPIException(Throwable cause) { + super(cause); + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/context/DeviceOperationContext.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/context/DeviceOperationContext.java new file mode 100644 index 000000000..aef30c70f --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/context/DeviceOperationContext.java @@ -0,0 +1,54 @@ +/* + * 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.mdm.api.context; + +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.List; + +/** + * Model object of DeviceOperation. + */ +@XmlRootElement +public class DeviceOperationContext { + + private List devices; + private Operation operation; + + @XmlElement + public List getDevices() { + return devices; + } + + public void setDevices(List devices) { + this.devices = devices; + } + + @XmlElement + public Operation getOperation() { + return operation; + } + + public void setOperation(Operation operation) { + this.operation = operation; + } +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/MDMAPIUtils.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/MDMAPIUtils.java new file mode 100644 index 000000000..0c474818a --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/MDMAPIUtils.java @@ -0,0 +1,255 @@ +/* + * 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.mdm.api.util; + +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.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.PaginationResult; +import org.wso2.carbon.device.mgt.common.configuration.mgt.ConfigurationEntry; +import org.wso2.carbon.device.mgt.common.configuration.mgt.TenantConfiguration; +import org.wso2.carbon.device.mgt.common.configuration.mgt.TenantConfigurationManagementService; +import org.wso2.carbon.device.mgt.common.notification.mgt.NotificationManagementService; +import org.wso2.carbon.device.mgt.core.app.mgt.ApplicationManagementProviderService; +import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import org.wso2.carbon.ntask.core.TaskManager; +import org.wso2.carbon.policy.mgt.common.PolicyMonitoringTaskException; +import org.wso2.carbon.policy.mgt.core.PolicyManagerService; +import org.wso2.carbon.policy.mgt.core.task.TaskScheduleService; +import org.wso2.carbon.policy.mgt.core.util.PolicyManagementConstants; +import org.wso2.carbon.user.api.AuthorizationManager; +import org.wso2.carbon.user.api.UserRealm; +import org.wso2.carbon.user.api.UserStoreException; +import org.wso2.carbon.user.api.UserStoreManager; +import org.wso2.carbon.user.core.service.RealmService; +import org.wso2.carbon.utils.multitenancy.MultitenantConstants; + +import java.util.List; + +/** + * MDMAPIUtils class provides utility function used by CDM REST-API classes. + */ +public class MDMAPIUtils { + + private static final String NOTIFIER_FREQUENCY = "notifierFrequency"; + + private static Log log = LogFactory.getLog(MDMAPIUtils.class); + + public static int getNotifierFrequency(TenantConfiguration tenantConfiguration) { + List configEntryList = tenantConfiguration.getConfiguration(); + if (configEntryList != null && !configEntryList.isEmpty()) { + for(ConfigurationEntry entry : configEntryList) { + if (NOTIFIER_FREQUENCY.equals(entry.getName())) { + return Integer.parseInt((String) entry.getValue()); + } + } + } + return 0; + } + + public static void scheduleTaskService(int notifierFrequency) { + TaskScheduleService taskScheduleService; + try { + taskScheduleService = getPolicyManagementService().getTaskScheduleService(); + if (taskScheduleService.isTaskScheduled()) { + taskScheduleService.updateTask(notifierFrequency); + } else { + taskScheduleService.startTask(notifierFrequency); + } + } catch (PolicyMonitoringTaskException e) { + log.error("Exception occurred while starting the Task service.", e); + } + } + + public static DeviceManagementProviderService getDeviceManagementService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + DeviceManagementProviderService deviceManagementProviderService = + (DeviceManagementProviderService) ctx.getOSGiService(DeviceManagementProviderService.class, null); + if (deviceManagementProviderService == null) { + String msg = "Device Management provider service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return deviceManagementProviderService; + } + + public static int getTenantId(String tenantDomain) throws MDMAPIException { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + RealmService realmService = (RealmService) ctx.getOSGiService(RealmService.class, null); + try { + return realmService.getTenantManager().getTenantId(tenantDomain); + } catch (UserStoreException e) { + throw new MDMAPIException( + "Error obtaining tenant id from tenant domain " + tenantDomain); + } + } + + public static UserStoreManager getUserStoreManager() throws MDMAPIException { + RealmService realmService; + UserStoreManager userStoreManager; + try { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + realmService = (RealmService) ctx.getOSGiService(RealmService.class, null); + if (realmService == null) { + String msg = "Realm service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + int tenantId = ctx.getTenantId(); + userStoreManager = realmService.getTenantUserRealm(tenantId).getUserStoreManager(); + } catch (UserStoreException e) { + String msg = "Error occurred while retrieving current user store manager"; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + return userStoreManager; + } + + /** + * Getting the current tenant's user realm + * + * @return + * @throws MDMAPIException + */ + public static UserRealm getUserRealm() throws MDMAPIException { + RealmService realmService; + UserRealm realm; + try { + //PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + //ctx.setTenantDomain(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); + //ctx.setTenantId(MultitenantConstants.SUPER_TENANT_ID); + realmService = (RealmService) ctx.getOSGiService(RealmService.class, null); + + if (realmService == null) { + String msg = "Realm service not initialized"; + log.error(msg); + throw new MDMAPIException(msg); + } + int tenantId = CarbonContext.getThreadLocalCarbonContext().getTenantId(); + realm = realmService.getTenantUserRealm(tenantId); + } catch (UserStoreException e) { + String msg = "Error occurred while retrieving current user realm"; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } finally { + //PrivilegedCarbonContext.endTenantFlow(); + } + return realm; + } + + public static AuthorizationManager getAuthorizationManager() throws MDMAPIException { + RealmService realmService; + AuthorizationManager authorizationManager; + try { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + realmService = (RealmService) ctx.getOSGiService(RealmService.class, null); + if (realmService == null) { + String msg = "Realm service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + int tenantId = ctx.getTenantId(); + authorizationManager = realmService.getTenantUserRealm(tenantId).getAuthorizationManager(); + } catch (UserStoreException e) { + String msg = "Error occurred while retrieving current Authorization manager."; + log.error(msg, e); + throw new MDMAPIException(msg, e); + } + return authorizationManager; + } + + /** + * This method is used to get the current tenant id. + * + * @return returns the tenant id. + */ + public static int getTenantId() { + return CarbonContext.getThreadLocalCarbonContext().getTenantId(); + } + + public static DeviceIdentifier instantiateDeviceIdentifier(String deviceType, String deviceId) { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setType(deviceType); + deviceIdentifier.setId(deviceId); + return deviceIdentifier; + } + + public static ApplicationManagementProviderService getAppManagementService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + ApplicationManagementProviderService applicationManagementProviderService = + (ApplicationManagementProviderService) ctx.getOSGiService(ApplicationManagementProviderService.class, null); + if (applicationManagementProviderService == null) { + String msg = "Application management service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return applicationManagementProviderService; + } + + public static PolicyManagerService getPolicyManagementService() { + PolicyManagerService policyManagementService; + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + policyManagementService = + (PolicyManagerService) ctx.getOSGiService(PolicyManagerService.class, null); + if (policyManagementService == null) { + String msg = "Policy Management service not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return policyManagementService; + } + + public static TenantConfigurationManagementService getTenantConfigurationManagementService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + TenantConfigurationManagementService tenantConfigurationManagementService = + (TenantConfigurationManagementService) ctx.getOSGiService(TenantConfigurationManagementService.class, null); + if (tenantConfigurationManagementService == null) { + String msg = "Tenant configuration Management service not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return tenantConfigurationManagementService; + } + + public static NotificationManagementService getNotificationManagementService() { + NotificationManagementService notificationManagementService; + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + notificationManagementService = (NotificationManagementService) ctx.getOSGiService( + NotificationManagementService.class, null); + if (notificationManagementService == null) { + String msg = "Notification Management service not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return notificationManagementService; + } + + public static PaginationResult getPagingResponse(int recordsTotal, int recordsFiltered, int draw, List data) { + PaginationResult pagingResponse = new PaginationResult(); + pagingResponse.setRecordsTotal(recordsTotal); + pagingResponse.setRecordsFiltered(recordsFiltered); + pagingResponse.setDraw(draw); + pagingResponse.setData(data); + return pagingResponse; + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/MDMAndroidOperationUtil.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/MDMAndroidOperationUtil.java new file mode 100644 index 000000000..e50fc0bde --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/MDMAndroidOperationUtil.java @@ -0,0 +1,116 @@ +/* + * 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.mdm.api.util; + +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.core.operation.mgt.ProfileOperation; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import org.wso2.carbon.mdm.beans.MobileApp; +import org.wso2.carbon.mdm.beans.android.WebApplication; + +/** + * + * This class contains the all the operations related to Android. + */ +public class MDMAndroidOperationUtil { + + /** + * This method is used to create Install Application operation. + * + * @param application MobileApp application + * @return operation + * @throws MDMAPIException + * + */ + public static Operation createInstallAppOperation(MobileApp application) throws MDMAPIException { + + ProfileOperation operation = new ProfileOperation(); + operation.setCode(MDMAppConstants.AndroidConstants.OPCODE_INSTALL_APPLICATION); + operation.setType(Operation.Type.PROFILE); + + switch (application.getType()) { + case ENTERPRISE: + org.wso2.carbon.mdm.beans.android.EnterpriseApplication enterpriseApplication = + new org.wso2.carbon.mdm.beans.android.EnterpriseApplication(); + enterpriseApplication.setType(application.getType().toString()); + enterpriseApplication.setUrl(application.getLocation()); + operation.setPayLoad(enterpriseApplication.toJSON()); + break; + case PUBLIC: + org.wso2.carbon.mdm.beans.android.AppStoreApplication appStoreApplication = + new org.wso2.carbon.mdm.beans.android.AppStoreApplication(); + appStoreApplication.setType(application.getType().toString()); + appStoreApplication.setAppIdentifier(application.getIdentifier()); + operation.setPayLoad(appStoreApplication.toJSON()); + break; + case WEBAPP: + WebApplication webApplication = new WebApplication(); + webApplication.setUrl(application.getLocation()); + webApplication.setName(application.getName()); + webApplication.setType(application.getType().toString()); + operation.setPayLoad(webApplication.toJSON()); + break; + default: + String errorMessage = "Invalid application type."; + throw new MDMAPIException(errorMessage); + } + return operation; + } + + /** + * This method is used to create Uninstall Application operation. + * @param application MobileApp application + * @return operation + * @throws MDMAPIException + */ + public static Operation createAppUninstallOperation(MobileApp application) throws MDMAPIException { + + ProfileOperation operation = new ProfileOperation(); + operation.setCode(MDMAppConstants.AndroidConstants.OPCODE_UNINSTALL_APPLICATION); + operation.setType(Operation.Type.PROFILE); + + switch (application.getType()) { + case ENTERPRISE: + org.wso2.carbon.mdm.beans.android.EnterpriseApplication enterpriseApplication = + new org.wso2.carbon.mdm.beans.android.EnterpriseApplication(); + enterpriseApplication.setType(application.getType().toString()); + enterpriseApplication.setAppIdentifier(application.getAppIdentifier()); + operation.setPayLoad(enterpriseApplication.toJSON()); + break; + case PUBLIC: + org.wso2.carbon.mdm.beans.android.AppStoreApplication appStoreApplication = + new org.wso2.carbon.mdm.beans.android.AppStoreApplication(); + appStoreApplication.setType(application.getType().toString()); + appStoreApplication.setAppIdentifier(application.getAppIdentifier()); + operation.setPayLoad(appStoreApplication.toJSON()); + break; + case WEBAPP: + WebApplication webApplication = new WebApplication(); + webApplication.setUrl(application.getLocation()); + webApplication.setName(application.getName()); + webApplication.setType(application.getType().toString()); + operation.setPayLoad(webApplication.toJSON()); + break; + default: + String errorMessage = "Invalid application type."; + throw new MDMAPIException(errorMessage); + } + return operation; + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/MDMAppConstants.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/MDMAppConstants.java new file mode 100644 index 000000000..a4a7f6071 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/MDMAppConstants.java @@ -0,0 +1,55 @@ +/* + * + * 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.mdm.api.util; + +/** + * This class holds all the constants used for IOS and Android. + */ +public class MDMAppConstants { + + public class IOSConstants { + + private IOSConstants() { + throw new AssertionError(); + } + public static final String IS_REMOVE_APP = "isRemoveApp"; + public static final String IS_PREVENT_BACKUP = "isPreventBackup"; + public static final String I_TUNES_ID = "iTunesId"; + public static final String LABEL = "label"; + public static final String OPCODE_INSTALL_ENTERPRISE_APPLICATION = "INSTALL_ENTERPRISE_APPLICATION"; + public static final String OPCODE_INSTALL_STORE_APPLICATION = "INSTALL_STORE_APPLICATION"; + public static final String OPCODE_INSTALL_WEB_APPLICATION = "WEB_CLIP"; + public static final String OPCODE_REMOVE_APPLICATION = "REMOVE_APPLICATION"; + } + + public class AndroidConstants { + + private AndroidConstants() { + throw new AssertionError(); + } + public static final String OPCODE_INSTALL_APPLICATION = "INSTALL_APPLICATION"; + public static final String OPCODE_UNINSTALL_APPLICATION = "UNINSTALL_APPLICATION"; + } + + public class RegistryConstants { + + private RegistryConstants() { + throw new AssertionError(); + } + public static final String GENERAL_CONFIG_RESOURCE_PATH = "general"; + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/MDMIOSOperationUtil.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/MDMIOSOperationUtil.java new file mode 100644 index 000000000..261555d33 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/MDMIOSOperationUtil.java @@ -0,0 +1,106 @@ +/* + * + * 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.mdm.api.util; + +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.core.operation.mgt.ProfileOperation; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import org.wso2.carbon.mdm.beans.MobileApp; +import org.wso2.carbon.mdm.beans.ios.WebClip; + +import java.util.Properties; + +/** + * This class contains the all the operations related to IOS. + */ +public class MDMIOSOperationUtil { + + /** + * This method is used to create Install Application operation. + * + * @param application MobileApp application + * @return operation + * @throws MDMAPIException + * + */ + public static Operation createInstallAppOperation(MobileApp application) throws MDMAPIException { + + ProfileOperation operation = new ProfileOperation(); + + switch (application.getType()) { + case ENTERPRISE: + org.wso2.carbon.mdm.beans.ios.EnterpriseApplication enterpriseApplication = + new org.wso2.carbon.mdm.beans.ios.EnterpriseApplication(); + enterpriseApplication.setBundleId(application.getId()); + enterpriseApplication.setIdentifier(application.getIdentifier()); + enterpriseApplication.setManifestURL(application.getLocation()); + + Properties properties = application.getProperties(); + enterpriseApplication.setPreventBackupOfAppData((Boolean) properties. + get(MDMAppConstants.IOSConstants.IS_PREVENT_BACKUP)); + enterpriseApplication.setRemoveAppUponMDMProfileRemoval((Boolean) properties. + get(MDMAppConstants.IOSConstants.IS_REMOVE_APP)); + operation.setCode(MDMAppConstants.IOSConstants.OPCODE_INSTALL_ENTERPRISE_APPLICATION); + operation.setPayLoad(enterpriseApplication.toJSON()); + operation.setType(Operation.Type.COMMAND); + break; + case PUBLIC: + org.wso2.carbon.mdm.beans.ios.AppStoreApplication appStoreApplication = + new org.wso2.carbon.mdm.beans.ios.AppStoreApplication(); + appStoreApplication.setRemoveAppUponMDMProfileRemoval((Boolean) application.getProperties(). + get(MDMAppConstants.IOSConstants.IS_REMOVE_APP)); + appStoreApplication.setIdentifier(application.getIdentifier()); + appStoreApplication.setPreventBackupOfAppData((Boolean) application.getProperties(). + get(MDMAppConstants.IOSConstants.IS_PREVENT_BACKUP)); + appStoreApplication.setBundleId(application.getId()); + appStoreApplication.setiTunesStoreID((Integer) application.getProperties(). + get(MDMAppConstants.IOSConstants.I_TUNES_ID)); + operation.setCode(MDMAppConstants.IOSConstants.OPCODE_INSTALL_STORE_APPLICATION); + operation.setType(Operation.Type.COMMAND); + operation.setPayLoad(appStoreApplication.toJSON()); + break; + case WEBAPP: + WebClip webClip = new WebClip(); + webClip.setIcon(application.getIconImage()); + webClip.setIsRemovable(application.getProperties(). + getProperty(MDMAppConstants.IOSConstants.IS_REMOVE_APP)); + webClip.setLabel(application.getProperties(). + getProperty(MDMAppConstants.IOSConstants.LABEL)); + webClip.setURL(application.getLocation()); + + operation.setCode(MDMAppConstants.IOSConstants.OPCODE_INSTALL_WEB_APPLICATION); + operation.setType(Operation.Type.PROFILE); + operation.setPayLoad(webClip.toJSON()); + break; + } + return operation; + } + + public static Operation createAppUninstallOperation(MobileApp application) throws MDMAPIException{ + + ProfileOperation operation = new ProfileOperation(); + operation.setCode(MDMAppConstants.IOSConstants.OPCODE_REMOVE_APPLICATION); + operation.setType(Operation.Type.PROFILE); + + org.wso2.carbon.mdm.beans.ios.RemoveApplication removeApplication = + new org.wso2.carbon.mdm.beans.ios.RemoveApplication(); + removeApplication.setBundleId(application.getIdentifier()); + operation.setPayLoad(removeApplication.toJSON()); + + return operation; + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/ResponsePayload.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/ResponsePayload.java new file mode 100644 index 000000000..8cc7d3591 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/api/util/ResponsePayload.java @@ -0,0 +1,106 @@ +/* + * 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.mdm.api.util; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement +public class ResponsePayload { + + private int statusCode; + private String messageFromServer; + private Object responseContent; + + @XmlElement + public int getStatusCode() { + return statusCode; + } + + public void setStatusCode(int statusCode) { + this.statusCode = statusCode; + } + + @XmlElement + public String getMessageFromServer() { + return messageFromServer; + } + + public void setMessageFromServer(String messageFromServer) { + this.messageFromServer = messageFromServer; + } + + @XmlElement + public Object getResponseContent() { + return responseContent; + } + + public void setResponseContent(Object responseContent) { + this.responseContent = responseContent; + } + + private ResponsePayload.ResponsePayloadBuilder getBuilder() { + return new ResponsePayload.ResponsePayloadBuilder(); + } + + public static ResponsePayload.ResponsePayloadBuilder statusCode(int statusCode) { + ResponsePayload message = new ResponsePayload(); + return message.getBuilder().statusCode(statusCode); + } + + public static ResponsePayload.ResponsePayloadBuilder messageFromServer(String messageFromServer) { + ResponsePayload message = new ResponsePayload(); + return message.getBuilder().messageFromServer(messageFromServer); + } + + public static ResponsePayload.ResponsePayloadBuilder responseContent(String responseContent) { + ResponsePayload message = new ResponsePayload(); + return message.getBuilder().responseContent(responseContent); + } + + public class ResponsePayloadBuilder { + + private int statusCode; + private String messageFromServer; + private Object responseContent; + + public ResponsePayloadBuilder statusCode(int statusCode) { + this.statusCode = statusCode; + return this; + } + + public ResponsePayloadBuilder messageFromServer(String messageFromServer) { + this.messageFromServer = messageFromServer; + return this; + } + + public ResponsePayloadBuilder responseContent(String responseContent) { + this.responseContent = responseContent; + return this; + } + + public ResponsePayload build() { + ResponsePayload payload = new ResponsePayload(); + payload.setStatusCode(statusCode); + payload.setMessageFromServer(messageFromServer); + payload.setResponseContent(responseContent); + return payload; + } + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ApplicationWrapper.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ApplicationWrapper.java new file mode 100644 index 000000000..ad2db850d --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ApplicationWrapper.java @@ -0,0 +1,63 @@ +/* + * + * 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.mdm.beans; + +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import java.util.List; + +public class ApplicationWrapper { + + private List userNameList; + private List roleNameList; + private List deviceIdentifiers; + private MobileApp application; + + public MobileApp getApplication() { + return application; + } + + public void setApplication(MobileApp application) { + this.application = application; + } + public List getUserNameList() { + return userNameList; + } + + public void setUserNameList(List userNameList) { + this.userNameList = userNameList; + } + + public List getRoleNameList() { + return roleNameList; + } + + public void setRoleNameList(List roleNameList) { + this.roleNameList = roleNameList; + } + + public List getDeviceIdentifiers() { + return deviceIdentifiers; + } + + public void setDeviceIdentifiers(List deviceIdentifiers) { + this.deviceIdentifiers = deviceIdentifiers; + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/MobileApp.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/MobileApp.java new file mode 100644 index 000000000..2f1ef19b3 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/MobileApp.java @@ -0,0 +1,130 @@ +/* + * 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.mdm.beans; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * This class represents the generic mobile Application information + * which is used by AppM. + */ +public class MobileApp { + + private String id; + private String name; + private MobileAppTypes type; + private String platform; + private String version; + private String identifier; + private String iconImage; + private String packageName; + private String appIdentifier; + private String location; + private Properties properties; + + public MobileAppTypes getType() { + return type; + } + + public void setType(MobileAppTypes type) { + this.type = type; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPlatform() { + return platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public String getIconImage() { + return iconImage; + } + + public void setIconImage(String iconImage) { + this.iconImage = iconImage; + } + + public String getPackageName() { + return packageName; + } + + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + public String getAppIdentifier() { + return appIdentifier; + } + + public void setAppIdentifier(String appIdentifier) { + this.appIdentifier = appIdentifier; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public Properties getProperties() { + return properties; + } + + public void setProperties(Properties properties) { + this.properties = properties; + } + +} diff --git a/components/key-mgt/org.wso2.carbon.key.mgt.handler.valve/src/main/java/org/wso2/carbon/key/mgt/handler/valve/HandlerConstants.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/MobileAppTypes.java similarity index 69% rename from components/key-mgt/org.wso2.carbon.key.mgt.handler.valve/src/main/java/org/wso2/carbon/key/mgt/handler/valve/HandlerConstants.java rename to components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/MobileAppTypes.java index dac164659..151f57c5f 100644 --- a/components/key-mgt/org.wso2.carbon.key.mgt.handler.valve/src/main/java/org/wso2/carbon/key/mgt/handler/valve/HandlerConstants.java +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/MobileAppTypes.java @@ -15,14 +15,8 @@ * specific language governing permissions and limitations * under the License. */ +package org.wso2.carbon.mdm.beans; -package org.wso2.carbon.key.mgt.handler.valve; - -public class HandlerConstants { - - public static final String HEADER_AUTHORIZATION = "Authorization"; - public static final String TOKEN_NAME_BEARER = "Bearer"; - - public static final String NO_MATCHING_AUTH_SCHEME = "noMatchedAuthScheme"; - +public enum MobileAppTypes { + ENTERPRISE,WEBAPP,PUBLIC } diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/PolicyWrapper.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/PolicyWrapper.java new file mode 100644 index 000000000..931f98a86 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/PolicyWrapper.java @@ -0,0 +1,126 @@ +/* + * 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.mdm.beans; + +import org.wso2.carbon.device.mgt.common.Device; +import java.util.List; + +public class PolicyWrapper { + + private int id; + private Profile profile; + private String policyName; + private String description; + private String compliance; + private List roles; + private String ownershipType; + private List devices; + private List users; + private int tenantId; + private int profileId; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public Profile getProfile() { + return profile; + } + + public void setProfile(Profile profile) { + this.profile = profile; + } + + public String getCompliance() { + return compliance; + } + + public void setCompliance(String compliance) { + this.compliance = compliance; + } + + public String getPolicyName() { + return policyName; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setPolicyName(String policyName) { + this.policyName = policyName; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + public String getOwnershipType() { + return ownershipType; + } + + public void setOwnershipType(String ownershipType) { + this.ownershipType = ownershipType; + } + + public List getDevices() { + return devices; + } + + public void setDevices(List devices) { + this.devices = devices; + } + + public List getUsers() { + return users; + } + + public void setUsers(List users) { + this.users = users; + } + + public int getTenantId() { + return tenantId; + } + + public void setTenantId(int tenantId) { + this.tenantId = tenantId; + } + + public int getProfileId() { + return profileId; + } + + public void setProfileId(int profileId) { + this.profileId = profileId; + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/PriorityUpdatedPolicyWrapper.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/PriorityUpdatedPolicyWrapper.java new file mode 100644 index 000000000..ed0b7c56e --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/PriorityUpdatedPolicyWrapper.java @@ -0,0 +1,41 @@ +/* + * 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.mdm.beans; + +public class PriorityUpdatedPolicyWrapper { + + private int id; + private int priority; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/Profile.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/Profile.java new file mode 100644 index 000000000..19e41d885 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/Profile.java @@ -0,0 +1,107 @@ +/* +* 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.mdm.beans; + + + +import org.wso2.carbon.device.mgt.core.dto.DeviceType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.sql.Timestamp; +import java.util.List; + +@XmlRootElement +public class Profile { + + private int profileId; + private String profileName; + private int tenantId; + private DeviceType deviceType; + private Timestamp createdDate; + private Timestamp updatedDate; + private List profileFeaturesList; // Features included in the policies. + + public DeviceType getDeviceType() { + return deviceType; + } + + public void setDeviceType(DeviceType deviceType) { + this.deviceType = deviceType; + } + @XmlElement + public int getTenantId() { + return tenantId; + } + + public void setTenantId(int tenantId) { + this.tenantId = tenantId; + } + +/* public List getFeaturesList() { + return featuresList; + } + + public void setFeaturesList(List featuresList) { + this.featuresList = featuresList; + }*/ + @XmlElement + public int getProfileId() { + return profileId; + } + + public void setProfileId(int profileId) { + this.profileId = profileId; + } + + @XmlElement + public String getProfileName() { + return profileName; + } + + public void setProfileName(String profileName) { + this.profileName = profileName; + } + + @XmlElement + public Timestamp getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(Timestamp createdDate) { + this.createdDate = createdDate; + } + + @XmlElement + public Timestamp getUpdatedDate() { + return updatedDate; + } + + public void setUpdatedDate(Timestamp updatedDate) { + this.updatedDate = updatedDate; + } + + @XmlElement + public List getProfileFeaturesList() { + return profileFeaturesList; + } + + public void setProfileFeaturesList(List profileFeaturesList) { + this.profileFeaturesList = profileFeaturesList; + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ProfileFeature.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ProfileFeature.java new file mode 100644 index 000000000..0d56de090 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ProfileFeature.java @@ -0,0 +1,85 @@ +/* +* 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.mdm.beans; + +import com.google.gson.Gson; +import java.io.Serializable; +import java.util.LinkedHashMap; + +public class ProfileFeature implements Serializable { + + private int id; + private String featureCode; + private int profileId; + private int deviceTypeId; + private Object content; + private String payLoad; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getFeatureCode() { + return featureCode; + } + + public void setFeatureCode(String featureCode) { + this.featureCode = featureCode; + } + + public int getProfileId() { + return profileId; + } + + public void setProfileId(int profileId) { + this.profileId = profileId; + } + + public int getDeviceTypeId() { + return deviceTypeId; + } + + public void setDeviceTypeId(int deviceTypeId) { + this.deviceTypeId = deviceTypeId; + } + + + public String getPayLoad() { + Gson gson = new Gson(); + this.payLoad = gson.toJson(content); + return payLoad; + } + + public void setPayLoad(String payLoad) { + this.payLoad = payLoad; + } + + + public Object getContent() { + return content; + } + + public void setContent(Object content) { + this.content = content; + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/RoleWrapper.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/RoleWrapper.java new file mode 100644 index 000000000..c87640a7c --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/RoleWrapper.java @@ -0,0 +1,59 @@ +package org.wso2.carbon.mdm.beans; + +import org.wso2.carbon.user.mgt.common.UIPermissionNode; + +/* + * 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. + */ +public class RoleWrapper { + private String roleName; + private String[] permissions; + private String[] users; + private UIPermissionNode permissionList; + + public String getRoleName() { + return roleName; + } + + public void setRoleName(String roleName) { + this.roleName = roleName; + } + + public String[] getPermissions() { + return permissions; + } + + public void setPermissions(String[] permissions) { + this.permissions = permissions; + } + + public String[] getUsers() { + return users; + } + + public void setUsers(String[] users) { + this.users = users; + } + + public UIPermissionNode getPermissionList() { + return permissionList; + } + + public void setPermissionList(UIPermissionNode permissionList) { + this.permissionList = permissionList; + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/UserCredentialWrapper.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/UserCredentialWrapper.java new file mode 100644 index 000000000..91e2fd0ff --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/UserCredentialWrapper.java @@ -0,0 +1,53 @@ +/* + * 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.mdm.beans; + +public class UserCredentialWrapper { + + private String username; + /* + Base64 encoded password + */ + private String oldPassword; + private String newPassword; + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getOldPassword() { + return oldPassword; + } + + public void setOldPassword(String oldPassword) { + this.oldPassword = oldPassword; + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/UserWrapper.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/UserWrapper.java new file mode 100644 index 000000000..0e4778d23 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/UserWrapper.java @@ -0,0 +1,87 @@ +/* + * 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.mdm.beans; + +public class UserWrapper { + + private String username; + /* + Base64 encoded password + */ + private String password; + private String firstname; + private String lastname; + private String emailAddress; + private String[] roles; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public String getEmailAddress() { + return emailAddress; + } + + public void setEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + } + + /* + Giving a clone of the array since arrays are mutable + */ + public String[] getRoles() { + String[] copiedRoles = roles; + if (roles != null){ + copiedRoles = roles.clone(); + } + return copiedRoles; + } + + public void setRoles(String[] roles) { + this.roles = roles; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/android/AppStoreApplication.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/android/AppStoreApplication.java new file mode 100644 index 000000000..b60ff7cdb --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/android/AppStoreApplication.java @@ -0,0 +1,52 @@ +/* + * + * 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.mdm.beans.android; + +import com.google.gson.Gson; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import java.io.IOException; +import java.io.Serializable; + +/** + * This class represents the Appstore Application information. + */ +public class AppStoreApplication implements Serializable { + + private String type; + private String appIdentifier; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getAppIdentifier() { + return appIdentifier; + } + + public void setAppIdentifier(String appIdentifier) { + this.appIdentifier = appIdentifier; + } + + public String toJSON() throws MDMAPIException { + Gson gson = new Gson(); + return gson.toJson(this); + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/android/EnterpriseApplication.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/android/EnterpriseApplication.java new file mode 100644 index 000000000..f19dbd44c --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/android/EnterpriseApplication.java @@ -0,0 +1,61 @@ +/* + * + * 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.mdm.beans.android; + +import com.google.gson.Gson; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import java.io.IOException; +import java.io.Serializable; + +/** + * This class represents the Enterprise Application information. + */ +public class EnterpriseApplication implements Serializable { + + private String type; + private String url; + private String appIdentifier; + + public String getAppIdentifier() { + return appIdentifier; + } + + public void setAppIdentifier(String appIdentifier) { + this.appIdentifier = appIdentifier; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String toJSON() throws MDMAPIException { + Gson gson = new Gson(); + return gson.toJson(this); + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/android/WebApplication.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/android/WebApplication.java new file mode 100644 index 000000000..7632524c9 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/android/WebApplication.java @@ -0,0 +1,61 @@ +/* + * + * 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.mdm.beans.android; + +import com.google.gson.Gson; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import java.io.IOException; +import java.io.Serializable; + +/** + * This class represents the Web Application information. + */ +public class WebApplication implements Serializable { + + private String name; + private String url; + private String type; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String toJSON() throws MDMAPIException { + Gson gson = new Gson(); + return gson.toJson(this); + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ios/AppStoreApplication.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ios/AppStoreApplication.java new file mode 100644 index 000000000..aa6c0a012 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ios/AppStoreApplication.java @@ -0,0 +1,86 @@ +/* + * 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.mdm.beans.ios; + +import com.google.gson.Gson; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import java.io.IOException; +import java.io.Serializable; + +public class AppStoreApplication implements Serializable { + + private String identifier; + private int iTunesStoreID; + private boolean removeAppUponMDMProfileRemoval; + private boolean preventBackupOfAppData; + private String bundleId; + private String UUID; + + public String getUUID() { + return UUID; + } + + public void setUUID(String UUID) { + this.UUID = UUID; + } + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public int getiTunesStoreID() { + return iTunesStoreID; + } + + public void setiTunesStoreID(int iTunesStoreID) { + this.iTunesStoreID = iTunesStoreID; + } + + public boolean isRemoveAppUponMDMProfileRemoval() { + return removeAppUponMDMProfileRemoval; + } + + public void setRemoveAppUponMDMProfileRemoval(boolean removeAppUponMDMProfileRemoval) { + this.removeAppUponMDMProfileRemoval = removeAppUponMDMProfileRemoval; + } + + public boolean isPreventBackupOfAppData() { + return preventBackupOfAppData; + } + + public void setPreventBackupOfAppData(boolean preventBackupOfAppData) { + this.preventBackupOfAppData = preventBackupOfAppData; + } + + public String getBundleId() { + return bundleId; + } + + public void setBundleId(String bundleId) { + this.bundleId = bundleId; + } + + public String toJSON() throws MDMAPIException { + Gson gson = new Gson(); + return gson.toJson(this); + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ios/EnterpriseApplication.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ios/EnterpriseApplication.java new file mode 100644 index 000000000..d310779fa --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ios/EnterpriseApplication.java @@ -0,0 +1,83 @@ +/* + * 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.mdm.beans.ios; + +import com.google.gson.Gson; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import java.io.IOException; +import java.io.Serializable; + +public class EnterpriseApplication implements Serializable { + + private String identifier; + private String manifestURL; + private boolean removeAppUponMDMProfileRemoval; + private boolean preventBackupOfAppData; + private String bundleId; + private String UUID; + + public void setUUID(String UUID) { + this.UUID = UUID; + } + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + + public String getManifestURL() { + return manifestURL; + } + + public void setManifestURL(String manifestURL) { + this.manifestURL = manifestURL; + } + + public boolean isRemoveAppUponMDMProfileRemoval() { + return removeAppUponMDMProfileRemoval; + } + + public void setRemoveAppUponMDMProfileRemoval(boolean removeAppUponMDMProfileRemoval) { + this.removeAppUponMDMProfileRemoval = removeAppUponMDMProfileRemoval; + } + + public boolean isPreventBackupOfAppData() { + return preventBackupOfAppData; + } + + public void setPreventBackupOfAppData(boolean preventBackupOfAppData) { + this.preventBackupOfAppData = preventBackupOfAppData; + } + + public String getBundleId() { + return bundleId; + } + + public void setBundleId(String bundleId) { + this.bundleId = bundleId; + } + + public String toJSON() throws MDMAPIException { + Gson gson = new Gson(); + return gson.toJson(this); + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ios/RemoveApplication.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ios/RemoveApplication.java new file mode 100644 index 000000000..9c15ba6a3 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ios/RemoveApplication.java @@ -0,0 +1,24 @@ +package org.wso2.carbon.mdm.beans.ios; + +import com.google.gson.Gson; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import java.io.IOException; +import java.io.Serializable; + +public class RemoveApplication implements Serializable { + + private String bundleId; + + public String getBundleId() { + return bundleId; + } + + public void setBundleId(String bundleId) { + this.bundleId = bundleId; + } + + public String toJSON() throws MDMAPIException { + Gson gson = new Gson(); + return gson.toJson(this); + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ios/WebClip.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ios/WebClip.java new file mode 100644 index 000000000..4604eab46 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/beans/ios/WebClip.java @@ -0,0 +1,60 @@ +package org.wso2.carbon.mdm.beans.ios; + +import com.google.gson.Gson; +import org.wso2.carbon.mdm.api.common.MDMAPIException; +import java.io.IOException; + +public class WebClip { + + private String URL; + private String label; + private String icon; + private String isRemovable; + private String UUID; + + public String getUUID() { + return UUID; + } + + public void setUUID(String UUID) { + this.UUID = UUID; + } + + public String getURL() { + return URL; + } + + public void setURL(String URL) { + this.URL = URL; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + + public String getIsRemovable() { + return isRemovable; + } + + public void setIsRemovable(String isRemovable) { + this.isRemovable = isRemovable; + } + + public String toJSON() throws MDMAPIException { + Gson gson = new Gson(); + return gson.toJson(this); + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/util/Constants.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/util/Constants.java new file mode 100644 index 000000000..e9caa31d2 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/util/Constants.java @@ -0,0 +1,30 @@ +/* + * 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.mdm.util; + +/** + * Holds the constants used by MDM-Admin web application. + */ +public class Constants { + + public static final String USER_CLAIM_EMAIL_ADDRESS = "http://wso2.org/claims/emailaddress"; + public static final String USER_CLAIM_FIRST_NAME = "http://wso2.org/claims/givenname"; + public static final String USER_CLAIM_LAST_NAME = "http://wso2.org/claims/lastname"; + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/util/MDMUtil.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/util/MDMUtil.java new file mode 100644 index 000000000..d766776cc --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/util/MDMUtil.java @@ -0,0 +1,60 @@ +/* + * 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.mdm.util; + +import org.wso2.carbon.mdm.beans.ProfileFeature; +import org.wso2.carbon.policy.mgt.common.Profile; + +import java.util.ArrayList; +import java.util.List; + +public class MDMUtil { + + public static Profile convertProfile(org.wso2.carbon.mdm.beans.Profile mdmProfile) { + Profile profile = new Profile(); + profile.setTenantId(mdmProfile.getTenantId()); + profile.setCreatedDate(mdmProfile.getCreatedDate()); + profile.setDeviceType(mdmProfile.getDeviceType()); + + List profileFeatures = + new ArrayList(mdmProfile.getProfileFeaturesList().size()); + for (ProfileFeature mdmProfileFeature : mdmProfile.getProfileFeaturesList()) { + profileFeatures.add(convertProfileFeature(mdmProfileFeature)); + } + profile.setProfileFeaturesList(profileFeatures); + profile.setProfileId(mdmProfile.getProfileId()); + profile.setProfileName(mdmProfile.getProfileName()); + profile.setUpdatedDate(mdmProfile.getUpdatedDate()); + return profile; + } + + public static org.wso2.carbon.policy.mgt.common.ProfileFeature convertProfileFeature(ProfileFeature + mdmProfileFeature) { + + org.wso2.carbon.policy.mgt.common.ProfileFeature profileFeature = + new org.wso2.carbon.policy.mgt.common.ProfileFeature(); + profileFeature.setProfileId(mdmProfileFeature.getProfileId()); + profileFeature.setContent(mdmProfileFeature.getPayLoad()); + profileFeature.setDeviceTypeId(mdmProfileFeature.getDeviceTypeId()); + profileFeature.setFeatureCode(mdmProfileFeature.getFeatureCode()); + profileFeature.setId(mdmProfileFeature.getId()); + return profileFeature; + + } +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/util/SetReferenceTransformer.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/util/SetReferenceTransformer.java new file mode 100644 index 000000000..da4136593 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/carbon/mdm/util/SetReferenceTransformer.java @@ -0,0 +1,60 @@ +/* + * 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.mdm.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.TreeSet; + +public class SetReferenceTransformer{ + private List objectsToRemove; + private List objectsToAdd; + + /** + * Use the Set theory to find the objects to delete and objects to add + + The difference of objects in existingSet and newSet needed to be deleted + + new roles to add = newSet - The intersection of roles in existingSet and newSet + * @param currentList + * @param nextList + */ + public void transform(List currentList, List nextList){ + TreeSet existingSet = new TreeSet(currentList); + TreeSet newSet = new TreeSet(nextList); + + existingSet.removeAll(newSet); + + objectsToRemove = new ArrayList(existingSet); + + // Clearing and re-initializing the set + existingSet = new TreeSet(currentList); + + newSet.removeAll(existingSet); + objectsToAdd = new ArrayList(newSet); + } + + public List getObjectsToRemove() { + return objectsToRemove; + } + + public List getObjectsToAdd() { + return objectsToAdd; + } +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/mdm/common/Application.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/mdm/common/Application.java new file mode 100644 index 000000000..0a11ad1bc --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/mdm/common/Application.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.mdm.common; + +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; + +import java.util.List; + +public class Application { + + private String applicationName; + private String appId; + private String locationUrl; + private String imageUrl; + private String platform; + private String version; + private List userNameList; + private List roleNameList; + + public List getDeviceIdentifiers() { + return deviceIdentifiers; + } + + public void setDeviceIdentifiers(List deviceIdentifiers) { + this.deviceIdentifiers = deviceIdentifiers; + } + + private List deviceIdentifiers; + + + public String getApplicationName() { + return applicationName; + } + + public void setApplicationName(String applicationName) { + this.applicationName = applicationName; + } + + + public String getAppId() { + return appId; + } + + public void setAppId(String appId) { + this.appId = appId; + } + + + public String getLocationUrl() { + return locationUrl; + } + + public void setLocationUrl(String locationUrl) { + this.locationUrl = locationUrl; + } + + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + + public String getPlatform() { + return platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + + public List getUserNameList() { + return userNameList; + } + + public void setUserNameList(List userNameList) { + this.userNameList = userNameList; + } + + + public List getRoleNameList() { + return roleNameList; + } + + public void setRoleNameList(List roleNameList) { + this.roleNameList = roleNameList; + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/mdm/common/GsonMessageBodyHandler.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/mdm/common/GsonMessageBodyHandler.java new file mode 100644 index 000000000..09b49e6f1 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/java/org/wso2/mdm/common/GsonMessageBodyHandler.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 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. + */ + +package org.wso2.mdm.common; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import java.io.*; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +@Provider +@Produces(APPLICATION_JSON) +@Consumes(APPLICATION_JSON) +public class GsonMessageBodyHandler implements MessageBodyWriter, MessageBodyReader { + + private Gson gson; + private static final String UTF_8 = "UTF-8"; + + public boolean isReadable(Class aClass, Type type, Annotation[] annotations, MediaType mediaType) { + return true; + } + + private Gson getGson() { + if (gson == null) { + final GsonBuilder gsonBuilder = new GsonBuilder(); + gson = gsonBuilder.create(); + } + return gson; + } + + public Object readFrom(Class objectClass, Type type, Annotation[] annotations, MediaType mediaType, + MultivaluedMap stringStringMultivaluedMap, InputStream entityStream) + throws IOException, WebApplicationException { + + InputStreamReader reader = new InputStreamReader(entityStream, "UTF-8"); + + try { + return getGson().fromJson(reader, type); + } finally { + reader.close(); + } + } + + public boolean isWriteable(Class aClass, Type type, Annotation[] annotations, MediaType mediaType) { + return true; + } + + public long getSize(Object o, Class aClass, Type type, Annotation[] annotations, MediaType mediaType) { + return -1; + } + + public void writeTo(Object object, Class aClass, Type type, Annotation[] annotations, MediaType mediaType, + MultivaluedMap stringObjectMultivaluedMap, OutputStream entityStream) + throws IOException, WebApplicationException { + + OutputStreamWriter writer = new OutputStreamWriter(entityStream, UTF_8); + try { + Type jsonType = null; + if (type.equals(type)) { + jsonType = type; + } + getGson().toJson(object, jsonType, writer); + } finally { + writer.close(); + } + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/webapp/META-INF/permissions.xml b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/webapp/META-INF/permissions.xml new file mode 100644 index 000000000..0e11fe9ba --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/webapp/META-INF/permissions.xml @@ -0,0 +1,565 @@ + + + + + + + + + Get all devices + /device-mgt/devices/list + /devices + GET + emm_admin + + + + Get all device types + /device-mgt/devices/types + /devices/types + GET + emm_admin + + + + View device + /device-mgt/devices/view + /devices/view + GET + emm_admin + + + + Get device + /device-mgt/devices/view + /devices/*/* + GET + emm_admin,emm_user + + + + Get users' devices + /device-mgt/devices/view + /devices/user/*/* + GET + emm_admin,emm_user + + + + Get device count + /device-mgt/devices/count + /devices/count + GET + emm_admin + + + + Get all users' devices + /device-mgt/devices/list + /devices/name/*/* + GET + emm_admin,emm_user + + + + + + Get all notifications + /device-mgt/notifications/view + /notifications + GET + emm_admin + + + + Add notification + /device-mgt/notifications/add + /notifications + POST + emm_admin + + + + Update notification + /device-mgt/notifications/view + /notifications/*/* + PUT + emm_admin + + + + Get new notifications + /device-mgt/notifications/view + /notifications/* + GET + emm_admin + + + + + + Get all operations + /device-mgt/operations/view + /operations + GET + emm_admin + + + + Add Operation + /device-mgt/operations/add + /operations + POST + emm_admin + + + + Install Application + /device-mgt/operations/application/install + /operations/installApp/* + POST + emm_admin,emm_user + + + + Uninstall Application + /device-mgt/operations/application/uninstall + /operations/uninstallApp/* + POST + emm_admin,emm_user + + + + Get Applications For Devices + /device-mgt/operations/application/view + /operations/*/*/* + GET + emm_admin,emm_user + + + Get Device Operations + /device-mgt/operations/view + /operations/*/* + GET + emm_admin,emm_user + + + + Get Applications For Device Type + /device-mgt/operations/application/view + /operations/* + GET + emm_admin,emm_user + + + + + + Get features + /device-mgt/features/view + /features/* + GET + emm_admin + + + Get all features + /device-mgt/features/view + /features + GET + emm_admin + + + + + + Get all roles + /device-mgt/roles + /roles + GET + emm_admin + + + Get role permissions + /device-mgt/roles + /roles/permissions + GET + emm_admin + + + Get User roles + /device-mgt/roles + /roles/* + GET + emm_admin + + + Update User roles + /device-mgt/roles/update + /roles + PUT + emm_admin + + + Update a specific role + /device-mgt/roles/update + /roles/* + PUT + emm_admin + + + Update User roles + /device-mgt/roles + /roles/*/users + PUT + emm_admin + + + Save User roles + /device-mgt/roles/add + /roles + POST + emm_admin + + + Delete User roles + /device-mgt/roles/delete + /roles + DELETE + emm_admin + + + + + + Get all users + /device-mgt/users/view + /users + GET + emm_admin + + + Add user + /device-mgt/users/add + /users + POST + emm_admin + + + Get user + /device-mgt/users/view + /users/view + GET + emm_admin + + + Update user + /device-mgt/users/update + /users + PUT + emm_admin + + + Change user password + /device-mgt/users/reset-password + /users/reset-password + POST + emm_admin + + + Delete user + /device-mgt/users/delete + /users + DELETE + emm_admin + + + Get user roles by name + /device-mgt/users/roles + /users/roles + GET + emm_admin + + + Get user roles by name + /device-mgt/users/roles + /roles + GET + emm_admin + + + Save Roles + /device-mgt/users/roles + /roles + POST + emm_admin + + + Get all devices of user + /device-mgt/users/devices + /users/devices + GET + emm_admin + + + Get user roles + /device-mgt/users/roles + /users/*/* + GET + emm_admin + + + Get user count + /device-mgt/users/view + /users/count + GET + emm_admin + + + Get all user names + /device-mgt/users/view + /users/view-users + GET + emm_admin + + + Get all usernames + /device-mgt/users/view + /users/users-by-username + GET + emm_admin + + + Get filtered usernames + /device-mgt/users/view + /users/users-by-username/* + GET + emm_admin + + + Send invitation + /device-mgt/users/invite + /users/email-invitation + POST + emm_admin + + + + + + Add Policy + /device-mgt/policies/add + /policies/inactive-policy + POST + emm_admin + + + Get Policy For Device + /device-mgt/policies/view + /policies/*/* + POST + emm_admin + + + Get Policy For Device By Type + /device-mgt/policies/view + /policies/*/*/* + GET + emm_admin + + + Add Active Policy + /device-mgt/policies/add + /policies/active-policy + POST + emm_admin + + + Bulk Policy Removal + /device-mgt/policies/bulk-remove + /policies/bulk-remove + POST + emm_admin + + + Get Policies + /device-mgt/policies/view + /policies + GET + emm_admin + + + Get Policy + /device-mgt/policies/view + /policies/* + GET + emm_admin + + + Update Policy + /device-mgt/policies/update + /policies/* + PUT + emm_admin + + + Delete Policy + /device-mgt/policies/delete + /policies + DELETE + emm_admin + + + Policy Count + /device-mgt/policies/view + /policies/count + GET + emm_admin + + + Policy priorities + /device-mgt/policies/update + /policies/priorities + PUT + emm_admin + + + Activate Policy + /device-mgt/policies/update + /policies/activate + PUT + emm_admin + + + Inactivate Policy + /device-mgt/policies/update + /policies/inactivate + PUT + emm_admin + + + Apply Policy changes + /device-mgt/policies/update + /policies/apply-changes + PUT + emm_admin + + + Start Task + /device-mgt/policies/task + /policies/start-task/* + GET + emm_admin + + + Update Task + /device-mgt/policies/task + /policies/update-task/* + GET + emm_admin + + + Stop Task + /device-mgt/policies/task + /policies/stop-task + GET + emm_admin + + + Get Policy Compliance + /device-mgt/policies/compliance + /policies/*/* + GET + emm_admin + + + + + + Add profile + /device-mgt/profiles/add + /profiles + POST + emm_admin + + + Update profile + /device-mgt/profiles/update + /profiles/* + PUT + emm_admin + + + Add profile + /device-mgt/profiles/delete + /profiles/* + DELETE + emm_admin + + + + + + + + + + + + + + + + + + + + + + + Get configuration + /device-mgt/tenant/configuration/view + /configuration + GET + emm_admin + + + Add configuration + /device-mgt/tenant/configuration/modify + /configuration + POST + emm_admin + + + Update configuration + /device-mgt/tenant/configuration/modify + /configuration + PUT + emm_admin + + + \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/webapp/META-INF/webapp-classloading.xml b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/webapp/META-INF/webapp-classloading.xml new file mode 100644 index 000000000..ed2ed2162 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/webapp/META-INF/webapp-classloading.xml @@ -0,0 +1,35 @@ + + + + + + + + + false + + + CXF,Carbon + diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/webapp/WEB-INF/cxf-servlet.xml b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/webapp/WEB-INF/cxf-servlet.xml new file mode 100644 index 000000000..dc53a029b --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/webapp/WEB-INF/cxf-servlet.xml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/webapp/WEB-INF/web.xml b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..1ee664987 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.api/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,74 @@ + + + + Admin-Webapp + + JAX-WS/JAX-RS MDM Endpoint + JAX-WS/JAX-RS Servlet + CXFServlet + + org.apache.cxf.transport.servlet.CXFServlet + + 1 + + + CXFServlet + /* + + + 60 + + + + isAdminService + false + + + doAuthentication + true + + + + + + + MDM-Admin + /* + + + CONFIDENTIAL + + + + diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/pom.xml b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/pom.xml new file mode 100644 index 000000000..530e3c8b6 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/pom.xml @@ -0,0 +1,62 @@ + + + + + + + + mobile-base-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.mobile.ui + WSO2 Carbon - EMM Server UI + pom + + + + + maven-assembly-plugin + 2.5.5 + + ${project.artifactId}-${carbon.device.mgt.version} + false + + src/assembly/src.xml + + + + + create-archive + package + + single + + + + + + + + \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/assembly/src.xml b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/assembly/src.xml new file mode 100644 index 000000000..2797034e0 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/assembly/src.xml @@ -0,0 +1,36 @@ + + + + src + + zip + + false + ${basedir}/src + + + ${basedir}/src/main/resources/jaggeryapps/devicemgt + / + true + + + \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/cdmf.page.platform.configuration/configuration.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/cdmf.page.platform.configuration/configuration.hbs new file mode 100644 index 000000000..302cc5e9a --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/cdmf.page.platform.configuration/configuration.hbs @@ -0,0 +1,5 @@ +{{unit "cdmf.unit.ui.title" pageTitle="Platform Configuration"}} + +{{#zone "content"}} + {{unit "cdmf.unit.platform.configuration"}} +{{/zone}} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/cdmf.page.platform.configuration/configuration.json b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/cdmf.page.platform.configuration/configuration.json new file mode 100644 index 000000000..7316b1146 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/pages/cdmf.page.platform.configuration/configuration.json @@ -0,0 +1,5 @@ +{ + "version": "1.0.0", + "uri": "/platform-configuration", + "layout": "cdmf.layout.default" +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/operation-bar.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/operation-bar.hbs new file mode 100644 index 000000000..78813761e --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/operation-bar.hbs @@ -0,0 +1,7 @@ +
      + +{{#zone "bottomJs"}} + + {{js "js/operation-bar.js"}} +{{/zone}} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/operation-bar.json b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/operation-bar.json new file mode 100644 index 000000000..56a988c76 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/operation-bar.json @@ -0,0 +1,4 @@ +{ + "version": "1.0.0", + "extends": "cdmf.unit.device.operation-bar" +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/public/js/operation-bar.js b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/public/js/operation-bar.js new file mode 100644 index 000000000..fe6c85bd0 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/public/js/operation-bar.js @@ -0,0 +1,195 @@ +/* + * 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. + */ + +/* + * Setting-up global variables. + */ + +var operations = '.wr-operations', + modalPopup = '.wr-modalpopup', + modalPopupContent = modalPopup + ' .modalpopup-content', + navHeight = $('#nav').height(), + headerHeight = $('header').height(), + offset = (headerHeight + navHeight), + deviceSelection = '.device-select', + platformTypeConstants = { + "ANDROID": "android", + "IOS": "ios", + "WINDOWS": "windows" + }; + +/* + * Function to get selected devices ID's + */ +function getSelectedDeviceIds() { + var deviceIdentifierList = []; + $(deviceSelection).each(function (index) { + var device = $(this); + var deviceId = device.data('deviceid'); + var deviceType = device.data('type'); + deviceIdentifierList.push({ + "id": deviceId, + "type": deviceType + }); + }); + if (deviceIdentifierList.length == 0) { + var thisTable = $(".DTTT_selected").closest('.dataTables_wrapper').find('.dataTable').dataTable(); + thisTable.api().rows().every(function () { + if ($(this.node()).hasClass('DTTT_selected')) { + var deviceId = $(thisTable.api().row(this).node()).data('deviceid'); + var deviceType = $(thisTable.api().row(this).node()).data('devicetype'); + deviceIdentifierList.push({ + "id": deviceId, + "type": deviceType + }); + } + }); + } + + return deviceIdentifierList; +} + +/* + * On operation click function. + * @param selection: Selected operation + */ +function operationSelect(selection) { + var deviceIdList = getSelectedDeviceIds(); + if (deviceIdList == 0) { + $(modalPopupContent).html($("#errorOperations").html()); + } else { + $(modalPopupContent).addClass("operation-data"); + $(modalPopupContent).html($(operations + " .operation[data-operation-code=" + selection + "]").html()); + $(modalPopupContent).data("operation-code", selection); + } + showPopup(); +} + +function getDevicesByTypes(deviceList) { + var deviceTypes = {}; + $.each(deviceList, function (index, item) { + if (!deviceTypes[item.type]) { + deviceTypes[item.type] = []; + } + if (item.type == platformTypeConstants.ANDROID || + item.type == platformTypeConstants.IOS || item.type == platformTypeConstants.WINDOWS) { + deviceTypes[item.type].push(item.id); + } + }); + return deviceTypes; +} + +function unloadOperationBar() { + $("#showOperationsBtn").addClass("hidden"); + $(".wr-operations").html(""); +} + +function loadOperationBar(deviceType) { + var operationBar = $("#operations-bar"); + var operationBarSrc = operationBar.attr("src"); + var platformType = deviceType; + $.template("operations-bar", operationBarSrc, function (template) { + var serviceURL = "/devicemgt_admin/features/" + platformType; + var successCallback = function (data) { + var viewModel = {}; + data = JSON.parse(data).filter(function (current) { + var iconName; + switch (deviceType) { + case platformTypeConstants.ANDROID: + iconName = operationModule.getAndroidIconForFeature(current.code); + current.type = deviceType; + break; + case platformTypeConstants.WINDOWS: + iconName = operationModule.getWindowsIconForFeature(current.code); + break; + case platformTypeConstants.IOS: + iconName = operationModule.getIOSIconForFeature(current.code); + break; + } + + if (iconName) { + current.icon = iconName; + return current; + } + }); + viewModel.features = data; + var content = template(viewModel); + $(".wr-operations").html(content); + }; + invokerUtil.get(serviceURL, successCallback, function (message) { + $(".wr-operations").html(message); + }); + }); +} + +function runOperation(operationName) { + var deviceIdList = getSelectedDeviceIds(); + var list = getDevicesByTypes(deviceIdList); + + var successCallback = function (data) { + if (operationName == "NOTIFICATION") { + $(modalPopupContent).html($("#messageSuccess").html()); + } else { + $(modalPopupContent).html($("#operationSuccess").html()); + } + showPopup(); + }; + var errorCallback = function (data) { + $(modalPopupContent).html($("#errorOperationUnexpected").html()); + showPopup(); + }; + + var payload, serviceEndPoint; + if (list[platformTypeConstants.IOS]) { + payload = operationModule. + generatePayload(platformTypeConstants.IOS, operationName, list[platformTypeConstants.IOS]); + serviceEndPoint = operationModule.getIOSServiceEndpoint(operationName); + } else if (list[platformTypeConstants.ANDROID]) { + payload = operationModule + .generatePayload(platformTypeConstants.ANDROID, operationName, list[platformTypeConstants.ANDROID]); + serviceEndPoint = operationModule.getAndroidServiceEndpoint(operationName); + } else if (list[platformTypeConstants.WINDOWS]) { + payload = operationModule. + generatePayload(platformTypeConstants.WINDOWS, operationName, list[platformTypeConstants.WINDOWS]); + serviceEndPoint = operationModule.getWindowsServiceEndpoint(operationName); + } + if (operationName == "NOTIFICATION") { + var errorMsgWrapper = "#notification-error-msg"; + var errorMsg = "#notification-error-msg span"; + var message = $("#message").val(); + if (!message) { + $(errorMsg).text("Enter a message. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else { + invokerUtil.post(serviceEndPoint, payload, successCallback, errorCallback); + $(modalPopupContent).removeData(); + hidePopup(); + } + } else { + invokerUtil.post(serviceEndPoint, payload, successCallback, errorCallback); + $(modalPopupContent).removeData(); + hidePopup(); + } +} + +/* + * DOM ready functions. + */ +$(document).ready(function () { + $(operations).show(); +}); diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/public/templates/hidden-operations-android.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/public/templates/hidden-operations-android.hbs new file mode 100644 index 000000000..ad8959bf0 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/public/templates/hidden-operations-android.hbs @@ -0,0 +1,304 @@ +
      + +
      + + +
      +
      + +
      +
      + +
      + Configure +
      +
      +
      + +
      + + + +
      + +
      + + + +
      + +
      + + + +
      + +
      + + + +
      + +
      + + + +
      + +
      + +
      + +
      + +
      + +
      + + Configure +
      +
      +
      + + + +
      +
      + + +
      + +
      + + + +
      + +
      + Configure +
      + +
      + + +
      +
      + +
      + + +
      + +
      +
      + +
      + + +
      + +
      + Install +
      +
      +
      + +
      + + +
      + +
      + + +
      + +
      + Install +
      +
      +
      + +
      + + +
      + +
      + Uninstall +
      +
      +
      + + + +
      +
      + +
      +
      + +
      + Configure +
      +
      +
      + +
      +
      \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/public/templates/hidden-operations-ios.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/public/templates/hidden-operations-ios.hbs new file mode 100644 index 000000000..4cb006ef0 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/public/templates/hidden-operations-ios.hbs @@ -0,0 +1,366 @@ +
      + +
      + + +
      +
      + +
      + +
      + +
      + +
      + +
      + + +
      + +
      +
      + + +
      +
      + + +
      + Install +
      + +
      +
      + +
      + +
      + +
      + +
      + +
      + + +
      + +
      +
      + + +
      +
      + + +
      + Install +
      +
      +
      + +
      + +
      + +
      + Uninstall +
      +
      +
      + + + +
      + + +
      + +
      + +
      + + +
      + +
      + + +
      + + + +
      + +
      + + + +
      + +
      + + + +
      + +
      + + + +
      + +
      + Configure +
      + + + +
      + + +
      + +
      + + + +
      +
      + +
      +
      + Path Prefix +
      +
      +
      + + + +
      + +
      + + + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + + + +
      +
      + +
      +
      + : +
      +
      +
      + + + +
      + +
      + + + +
      + +
      + + + +
      + +
      + +
      + + +
      +
      + + + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      +
      + +
      + Configure +
      + + +
      + +
      + +
      + +
      + +
      AirPlay password +
      + +
      + Configure +
      + +
      +
      \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/public/templates/operations.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/public/templates/operations.hbs new file mode 100644 index 000000000..e2fcd8f2c --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-bar/public/templates/operations.hbs @@ -0,0 +1,156 @@ +
      +
      +
      +
      +

      + + + + + Operation cannot be performed ! +

      +

      + Please select a device or a list of devices to perform an operation. +

      + +
      + Ok +
      +
      +
      +
      +
      +
      +
      +
      +
      +

      + + + + + Operation cannot be performed ! +

      +

      + Unexpected error occurred. Please Try again later. +

      + +
      + Ok +
      +
      +
      +
      +
      +
      +
      +
      +
      +

      + + + + + Operation queued successfully ! +

      +

      + Operation has been queued successfully to be sent to the device. +

      + +
      + Ok +
      +
      +
      +
      +
      + +
      +
      +
      +
      +

      + + + + + Message sent successfully ! +

      +

      + Message has been queued to be sent to the device. +

      + +
      + Ok +
      +
      +
      +
      +
      +{{#each features}} + + + {{name}} + +
      +
      +
      +
      +

      + + + + + {{name}} +
      +

      +

      + {{#equal code "WIPE_DATA"}} + {{#equal type "android"}} + Enter PIN code (Optional - This is required only if the device type + is BYOD). +

      + +
      + +
      +
      + {{/equal}} + {{/equal}} + {{#equal code "NOTIFICATION"}} + Type your message below. +

      + + +
      + +
      +
      + {{/equal}} + {{#equal code "CHANGE_LOCK_CODE"}} + Type new lock-code below. +

      + +
      + {{/equal}} + Do you want to perform this operation on selected device(s) ? +
      +

      +
      + Yes + No +
      +
      +
      +
      +
      +{{/each}} +
      \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-mod/operation-mod.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-mod/operation-mod.hbs new file mode 100644 index 000000000..d193515b4 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-mod/operation-mod.hbs @@ -0,0 +1,3 @@ +{{#zone "bottomJs"}} + {{js "js/operation-mod.js"}} +{{/zone}} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-mod/operation-mod.json b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-mod/operation-mod.json new file mode 100644 index 000000000..62346cb87 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-mod/operation-mod.json @@ -0,0 +1,4 @@ +{ + "version": "1.0.0", + "extends": "cdmf.unit.device.operation-mod" +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-mod/public/js/operation-mod.js b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-mod/public/js/operation-mod.js new file mode 100644 index 000000000..b35faac5a --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.device.operation-mod/public/js/operation-mod.js @@ -0,0 +1,1228 @@ +/* + * 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. + */ + +var operationModule = function () { + var publicMethods = {}; + var privateMethods = {}; + + // Constants to define platform types available + var platformTypeConstants = { + "ANDROID": "android", + "IOS": "ios", + "WINDOWS": "windows" + }; + + // Constants to define operation types available + var operationTypeConstants = { + "PROFILE": "profile", + "CONFIG": "config", + "COMMAND": "command" + }; + + // Constants to define Android Operation Constants + var androidOperationConstants = { + "PASSCODE_POLICY_OPERATION_CODE": "PASSCODE_POLICY", + "CAMERA_OPERATION_CODE": "CAMERA", + "ENCRYPT_STORAGE_OPERATION_CODE": "ENCRYPT_STORAGE", + "WIFI_OPERATION_CODE": "WIFI", + "WIPE_OPERATION_CODE": "WIPE_DATA", + "NOTIFICATION_OPERATION_CODE": "NOTIFICATION", + "CHANGE_LOCK_CODE_OPERATION_CODE": "CHANGE_LOCK_CODE" + }; + + // Constants to define Windows Operation Constants + var windowsOperationConstants = { + "PASSCODE_POLICY_OPERATION_CODE": "PASSCODE_POLICY", + "CAMERA_OPERATION_CODE": "CAMERA", + "ENCRYPT_STORAGE_OPERATION_CODE": "ENCRYPT_STORAGE", + "NOTIFICATION_OPERATION_CODE": "NOTIFICATION", + "CHANGE_LOCK_CODE_OPERATION_CODE": "CHANGE_LOCK_CODE" + }; + + // Constants to define iOS Operation Constants + var iosOperationConstants = { + "PASSCODE_POLICY_OPERATION_CODE": "PASSCODE_POLICY", + "RESTRICTIONS_OPERATION_CODE": "RESTRICTION", + "WIFI_OPERATION_CODE": "WIFI", + "EMAIL_OPERATION_CODE": "EMAIL", + "AIRPLAY_OPERATION_CODE": "AIR_PLAY", + "LDAP_OPERATION_CODE": "LDAP", + "CALENDAR_OPERATION_CODE": "CALDAV", + "NOTIFICATION_OPERATION_CODE": "NOTIFICATION", + "CALENDAR_SUBSCRIPTION_OPERATION_CODE": "CALENDAR_SUBSCRIPTION", + "APN_OPERATION_CODE": "APN", + "CELLULAR_OPERATION_CODE": "CELLULAR" + }; + + publicMethods.getIOSServiceEndpoint = function (operationCode) { + var featureMap = { + "DEVICE_LOCK": "lock", + "ALARM": "alarm", + "LOCATION": "location", + "NOTIFICATION": "notification", + "AIR_PLAY": "airplay", + "RESTRICTION": "restriction", + "CELLULAR": "cellular", + "WIFI": "wifi", + "INSTALL_STORE_APPLICATION": "storeapplication", + "INSTALL_ENTERPRISE_APPLICATION": "enterpriseapplication", + "REMOVE_APPLICATION": "removeapplication", + "ENTERPRISE_WIPE": "enterprisewipe" + }; + return "/ios/operation/" + featureMap[operationCode]; + }; + + /** + * Convert the ios platform specific code to the generic payload. + * TODO: think of the possibility to follow a pattern to the key name (namespace?) + * @param operationCode + * @param operationPayload + * @returns {{}} + */ + privateMethods.generateGenericPayloadFromIOSPayload = function (operationCode, operationPayload) { + var payload = {}; + operationPayload = JSON.parse(operationPayload); + switch (operationCode) { + case iosOperationConstants["PASSCODE_POLICY_OPERATION_CODE"]: + payload = { + "passcodePolicyForcePIN": operationPayload["forcePIN"], + "passcodePolicyAllowSimple": operationPayload["allowSimple"], + "passcodePolicyRequireAlphanumeric": operationPayload["requireAlphanumeric"], + "passcodePolicyMinLength": operationPayload["minLength"], + "passcodePolicyMinComplexChars": operationPayload["minComplexChars"], + "passcodePolicyMaxPasscodeAgeInDays": operationPayload["maxPINAgeInDays"], + "passcodePolicyPasscodeHistory": operationPayload["pinHistory"], + "passcodePolicyMaxAutoLock": operationPayload["maxInactivity"], + "passcodePolicyGracePeriod": operationPayload["maxGracePeriod"], + "passcodePolicyMaxFailedAttempts": operationPayload["maxFailedAttempts"] + }; + break; + case iosOperationConstants["RESTRICTIONS_OPERATION_CODE"]: + payload = { + "restrictionsAllowAccountModification": operationPayload["allowAccountModification"], + "restrictionsAllowAddingGameCenterFriends": operationPayload["allowAddingGameCenterFriends"], + "restrictionsAllowAirDrop": operationPayload["allowAirDrop"], + "restrictionsAllowAppCellularDataModification": operationPayload["allowAppCellularDataModification"], + "restrictionsAllowAppInstallation": operationPayload["allowAppInstallation"], + "restrictionsAllowAppRemoval": operationPayload["allowAppRemoval"], + "restrictionsAllowAssistant": operationPayload["allowAssistant"], + "restrictionsAllowAssistantUserGeneratedContent": operationPayload["allowAssistantUserGeneratedContent"], + "restrictionsAllowAssistantWhileLocked": operationPayload["allowAssistantWhileLocked"], + "restrictionsAllowBookstore": operationPayload["allowBookstore"], + "restrictionsAllowBookstoreErotica": operationPayload["allowBookstoreErotica"], + "restrictionsAllowCamera": operationPayload["allowCamera"], + "restrictionsAllowChat": operationPayload["allowChat"], + "restrictionsAllowCloudBackup": operationPayload["allowCloudBackup"], + "restrictionsAllowCloudDocumentSync": operationPayload["allowCloudDocumentSync"], + "restrictionsAllowCloudKeychainSync": operationPayload["allowCloudKeychainSync"], + "restrictionsAllowDiagnosticSubmission": operationPayload["allowDiagnosticSubmission"], + "restrictionsAllowExplicitContent": operationPayload["allowExplicitContent"], + "restrictionsAllowFindMyFriendsModification": operationPayload["allowFindMyFriendsModification"], + "restrictionsAllowFingerprintForUnlock": operationPayload["allowFingerprintForUnlock"], + "restrictionsAllowGameCenter": operationPayload["allowGameCenter"], + "restrictionsAllowGlobalBackgroundFetchWhenRoaming": operationPayload["allowGlobalBackgroundFetchWhenRoaming"], + "restrictionsAllowInAppPurchases": operationPayload["allowInAppPurchases"], + "restrictionsAllowLockScreenControlCenter": operationPayload["allowLockScreenControlCenter"], + "restrictionsAllowHostPairing": operationPayload["allowHostPairing"], + "restrictionsAllowLockScreenNotificationsView": operationPayload["allowLockScreenNotificationsView"], + "restrictionsAllowLockScreenTodayView": operationPayload["allowLockScreenTodayView"], + "restrictionsAllowMultiplayerGaming": operationPayload["allowMultiplayerGaming"], + "restrictionsAllowOpenFromManagedToUnmanaged": operationPayload["allowOpenFromManagedToUnmanaged"], + "restrictionsAllowOpenFromUnmanagedToManaged": operationPayload["allowOpenFromUnmanagedToManaged"], + "restrictionsAllowOTAPKIUpdates": operationPayload["allowOTAPKIUpdates"], + "restrictionsAllowPassbookWhileLocked": operationPayload["allowPassbookWhileLocked"], + "restrictionsAllowPhotoStream": operationPayload["allowPhotoStream"], + "restrictionsAllowSafari": operationPayload["allowSafari"], + "restrictionsSafariAllowAutoFill": operationPayload["safariAllowAutoFill"], + "restrictionsSafariForceFraudWarning": operationPayload["safariForceFraudWarning"], + "restrictionsSafariAllowJavaScript": operationPayload["safariAllowJavaScript"], + "restrictionsSafariAllowPopups": operationPayload["safariAllowPopups"], + "restrictionsAllowScreenShot": operationPayload["allowScreenShot"], + "restrictionsAllowSharedStream": operationPayload["allowSharedStream"], + "restrictionsAllowUIConfigurationProfileInstallation": operationPayload["allowUIConfigurationProfileInstallation"], + "restrictionsAllowUntrustedTLSPrompt": operationPayload["allowUntrustedTLSPrompt"], + "restrictionsAllowVideoConferencing": operationPayload["allowVideoConferencing"], + "restrictionsAllowVoiceDialing": operationPayload["allowVoiceDialing"], + "restrictionsAllowYouTube": operationPayload["allowYouTube"], + "restrictionsAllowITunes": operationPayload["allowiTunes"], + "restrictionsForceAssistantProfanityFilter": operationPayload["forceAssistantProfanityFilter"], + "restrictionsForceEncryptedBackup": operationPayload["forceEncryptedBackup"], + "restrictionsForceITunesStorePasswordEntry": operationPayload["forceITunesStorePasswordEntry"], + "restrictionsForceLimitAdTracking": operationPayload["forceLimitAdTracking"], + "restrictionsForceAirPlayOutgoingRequestsPairingPassword": operationPayload["forceAirPlayOutgoingRequestsPairingPassword"], + "restrictionsForceAirPlayIncomingRequestsPairingPassword": operationPayload["forceAirPlayIncomingRequestsPairingPassword"], + "restrictionsAllowManagedAppsCloudSync": operationPayload["allowManagedAppsCloudSync"], + "restrictionsAllowEraseContentAndSettings": operationPayload["allowEraseContentAndSettings"], + "restrictionsAllowSpotlightInternetResults": operationPayload["allowSpotlightInternetResults"], + "restrictionsAllowEnablingRestrictions": operationPayload["allowEnablingRestrictions"], + "restrictionsAllowActivityContinuation": operationPayload["allowActivityContinuation"], + "restrictionsAllowEnterpriseBookBackup": operationPayload["allowEnterpriseBookBackup"], + "restrictionsAllowEnterpriseBookMetadataSync": operationPayload["allowEnterpriseBookMetadataSync"], + "restrictionsAllowPodcasts": operationPayload["allowPodcasts"], + "restrictionsAllowDefinitionLookup": operationPayload["allowDefinitionLookup"], + "restrictionsAllowPredictiveKeyboard": operationPayload["allowPredictiveKeyboard"], + "restrictionsAllowAutoCorrection": operationPayload["allowAutoCorrection"], + "restrictionsAllowSpellCheck": operationPayload["allowSpellCheck"], + "restrictionsSafariAcceptCookies": operationPayload["safariAcceptCookies"], + "restrictionsAutonomousSingleAppModePermittedAppIDs": operationPayload["autonomousSingleAppModePermittedAppIDs"] + }; + break; + case iosOperationConstants["WIFI_OPERATION_CODE"]: + payload = { + "wifiHiddenNetwork": operationPayload["hiddenNetwork"], + "wifiSSID": operationPayload["ssid"], + "wifiAutoJoin": operationPayload["autoJoin"], + "wifiProxyType": operationPayload["proxyType"], + "wifiEncryptionType": operationPayload["encryptionType"], + "wifiIsHotSpot": operationPayload["hotspot"], + "wifiDomainName": operationPayload["domainName"], + "wifiServiceProviderRoamingEnabled": operationPayload["serviceProviderRoamingEnabled"], + "wifiDisplayedOperatorName": operationPayload["displayedOperatorName"], + "wifiRoamingConsortiumOIs": operationPayload["roamingConsortiumOIs"], + "wifiPassword": operationPayload["password"], + "wifiPayloadCertUUID": operationPayload["payloadCertificateUUID"], + "wifiProxyServer": operationPayload["proxyServer"], + "wifiProxyPort": operationPayload["proxyPort"], + "wifiProxyUsername": operationPayload["proxyUsername"], + "wifiProxyPassword": operationPayload["proxyPassword"], + "wifiProxyPACURL": operationPayload["proxyPACURL"], + "wifiProxyPACFallbackAllowed": operationPayload["proxyPACFallbackAllowed"], + "wifiNAIRealmNames": operationPayload["nairealmNames"], + "wifiMCCAndMNCs": operationPayload["mccandMNCs"], + "wifiEAPUsername": operationPayload.clientConfiguration["username"], + "wifiAcceptedEAPTypes": operationPayload.clientConfiguration["acceptEAPTypes"], + "wifiEAPPassword": operationPayload.clientConfiguration["userPassword"], + "wifiEAPOneTimePassword": operationPayload.clientConfiguration["oneTimePassword"], + "wifiPayloadCertificateAnchorUUIDs": operationPayload.clientConfiguration["payloadCertificateAnchorUUID"], + "wifiEAPOuterIdentity": operationPayload.clientConfiguration["outerIdentity"], + "wifiTLSTrustedServerNames": operationPayload.clientConfiguration["tlstrustedServerNames"], + "wifiEAPTLSAllowTrustExceptions": operationPayload.clientConfiguration["tlsallowTrustExceptions"], + "wifiEAPTLSCertIsRequired": operationPayload.clientConfiguration["tlscertificateIsRequired"], + "wifiEAPTLSInnerAuthType": operationPayload.clientConfiguration["ttlsinnerAuthentication"], + "wifiEAPFastUsePAC": operationPayload.clientConfiguration["eapfastusePAC"], + "wifiEAPFastProvisionPAC": operationPayload.clientConfiguration["eapfastprovisionPAC"], + "wifiEAPFastProvisionPACAnonymously": operationPayload.clientConfiguration["eapfastprovisionPACAnonymously"], + "wifiEAPSIMNoOfRands": operationPayload.clientConfiguration["eapsimnumberOfRANDs"] + }; + break; + case iosOperationConstants["EMAIL_OPERATION_CODE"]: + payload = { + "emailAccountDescription": operationPayload["emailAccountDescription"], + "emailAccountName": operationPayload["emailAccountName"], + "emailAccountType": operationPayload["emailAccountType"], + "emailAddress": operationPayload["emailAddress"], + "emailIncomingMailServerAuthentication": operationPayload["incomingMailServerAuthentication"], + "emailIncomingMailServerHostname": operationPayload["incomingMailServerHostName"], + "emailIncomingMailServerPort": operationPayload["incomingMailServerPortNumber"], + "emailIncomingUseSSL": operationPayload["incomingMailServerUseSSL"], + "emailIncomingMailServerUsername": operationPayload["incomingMailServerUsername"], + "emailIncomingMailServerPassword": operationPayload["incomingPassword"], + "emailOutgoingMailServerPassword": operationPayload["outgoingPassword"], + "emailOutgoingPasswordSameAsIncomingPassword": operationPayload["outgoingPasswordSameAsIncomingPassword"], + "emailOutgoingMailServerAuthentication": operationPayload["outgoingMailServerAuthentication"], + "emailOutgoingMailServerHostname": operationPayload["outgoingMailServerHostName"], + "emailOutgoingMailServerPort": operationPayload["outgoingMailServerPortNumber"], + "emailOutgoingUseSSL": operationPayload["outgoingMailServerUseSSL"], + "emailOutgoingMailServerUsername": operationPayload["outgoingMailServerUsername"], + "emailPreventMove": operationPayload["preventMove"], + "emailPreventAppSheet": operationPayload["preventAppSheet"], + "emailDisableMailRecentSyncing": operationPayload["disableMailRecentSyncing"], + "emailIncomingMailServerIMAPPathPrefix": operationPayload["incomingMailServerIMAPPathPrefix"], + "emailSMIMEEnabled": operationPayload["smimeenabled"], + "emailSMIMESigningCertificateUUID": operationPayload["smimesigningCertificateUUID"], + "emailSMIMEEncryptionCertificateUUID": operationPayload["smimeencryptionCertificateUUID"], + "emailSMIMEEnablePerMessageSwitch": operationPayload["smimeenablePerMessageSwitch"] + }; + break; + case iosOperationConstants["AIRPLAY_OPERATION_CODE"]: + payload = { + "airplayDestinations": operationPayload["airPlayDestinations"], + "airplayCredentials": operationPayload["airPlayCredentials"] + }; + break; + case iosOperationConstants["LDAP_OPERATION_CODE"]: + payload = { + "ldapAccountDescription": operationPayload["accountDescription"], + "ldapAccountHostname": operationPayload["accountHostName"], + "ldapUseSSL": operationPayload["accountUseSSL"], + "ldapAccountUsername": operationPayload["accountUsername"], + "ldapAccountPassword": operationPayload["accountPassword"], + "ldapSearchSettings": operationPayload["ldapSearchSettings"] + }; + break; + case iosOperationConstants["CALENDAR_OPERATION_CODE"]: + payload = { + "calendarAccountDescription": operationPayload["accountDescription"], + "calendarAccountHostname": operationPayload["hostName"], + "calendarAccountUsername": operationPayload["username"], + "calendarAccountPassword": operationPayload["password"], + "calendarUseSSL": operationPayload["useSSL"], + "calendarAccountPort": operationPayload["port"], + "calendarPrincipalURL": operationPayload["principalURL"] + }; + break; + case iosOperationConstants["CALENDAR_SUBSCRIPTION_OPERATION_CODE"]: + payload = { + "calendarSubscriptionDescription": operationPayload["accountDescription"], + "calendarSubscriptionHostname": operationPayload["hostName"], + "calendarSubscriptionUsername": operationPayload["username"], + "calendarSubscriptionPassword": operationPayload["password"], + "calendarSubscriptionUseSSL": operationPayload["useSSL"] + }; + break; + case iosOperationConstants["APN_OPERATION_CODE"]: + payload = { + "apnConfigurations": operationPayload["apnConfigurations"] + }; + break; + case iosOperationConstants["CELLULAR_OPERATION_CODE"]: + payload = { + "cellularAttachAPNName": operationPayload["attachAPNName"], + "cellularAuthenticationType": operationPayload["authenticationType"], + "cellularUsername": operationPayload["username"], + "cellularPassword": operationPayload["password"], + "cellularAPNConfigurations": operationPayload["apnConfigurations"] + }; + break; + } + return payload; + }; + + privateMethods.generateIOSOperationPayload = function (operationCode, operationData, deviceList) { + var payload; + var operationType; + switch (operationCode) { + case iosOperationConstants["PASSCODE_POLICY_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "forcePIN": operationData["passcodePolicyForcePIN"], + "allowSimple": operationData["passcodePolicyAllowSimple"], + "requireAlphanumeric": operationData["passcodePolicyRequireAlphanumeric"], + "minLength": operationData["passcodePolicyMinLength"], + "minComplexChars": operationData["passcodePolicyMinComplexChars"], + "maxPINAgeInDays": operationData["passcodePolicyMaxPasscodeAgeInDays"], + "pinHistory": operationData["passcodePolicyPasscodeHistory"], + "maxInactivity": operationData["passcodePolicyMaxAutoLock"], + "maxGracePeriod": operationData["passcodePolicyGracePeriod"], + "maxFailedAttempts": operationData["passcodePolicyMaxFailedAttempts"] + } + }; + break; + case iosOperationConstants["WIFI_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "ssid": operationData["wifiSSID"], + "hiddenNetwork": operationData["wifiHiddenNetwork"], + "autoJoin": operationData["wifiAutoJoin"], + "proxyType": operationData["wifiProxyType"], + "encryptionType": operationData["wifiEncryptionType"], + "hotspot": operationData["wifiIsHotSpot"], + "domainName": operationData["wifiDomainName"], + "serviceProviderRoamingEnabled": operationData["wifiServiceProviderRoamingEnabled"], + "displayedOperatorName": operationData["wifiDisplayedOperatorName"], + "roamingConsortiumOIs": operationData["wifiRoamingConsortiumOIs"], + "password": operationData["wifiPassword"], + "clientConfiguration": { + "username": operationData["wifiEAPUsername"], + "acceptEAPTypes": operationData["wifiAcceptedEAPTypes"], + "userPassword": operationData["wifiEAPPassword"], + "oneTimePassword": operationData["wifiEAPOneTimePassword"], + "payloadCertificateAnchorUUID": operationData["wifiPayloadCertificateAnchorUUIDs"], + "outerIdentity": operationData["wifiEAPOuterIdentity"], + "tlstrustedServerNames": operationData["wifiTLSTrustedServerNames"], + "tlsallowTrustExceptions": operationData["wifiEAPTLSAllowTrustExceptions"], + "tlscertificateIsRequired": operationData["wifiEAPTLSCertIsRequired"], + "ttlsinnerAuthentication": operationData["wifiEAPTLSInnerAuthType"], + "eapfastusePAC": operationData["wifiEAPFastUsePAC"], + "eapfastprovisionPAC": operationData["wifiEAPFastProvisionPAC"], + "eapfastprovisionPACAnonymously": operationData["wifiEAPFastProvisionPACAnonymously"], + "eapsimnumberOfRANDs": operationData["wifiEAPSIMNoOfRands"] + }, + "payloadCertificateUUID": operationData["wifiPayloadCertUUID"], + "proxyServer": operationData["wifiProxyServer"], + "proxyPort": operationData["wifiProxyPort"], + "proxyUsername": operationData["wifiProxyUsername"], + "proxyPassword": operationData["wifiProxyPassword"], + "proxyPACURL": operationData["wifiProxyPACURL"], + "proxyPACFallbackAllowed": operationData["wifiProxyPACFallbackAllowed"], + "nairealmNames": operationData["wifiNAIRealmNames"], + "mccandMNCs": operationData["wifiMCCAndMNCs"] + } + }; + break; + case iosOperationConstants["RESTRICTIONS_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "allowAccountModification": operationData["restrictionsAllowAccountModification"], + "allowAddingGameCenterFriends": operationData["restrictionsAllowAddingGameCenterFriends"], + "allowAirDrop": operationData["restrictionsAllowAirDrop"], + "allowAppCellularDataModification": operationData["restrictionsAllowAppCellularDataModification"], + "allowAppInstallation": operationData["restrictionsAllowAppInstallation"], + "allowAppRemoval": operationData["restrictionsAllowAppRemoval"], + "allowAssistant": operationData["restrictionsAllowAssistant"], + "allowAssistantUserGeneratedContent": operationData["restrictionsAllowAssistantUserGeneratedContent"], + "allowAssistantWhileLocked": operationData["restrictionsAllowAssistantWhileLocked"], + "allowBookstore": operationData["restrictionsAllowBookstore"], + "allowBookstoreErotica": operationData["restrictionsAllowBookstoreErotica"], + "allowCamera": operationData["restrictionsAllowCamera"], + "allowChat": operationData["restrictionsAllowChat"], + "allowCloudBackup": operationData["restrictionsAllowCloudBackup"], + "allowCloudDocumentSync": operationData["restrictionsAllowCloudDocumentSync"], + "allowCloudKeychainSync": operationData["restrictionsAllowCloudKeychainSync"], + "allowDiagnosticSubmission": operationData["restrictionsAllowDiagnosticSubmission"], + "allowExplicitContent": operationData["restrictionsAllowExplicitContent"], + "allowFindMyFriendsModification": operationData["restrictionsAllowFindMyFriendsModification"], + "allowFingerprintForUnlock": operationData["restrictionsAllowFingerprintForUnlock"], + "allowGameCenter": operationData["restrictionsAllowGameCenter"], + "allowGlobalBackgroundFetchWhenRoaming": operationData["restrictionsAllowGlobalBackgroundFetchWhenRoaming"], + "allowInAppPurchases": operationData["restrictionsAllowInAppPurchases"], + "allowLockScreenControlCenter": operationData["restrictionsAllowLockScreenControlCenter"], + "allowHostPairing": operationData["restrictionsAllowHostPairing"], + "allowLockScreenNotificationsView": operationData["restrictionsAllowLockScreenNotificationsView"], + "allowLockScreenTodayView": operationData["restrictionsAllowLockScreenTodayView"], + "allowMultiplayerGaming": operationData["restrictionsAllowMultiplayerGaming"], + "allowOpenFromManagedToUnmanaged": operationData["restrictionsAllowOpenFromManagedToUnmanaged"], + "allowOpenFromUnmanagedToManaged": operationData["restrictionsAllowOpenFromUnmanagedToManaged"], + "allowOTAPKIUpdates": operationData["restrictionsAllowOTAPKIUpdates"], + "allowPassbookWhileLocked": operationData["restrictionsAllowPassbookWhileLocked"], + "allowPhotoStream": operationData["restrictionsAllowPhotoStream"], + "allowSafari": operationData["restrictionsAllowSafari"], + "safariAllowAutoFill": operationData["restrictionsSafariAllowAutoFill"], + "safariForceFraudWarning": operationData["restrictionsSafariForceFraudWarning"], + "safariAllowJavaScript": operationData["restrictionsSafariAllowJavaScript"], + "safariAllowPopups": operationData["restrictionsSafariAllowPopups"], + "allowScreenShot": operationData["restrictionsAllowScreenShot"], + "allowSharedStream": operationData["restrictionsAllowSharedStream"], + "allowUIConfigurationProfileInstallation": operationData["restrictionsAllowUIConfigurationProfileInstallation"], + "allowUntrustedTLSPrompt": operationData["restrictionsAllowUntrustedTLSPrompt"], + "allowVideoConferencing": operationData["restrictionsAllowVideoConferencing"], + "allowVoiceDialing": operationData["restrictionsAllowVoiceDialing"], + "allowYouTube": operationData["restrictionsAllowYouTube"], + "allowiTunes": operationData["restrictionsAllowITunes"], + "forceAssistantProfanityFilter": operationData["restrictionsForceAssistantProfanityFilter"], + "forceEncryptedBackup": operationData["restrictionsForceEncryptedBackup"], + "forceITunesStorePasswordEntry": operationData["restrictionsForceITunesStorePasswordEntry"], + "forceLimitAdTracking": operationData["restrictionsForceLimitAdTracking"], + "forceAirPlayOutgoingRequestsPairingPassword": operationData["restrictionsForceAirPlayOutgoingRequestsPairingPassword"], + "forceAirPlayIncomingRequestsPairingPassword": operationData["restrictionsForceAirPlayIncomingRequestsPairingPassword"], + "allowManagedAppsCloudSync": operationData["restrictionsAllowManagedAppsCloudSync"], + "allowEraseContentAndSettings": operationData["restrictionsAllowEraseContentAndSettings"], + "allowSpotlightInternetResults": operationData["restrictionsAllowSpotlightInternetResults"], + "allowEnablingRestrictions": operationData["restrictionsAllowEnablingRestrictions"], + "allowActivityContinuation": operationData["restrictionsAllowActivityContinuation"], + "allowEnterpriseBookBackup": operationData["restrictionsAllowEnterpriseBookBackup"], + "allowEnterpriseBookMetadataSync": operationData["restrictionsAllowEnterpriseBookMetadataSync"], + "allowPodcasts": operationData["restrictionsAllowPodcasts"], + "allowDefinitionLookup": operationData["restrictionsAllowDefinitionLookup"], + "allowPredictiveKeyboard": operationData["restrictionsAllowPredictiveKeyboard"], + "allowAutoCorrection": operationData["restrictionsAllowAutoCorrection"], + "allowSpellCheck": operationData["restrictionsAllowSpellCheck"], + "safariAcceptCookies": operationData["restrictionsSafariAcceptCookies"], + "autonomousSingleAppModePermittedAppIDs": operationData["restrictionsAutonomousSingleAppModePermittedAppIDs"] + } + }; + break; + case iosOperationConstants["EMAIL_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "emailAccountDescription": operationData["emailAccountDescription"], + "emailAccountName": operationData["emailAccountName"], + "emailAccountType": operationData["emailAccountType"], + "emailAddress": operationData["emailAddress"], + "incomingMailServerAuthentication": operationData["emailIncomingMailServerAuthentication"], + "incomingMailServerHostName": operationData["emailIncomingMailServerHostname"], + "incomingMailServerPortNumber": operationData["emailIncomingMailServerPort"], + "incomingMailServerUseSSL": operationData["emailIncomingUseSSL"], + "incomingMailServerUsername": operationData["emailIncomingMailServerUsername"], + "incomingPassword": operationData["emailIncomingMailServerPassword"], + "outgoingPassword": operationData["emailOutgoingMailServerPassword"], + "outgoingPasswordSameAsIncomingPassword": operationData["emailOutgoingPasswordSameAsIncomingPassword"], + "outgoingMailServerAuthentication": operationData["emailOutgoingMailServerAuthentication"], + "outgoingMailServerHostName": operationData["emailOutgoingMailServerHostname"], + "outgoingMailServerPortNumber": operationData["emailOutgoingMailServerPort"], + "outgoingMailServerUseSSL": operationData["emailOutgoingUseSSL"], + "outgoingMailServerUsername": operationData["emailOutgoingMailServerUsername"], + "preventMove": operationData["emailPreventMove"], + "preventAppSheet": operationData["emailPreventAppSheet"], + "disableMailRecentSyncing": operationData["emailDisableMailRecentSyncing"], + "incomingMailServerIMAPPathPrefix": operationData["emailIncomingMailServerIMAPPathPrefix"], + "smimeenabled": operationData["emailSMIMEEnabled"], + "smimesigningCertificateUUID": operationData["emailSMIMESigningCertificateUUID"], + "smimeencryptionCertificateUUID": operationData["emailSMIMEEncryptionCertificateUUID"], + "smimeenablePerMessageSwitch": operationData["emailSMIMEEnablePerMessageSwitch"] + } + }; + break; + case iosOperationConstants["AIRPLAY_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "airPlayDestinations": operationData["airplayDestinations"], + "airPlayCredentials": operationData["airplayCredentials"] + } + }; + break; + case iosOperationConstants["LDAP_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "accountDescription": operationData["ldapAccountDescription"], + "accountHostName": operationData["ldapAccountHostname"], + "accountUseSSL": operationData["ldapUseSSL"], + "accountUsername": operationData["ldapAccountUsername"], + "accountPassword": operationData["ldapAccountPassword"], + "ldapSearchSettings": operationData["ldapSearchSettings"] + } + }; + break; + case iosOperationConstants["CALENDAR_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "accountDescription": operationData["calendarAccountDescription"], + "hostName": operationData["calendarAccountHostname"], + "username": operationData["calendarAccountUsername"], + "password": operationData["calendarAccountPassword"], + "useSSL": operationData["calendarUseSSL"], + "port": operationData["calendarAccountPort"], + "principalURL": operationData["calendarPrincipalURL"] + } + }; + break; + case iosOperationConstants["CALENDAR_SUBSCRIPTION_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "accountDescription": operationData["calendarSubscriptionDescription"], + "hostName": operationData["calendarSubscriptionHostname"], + "username": operationData["calendarSubscriptionUsername"], + "password": operationData["calendarSubscriptionPassword"], + "useSSL": operationData["calendarSubscriptionUseSSL"] + } + }; + break; + case iosOperationConstants["APN_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "apnConfigurations": operationData["apnConfigurations"] + } + }; + break; + case iosOperationConstants["CELLULAR_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "attachAPNName": operationData["cellularAttachAPNName"], + "authenticationType": operationData["cellularAuthenticationType"], + "username": operationData["cellularUsername"], + "password": operationData["cellularPassword"], + "apnConfigurations": operationData["cellularAPNConfigurations"] + } + }; + break; + case iosOperationConstants["NOTIFICATION_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "message": operationData["message"] + } + }; + break; + default: + // If the operation is neither of above, it is a command operation + operationType = operationTypeConstants["COMMAND"]; + // Operation payload of a command operation is simply an array of device IDs + payload = deviceList; + } + + if (operationType == operationTypeConstants["PROFILE"] && deviceList) { + payload["deviceIDs"] = deviceList; + } + return payload; + }; + + /** + * Convert the android platform specific code to the generic payload. + * TODO: think of the possibility to follow a pattern to the key name (namespace?) + * @param operationCode + * @param operationPayload + * @returns {{}} + */ + privateMethods.generateGenericPayloadFromAndroidPayload = function (operationCode, operationPayload) { + var payload = {}; + operationPayload = JSON.parse(operationPayload); + switch (operationCode) { + case androidOperationConstants["PASSCODE_POLICY_OPERATION_CODE"]: + payload = { + "passcodePolicyAllowSimple": operationPayload["allowSimple"], + "passcodePolicyRequireAlphanumeric": operationPayload["requireAlphanumeric"], + "passcodePolicyMinLength": operationPayload["minLength"], + "passcodePolicyMinComplexChars": operationPayload["minComplexChars"], + "passcodePolicyMaxPasscodeAgeInDays": operationPayload["maxPINAgeInDays"], + "passcodePolicyPasscodeHistory": operationPayload["pinHistory"], + "passcodePolicyMaxFailedAttempts": operationPayload["maxFailedAttempts"] + }; + break; + case androidOperationConstants["CAMERA_OPERATION_CODE"]: + payload = { + "cameraEnabled": operationPayload["enabled"] + }; + break; + case androidOperationConstants["ENCRYPT_STORAGE_OPERATION_CODE"]: + payload = { + "encryptStorageEnabled": operationPayload["encrypted"] + }; + break; + case androidOperationConstants["WIFI_OPERATION_CODE"]: + payload = { + "wifiSSID": operationPayload["ssid"], + "wifiPassword": operationPayload["password"] + }; + break; + } + return payload; + }; + + privateMethods.generateAndroidOperationPayload = function (operationCode, operationData, deviceList) { + var payload; + var operationType; + switch (operationCode) { + case androidOperationConstants["CAMERA_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "enabled": operationData["cameraEnabled"] + } + }; + break; + case androidOperationConstants["CHANGE_LOCK_CODE_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "lockCode": operationData["lockCode"] + } + }; + break; + case androidOperationConstants["ENCRYPT_STORAGE_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "encrypted": operationData["encryptStorageEnabled"] + } + }; + break; + case androidOperationConstants["NOTIFICATION_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "message": operationData["message"] + } + }; + break; + case androidOperationConstants["WIPE_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "pin": operationData["pin"] + } + }; + break; + case androidOperationConstants["WIFI_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "ssid": operationData["wifiSSID"], + "password": operationData["wifiPassword"] + } + }; + break; + case androidOperationConstants["PASSCODE_POLICY_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "allowSimple": operationData["passcodePolicyAllowSimple"], + "requireAlphanumeric": operationData["passcodePolicyRequireAlphanumeric"], + "minLength": operationData["passcodePolicyMinLength"], + "minComplexChars": operationData["passcodePolicyMinComplexChars"], + "maxPINAgeInDays": operationData["passcodePolicyMaxPasscodeAgeInDays"], + "pinHistory": operationData["passcodePolicyPasscodeHistory"], + "maxFailedAttempts": operationData["passcodePolicyMaxFailedAttempts"] + } + }; + break; + default: + // If the operation is neither of above, it is a command operation + operationType = operationTypeConstants["COMMAND"]; + // Operation payload of a command operation is simply an array of device IDs + payload = deviceList; + } + + if (operationType == operationTypeConstants["PROFILE"] && deviceList) { + payload["deviceIDs"] = deviceList; + } + + return payload; + }; + + publicMethods.getAndroidServiceEndpoint = function (operationCode) { + var featureMap = { + "WIFI": "wifi", + "CAMERA": "camera", + "DEVICE_LOCK": "lock", + "DEVICE_LOCATION": "location", + "CLEAR_PASSWORD": "clear-password", + "APPLICATION_LIST": "get-application-list", + "DEVICE_RING": "ring-device", + "DEVICE_REBOOT": "reboot-device", + "UPGRADE_FIRMWARE": "upgrade-firmware", + "DEVICE_MUTE": "mute", + "NOTIFICATION": "notification", + "ENCRYPT_STORAGE": "encrypt", + "CHANGE_LOCK_CODE": "change-lock-code", + "WEBCLIP": "webclip", + "INSTALL_APPLICATION": "install-application", + "UNINSTALL_APPLICATION": "uninstall-application", + "BLACKLIST_APPLICATIONS": "blacklist-applications", + "PASSCODE_POLICY": "password-policy", + "ENTERPRISE_WIPE": "enterprise-wipe", + "WIPE_DATA": "wipe-data" + }; + return "/mdm-android-agent/operation/" + featureMap[operationCode]; + }; + + /** + * Convert the windows platform specific code to the generic payload. + * TODO: think of the possibility to follow a pattern to the key name (namespace?) + * @param operationCode + * @param operationPayload + * @returns {{}} + */ + privateMethods.generateGenericPayloadFromWindowsPayload = function (operationCode, operationPayload) { + var payload = {}; + operationPayload = JSON.parse(operationPayload); + switch (operationCode) { + case windowsOperationConstants["PASSCODE_POLICY_OPERATION_CODE"]: + payload = { + "passcodePolicyAllowSimple": operationPayload["allowSimple"], + "passcodePolicyRequireAlphanumeric": operationPayload["requireAlphanumeric"], + "passcodePolicyMinLength": operationPayload["minLength"], + "passcodePolicyMinComplexChars": operationPayload["minComplexChars"], + "passcodePolicyMaxPasscodeAgeInDays": operationPayload["maxPINAgeInDays"], + "passcodePolicyPasscodeHistory": operationPayload["pinHistory"], + "passcodePolicyMaxFailedAttempts": operationPayload["maxFailedAttempts"] + }; + break; + case windowsOperationConstants["CAMERA_OPERATION_CODE"]: + payload = { + "cameraEnabled": operationPayload["enabled"] + }; + break; + case windowsOperationConstants["ENCRYPT_STORAGE_OPERATION_CODE"]: + payload = { + "encryptStorageEnabled": operationPayload["encrypted"] + }; + break; + } + return payload; + }; + + privateMethods.generateWindowsOperationPayload = function (operationCode, operationData, deviceList) { + var payload; + var operationType; + switch (operationCode) { + case windowsOperationConstants["CAMERA_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "enabled": operationData["cameraEnabled"] + } + }; + break; + case windowsOperationConstants["CHANGE_LOCK_CODE_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "lockCode": operationData["lockCode"] + } + }; + break; + case windowsOperationConstants["ENCRYPT_STORAGE_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "encrypted": operationData["encryptStorageEnabled"] + } + }; + break; + case windowsOperationConstants["NOTIFICATION_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "message": operationData["message"] + } + }; + break; + case windowsOperationConstants["PASSCODE_POLICY_OPERATION_CODE"]: + operationType = operationTypeConstants["PROFILE"]; + payload = { + "operation": { + "allowSimple": operationData["passcodePolicyAllowSimple"], + "requireAlphanumeric": operationData["passcodePolicyRequireAlphanumeric"], + "minLength": operationData["passcodePolicyMinLength"], + "minComplexChars": operationData["passcodePolicyMinComplexChars"], + "maxPINAgeInDays": operationData["passcodePolicyMaxPasscodeAgeInDays"], + "pinHistory": operationData["passcodePolicyPasscodeHistory"], + "maxFailedAttempts": operationData["passcodePolicyMaxFailedAttempts"] + } + }; + break; + default: + // If the operation is neither of above, it is a command operation + operationType = operationTypeConstants["COMMAND"]; + // Operation payload of a command operation is simply an array of device IDs + payload = deviceList; + } + + if (operationType == operationTypeConstants["PROFILE"] && deviceList) { + payload["deviceIDs"] = deviceList; + } + + return payload; + }; + + + publicMethods.getWindowsServiceEndpoint = function (operationCode) { + var featureMap = { + "CAMERA": "camera", + "DEVICE_LOCK": "lock", + "DEVICE_LOCATION": "location", + "CLEAR_PASSWORD": "clear-password", + "APPLICATION_LIST": "get-application-list", + "DEVICE_RING": "ring-device", + "DEVICE_REBOOT": "reboot-device", + "UPGRADE_FIRMWARE": "upgrade-firmware", + "DEVICE_MUTE": "mute", + "LOCK_RESET": "lock-reset", + "NOTIFICATION": "notification", + "ENCRYPT_STORAGE": "encrypt", + "CHANGE_LOCK_CODE": "change-lock-code", + "WEBCLIP": "webclip", + "INSTALL_APPLICATION": "install-application", + "UNINSTALL_APPLICATION": "uninstall-application", + "BLACKLIST_APPLICATIONS": "blacklist-applications", + "PASSCODE_POLICY": "password-policy", + "ENTERPRISE_WIPE": "enterprise-wipe", + "WIPE_DATA": "wipe-data", + "DISENROLL": "disenroll" + }; + return "/mdm-windows-agent/services/windows/operation/" + featureMap[operationCode]; + }; + /** + * Get the icon for the featureCode + * @param operationCode + * @returns icon class + */ + publicMethods.getAndroidIconForFeature = function (operationCode) { + var featureMap = { + "DEVICE_LOCK": "fw-lock", + "DEVICE_LOCATION": "fw-map-location", + "CLEAR_PASSWORD": "fw-key", + "ENTERPRISE_WIPE": "fw-clear", + "WIPE_DATA": "fw-database", + "DEVICE_RING": "fw-dial-up", + "DEVICE_REBOOT": "fw-refresh", + "UPGRADE_FIRMWARE": "fw-up-arrow", + "DEVICE_MUTE": "fw-incoming-call", + "NOTIFICATION": "fw-message", + "CHANGE_LOCK_CODE": "fw-security" + }; + return featureMap[operationCode]; + }; + + /** + * Get the icon for the featureCode + * @param operationCode + * @returns icon class + */ + publicMethods.getWindowsIconForFeature = function (operationCode) { + var featureMap = { + "DEVICE_LOCK": "fw-lock", + "DEVICE_LOCATION": "fw-map-location", + "DISENROLL": "fw-delete", + "WIPE_DATA": "fw-clear", + "DEVICE_RING": "fw-dial-up", + "DEVICE_REBOOT": "fw-refresh", + "UPGRADE_FIRMWARE": "fw-up-arrow", + "DEVICE_MUTE": "fw-incoming-call", + "NOTIFICATION": "fw-message", + "LOCK_RESET": "fw-key" + }; + return featureMap[operationCode]; + }; + + /** + * Get the icon for the featureCode + * @param operationCode + * @returns icon class + */ + publicMethods.getIOSIconForFeature = function (operationCode) { + var featureMap = { + "DEVICE_LOCK": "fw-lock", + "LOCATION": "fw-map-location", + "ENTERPRISE_WIPE": "fw-clear", + "NOTIFICATION": "fw-message", + "ALARM": "fw-dial-up" + }; + return featureMap[operationCode]; + }; + + /** + * Filter a list by a data attribute. + * @param prop + * @param val + * @returns {Array} + */ + $.fn.filterByData = function (prop, val) { + return this.filter( + function () { + return $(this).data(prop) == val; + } + ); + }; + + /** + * Method to generate Platform specific operation payload. + * + * @param platformType Platform Type of the profile + * @param operationCode Operation Codes to generate the profile from + * @param deviceList Optional device list to include in payload body for operations + * @returns {*} + */ + publicMethods.generatePayload = function (platformType, operationCode, deviceList) { + var payload; + var operationData = {}; + // capturing form input data designated by .operationDataKeys + $(".operation-data").filterByData("operation-code", operationCode).find(".operationDataKeys").each( + function () { + var operationDataObj = $(this); + var key = operationDataObj.data("key"); + var value; + if (operationDataObj.is(":text") || operationDataObj.is("textarea") || + operationDataObj.is(":password")) { + value = operationDataObj.val(); + } else if (operationDataObj.is(":checkbox")) { + value = operationDataObj.is(":checked"); + } else if (operationDataObj.is("select")) { + value = operationDataObj.find("option:selected").attr("value"); + } else if (operationDataObj.hasClass("grouped-array-input")) { + value = []; + var childInput; + var childInputValue; + if (operationDataObj.hasClass("one-column-input-array")) { + $(".child-input", this).each(function () { + childInput = $(this); + if (childInput.is(":text") || childInput.is("textarea") || childInput.is(":password")) { + childInputValue = childInput.val(); + } else if (childInput.is(":checkbox")) { + childInputValue = childInput.is(":checked"); + } else if (childInput.is("select")) { + childInputValue = childInput.find("option:selected").attr("value"); + } + // push to value + value.push(childInputValue); + }); + } else if (operationDataObj.hasClass("valued-check-box-array")) { + $(".child-input", this).each(function () { + childInput = $(this); + if (childInput.is(":checked")) { + // get associated value with check-box + childInputValue = childInput.data("value"); + // push to value + value.push(childInputValue); + } + }); + } else if (operationDataObj.hasClass("multi-column-joined-input-array")) { + var columnCount = operationDataObj.data("column-count"); + var inputCount = 0; + var joinedInput; + $(".child-input", this).each(function () { + childInput = $(this); + if (childInput.is(":text") || childInput.is("textarea") || childInput.is(":password")) { + childInputValue = childInput.val(); + } else if (childInput.is(":checkbox")) { + childInputValue = childInput.is(":checked"); + } else if (childInput.is("select")) { + childInputValue = childInput.find("option:selected").attr("value"); + } + inputCount++; + if (inputCount % columnCount == 1) { + // initialize joinedInput value + joinedInput = ""; + // append childInputValue to joinedInput + joinedInput += childInputValue; + } else if ((inputCount % columnCount) >= 2) { + // append childInputValue to joinedInput + joinedInput += childInputValue; + } else { + // append childInputValue to joinedInput + joinedInput += childInputValue; + // push to value + value.push(joinedInput); + } + }); + } else if (operationDataObj.hasClass("multi-column-key-value-pair-array")) { + columnCount = operationDataObj.data("column-count"); + inputCount = 0; + var childInputKey; + var keyValuePairJson; + $(".child-input", this).each(function () { + childInput = $(this); + childInputKey = childInput.data("child-key"); + if (childInput.is(":text") || childInput.is("textarea") || childInput.is(":password")) { + childInputValue = childInput.val(); + } else if (childInput.is(":checkbox")) { + childInputValue = childInput.is(":checked"); + } else if (childInput.is("select")) { + childInputValue = childInput.find("option:selected").attr("value"); + } + inputCount++; + if ((inputCount % columnCount) == 1) { + // initialize keyValuePairJson value + keyValuePairJson = {}; + // set key-value-pair + keyValuePairJson[childInputKey] = childInputValue; + } else if ((inputCount % columnCount) >= 2) { + // set key-value-pair + keyValuePairJson[childInputKey] = childInputValue; + } else { + // set key-value-pair + keyValuePairJson[childInputKey] = childInputValue; + // push to value + value.push(keyValuePairJson); + } + }); + } + } + operationData[key] = value; + } + ); + switch (platformType) { + case platformTypeConstants["ANDROID"]: + payload = privateMethods.generateAndroidOperationPayload(operationCode, operationData, deviceList); + break; + case platformTypeConstants["IOS"]: + payload = privateMethods.generateIOSOperationPayload(operationCode, operationData, deviceList); + break; + case platformTypeConstants["WINDOWS"]: + payload = privateMethods.generateWindowsOperationPayload(operationCode, operationData, deviceList); + break; + } + return payload; + }; + + /** + * Method to populate the Platform specific operation payload. + * + * @param platformType Platform Type of the profile + * @param operationCode Operation Codes to generate the profile from + * @param operationPayload payload + * @returns {*} + */ + publicMethods.populateUI = function (platformType, operationCode, operationPayload) { + var uiPayload; + switch (platformType) { + case platformTypeConstants["ANDROID"]: + uiPayload = privateMethods.generateGenericPayloadFromAndroidPayload(operationCode, operationPayload); + break; + case platformTypeConstants["IOS"]: + uiPayload = privateMethods.generateGenericPayloadFromIOSPayload(operationCode, operationPayload); + break; + case platformTypeConstants["WINDOWS"]: + uiPayload = privateMethods.generateGenericPayloadFromWindowsPayload(operationCode, operationPayload); + break; + } + // capturing form input data designated by .operationDataKeys + $(".operation-data").filterByData("operation-code", operationCode).find(".operationDataKeys").each( + function () { + var operationDataObj = $(this); + //TODO :remove + //operationDataObj.prop('disabled', true) + var key = operationDataObj.data("key"); + // retrieve corresponding input value associated with the key + var value = uiPayload[key]; + // populating input value according to the type of input + if (operationDataObj.is(":text") || + operationDataObj.is("textarea") || + operationDataObj.is(":password")) { + operationDataObj.val(value); + } else if (operationDataObj.is(":checkbox")) { + operationDataObj.prop("checked", value); + } else if (operationDataObj.is("select")) { + operationDataObj.val(value); + /* trigger a change of value, so that if slidable panes exist, + make them slide-down or slide-up accordingly */ + operationDataObj.trigger("change"); + } else if (operationDataObj.hasClass("grouped-array-input")) { + // then value is complex + var i, childInput; + var childInputIndex = 0; + // var childInputValue; + if (operationDataObj.hasClass("one-column-input-array")) { + // generating input fields to populate complex value + for (i = 0; i < value.length; ++i) { + operationDataObj.parent().find("a").filterByData("click-event", "add-form").click(); + } + // traversing through each child input + $(".child-input", this).each(function () { + childInput = $(this); + var childInputValue = value[childInputIndex]; + // populating extracted value in the UI according to the input type + if (childInput.is(":text") || + childInput.is("textarea") || + childInput.is(":password") || + childInput.is("select")) { + childInput.val(childInputValue); + } else if (childInput.is(":checkbox")) { + operationDataObj.prop("checked", childInputValue); + } + // incrementing childInputIndex + childInputIndex++; + }); + } else if (operationDataObj.hasClass("valued-check-box-array")) { + // traversing through each child input + $(".child-input", this).each(function () { + childInput = $(this); + // check if corresponding value of current checkbox exists in the array of values + if (value.indexOf(childInput.data("value")) != -1) { + // if YES, set checkbox as checked + childInput.prop("checked", true); + } + }); + } else if (operationDataObj.hasClass("multi-column-joined-input-array")) { + // generating input fields to populate complex value + for (i = 0; i < value.length; ++i) { + operationDataObj.parent().find("a").filterByData("click-event", "add-form").click(); + } + var columnCount = operationDataObj.data("column-count"); + var multiColumnJoinedInputArrayIndex = 0; + // handling scenarios specifically + if (operationDataObj.attr("id") == "wifi-mcc-and-mncs") { + // traversing through each child input + $(".child-input", this).each(function () { + childInput = $(this); + var multiColumnJoinedInput = value[multiColumnJoinedInputArrayIndex]; + var childInputValue; + if ((childInputIndex % columnCount) == 0) { + childInputValue = multiColumnJoinedInput.substring(3, 0) + } else { + childInputValue = multiColumnJoinedInput.substring(3); + // incrementing childInputIndex + multiColumnJoinedInputArrayIndex++; + } + // populating extracted value in the UI according to the input type + if (childInput.is(":text") || + childInput.is("textarea") || + childInput.is(":password") || + childInput.is("select")) { + childInput.val(childInputValue); + } else if (childInput.is(":checkbox")) { + operationDataObj.prop("checked", childInputValue); + } + // incrementing childInputIndex + childInputIndex++; + }); + } + } else if (operationDataObj.hasClass("multi-column-key-value-pair-array")) { + // generating input fields to populate complex value + for (i = 0; i < value.length; ++i) { + operationDataObj.parent().find("a").filterByData("click-event", "add-form").click(); + } + columnCount = operationDataObj.data("column-count"); + var multiColumnKeyValuePairArrayIndex = 0; + // traversing through each child input + $(".child-input", this).each(function () { + childInput = $(this); + var multiColumnKeyValuePair = value[multiColumnKeyValuePairArrayIndex]; + var childInputKey = childInput.data("child-key"); + var childInputValue = multiColumnKeyValuePair[childInputKey]; + // populating extracted value in the UI according to the input type + if (childInput.is(":text") || + childInput.is("textarea") || + childInput.is(":password") || + childInput.is("select")) { + childInput.val(childInputValue); + } else if (childInput.is(":checkbox")) { + operationDataObj.prop("checked", childInputValue); + } + // incrementing multiColumnKeyValuePairArrayIndex for the next row of inputs + if ((childInputIndex % columnCount) == (columnCount - 1)) { + multiColumnKeyValuePairArrayIndex++; + } + // incrementing childInputIndex + childInputIndex++; + }); + } + } + } + ); + }; + + /** + * generateProfile method is only used for policy-creation UIs. + * + * @param platformType Platform Type of the profile + * @param operationCodes Operation codes to generate the profile from + * @returns {{}} + */ + publicMethods.generateProfile = function (platformType, operationCodes) { + var generatedProfile = {}; + for (var i = 0; i < operationCodes.length; ++i) { + var operationCode = operationCodes[i]; + var payload = publicMethods.generatePayload(platformType, operationCode, null); + generatedProfile[operationCode] = payload["operation"]; + } + return generatedProfile; + }; + + /** + * populateProfile method is used to populate the html ui with saved payload. + * + * @param platformType Platform Type of the profile + * @param payload List of profileFeatures + * @returns [] configuredOperations array + */ + publicMethods.populateProfile = function (platformType, payload) { + var i, configuredOperations = []; + for (i = 0; i < payload.length; ++i) { + var configuredFeature = payload[i]; + var featureCode = configuredFeature["featureCode"]; + var operationPayload = configuredFeature["content"]; + //push the feature-code to the configuration array + configuredOperations.push(featureCode); + publicMethods.populateUI(platformType, featureCode, operationPayload); + } + return configuredOperations; + }; + + return publicMethods; +}(); diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.platform.configuration/configuration.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.platform.configuration/configuration.hbs new file mode 100644 index 000000000..293177871 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.platform.configuration/configuration.hbs @@ -0,0 +1,484 @@ +
      +
      + +
      +
      + General and Platform Specific Server Settings for the Tenant +
      +
      +
      +
      + + +
      + + + +
      +
      +
      + +

      + Policy Compliance Monitoring +
      +

      +
      + + +
      +
      + +
      +
      + + +
      +
      + + + +
      +
      +
      + +

      + Communication Protocol Configuration +
      +

      +
      + + +
      + +
      +
      + + +
      +
      + +
      +
      + + +
      + +
      + + +
      +
      +

      + End User License Agreement ( EULA ) +
      +

      + +
      + +
      +
      + +
      +
      +
      +
      + + + +
      +
      +
      + +

      + iOS SCEP Certificate Configurations +
      +

      + + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +

      + iOS Profile Configurations +
      +

      + + +
      + + +
      + +

      + iOS MDM Configurations +
      +

      + + +
      +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +

      + iOS APNS Configurations +
      +

      + + +
      +
      + + +
      + +
      + + +
      + +
      + + +
      + +

      + End User License Agreement (EULA) +
      +

      + +
      + +
      +
      + +
      +
      +
      +
      + + + +
      +
      +
      + +

      + Device Polling Configuration +
      +

      +
      +
      + + +
      +
      +

      + End User License Agreement ( EULA ) +
      +

      +
      + +
      +
      + +
      +
      +
      +
      + +
      +
      +
      +
      + + + +
      +
      +{{#zone "bottomJs"}} + {{js "js/platform-configuration.js"}} +{{/zone}} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.platform.configuration/configuration.js b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.platform.configuration/configuration.js new file mode 100644 index 000000000..354702f18 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.platform.configuration/configuration.js @@ -0,0 +1,9 @@ +function onRequest(context) { + // var log = new Log("platform-configuration-unit backend js"); + var userModule = require("/app/modules/user.js")["userModule"]; + var typesListResponse = userModule.getPlatforms(); + if (typesListResponse["status"] == "success") { + context["types"] = typesListResponse["content"]; + } + return context; +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.platform.configuration/configuration.json b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.platform.configuration/configuration.json new file mode 100644 index 000000000..be0496bf6 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.platform.configuration/configuration.json @@ -0,0 +1,4 @@ +{ + "version" : "1.0.0", + "extends": "cdmf.unit.platform.configuration" +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.platform.configuration/public/js/platform-configuration.js b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.platform.configuration/public/js/platform-configuration.js new file mode 100644 index 000000000..7bbbad0b4 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.platform.configuration/public/js/platform-configuration.js @@ -0,0 +1,878 @@ +/* + * 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. + */ + +/** + * Checks if provided input is valid against RegEx input. + * + * @param regExp Regular expression + * @param inputString Input string to check + * @returns {boolean} Returns true if input matches RegEx + */ +function inputIsValid(regExp, inputString) { + return regExp.test(inputString); +} + +/** + * Checks if provided input is valid against RegEx input. + * + * @param regExp Regular expression + * @param inputString Input string to check + * @returns {boolean} Returns true if input matches RegEx + */ +function isPositiveInteger(str) { + return /^\+?(0|[1-9]\d*)$/.test(str); +} + +/** + * Get valid param. + * + * @param certificate + * @param cached param (in the registry) + * @returns {String} Returns the valid param + */ +function validateCertificateParams(param, cachedParam) { + if (param == '' && cachedParam != null) { + return cachedParam; + } else { + return param; + } +} + +/** + * Checks if an email address has the valid format or not. + * + * @param email Email address + * @returns {boolean} true if email has the valid format, otherwise false. + */ +function emailIsValid(email) { + var regExp = /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/; + return regExp.test(email); +} + +var iOSMDMCertificateName = null; +var iOSMDMCertificate = null; +var iOSAPNSCertificateName = null; +var iOSAPNSCertificate = null; + +var notifierTypeConstants = { + "LOCAL": "1", + "GCM": "2" +}; +// Constants to define platform types available +var platformTypeConstants = { + "ANDROID": "android", + "IOS": "ios", + "WINDOWS": "windows" +}; + +var responseCodes = { + "CREATED": "Created", + "SUCCESS": "201", + "INTERNAL_SERVER_ERROR": "Internal Server Error" +}; + +var configParams = { + "NOTIFIER_TYPE": "notifierType", + "NOTIFIER_FREQUENCY": "notifierFrequency", + "GCM_API_KEY": "gcmAPIKey", + "GCM_SENDER_ID": "gcmSenderId", + "ANDROID_EULA": "androidEula", + "IOS_EULA": "iosEula", + "CONFIG_COUNTRY": "configCountry", + "CONFIG_STATE": "configState", + "CONFIG_LOCALITY": "configLocality", + "CONFIG_ORGANIZATION": "configOrganization", + "CONFIG_ORGANIZATION_UNIT": "configOrganizationUnit", + "MDM_CERT_PASSWORD": "MDMCertPassword", + "MDM_CERT_TOPIC_ID": "MDMCertTopicID", + "APNS_CERT_PASSWORD": "APNSCertPassword", + "MDM_CERT": "MDMCert", + "MDM_CERT_NAME": "MDMCertName", + "APNS_CERT": "APNSCert", + "APNS_CERT_NAME": "APNSCertName", + "ORG_DISPLAY_NAME": "organizationDisplayName", + "GENERAL_EMAIL_HOST": "emailHost", + "GENERAL_EMAIL_PORT": "emailPort", + "GENERAL_EMAIL_USERNAME": "emailUsername", + "GENERAL_EMAIL_PASSWORD": "emailPassword", + "GENERAL_EMAIL_SENDER_ADDRESS": "emailSender", + "GENERAL_EMAIL_TEMPLATE": "emailTemplate", + "COMMON_NAME": "commonName", + "KEYSTORE_PASSWORD": "keystorePassword", + "PRIVATE_KEY_PASSWORD": "privateKeyPassword", + "BEFORE_EXPIRE": "beforeExpire", + "AFTER_EXPIRE": "afterExpire", + "WINDOWS_EULA": "windowsLicense" +}; + +function promptErrorPolicyPlatform(errorMsg) { + var mainErrorMsgWrapper = "#platform-config-main-error-msg"; + var mainErrorMsg = mainErrorMsgWrapper + " span"; + $(mainErrorMsg).text(errorMsg); + $(mainErrorMsgWrapper).show(); +} + +$(document).ready(function () { + + var platformsSupported = $("#typeDiv").attr("typeData"); + $("#gcm-inputs").hide(); + tinymce.init({ + selector: "textarea", + height: 500, + theme: "modern", + plugins: [ + "autoresize", + "advlist autolink lists link image charmap print preview anchor", + "searchreplace visualblocks code fullscreen", + "insertdatetime image table contextmenu paste" + ], + toolbar: "undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image" + }); + + var getAndroidConfigAPI = "/mdm-android-agent/configuration"; + var getGeneralConfigAPI = "/devicemgt_admin/configuration"; + var getIosConfigAPI = "/ios/configuration"; + var getWindowsConfigAPI = "/mdm-windows-agent/services/configuration"; + + /** + * Following requests would execute + * on page load event of platform configuration page in WSO2 EMM Console. + * Upon receiving the response, the parameters will be set to the fields, + * in case those configurations are already set. + */ + + if (platformsSupported.indexOf('android') != -1) { + invokerUtil.get( + getAndroidConfigAPI, + function (data) { + data = JSON.parse(data); + if (data != null && data.configuration != null) { + for (var i = 0; i < data.configuration.length; i++) { + var config = data.configuration[i]; + if (config.name == configParams["NOTIFIER_TYPE"]) { + $("#android-config-notifier").val(config.value); + if (config.value != notifierTypeConstants["GCM"]) { + $("#gcm-inputs").hide(); + $("#local-inputs").show(); + } else { + $("#gcm-inputs").show(); + $("#local-inputs").hide(); + } + } else if (config.name == configParams["NOTIFIER_FREQUENCY"]) { + $("input#android-config-notifier-frequency").val(config.value / 1000); + } else if (config.name == configParams["GCM_API_KEY"]) { + $("input#android-config-gcm-api-key").val(config.value); + } else if (config.name == configParams["GCM_SENDER_ID"]) { + $("input#android-config-gcm-sender-id").val(config.value); + } else if (config.name == configParams["ANDROID_EULA"]) { + $("#android-eula").val(config.value); + } + } + } + }, function (data) { + console.log(data); + }); + } + + invokerUtil.get( + getGeneralConfigAPI, + function (data) { + data = JSON.parse(data); + if (data && data.configuration) { + for (var i = 0; i < data.configuration.length; i++) { + var config = data.configuration[i]; + if (config.name == configParams["NOTIFIER_FREQUENCY"]) { + $("input#monitoring-config-frequency").val(config.value / 1000); + } + } + } + }, function (data) { + console.log(data); + }); + + if (platformsSupported.indexOf('windows') != -1) { + invokerUtil.get( + getWindowsConfigAPI, + function (data) { + data = JSON.parse(data); + if (data != null && data.configuration != null) { + for (var i = 0; i < data.configuration.length; i++) { + var config = data.configuration[i]; + if (config.name == configParams["NOTIFIER_FREQUENCY"]) { + $("input#windows-config-notifier-frequency").val(config.value / 1000); + } else if (config.name == configParams["WINDOWS_EULA"]) { + $("#windows-eula").val(config.value); + } + } + } + }, function (data) { + console.log(data); + } + ); + } + + if (platformsSupported.indexOf('ios') != -1) { + invokerUtil.get( + getIosConfigAPI, + function (data) { + data = JSON.parse(data); + if (data != null && data.configuration != null) { + for (var i = 0; i < data.configuration.length; i++) { + var config = data.configuration[i]; + if (config.name == configParams["CONFIG_COUNTRY"]) { + $("input#ios-config-country").val(config.value); + } else if (config.name == configParams["CONFIG_STATE"]) { + $("input#ios-config-state").val(config.value); + } else if (config.name == configParams["CONFIG_LOCALITY"]) { + $("input#ios-config-locality").val(config.value); + } else if (config.name == configParams["CONFIG_ORGANIZATION"]) { + $("input#ios-config-organization").val(config.value); + } else if (config.name == configParams["CONFIG_ORGANIZATION_UNIT"]) { + $("input#ios-config-organization-unit").val(config.value); + } else if (config.name == configParams["MDM_CERT_PASSWORD"]) { + $("input#ios-config-mdm-certificate-password").val(config.value); + } else if (config.name == configParams["MDM_CERT_TOPIC_ID"]) { + $("input#ios-config-mdm-certificate-topic-id").val(config.value); + } else if (config.name == configParams["APNS_CERT_PASSWORD"]) { + $("input#ios-config-apns-certificate-password").val(config.value); + } else if (config.name == configParams["MDM_CERT_NAME"]) { + $("#mdm-cert-file-name").html(config.value); + iOSMDMCertificateName = config.value; + } else if (config.name == configParams["MDM_CERT"]) { + iOSMDMCertificate = config.value; + } else if (config.name == configParams["APNS_CERT_NAME"]) { + $("#apns-cert-file-name").html(config.value); + iOSAPNSCertificateName = config.value; + } else if (config.name == configParams["APNS_CERT"]) { + iOSAPNSCertificate = config.value; + } else if (config.name == configParams["ORG_DISPLAY_NAME"]) { + $("input#ios-org-display-name").val(config.value); + } else if (config.name == configParams["IOS_EULA"]) { + $("#ios-eula").val(config.value); + } + } + } + }, function (data) { + console.log(data); + } + ); + } + + $("select.select2[multiple=multiple]").select2({ + tags: true + }); + + $("#android-config-notifier").change(function () { + var notifierType = $("#android-config-notifier").find("option:selected").attr("value"); + if (notifierType != notifierTypeConstants["GCM"]) { + $("#gcm-inputs").hide(); + $("#local-inputs").show(); + } else { + $("#local-inputs").hide(); + $("#gcm-inputs").show(); + } + }); + + /** + * Following click function would execute + * when a user clicks on "Save" button + * on Android platform configuration page in WSO2 EMM Console. + */ + $("button#save-android-btn").click(function () { + var notifierType = $("#android-config-notifier").find("option:selected").attr("value"); + var notifierFrequency = $("input#android-config-notifier-frequency").val(); + var gcmAPIKey = $("input#android-config-gcm-api-key").val(); + var gcmSenderId = $("input#android-config-gcm-sender-id").val(); + var androidLicense = tinymce.get('android-eula').getContent(); + + var errorMsgWrapper = "#android-config-error-msg"; + var errorMsg = "#android-config-error-msg span"; + if (notifierType == notifierTypeConstants["LOCAL"] && !notifierFrequency) { + $(errorMsg).text("Notifier frequency is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (notifierType == notifierTypeConstants["LOCAL"] && !isPositiveInteger(notifierFrequency)) { + $(errorMsg).text("Provided notifier frequency is invalid. "); + $(errorMsgWrapper).removeClass("hidden"); + } else if (notifierType == notifierTypeConstants["GCM"] && !gcmAPIKey) { + $(errorMsg).text("GCM API Key is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (notifierType == notifierTypeConstants["GCM"] && !gcmSenderId) { + $(errorMsg).text("GCM Sender ID is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else { + + var addConfigFormData = {}; + var configList = new Array(); + + var type = { + "name": configParams["NOTIFIER_TYPE"], + "value": notifierType, + "contentType": "text" + }; + + var frequency = { + "name": configParams["NOTIFIER_FREQUENCY"], + "value": String(notifierFrequency * 1000), + "contentType": "text" + }; + + var gcmKey = { + "name": configParams["GCM_API_KEY"], + "value": gcmAPIKey, + "contentType": "text" + }; + + var gcmId = { + "name": configParams["GCM_SENDER_ID"], + "value": gcmSenderId, + "contentType": "text" + }; + + var androidEula = { + "name": configParams["ANDROID_EULA"], + "value": androidLicense, + "contentType": "text" + }; + + configList.push(type); + configList.push(frequency); + configList.push(androidEula); + if (notifierType == notifierTypeConstants["GCM"]) { + configList.push(gcmKey); + configList.push(gcmId); + } + + addConfigFormData.type = platformTypeConstants["ANDROID"]; + addConfigFormData.configuration = configList; + + var addConfigAPI = "/mdm-android-agent/configuration"; + + invokerUtil.post( + addConfigAPI, + addConfigFormData, + function (data) { + data = JSON.parse(data); + if (data.responseCode == responseCodes["CREATED"]) { + $("#config-save-form").addClass("hidden"); + $("#record-created-msg").removeClass("hidden"); + } else if (data == 500) { + $(errorMsg).text("Exception occurred at backend."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (data == 403) { + $(errorMsg).text("Action was not permitted."); + $(errorMsgWrapper).removeClass("hidden"); + } else { + $(errorMsg).text("An unexpected error occurred."); + $(errorMsgWrapper).removeClass("hidden"); + } + + + }, function (data) { + data = data.status; + if (data == 500) { + $(errorMsg).text("Exception occurred at backend."); + } else if (data == 403) { + $(errorMsg).text("Action was not permitted."); + } else { + $(errorMsg).text("An unexpected error occurred."); + } + $(errorMsgWrapper).removeClass("hidden"); + } + ); + } + }); + + /** + * Following click function would execute + * when a user clicks on "Save" button + * on General platform configuration page in WSO2 EMM Console. + */ + $("button#save-general-btn").click(function () { + var notifierFrequency = $("input#monitoring-config-frequency").val(); + var errorMsgWrapper = "#email-config-error-msg"; + var errorMsg = "#email-config-error-msg span"; + + if (!notifierFrequency) { + $(errorMsg).text("Monitoring frequency is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!isPositiveInteger(notifierFrequency)) { + $(errorMsg).text("Provided monitoring frequency is invalid. "); + $(errorMsgWrapper).removeClass("hidden"); + } else { + var addConfigFormData = {}; + var configList = new Array(); + + var monitorFrequency = { + "name": configParams["NOTIFIER_FREQUENCY"], + "value": String((notifierFrequency * 1000)), + "contentType": "text" + }; + + configList.push(monitorFrequency); + addConfigFormData.configuration = configList; + + var addConfigAPI = "/devicemgt_admin/configuration"; + invokerUtil.post( + addConfigAPI, + addConfigFormData, + function (data) { + data = JSON.parse(data); + if (data.statusCode == responseCodes["SUCCESS"]) { + $("#config-save-form").addClass("hidden"); + $("#record-created-msg").removeClass("hidden"); + } else if (data == 500) { + $(errorMsg).text("Exception occurred at backend."); + } else if (data == 403) { + $(errorMsg).text("Action was not permitted."); + } else { + $(errorMsg).text("An unexpected error occurred."); + } + + $(errorMsgWrapper).removeClass("hidden"); + }, function (data) { + data = data.status; + if (data == 500) { + $(errorMsg).text("Exception occurred at backend."); + } else if (data == 403) { + $(errorMsg).text("Action was not permitted."); + } else { + $(errorMsg).text("An unexpected error occurred."); + } + $(errorMsgWrapper).removeClass("hidden"); + } + ); + } + }); + + var errorMsgWrapper = "#ios-config-error-msg"; + var errorMsg = "#ios-config-error-msg span"; + var fileTypes = ['pfx']; + var notSupportedError = false; + + var base64MDMCert = ""; + var fileInputMDMCert = $('#ios-config-mdm-certificate'); + var fileNameMDMCert = ""; + var invalidFormatMDMCert = false; + + var base64APNSCert = ""; + var fileInputAPNSCert = $('#ios-config-apns-certificate'); + var fileNameAPNSCert = ""; + var invalidFormatAPNSCert = false; + + $(fileInputMDMCert).change(function () { + + if (!window.File || !window.FileReader || !window.FileList || !window.Blob) { + $(errorMsg).text("The File APIs are not fully supported in this browser."); + $(errorMsgWrapper).removeClass("hidden"); + notSupportedError = true; + return; + } + + var file = fileInputMDMCert[0].files[0]; + fileNameMDMCert = file.name; + var extension = file.name.split('.').pop().toLowerCase(), + isSuccess = fileTypes.indexOf(extension) > -1; + + if (isSuccess) { + var fileReader = new FileReader(); + fileReader.onload = function (event) { + base64MDMCert = event.target.result; + }; + fileReader.readAsDataURL(file); + invalidFormatMDMCert = false; + } else { + base64MDMCert = ""; + invalidFormatMDMCert = true; + } + }); + + $(fileInputAPNSCert).change(function () { + + if (!window.File || !window.FileReader || !window.FileList || !window.Blob) { + $(errorMsg).text("The File APIs are not fully supported in this browser."); + $(errorMsgWrapper).removeClass("hidden"); + notSupportedError = true; + return; + } + + var file = fileInputAPNSCert[0].files[0]; + fileNameAPNSCert = file.name; + var extension = file.name.split('.').pop().toLowerCase(), + isSuccess = fileTypes.indexOf(extension) > -1; + + if (isSuccess) { + var fileReader = new FileReader(); + fileReader.onload = function (event) { + base64APNSCert = event.target.result; + }; + fileReader.readAsDataURL(file); + invalidFormatAPNSCert = false; + } else { + base64MDMCert = ""; + invalidFormatAPNSCert = true; + } + }); + + $("button#save-ios-btn").click(function () { + + var configCountry = $("#ios-config-country").val(); + var configState = $("#ios-config-state").val(); + var configLocality = $("#ios-config-locality").val(); + var configOrganization = $("#ios-config-organization").val(); + var configOrganizationUnit = $("#ios-config-organization-unit").val(); + var MDMCertPassword = $("#ios-config-mdm-certificate-password").val(); + var MDMCertTopicID = $("#ios-config-mdm-certificate-topic-id").val(); + var APNSCertPassword = $("#ios-config-apns-certificate-password").val(); + var configOrgDisplayName = $("#ios-org-display-name").val(); + var iosLicense = tinymce.get('ios-eula').getContent(); + + fileNameMDMCert = validateCertificateParams(fileNameMDMCert, iOSMDMCertificateName); + fileNameAPNSCert = validateCertificateParams(fileNameAPNSCert, iOSAPNSCertificateName); + base64MDMCert = validateCertificateParams(base64MDMCert, iOSMDMCertificate); + base64APNSCert = validateCertificateParams(base64APNSCert, iOSAPNSCertificate); + + if (!configCountry) { + $(errorMsg).text("SCEP country is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!configState) { + $(errorMsg).text("SCEP state is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!configLocality) { + $(errorMsg).text("SCEP locality is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!configOrganization) { + $(errorMsg).text("SCEP organization is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!configOrganizationUnit) { + $(errorMsg).text("SCEP organization unit is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!MDMCertPassword) { + $(errorMsg).text("MDM certificate password is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!MDMCertTopicID) { + $(errorMsg).text("MDM certificate topic ID is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!APNSCertPassword) { + $(errorMsg).text("APNS certificate password is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (notSupportedError) { + $(errorMsg).text("The File APIs are not fully supported in this browser."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (invalidFormatMDMCert) { + $(errorMsg).text("MDM certificate needs to be in pfx format."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (base64MDMCert == '') { + $(errorMsg).text("MDM certificate is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (invalidFormatAPNSCert) { + $(errorMsg).text("APNS certificate needs to be in pfx format."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (base64APNSCert == '') { + $(errorMsg).text("APNS certificate is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else if (!configOrgDisplayName) { + $(errorMsg).text("Organization display name is a required field. It cannot be empty."); + $(errorMsgWrapper).removeClass("hidden"); + } else { + var addConfigFormData = {}; + var configList = new Array(); + + var configCountry = { + "name": configParams["CONFIG_COUNTRY"], + "value": configCountry, + "contentType": "text" + }; + + var configState = { + "name": configParams["CONFIG_STATE"], + "value": configState, + "contentType": "text" + }; + + var configLocality = { + "name": configParams["CONFIG_LOCALITY"], + "value": configLocality, + "contentType": "text" + }; + + var configOrganization = { + "name": configParams["CONFIG_ORGANIZATION"], + "value": configOrganization, + "contentType": "text" + }; + + var configOrganizationUnit = { + "name": configParams["CONFIG_ORGANIZATION_UNIT"], + "value": configOrganizationUnit, + "contentType": "text" + }; + + var MDMCertPassword = { + "name": configParams["MDM_CERT_PASSWORD"], + "value": MDMCertPassword, + "contentType": "text" + }; + + var MDMCertTopicID = { + "name": configParams["MDM_CERT_TOPIC_ID"], + "value": MDMCertTopicID, + "contentType": "text" + }; + + var APNSCertPassword = { + "name": configParams["APNS_CERT_PASSWORD"], + "value": APNSCertPassword, + "contentType": "text" + }; + + var paramBase64MDMCert = { + "name": configParams["MDM_CERT"], + "value": base64MDMCert, + "contentType": "text" + }; + + var MDMCertName = { + "name": configParams["MDM_CERT_NAME"], + "value": fileNameMDMCert, + "contentType": "text" + }; + + var paramBase64APNSCert = { + "name": configParams["APNS_CERT"], + "value": base64APNSCert, + "contentType": "text" + }; + + var APNSCertName = { + "name": configParams["APNS_CERT_NAME"], + "value": fileNameAPNSCert, + "contentType": "text" + }; + + var paramOrganizationDisplayName = { + "name": configParams["ORG_DISPLAY_NAME"], + "value": configOrgDisplayName, + "contentType": "text" + }; + + var iosEula = { + "name": configParams["IOS_EULA"], + "value": iosLicense, + "contentType": "text" + }; + + configList.push(configCountry); + configList.push(configState); + configList.push(configLocality); + configList.push(configOrganization); + configList.push(configOrganizationUnit); + configList.push(MDMCertPassword); + configList.push(MDMCertTopicID); + configList.push(APNSCertPassword); + configList.push(paramBase64MDMCert); + configList.push(MDMCertName); + configList.push(paramBase64APNSCert); + configList.push(APNSCertName); + configList.push(paramOrganizationDisplayName); + configList.push(iosEula); + + addConfigFormData.type = platformTypeConstants["IOS"]; + addConfigFormData.configuration = configList; + + var addConfigAPI = "/ios/configuration"; + + invokerUtil.post( + addConfigAPI, + addConfigFormData, + function (data) { + data = JSON.parse(data); + if (data.responseCode == responseCodes["CREATED"]) { + $("#config-save-form").addClass("hidden"); + $("#record-created-msg").removeClass("hidden"); + } else if (data == 500) { + $(errorMsg).text("Exception occurred at backend."); + } else if (data == 400) { + $(errorMsg).text("Configurations cannot be empty."); + } else { + $(errorMsg).text("An unexpected error occurred."); + } + + $(errorMsgWrapper).removeClass("hidden"); + }, function (data) { + data = data.status; + if (data == 500) { + $(errorMsg).text("Exception occurred at backend."); + } else if (data == 403) { + $(errorMsg).text("Action was not permitted."); + } else { + $(errorMsg).text("An unexpected error occurred."); + } + $(errorMsgWrapper).removeClass("hidden"); + } + ); + } + + }); + + var errorMsgWrapperWindows = "#windows-config-error-msg"; + var errorMsgWindows = "#windows-config-error-msg span"; + var fileTypesWindows = ['jks']; + var notSupportedError = false; + + var base64WindowsMDMCert = ""; + var fileInputWindowsMDMCert = $('#windows-config-mdm-certificate'); + var fileNameWindowsMDMCert = ""; + var invalidFormatWindowsMDMCert = false; + + $(fileInputWindowsMDMCert).change(function () { + + if (!window.File || !window.FileReader || !window.FileList || !window.Blob) { + $(errorMsgWindows).text("The File APIs are not fully supported in this browser."); + $(errorMsgWrapperWindows).removeClass("hidden"); + notSupportedError = true; + return; + } + + var file = fileInputWindowsMDMCert[0].files[0]; + fileNameWindowsMDMCert = file.name; + var extension = file.name.split('.').pop().toLowerCase(), + isSuccess = fileTypesWindows.indexOf(extension) > -1; + + if (isSuccess) { + var fileReader = new FileReader(); + fileReader.onload = function (event) { + base64WindowsMDMCert = event.target.result; + }; + fileReader.readAsDataURL(file); + invalidFormatWindowsMDMCert = false; + } else { + base64MDMCert = ""; + invalidFormatWindowsMDMCert = true; + } + }); + + $("button#save-windows-btn").click(function () { + + var notifierFrequency = $("#windows-config-notifier-frequency").val(); + var windowsLicense = tinymce.get('windows-eula').getContent(); + + if (!notifierFrequency) { + $(errorMsgWindows).text("Polling Interval is a required field. It cannot be empty."); + $(errorMsgWrapperWindows).removeClass("hidden"); + } else if (!windowsLicense) { + $(errorMsgWindows).text("License is a required field. It cannot be empty."); + $(errorMsgWrapperWindows).removeClass("hidden"); + } else if (!$.isNumeric(notifierFrequency)) { + $(errorMsgWindows).text("Provided Notifier frequency is invalid. It must be a number."); + $(errorMsgWrapperWindows).removeClass("hidden"); + } else { + var addConfigFormData = {}; + var configList = new Array(); + + var paramNotifierFrequency = { + "name": configParams["NOTIFIER_FREQUENCY"], + "value": String(notifierFrequency * 1000), + "contentType": "text" + }; + + var windowsEula = { + "name": configParams["WINDOWS_EULA"], + "value": windowsLicense, + "contentType": "text" + }; + + configList.push(paramNotifierFrequency); + configList.push(windowsEula); + + addConfigFormData.type = platformTypeConstants["WINDOWS"]; + addConfigFormData.configuration = configList; + + var addConfigAPI = "/mdm-windows-agent/services/configuration"; + + invokerUtil.post( + addConfigAPI, + addConfigFormData, + function (data) { + data = JSON.parse(data); + if (data.responseCode == responseCodes["CREATED"]) { + $("#config-save-form").addClass("hidden"); + $("#record-created-msg").removeClass("hidden"); + } else if (data == 500) { + $(errorMsg).text("Exception occurred at backend."); + } else if (data == 400) { + $(errorMsg).text("Configurations cannot be empty."); + } else { + $(errorMsg).text("An unexpected error occurred."); + } + + $(errorMsgWrapperWindows).removeClass("hidden"); + }, function (data) { + data = data.status; + if (data == 500) { + $(errorMsg).text("Exception occurred at backend."); + } else if (data == 403) { + $(errorMsg).text("Action was not permitted."); + } else { + $(errorMsg).text("An unexpected error occurred."); + } + $(errorMsgWrapper).removeClass("hidden"); + } + ); + } + + }); +}); + +// Start of HTML embedded invoke methods +var showAdvanceOperation = function (operation, button) { + $(button).addClass('selected'); + $(button).siblings().removeClass('selected'); + var enabledPlatforms = $("#supportedPlatforms"); + var isPluginEnabled = false; + switch (operation) { + case 'ios': + if (enabledPlatforms.data("ios")) { + isPluginEnabled = true; + } + break; + case 'windows': + if (enabledPlatforms.data("windows")) { + isPluginEnabled = true; + } + break; + case 'android': + if (enabledPlatforms.data("android")) { + isPluginEnabled = true; + } + break; + case 'general': + isPluginEnabled = true; + break; + } + if (isPluginEnabled) { + var hiddenOperation = ".wr-hidden-operations-content > div"; + $(hiddenOperation + '[data-operation="' + operation + '"]').show(); + $(hiddenOperation + '[data-operation="' + operation + '"]').siblings().hide(); + } else { + var hiddenOperation = ".wr-hidden-operations-content > div"; + $(hiddenOperation + '[data-operation="error"]').show(); + $(hiddenOperation + '[data-operation="error"]').siblings().hide(); + promptErrorPolicyPlatform("To use " + operation + " related functionalities you need to configure the server " + + "accordingly.Please refer to the user guiled."); + } +}; diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/edit.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/edit.hbs new file mode 100644 index 000000000..4f85e36c7 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/edit.hbs @@ -0,0 +1,223 @@ + +
      +
      + + + + + + + + + +
      +
      +

      EDIT POLICY

      +
      +
      +
      +
      +
      +

      Step 1: Edit current profile

      +
      + +
      +
      +
      + + Loading platform features . . . +
      +
      +
      +
      + +
      +
      +
      +
      + + + +
      +
      +{{#zone "bottomJs"}} + + + + {{js "js/policy-create.js"}} +{{/zone}} + diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/edit.js b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/edit.js new file mode 100644 index 000000000..94f7423c1 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/edit.js @@ -0,0 +1,26 @@ +/* + * 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. + */ + +function onRequest(context) { + var log = new Log("policy-view-edit-unit backend js"); + log.debug("calling policy-view-edit-unit"); + var userModule = require("/app/modules/user.js").userModule; + context.roles = userModule.getRoles().content; + context.users = userModule.getUsersByUsername().content; + return context; +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/edit.json b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/edit.json new file mode 100644 index 000000000..fd2590129 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/edit.json @@ -0,0 +1,3 @@ +{ + "version" : "1.0.0" +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/public/js/policy-create.js b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/public/js/policy-create.js new file mode 100644 index 000000000..fafc43318 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/public/js/policy-create.js @@ -0,0 +1,2321 @@ +/* + * 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. + */ + +var validateStep = {}; +var skipStep = {}; +var stepForwardFrom = {}; +var stepBackFrom = {}; +var policy = {}; +var configuredOperations = []; +var currentlyEffected = {}; + +// Constants to define platform types available +var platformTypeConstants = { + "ANDROID": "android", + "IOS": "ios", + "WINDOWS": "windows" +}; + +// Constants to define platform types ids. +var platformTypeIds = { + "ANDROID": 1, + "IOS": 3, + "WINDOWS": 2 +}; + +// Constants to define Android Operation Constants +var androidOperationConstants = { + "PASSCODE_POLICY_OPERATION": "passcode-policy", + "PASSCODE_POLICY_OPERATION_CODE": "PASSCODE_POLICY", + "CAMERA_OPERATION": "camera", + "CAMERA_OPERATION_CODE": "CAMERA", + "ENCRYPT_STORAGE_OPERATION": "encrypt-storage", + "ENCRYPT_STORAGE_OPERATION_CODE": "ENCRYPT_STORAGE", + "WIFI_OPERATION": "wifi", + "WIFI_OPERATION_CODE": "WIFI" +}; + +// Constants to define Android Operation Constants +var windowsOperationConstants = { + "PASSCODE_POLICY_OPERATION": "passcode-policy", + "PASSCODE_POLICY_OPERATION_CODE": "PASSCODE_POLICY", + "CAMERA_OPERATION": "camera", + "CAMERA_OPERATION_CODE": "CAMERA", + "ENCRYPT_STORAGE_OPERATION": "encrypt-storage", + "ENCRYPT_STORAGE_OPERATION_CODE": "ENCRYPT_STORAGE" +}; + +// Constants to define iOS Operation Constants +var iosOperationConstants = { + "PASSCODE_POLICY_OPERATION": "passcode-policy", + "PASSCODE_POLICY_OPERATION_CODE": "PASSCODE_POLICY", + "RESTRICTIONS_OPERATION": "restrictions", + "RESTRICTIONS_OPERATION_CODE": "RESTRICTION", + "WIFI_OPERATION": "wifi", + "WIFI_OPERATION_CODE": "WIFI", + "EMAIL_OPERATION": "email", + "EMAIL_OPERATION_CODE": "EMAIL", + "AIRPLAY_OPERATION": "airplay", + "AIRPLAY_OPERATION_CODE": "AIR_PLAY", + "LDAP_OPERATION": "ldap", + "LDAP_OPERATION_CODE": "LDAP", + "CALENDAR_OPERATION": "calendar", + "CALENDAR_OPERATION_CODE": "CALDAV", + "CALENDAR_SUBSCRIPTION_OPERATION": "calendar-subscription", + "CALENDAR_SUBSCRIPTION_OPERATION_CODE": "CALENDAR_SUBSCRIPTION", + "APN_OPERATION": "apn", + "APN_OPERATION_CODE": "APN", + "CELLULAR_OPERATION": "cellular", + "CELLULAR_OPERATION_CODE": "CELLULAR" +}; + +/** + * Method to update the visibility (i.e. disabled or enabled view) + * of grouped input according to the values + * that they currently possess. + * @param domElement HTML grouped-input element with class name "grouped-input" + */ +var updateGroupedInputVisibility = function (domElement) { + if ($(".parent-input:first", domElement).is(":checked")) { + if ($(".grouped-child-input:first", domElement).hasClass("disabled")) { + $(".grouped-child-input:first", domElement).removeClass("disabled"); + } + $(".child-input", domElement).each(function () { + $(this).prop('disabled', false); + }); + } else { + if (!$(".grouped-child-input:first", domElement).hasClass("disabled")) { + $(".grouped-child-input:first", domElement).addClass("disabled"); + } + $(".child-input", domElement).each(function () { + $(this).prop('disabled', true); + }); + } +}; + +skipStep["policy-platform"] = function (policyPayloadObj) { + policy["name"] = policyPayloadObj["policyName"]; + policy["platform"] = policyPayloadObj["profile"]["deviceType"]["name"]; + policy["platformId"] = policyPayloadObj["profile"]["deviceType"]["id"]; + var userRoleInput = $("#user-roles-input"); + var ownershipInput = $("#ownership-input"); + var userInput = $("#users-input"); + var actionInput = $("#action-input"); + var policyNameInput = $("#policy-name-input"); + var policyDescriptionInput = $("#policy-description-input"); + currentlyEffected["roles"] = policyPayloadObj.roles; + currentlyEffected["users"] = policyPayloadObj.users; + userRoleInput.val(currentlyEffected["roles"]).trigger("change"); + userInput.val(currentlyEffected["users"]).trigger("change"); + + if (currentlyEffected["users"].length > 0) { + $("#users-radio-btn").prop("checked", true) + $("#users-select-field").show(); + $("#user-roles-select-field").hide(); + } + ownershipInput.val(policyPayloadObj.ownershipType); + actionInput.val(policyPayloadObj.compliance); + policyNameInput.val(policyPayloadObj["policyName"]); + policyDescriptionInput.val(policyPayloadObj["description"]); + // updating next-page wizard title with selected platform + $("#policy-profile-page-wizard-title").text("EDIT " + policy["platform"] + " POLICY - " + policy["name"]); + + var deviceType = policy["platform"]; + var hiddenOperationsByDeviceType = $("#hidden-operations-" + deviceType); + var hiddenOperationsByDeviceTypeCacheKey = deviceType + "HiddenOperations"; + var hiddenOperationsByDeviceTypeSrc = hiddenOperationsByDeviceType.attr("src"); + + setTimeout( + function () { + $.template(hiddenOperationsByDeviceTypeCacheKey, hiddenOperationsByDeviceTypeSrc, function (template) { + var content = template(); + // pushing profile feature input elements + $(".wr-advance-operations").html(content); + // populating values and getting the list of configured features + var configuredOperations = operationModule. + populateProfile(policy["platform"], policyPayloadObj["profile"]["profileFeaturesList"]); + // updating grouped input visibility according to the populated values + $(".wr-advance-operations li.grouped-input").each(function () { + updateGroupedInputVisibility(this); + }); + // enabling previously configured options of last update + for (var i = 0; i < configuredOperations.length; ++i) { + var configuredOperation = configuredOperations[i]; + $(".operation-data").filterByData("operation-code", configuredOperation). + find(".panel-title .wr-input-control.switch input[type=checkbox]").each(function () { + $(this).click(); + }); + } + }); + }, + 250 // time delayed for the execution of above function, 250 milliseconds + ); +}; + +/** + * Checks if provided number is valid against a range. + * + * @param numberInput Number Input + * @param min Minimum Limit + * @param max Maximum Limit + * @returns {boolean} Returns true if input is within the specified range + */ +var inputIsValidAgainstRange = function (numberInput, min, max) { + return (numberInput == min || (numberInput > min && numberInput < max) || numberInput == max); +}; + +/** + * Checks if provided input is valid against RegEx input. + * + * @param regExp Regular expression + * @param input Input string to check + * @returns {boolean} Returns true if input matches RegEx + */ +var inputIsValidAgainstRegExp = function (regExp, input) { + return regExp.test(input); +}; + +validateStep["policy-profile"] = function () { + var validationStatusArray = []; + var validationStatus; + var operation; + + // starting validation process and updating validationStatus + if (policy["platform"] == platformTypeConstants["ANDROID"]) { + if (configuredOperations.length == 0) { + // updating validationStatus + validationStatus = { + "error": true, + "mainErrorMsg": "You cannot continue. Zero configured features." + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } else { + // validating each and every configured Operation + // Validating PASSCODE_POLICY + if ($.inArray(androidOperationConstants["PASSCODE_POLICY_OPERATION_CODE"], configuredOperations) != -1) { + // if PASSCODE_POLICY is configured + operation = androidOperationConstants["PASSCODE_POLICY_OPERATION"]; + // initializing continueToCheckNextInputs to true + var continueToCheckNextInputs = true; + + // validating first input: passcodePolicyMaxPasscodeAgeInDays + var passcodePolicyMaxPasscodeAgeInDays = $("input#passcode-policy-max-passcode-age-in-days").val(); + if (passcodePolicyMaxPasscodeAgeInDays) { + if (!$.isNumeric(passcodePolicyMaxPasscodeAgeInDays)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode age is not a number.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(passcodePolicyMaxPasscodeAgeInDays, 1, 730)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode age is not with in the range of 1-to-730.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + // validating second and last input: passcodePolicyPasscodeHistory + if (continueToCheckNextInputs) { + var passcodePolicyPasscodeHistory = $("input#passcode-policy-passcode-history").val(); + if (passcodePolicyPasscodeHistory) { + if (!$.isNumeric(passcodePolicyPasscodeHistory)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode history is not a number.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(passcodePolicyPasscodeHistory, 1, 50)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode history is not with in the range of 1-to-50.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating CAMERA + if ($.inArray(androidOperationConstants["CAMERA_OPERATION_CODE"], configuredOperations) != -1) { + // if CAMERA is configured + operation = androidOperationConstants["CAMERA_OPERATION"]; + // updating validationStatus + validationStatus = { + "error": false, + "okFeature": operation + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating ENCRYPT_STORAGE + if ($.inArray(androidOperationConstants["ENCRYPT_STORAGE_OPERATION_CODE"], configuredOperations) != -1) { + // if ENCRYPT_STORAGE is configured + operation = androidOperationConstants["ENCRYPT_STORAGE_OPERATION"]; + // updating validationStatus + validationStatus = { + "error": false, + "okFeature": operation + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating WIFI + if ($.inArray(androidOperationConstants["WIFI_OPERATION_CODE"], configuredOperations) != -1) { + // if WIFI is configured + operation = androidOperationConstants["WIFI_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var wifiSSID = $("input#wifi-ssid").val(); + if (!wifiSSID) { + validationStatus = { + "error": true, + "subErrorMsg": "WIFI SSID is not given. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + } + } + if (policy["platform"] == platformTypeConstants["WINDOWS"]) { + if (configuredOperations.length == 0) { + // updating validationStatus + validationStatus = { + "error": true, + "mainErrorMsg": "You cannot continue. Zero configured features." + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } else { + // validating each and every configured Operation + // Validating PASSCODE_POLICY + if ($.inArray(windowsOperationConstants["PASSCODE_POLICY_OPERATION_CODE"], configuredOperations) != -1) { + // if PASSCODE_POLICY is configured + operation = windowsOperationConstants["PASSCODE_POLICY_OPERATION"]; + // initializing continueToCheckNextInputs to true + var continueToCheckNextInputs = true; + + // validating first input: passcodePolicyMaxPasscodeAgeInDays + var passcodePolicyMaxPasscodeAgeInDays = $("input#passcode-policy-max-passcode-age-in-days").val(); + if (passcodePolicyMaxPasscodeAgeInDays) { + if (!$.isNumeric(passcodePolicyMaxPasscodeAgeInDays)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode age is not a number.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(passcodePolicyMaxPasscodeAgeInDays, 1, 730)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode age is not with in the range of 1-to-730.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + // validating second and last input: passcodePolicyPasscodeHistory + if (continueToCheckNextInputs) { + var passcodePolicyPasscodeHistory = $("input#passcode-policy-passcode-history").val(); + if (passcodePolicyPasscodeHistory) { + if (!$.isNumeric(passcodePolicyPasscodeHistory)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode history is not a number.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(passcodePolicyPasscodeHistory, 1, 50)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode history is not with in the range of 1-to-50.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating CAMERA + if ($.inArray(windowsOperationConstants["CAMERA_OPERATION_CODE"], configuredOperations) != -1) { + // if CAMERA is configured + operation = windowsOperationConstants["CAMERA_OPERATION"]; + // updating validationStatus + validationStatus = { + "error": false, + "okFeature": operation + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating ENCRYPT_STORAGE + if ($.inArray(windowsOperationConstants["ENCRYPT_STORAGE_OPERATION_CODE"], configuredOperations) != -1) { + // if ENCRYPT_STORAGE is configured + operation = windowsOperationConstants["ENCRYPT_STORAGE_OPERATION"]; + // updating validationStatus + validationStatus = { + "error": false, + "okFeature": operation + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + + } + } else if (policy["platform"] == platformTypeConstants["IOS"]) { + if (configuredOperations.length == 0) { + // updating validationStatus + validationStatus = { + "error": true, + "mainErrorMsg": "You cannot continue. Zero configured features." + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } else { + // validating each and every configured Operation + // Validating PASSCODE_POLICY + if ($.inArray(iosOperationConstants["PASSCODE_POLICY_OPERATION_CODE"], configuredOperations) != -1) { + // if PASSCODE_POLICY is configured + operation = iosOperationConstants["PASSCODE_POLICY_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + // validating first input: passcodePolicyMaxPasscodeAgeInDays + passcodePolicyMaxPasscodeAgeInDays = $("input#passcode-policy-max-passcode-age-in-days").val(); + if (passcodePolicyMaxPasscodeAgeInDays) { + if (!$.isNumeric(passcodePolicyMaxPasscodeAgeInDays)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode age is not a number.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(passcodePolicyMaxPasscodeAgeInDays, 1, 730)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode age is not with in the range of 1-to-730.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + // validating second and last input: passcodePolicyPasscodeHistory + if (continueToCheckNextInputs) { + passcodePolicyPasscodeHistory = $("input#passcode-policy-passcode-history").val(); + if (passcodePolicyPasscodeHistory) { + if (!$.isNumeric(passcodePolicyPasscodeHistory)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode history is not a number.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(passcodePolicyPasscodeHistory, 1, 50)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode history is not with in the range of 1-to-50.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating RESTRICTIONS + if ($.inArray(iosOperationConstants["RESTRICTIONS_OPERATION_CODE"], configuredOperations) != -1) { + // if RESTRICTION is configured + operation = iosOperationConstants["RESTRICTIONS_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + // getting input values to be validated + var restrictionsAutonomousSingleAppModePermittedAppIDsGridChildInputs = + "div#restrictions-autonomous-single-app-mode-permitted-app-ids .child-input"; + if ($(restrictionsAutonomousSingleAppModePermittedAppIDsGridChildInputs).length > 0) { + var childInput; + var childInputArray = []; + var emptyChildInputCount = 0; + var duplicatesExist = false; + // looping through each child input + $(restrictionsAutonomousSingleAppModePermittedAppIDsGridChildInputs).each(function () { + childInput = $(this).val(); + childInputArray.push(childInput); + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + }); + // checking for duplicates + var initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + var m, poppedChildInput; + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + var n; + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more permitted App ID entries in " + + "Autonomous Single App Mode are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with permitted App ID entries in " + + "Autonomous Single App Mode.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating WIFI + if ($.inArray(iosOperationConstants["WIFI_OPERATION_CODE"], configuredOperations) != -1) { + // if WIFI is configured + operation = iosOperationConstants["WIFI_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + // getting input values to be validated + wifiSSID = $("input#wifi-ssid").val(); + var wifiDomainName = $("input#wifi-domain-name").val(); + if (!wifiSSID && !wifiDomainName) { + validationStatus = { + "error": true, + "subErrorMsg": "Both Wi-Fi SSID and Wi-Fi Domain Name are not given. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + // getting proxy-setup value + var wifiProxyType = $("select#wifi-proxy-type").find("option:selected").attr("value"); + if (wifiProxyType == "Manual") { + // adds up additional fields to be validated + var wifiProxyServer = $("input#wifi-proxy-server").val(); + if (!wifiProxyServer) { + validationStatus = { + "error": true, + "subErrorMsg": "Wi-Fi Proxy Server is required. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + var wifiProxyPort = $("input#wifi-proxy-port").val(); + if (!wifiProxyPort) { + validationStatus = { + "error": true, + "subErrorMsg": "Wi-Fi Proxy Port is required. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!$.isNumeric(wifiProxyPort)) { + validationStatus = { + "error": true, + "subErrorMsg": "Wi-Fi Proxy Port requires a number input.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(wifiProxyPort, 0, 65535)) { + validationStatus = { + "error": true, + "subErrorMsg": "Wi-Fi Proxy Port is not within the range " + + "of valid port numbers.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + } + + if (continueToCheckNextInputs) { + // getting encryption-type value + var wifiEncryptionType = $("select#wifi-encryption-type").find("option:selected").attr("value"); + if (wifiEncryptionType != "None") { + var wifiPayloadCertificateAnchorUUIDsGridChildInputs = + "div#wifi-payload-certificate-anchor-uuids .child-input"; + if ($(wifiPayloadCertificateAnchorUUIDsGridChildInputs).length > 0) { + emptyChildInputCount = 0; + childInputArray = []; + duplicatesExist = false; + // looping through each child input + $(wifiPayloadCertificateAnchorUUIDsGridChildInputs).each(function () { + childInput = $(this).val(); + childInputArray.push(childInput); + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more Payload Certificate " + + "Anchor UUIDs are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist " + + "with Payload Certificate Anchor UUIDs.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + if (continueToCheckNextInputs) { + var wifiTLSTrustedServerNamesGridChildInputs = + "div#wifi-tls-trusted-server-names .child-input"; + if ($(wifiTLSTrustedServerNamesGridChildInputs).length > 0) { + emptyChildInputCount = 0; + childInputArray = []; + duplicatesExist = false; + // looping through each child input + $(wifiTLSTrustedServerNamesGridChildInputs).each(function () { + childInput = $(this).val(); + childInputArray.push(childInput); + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more TLS Trusted Server Names are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist " + + "with TLS Trusted Server Names.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + } + } + + if (continueToCheckNextInputs) { + var wifiRoamingConsortiumOIsGridChildInputs = "div#wifi-roaming-consortium-ois .child-input"; + if ($(wifiRoamingConsortiumOIsGridChildInputs).length > 0) { + emptyChildInputCount = 0; + var outOfAllowedLengthCount = 0; + var invalidAgainstRegExCount = 0; + childInputArray = []; + duplicatesExist = false; + // looping through each child input + $(wifiRoamingConsortiumOIsGridChildInputs).each(function () { + childInput = $(this).val(); + childInputArray.push(childInput); + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } else if (!inputIsValidAgainstLength(childInput, 6, 6) && !inputIsValidAgainstLength(childInput, 10, 10)) { + outOfAllowedLengthCount++; + } else if (!inputIsValidAgainstRegExp(/^[a-fA-F0-9]+$/, childInput)) { + invalidAgainstRegExCount++; + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more Roaming Consortium OIs are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (outOfAllowedLengthCount > 0) { + // if outOfMaxAllowedLength input is present + validationStatus = { + "error": true, + "subErrorMsg": "One or more Roaming Consortium OIs " + + "are out of allowed length.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (invalidAgainstRegExCount > 0) { + // if invalid inputs in terms of hexadecimal format are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more Roaming Consortium OIs " + + "contain non-hexadecimal characters.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with Roaming Consortium OIs.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + if (continueToCheckNextInputs) { + var wifiNAIRealmNamesGridChildInputs = "div#wifi-nai-realm-names .child-input"; + if ($(wifiNAIRealmNamesGridChildInputs).length > 0) { + emptyChildInputCount = 0; + childInputArray = []; + duplicatesExist = false; + // looping through each child input + $(wifiNAIRealmNamesGridChildInputs).each(function () { + childInput = $(this).val(); + childInputArray.push(childInput); + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more NAI Realm Names are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with NAI Realm Names.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + if (continueToCheckNextInputs) { + var wifiMCCAndMNCsGridChildInputs = "div#wifi-mcc-and-mncs .child-input"; + if ($(wifiMCCAndMNCsGridChildInputs).length > 0) { + var childInputCount = 0; + var stringPair; + emptyChildInputCount = 0; + outOfAllowedLengthCount = 0; + var notNumericInputCount = 0; + childInputArray = []; + duplicatesExist = false; + // looping through each child input + $(wifiMCCAndMNCsGridChildInputs).each(function () { + childInput = $(this).val(); + // pushing each string pair to childInputArray + childInputCount++; + if (childInputCount % 2 == 1) { + // initialize stringPair value + stringPair = ""; + // append first part of the string + stringPair += childInput; + } else { + // append second part of the string + stringPair += childInput; + childInputArray.push(stringPair); + } + // updating emptyChildInputCount & outOfAllowedLengthCount + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } else if (!$.isNumeric(childInput)) { + notNumericInputCount++; + } else if (!inputIsValidAgainstLength(childInput, 3, 3)) { + outOfAllowedLengthCount++; + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more MCC/MNC pairs are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (notNumericInputCount > 0) { + // if notNumeric input is present + validationStatus = { + "error": true, + "subErrorMsg": "One or more MCC/MNC pairs are not numeric.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (outOfAllowedLengthCount > 0) { + // if outOfAllowedLength input is present + validationStatus = { + "error": true, + "subErrorMsg": "One or more MCC/MNC pairs " + + "do not fulfill the accepted length of 6 digits.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with MCC/MNC pairs.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating EMAIL + if ($.inArray(iosOperationConstants["EMAIL_OPERATION_CODE"], configuredOperations) != -1) { + // if EMAIL is configured + operation = iosOperationConstants["EMAIL_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var emailAddress = $("input#email-address").val(); + if (emailAddress && !inputIsValidAgainstRegExp(/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/, emailAddress)) { + validationStatus = { + "error": true, + "subErrorMsg": "Email Address is not valid.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + var emailIncomingMailServerHostname = $("input#email-incoming-mail-server-hostname").val(); + if (!emailIncomingMailServerHostname) { + validationStatus = { + "error": true, + "subErrorMsg": "Incoming Mail Server Hostname is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + if (continueToCheckNextInputs) { + var emailIncomingMailServerPort = $("input#email-incoming-mail-server-port").val(); + if (emailIncomingMailServerPort && emailIncomingMailServerPort != '') { + if (!$.isNumeric(emailIncomingMailServerPort)) { + validationStatus = { + "error": true, + "subErrorMsg": "Incoming Mail Server Port requires a number input.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(emailIncomingMailServerPort, 0, 65535)) { + validationStatus = { + "error": true, + "subErrorMsg": "Incoming Mail Server Port is not within the range " + + "of valid port numbers.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + if (continueToCheckNextInputs) { + var emailOutgoingMailServerHostname = $("input#email-outgoing-mail-server-hostname").val(); + if (!emailOutgoingMailServerHostname) { + validationStatus = { + "error": true, + "subErrorMsg": "Outgoing Mail Server Hostname is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + if (continueToCheckNextInputs) { + var emailOutgoingMailServerPort = $("input#email-outgoing-mail-server-port").val(); + if (emailOutgoingMailServerPort && emailOutgoingMailServerPort != '') { + if (!$.isNumeric(emailOutgoingMailServerPort)) { + validationStatus = { + "error": true, + "subErrorMsg": "Outgoing Mail Server Port requires a number input.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(emailOutgoingMailServerPort, 0, 65535)) { + validationStatus = { + "error": true, + "subErrorMsg": "Outgoing Mail Server Port is not within the range " + + "of valid port numbers.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating AIRPLAY + if ($.inArray(iosOperationConstants["AIRPLAY_OPERATION_CODE"], configuredOperations) != -1) { + // if AIRPLAY is configured + operation = iosOperationConstants["AIRPLAY_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var airplayCredentialsGridChildInputs = "div#airplay-credentials .child-input"; + var airplayDestinationsGridChildInputs = "div#airplay-destinations .child-input"; + if ($(airplayCredentialsGridChildInputs).length == 0 && + $(airplayDestinationsGridChildInputs).length == 0) { + validationStatus = { + "error": true, + "subErrorMsg": "AirPlay settings have zero configurations attached.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + if ($(airplayCredentialsGridChildInputs).length > 0) { + childInputCount = 0; + childInputArray = []; + emptyChildInputCount = 0; + duplicatesExist = false; + // looping through each child input + $(airplayCredentialsGridChildInputs).each(function () { + childInputCount++; + if (childInputCount % 2 == 1) { + // if child input is of first column + childInput = $(this).val(); + childInputArray.push(childInput); + // updating emptyChildInputCount + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more Device Names of " + + "AirPlay Credentials are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + // if duplicate input is present + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with " + + "Device Names of AirPlay Credentials.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + if (continueToCheckNextInputs) { + if ($(airplayDestinationsGridChildInputs).length > 0) { + childInputArray = []; + emptyChildInputCount = 0; + invalidAgainstRegExCount = 0; + duplicatesExist = false; + // looping through each child input + $(airplayDestinationsGridChildInputs).each(function () { + childInput = $(this).val(); + childInputArray.push(childInput); + // updating emptyChildInputCount + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } else if (!inputIsValidAgainstRegExp( + /([a-z|A-Z|0-9][a-z|A-Z|0-9][:]){5}([a-z|A-Z|0-9][a-z|A-Z|0-9])$/, childInput)) { + // if child input field is invalid against RegEx + invalidAgainstRegExCount++ + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more AirPlay Destination fields are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (invalidAgainstRegExCount > 0) { + // if invalidAgainstRegEx inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more AirPlay Destination fields " + + "do not fulfill expected format.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + // if duplicate input is present + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with AirPlay Destinations.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating LDAP + if ($.inArray(iosOperationConstants["LDAP_OPERATION_CODE"], configuredOperations) != -1) { + // if LDAP is configured + operation = iosOperationConstants["LDAP_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var ldapAccountHostname = $("input#ldap-account-hostname").val(); + if (!ldapAccountHostname) { + validationStatus = { + "error": true, + "subErrorMsg": "LDAP Account Hostname URL is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + var ldapSearchSettingsGridChildInputs = "div#ldap-search-settings .child-input"; + if ($(ldapSearchSettingsGridChildInputs).length > 0) { + childInputCount = 0; + childInputArray = []; + emptyChildInputCount = 0; + duplicatesExist = false; + // looping through each child input + $(ldapSearchSettingsGridChildInputs).each(function () { + childInputCount++; + if (childInputCount % 3 == 2) { + // if child input is of second column + childInput = $(this).find("option:selected").attr("value"); + stringPair = ""; + stringPair += (childInput + " "); + } else if (childInputCount % 3 == 0) { + // if child input is of third column + childInput = $(this).val(); + stringPair += childInput; + childInputArray.push(stringPair); + // updating emptyChildInputCount + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more Search Setting Scope fields are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + // if duplicate input is present + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with " + + "Search Setting Search Base and Scope pairs.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating CALENDAR + if ($.inArray(iosOperationConstants["CALENDAR_OPERATION_CODE"], configuredOperations) != -1) { + // if CALENDAR is configured + operation = iosOperationConstants["CALENDAR_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var calendarAccountHostname = $("input#calendar-account-hostname").val(); + if (!calendarAccountHostname) { + validationStatus = { + "error": true, + "subErrorMsg": "Account Hostname URL is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + var calendarAccountPort = $("input#calendar-account-port").val(); + if (!calendarAccountPort) { + validationStatus = { + "error": true, + "subErrorMsg": "Account Port is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!$.isNumeric(calendarAccountPort)) { + validationStatus = { + "error": true, + "subErrorMsg": "Account Port requires a number input.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(calendarAccountPort, 0, 65535)) { + validationStatus = { + "error": true, + "subErrorMsg": "Account Port is not within the range " + + "of valid port numbers.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating CALENDAR_SUBSCRIPTION + if ($.inArray(iosOperationConstants["CALENDAR_SUBSCRIPTION_OPERATION_CODE"], configuredOperations) != -1) { + // if CALENDAR_SUBSCRIPTION is configured + operation = iosOperationConstants["CALENDAR_SUBSCRIPTION_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var calendarSubscriptionHostname = $("input#calendar-subscription-hostname").val(); + if (!calendarSubscriptionHostname) { + validationStatus = { + "error": true, + "subErrorMsg": "Account Hostname URL is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating APN + if ($.inArray(iosOperationConstants["APN_OPERATION_CODE"], configuredOperations) != -1) { + // if APN is configured + operation = iosOperationConstants["APN_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var apnConfigurationsGridChildInputs = "div#apn-configurations .child-input"; + if ($(apnConfigurationsGridChildInputs).length == 0) { + validationStatus = { + "error": true, + "subErrorMsg": "APN Settings have zero configurations attached.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if ($(apnConfigurationsGridChildInputs).length > 0) { + childInputCount = 0; + childInputArray = []; + // checking empty APN field count + emptyChildInputCount = 0; + duplicatesExist = false; + // looping through each child input + $(apnConfigurationsGridChildInputs).each(function () { + childInputCount++; + if (childInputCount % 5 == 1) { + // if child input is of first column + childInput = $(this).val(); + childInputArray.push(childInput); + // updating emptyChildInputCount + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more APN fields of Configurations are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + // if duplicate input is present + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with " + + "APN fields of Configurations.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating CELLULAR + if ($.inArray(iosOperationConstants["CELLULAR_OPERATION_CODE"], configuredOperations) != -1) { + // if CELLULAR is configured + operation = iosOperationConstants["CELLULAR_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var cellularAttachAPNName = $("input#cellular-attach-apn-name").val(); + if (!cellularAttachAPNName) { + validationStatus = { + "error": true, + "subErrorMsg": "Cellular Configuration Name is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + var cellularAPNConfigurationsGridChildInputs = "div#cellular-apn-configurations .child-input"; + if ($(cellularAPNConfigurationsGridChildInputs).length > 0) { + childInputCount = 0; + childInputArray = []; + // checking empty APN field count + emptyChildInputCount = 0; + duplicatesExist = false; + // looping through each child input + $(cellularAPNConfigurationsGridChildInputs).each(function () { + childInputCount++; + if (childInputCount % 6 == 1) { + // if child input is of first column + childInput = $(this).val(); + childInputArray.push(childInput); + // updating emptyChildInputCount + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more APN fields of APN Configurations are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + // if duplicate input is present + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with " + + "APN fields of APN Configurations.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + } + } + // ending validation process + + // start taking specific notifying actions upon validation + var wizardIsToBeContinued; + var errorCount = 0; + var mainErrorMsgWrapper, mainErrorMsg, + subErrorMsgWrapper, subErrorMsg, subErrorIcon, subOkIcon, featureConfiguredIcon; + var i; + for (i = 0; i < validationStatusArray.length; i++) { + validationStatus = validationStatusArray[i]; + if (validationStatus["error"]) { + errorCount++; + if (validationStatus["mainErrorMsg"]) { + mainErrorMsgWrapper = "#policy-profile-main-error-msg"; + mainErrorMsg = mainErrorMsgWrapper + " span"; + $(mainErrorMsg).text(validationStatus["mainErrorMsg"]); + $(mainErrorMsgWrapper).removeClass("hidden"); + } else if (validationStatus["subErrorMsg"]) { + subErrorMsgWrapper = "#" + validationStatus["erroneousFeature"] + "-feature-error-msg"; + subErrorMsg = subErrorMsgWrapper + " span"; + subErrorIcon = "#" + validationStatus["erroneousFeature"] + "-error"; + subOkIcon = "#" + validationStatus["erroneousFeature"] + "-ok"; + featureConfiguredIcon = "#" + validationStatus["erroneousFeature"] + "-configured"; + // hiding featureConfiguredState as the first step + if (!$(featureConfiguredIcon).hasClass("hidden")) { + $(featureConfiguredIcon).addClass("hidden"); + } + // updating error state and corresponding messages + $(subErrorMsg).text(validationStatus["subErrorMsg"]); + if ($(subErrorMsgWrapper).hasClass("hidden")) { + $(subErrorMsgWrapper).removeClass("hidden"); + } + if (!$(subOkIcon).hasClass("hidden")) { + $(subOkIcon).addClass("hidden"); + } + if ($(subErrorIcon).hasClass("hidden")) { + $(subErrorIcon).removeClass("hidden"); + } + } + } else { + if (validationStatus["okFeature"]) { + subErrorMsgWrapper = "#" + validationStatus["okFeature"] + "-feature-error-msg"; + subErrorIcon = "#" + validationStatus["okFeature"] + "-error"; + subOkIcon = "#" + validationStatus["okFeature"] + "-ok"; + featureConfiguredIcon = "#" + validationStatus["okFeature"] + "-configured"; + // hiding featureConfiguredState as the first step + if (!$(featureConfiguredIcon).hasClass("hidden")) { + $(featureConfiguredIcon).addClass("hidden"); + } + // updating success state and corresponding messages + if (!$(subErrorMsgWrapper).hasClass("hidden")) { + $(subErrorMsgWrapper).addClass("hidden"); + } + if (!$(subErrorIcon).hasClass("hidden")) { + $(subErrorIcon).addClass("hidden"); + } + if ($(subOkIcon).hasClass("hidden")) { + $(subOkIcon).removeClass("hidden"); + } + } + } + } + + wizardIsToBeContinued = (errorCount == 0); + return wizardIsToBeContinued; +}; + +stepForwardFrom["policy-profile"] = function () { + policy["profile"] = operationModule.generateProfile(policy["platform"], configuredOperations); + // updating next-page wizard title with selected platform + $("#policy-criteria-page-wizard-title").text("EDIT " + policy["platform"] + " POLICY - " + policy["name"]); + // updating ownership type options according to platform + if (policy["platform"] == platformTypeConstants["IOS"] || + policy["platform"] == platformTypeConstants["WINDOWS"]) { + var ownershipTypeSelectOptions = $("#ownership-input"); + ownershipTypeSelectOptions.empty(); + ownershipTypeSelectOptions.append($(""). + attr("value", "BYOD").text("BYOD (Bring Your Own Device)")); + ownershipTypeSelectOptions.attr("disabled", true); + } +}; + +stepForwardFrom["policy-criteria"] = function () { + $("input[type='radio'].select-users-radio").each(function () { + if ($(this).is(':radio')) { + if ($(this).is(":checked")) { + if ($(this).attr("id") == "users-radio-btn") { + policy["selectedUsers"] = $("#users-input").val(); + } else if ($(this).attr("id") == "user-roles-radio-btn") { + policy["selectedUserRoles"] = $("#user-roles-input").val(); + } + } + } + }); + policy["selectedNonCompliantAction"] = $("#action-input").find(":selected").data("action"); + policy["selectedOwnership"] = $("#ownership-input").val(); + // updating next-page wizard title with selected platform + $("#policy-naming-page-wizard-title").text("EDIT " + policy["platform"] + " POLICY - " + policy["name"]); +}; + +/** + * Checks if provided input is valid against provided length range. + * + * @param input Alphanumeric or non-alphanumeric input + * @param minLength Minimum Required Length + * @param maxLength Maximum Required Length + * @returns {boolean} Returns true if input matches the provided minimum length and maximum length + */ +var inputIsValidAgainstLength = function (input, minLength, maxLength) { + var length = input.length; + return (length == minLength || (length > minLength && length < maxLength) || length == maxLength); +}; + +validateStep["policy-criteria"] = function () { + var validationStatus = {}; + var selectedAssignees; + var selectedField = "Role(s)"; + + $("input[type='radio'].select-users-radio").each(function () { + if ($(this).is(":checked")) { + if ($(this).attr("id") == "users-radio-btn") { + selectedAssignees = $("#users-input").val(); + selectedField = "User(s)"; + } else if ($(this).attr("id") == "user-roles-radio-btn") { + selectedAssignees = $("#user-roles-input").val(); + } + return false; + } + }); + + if (selectedAssignees) { + validationStatus["error"] = false; + } else { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = selectedField + " is a required field. It cannot be empty"; + } + + var wizardIsToBeContinued; + if (validationStatus["error"]) { + wizardIsToBeContinued = false; + var mainErrorMsgWrapper = "#policy-criteria-main-error-msg"; + var mainErrorMsg = mainErrorMsgWrapper + " span"; + $(mainErrorMsg).text(validationStatus["mainErrorMsg"]); + $(mainErrorMsgWrapper).removeClass("hidden"); + } else { + wizardIsToBeContinued = true; + } + + return wizardIsToBeContinued; +}; + +validateStep["policy-naming"] = function () { + var validationStatus = {}; + + // taking values of inputs to be validated + var policyName = $("input#policy-name-input").val(); + // starting validation process and updating validationStatus + if (!policyName) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = "Policy name is empty. You cannot proceed."; + } else if (!inputIsValidAgainstLength(policyName, 1, 30)) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = + "Policy name exceeds maximum allowed length."; + } else { + validationStatus["error"] = false; + } + // ending validation process + + // start taking specific actions upon validation + var wizardIsToBeContinued; + if (validationStatus["error"]) { + wizardIsToBeContinued = false; + var mainErrorMsgWrapper = "#policy-naming-main-error-msg"; + var mainErrorMsg = mainErrorMsgWrapper + " span"; + $(mainErrorMsg).text(validationStatus["mainErrorMsg"]); + $(mainErrorMsgWrapper).removeClass("hidden"); + } else { + wizardIsToBeContinued = true; + } + + return wizardIsToBeContinued; +}; + +validateStep["policy-naming-publish"] = function () { + var validationStatus = {}; + + // taking values of inputs to be validated + var policyName = $("input#policy-name-input").val(); + // starting validation process and updating validationStatus + if (!policyName) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = "Policy name is empty. You cannot proceed."; + } else if (!inputIsValidAgainstLength(policyName, 1, 30)) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = + "Policy name exceeds maximum allowed length."; + } else { + validationStatus["error"] = false; + } + // ending validation process + + // start taking specific actions upon validation + var wizardIsToBeContinued; + if (validationStatus["error"]) { + wizardIsToBeContinued = false; + var mainErrorMsgWrapper = "#policy-naming-main-error-msg"; + var mainErrorMsg = mainErrorMsgWrapper + " span"; + $(mainErrorMsg).text(validationStatus["mainErrorMsg"]); + $(mainErrorMsgWrapper).removeClass("hidden"); + } else { + wizardIsToBeContinued = true; + } + + return wizardIsToBeContinued; +}; + +stepForwardFrom["policy-naming-publish"] = function () { + policy["policyName"] = $("#policy-name-input").val(); + policy["description"] = $("#policy-description-input").val(); + //All data is collected. Policy can now be updated. + updatePolicy(policy, "publish"); +}; +stepForwardFrom["policy-naming"] = function () { + policy["policyName"] = $("#policy-name-input").val(); + policy["description"] = $("#policy-description-input").val(); + //All data is collected. Policy can now be updated. + updatePolicy(policy, "save"); +}; + +var updatePolicy = function (policy, state) { + var profilePayloads = []; + // traverses key by key in policy["profile"] + var key; + for (key in policy["profile"]) { + if (policy["platformId"] == platformTypeIds["WINDOWS"] && key == windowsOperationConstants["PASSCODE_POLICY_OPERATION_CODE"]) { + policy["profile"][key].enablePassword = true; + } + + if (policy["profile"].hasOwnProperty(key)) { + profilePayloads.push({ + "featureCode": key, + "deviceTypeId": policy["platformId"], + "content": policy["profile"][key] + }); + } + } + + $.each(profilePayloads, function (i, item) { + $.each(item.content, function (key, value) { + if (value === "" || value === undefined) { + item.content[key] = null; + } + }); + }); + + var payload = { + "policyName": policy["policyName"], + "description": policy["description"], + "compliance": policy["selectedNonCompliantAction"], + "ownershipType": policy["selectedOwnership"], + "profile": { + "profileName": policy["policyName"], + "deviceType": { + "id": policy["platformId"] + }, + "profileFeaturesList": profilePayloads + } + }; + + if (policy["selectedUsers"]) { + payload["users"] = policy["selectedUsers"]; + } else if (policy["selectedUserRoles"]) { + payload["roles"] = policy["selectedUserRoles"]; + } else { + payload["users"] = []; + payload["roles"] = []; + } + + var serviceURL = "/devicemgt_admin/policies/" + getParameterByName("id"); + invokerUtil.put( + serviceURL, + payload, + // on success + function () { + if (state == "save") { + var policyList = []; + policyList.push(getParameterByName("id")); + serviceURL = "/devicemgt_admin/policies/inactivate"; + invokerUtil.put( + serviceURL, + policyList, + // on success + function () { + $(".add-policy").addClass("hidden"); + $(".policy-message").removeClass("hidden"); + }, + // on error + function (daa) { + console.log(data); + } + ); + } else if (state == "publish") { + var policyList = []; + policyList.push(getParameterByName("id")); + serviceURL = "/devicemgt_admin/policies/activate"; + invokerUtil.put( + serviceURL, + policyList, + // on success + function () { + $(".add-policy").addClass("hidden"); + $(".policy-naming").addClass("hidden"); + $(".policy-message").removeClass("hidden"); + }, + // on error + function (data) { + console.log(data); + } + ); + } + }, + // on error + function () { + + } + ); +}; + +// Start of HTML embedded invoke methods +var showAdvanceOperation = function (operation, button) { + $(button).addClass('selected'); + $(button).siblings().removeClass('selected'); + var hiddenOperation = ".wr-hidden-operations-content > div"; + $(hiddenOperation + '[data-operation="' + operation + '"]').show(); + $(hiddenOperation + '[data-operation="' + operation + '"]').siblings().hide(); +}; + +/** + * Method to slide down a provided pane upon provided value set. + * + * @param selectElement Select HTML Element to consider + * @param paneID HTML ID of div element to slide down + * @param valueSet Applicable Value Set + */ +var slideDownPaneAgainstValueSet = function (selectElement, paneID, valueSet) { + var selectedValueOnChange = $(selectElement).find("option:selected").val(); + var i, slideDownVotes = 0; + for (i = 0; i < valueSet.length; i++) { + if (selectedValueOnChange == valueSet[i]) { + slideDownVotes++; + } + } + var paneSelector = "#" + paneID; + if (slideDownVotes > 0) { + if (!$(paneSelector).hasClass("expanded")) { + $(paneSelector).addClass("expanded"); + } + $(paneSelector).slideDown(); + } else { + if ($(paneSelector).hasClass("expanded")) { + $(paneSelector).removeClass("expanded"); + } + $(paneSelector).slideUp(); + /** now follows the code to reinitialize all inputs of the slidable pane */ + // reinitializing input fields into the defaults + $(paneSelector + " input").each( + function () { + if ($(this).is("input:text")) { + $(this).val($(this).data("default")); + } else if ($(this).is("input:password")) { + $(this).val(""); + } else if ($(this).is("input:checkbox")) { + $(this).prop("checked", $(this).data("default")); + // if this checkbox is the parent input of a grouped-input + if ($(this).hasClass("parent-input")) { + var groupedInput = $(this).parent().parent().parent(); + updateGroupedInputVisibility(groupedInput); + } + } + } + ); + // reinitializing select fields into the defaults + $(paneSelector + " select").each( + function () { + var defaultOption = $(this).data("default"); + $("option:eq(" + defaultOption + ")", this).prop("selected", "selected"); + } + ); + // collapsing expanded-panes (upon the selection of html-select-options) if any + $(paneSelector + " .expanded").each( + function () { + if ($(this).hasClass("expanded")) { + $(this).removeClass("expanded"); + } + $(this).slideUp(); + } + ); + // removing all entries of grid-input elements if exist + $(paneSelector + " .grouped-array-input").each( + function () { + var gridInputs = $(this).find("[data-add-form-clone]"); + if (gridInputs.length > 0) { + gridInputs.remove(); + } + var helpTexts = $(this).find("[data-help-text=add-form]"); + if (helpTexts.length > 0) { + helpTexts.show(); + } + } + ); + } +}; +// End of HTML embedded invoke methods + + +// Start of functions related to grid-input-view + +/** + * Method to set count id to cloned elements. + * @param {object} addFormContainer + */ +var setId = function (addFormContainer) { + $(addFormContainer).find("[data-add-form-clone]").each(function (i) { + $(this).attr("id", $(this).attr("data-add-form-clone").slice(1) + "-" + (i + 1)); + if ($(this).find(".index").length > 0) { + $(this).find(".index").html(i + 1); + } + }); +}; + +/** + * Method to set count id to cloned elements. + * @param {object} addFormContainer + */ +var showHideHelpText = function (addFormContainer) { + var helpText = "[data-help-text=add-form]"; + if ($(addFormContainer).find("[data-add-form-clone]").length > 0) { + $(addFormContainer).find(helpText).hide(); + } else { + $(addFormContainer).find(helpText).show(); + } +}; + +// End of functions related to grid-input-view + +/** + * This method will return query parameter value given its name. + * @param name Query parameter name + * @returns {string} Query parameter value + */ +var getParameterByName = function (name) { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), + results = regex.exec(location.search); + return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); +}; + +function formatRepo(user) { + if (user.loading) { + return user.text + } + if (!user.username) { + return; + } + var markup = '
      ' + + '
      ' + + '
      ' + + '
      ' + user.username + '
      '; + if (user.firstname) { + markup += '
      ' + user.firstname + '
      '; + } + if (user.emailAddress) { + markup += '
      ' + user.emailAddress + '
      '; + } + markup += '
      '; + return markup; +} + +function formatRepoSelection(user) { + return user.username || user.text; +} + +$(document).ready(function () { + + // Adding initial state of wizard-steps. + + var policyPayloadObj; + invokerUtil.get( + "/devicemgt_admin/policies/" + getParameterByName("id"), + // on success + function (data) { + data = JSON.parse(data); + policyPayloadObj = data["responseContent"]; + skipStep["policy-platform"](policyPayloadObj); + }, + // on error + function (data) { + console.log(data); + // should be redirected to an error page + } + ); + + $("#users-select-field").hide(); + $("#user-roles-select-field").show(); + + $("input[type='radio'].select-users-radio").change(function () { + if ($("#users-radio-btn").is(":checked")) { + $("#user-roles-select-field").hide(); + $("#users-select-field").show(); + } + if ($("#user-roles-radio-btn").is(":checked")) { + $("#users-select-field").hide(); + $("#user-roles-select-field").show(); + } + }); + + // Support for special input type "ANY" on user(s) & user-role(s) selection + $("#user-roles-input,#user-input").select2({ + "tags": false + }).on("select2:select", function (e) { + if (e.params.data.id == "ANY") { + $(this).val("ANY").trigger("change"); + } else { + $("option[value=ANY]", this).prop("selected", false).parent().trigger("change"); + } + }); + $("#policy-profile-wizard-steps").html($(".wr-steps").html()); + + $("select.select2[multiple=multiple]").select2({ + "tags": false + }); + + // Maintains an array of configured features of the profile + var advanceOperations = ".wr-advance-operations"; + $(advanceOperations).on("click", ".wr-input-control.switch", function (event) { + var operationCode = $(this).parents(".operation-data").data("operation-code"); + var operation = $(this).parents(".operation-data").data("operation"); + var operationDataWrapper = $(this).data("target"); + // prevents event bubbling by figuring out what element it's being called from. + if (event.target.tagName == "INPUT") { + var featureConfiguredIcon; + if ($("input[type='checkbox']", this).is(":checked")) { + configuredOperations.push(operationCode); + // when a feature is enabled, if "zero-configured-features" msg is available, hide that. + var zeroConfiguredOperationsErrorMsg = "#policy-profile-main-error-msg"; + if (!$(zeroConfiguredOperationsErrorMsg).hasClass("hidden")) { + $(zeroConfiguredOperationsErrorMsg).addClass("hidden"); + } + // add configured-state-icon to the feature + featureConfiguredIcon = "#" + operation + "-configured"; + if ($(featureConfiguredIcon).hasClass("hidden")) { + $(featureConfiguredIcon).removeClass("hidden"); + } + } else { + //splicing the array if operation is present. + var index = $.inArray(operationCode, configuredOperations); + if (index != -1) { + configuredOperations.splice(index, 1); + } + // when a feature is disabled, clearing all its current configured, error or success states + var subErrorMsgWrapper = "#" + operation + "-feature-error-msg"; + var subErrorIcon = "#" + operation + "-error"; + var subOkIcon = "#" + operation + "-ok"; + featureConfiguredIcon = "#" + operation + "-configured"; + + if (!$(subErrorMsgWrapper).hasClass("hidden")) { + $(subErrorMsgWrapper).addClass("hidden"); + } + if (!$(subErrorIcon).hasClass("hidden")) { + $(subErrorIcon).addClass("hidden"); + } + if (!$(subOkIcon).hasClass("hidden")) { + $(subOkIcon).addClass("hidden"); + } + if (!$(featureConfiguredIcon).hasClass("hidden")) { + $(featureConfiguredIcon).addClass("hidden"); + } + // reinitializing input fields into the defaults + $(operationDataWrapper + " input").each( + function () { + if ($(this).is("input:text")) { + $(this).val($(this).data("default")); + } else if ($(this).is("input:password")) { + $(this).val(""); + } else if ($(this).is("input:checkbox")) { + $(this).prop("checked", $(this).data("default")); + // if this checkbox is the parent input of a grouped-input + if ($(this).hasClass("parent-input")) { + var groupedInput = $(this).parent().parent().parent(); + updateGroupedInputVisibility(groupedInput); + } + } + } + ); + // reinitializing select fields into the defaults + $(operationDataWrapper + " select").each( + function () { + var defaultOption = $(this).data("default"); + $("option:eq(" + defaultOption + ")", this).prop("selected", "selected"); + } + ); + // collapsing expanded-panes (upon the selection of html-select-options) if any + $(operationDataWrapper + " .expanded").each( + function () { + if ($(this).hasClass("expanded")) { + $(this).removeClass("expanded"); + } + $(this).slideUp(); + } + ); + // removing all entries of grid-input elements if exist + $(operationDataWrapper + " .grouped-array-input").each( + function () { + var gridInputs = $(this).find("[data-add-form-clone]"); + if (gridInputs.length > 0) { + gridInputs.remove(); + } + var helpTexts = $(this).find("[data-help-text=add-form]"); + if (helpTexts.length > 0) { + helpTexts.show(); + } + } + ); + } + } + }); + + // adding support for cloning multiple profiles per feature with cloneable class definitions + $(advanceOperations).on("click", ".multi-view.add.enabled", function () { + // get a copy of .cloneable and create new .cloned div element + var cloned = "

      " + $(".cloneable", $(this).parent().parent()).html() + "
      "; + // append newly created .cloned div element to panel-body + $(this).parent().parent().append(cloned); + // enable remove action of newly cloned div element + $(".cloned", $(this).parent().parent()).each( + function () { + if ($(".multi-view.remove", this).hasClass("disabled")) { + $(".multi-view.remove", this).removeClass("disabled"); + } + if (!$(".multi-view.remove", this).hasClass("enabled")) { + $(".multi-view.remove", this).addClass("enabled"); + } + } + ); + }); + + $(advanceOperations).on("click", ".multi-view.remove.enabled", function () { + $(this).parent().remove(); + }); + + // enabling or disabling grouped-input based on the status of a parent check-box + $(advanceOperations).on("click", ".grouped-input", function () { + updateGroupedInputVisibility(this); + }); + + // add form entry click function for grid inputs + $(advanceOperations).on("click", "[data-click-event=add-form]", function () { + var addFormContainer = $("[data-add-form-container=" + $(this).attr("href") + "]"); + var clonedForm = $("[data-add-form=" + $(this).attr("href") + "]").clone(). + find("[data-add-form-element=clone]").attr("data-add-form-clone", $(this).attr("href")); + + // adding class .child-input to capture text-input-array-values + $("input, select", clonedForm).addClass("child-input"); + + $(addFormContainer).append(clonedForm); + setId(addFormContainer); + showHideHelpText(addFormContainer); + }); + + // remove form entry click function for grid inputs + $(advanceOperations).on("click", "[data-click-event=remove-form]", function () { + var addFormContainer = $("[data-add-form-container=" + $(this).attr("href") + "]"); + + $(this).closest("[data-add-form-element=clone]").remove(); + setId(addFormContainer); + showHideHelpText(addFormContainer); + }); + + $(".wizard-stepper").click(function () { + // button clicked here can be either a continue button or a back button. + var currentStep = $(this).data("current"); + var validationIsRequired = $(this).data("validate"); + var wizardIsToBeContinued; + + if (validationIsRequired) { + wizardIsToBeContinued = validateStep[currentStep](); + } else { + wizardIsToBeContinued = true; + } + + if (wizardIsToBeContinued) { + // When moving back and forth, following code segment will + // remove if there are any visible error-messages. + var errorMsgWrappers = ".alert.alert-danger"; + $(errorMsgWrappers).each( + function () { + if (!$(this).hasClass("hidden")) { + $(this).addClass("hidden"); + } + } + ); + + var nextStep = $(this).data("next"); + var isBackBtn = $(this).data("is-back-btn"); + + // if current button is a continuation... + if (!isBackBtn) { + // initiate stepForwardFrom[*] functions to gather form data. + if (stepForwardFrom[currentStep]) { + stepForwardFrom[currentStep](this); + } + } else { + // initiate stepBackFrom[*] functions to rollback. + if (stepBackFrom[currentStep]) { + stepBackFrom[currentStep](); + } + } + + // following step occurs only at the last stage of the wizard. + if (!nextStep) { + window.location.href = $(this).data("direct"); + } + + // updating next wizard step as current. + $(".itm-wiz").each(function () { + var step = $(this).data("step"); + if (step == nextStep) { + $(this).addClass("itm-wiz-current"); + } else { + $(this).removeClass("itm-wiz-current"); + } + }); + + // adding next update of wizard-steps. + $("#" + nextStep + "-wizard-steps").html($(".wr-steps").html()); + + // hiding current section of the wizard and showing next section. + $("." + currentStep).addClass("hidden"); + $("." + nextStep).removeClass("hidden"); + } + }); + +}); \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/public/templates/hidden-operations-android.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/public/templates/hidden-operations-android.hbs new file mode 100644 index 000000000..d82da3c0f --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/public/templates/hidden-operations-android.hbs @@ -0,0 +1,457 @@ +
      + + +
      + +
      +
      + +
      + + + +
      + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      +
      +
      +
      + + + +
      +
      + +
      + + Un-check following checkbox in case you need to disable camera. +
      +
      +
      + +
      +
      +
      +
      +
      + + + +
      +
      + +
      + + Un-check following checkbox in case you do not need the device to be encrypted. +
      +
      +
      + +
      +
      +
      +
      +
      + + + +
      +
      + +
      + + + + + + + + + + + + + + + + + Please note that * sign represents required fields of data. +
      +
      + +
      + + +
      +
      + + +
      + +
      +
      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/public/templates/hidden-operations-ios.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/public/templates/hidden-operations-ios.hbs new file mode 100644 index 000000000..aec18bda8 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/public/templates/hidden-operations-ios.hbs @@ -0,0 +1,2923 @@ +
      + + +
      + +
      +
      + +
      + + + +
      + +
      + +
      + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      +
      +
      +
      + + + +
      +
      + +
      + + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + +
      + +
      + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + + + +
      + + +
      + + + +
      + +
      + + + + + + + + + + + + + +
      No:Roaming Consortium OI
      + No entries added yet . +
      + + + + + + + + + +
      +
      + +
      + +
      + +
      + + + + + + + + + + + + + +
      No:NAI Realm Name
      + No entries added yet . +
      + + + + + + + + + +
      +
      + +
      + +
      + +
      + + + + + + + + + + + + + + +
      No:Mobile Country Code ( MCC )Mobile Network Code ( MNC )
      + No entries added yet . +
      + + + + + + + + + + +
      +
      +
      +
      +
      + + + +
      +
      + +
      + + + +
      + + +
      + +
      + + +
      + +
      + +
      + + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + +
      + +
      + Incoming Mail Settings : +
      + +
      + + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + Outgoing Mail Settings : +
      + +
      + + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + + +
      + +
      +
      +
      + + + +
      +
      + +
      + + + +
      + +
      + + + + + + + + + + + + + + +
      No:Device NamePassword
      + No entries added yet . +
      + + + + + + + + + + +
      +
      + +
      + +
      + +
      + + + + + + + + + + + + + +
      No:Destination
      + No entries added yet . +
      + + + + + + + + + +
      +
      + +
      +
      +
      + + + +
      +
      + +
      + + + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + +
      + + + + + + + + + + + + + + + +
      No:DescriptionSearch BaseScope
      + No entries added yet . +
      + + + + + + + + + + + +
      +
      + +
      +
      +
      + + + +
      +
      + +
      + + + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      +
      +
      + + + +
      +
      + +
      + + + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      +
      +
      + + + +
      +
      + +
      + + + +
      + +
      + + + + + + + + + + + + + + + + + +
      No:APNUsernamePasswordProxyPort
      + No entries added yet . +
      + + + + + + + + + + + + + +
      +
      + +
      +
      +
      + + + +
      +
      + +
      + + + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + +
      + + + + + + + + + + + + + + + + + + +
      No:APNAuth.TypeUsernamePasswordProxyPort
      + No entries added yet . +
      + + + + + + + + + + + + + + +
      +
      + +
      +
      +
      + + + +
      +
      + +
      + + + + Restrictions on Device Functionality : +
      +
      +
        +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
          +
        • +
          + +
          +
        • +
        • +
          + +
          +
        • +
        • +
          + +
          +
        • +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      +
      +
      + Restrictions on Applications : +
      +
      +
        +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
          +
        • +
          + +
          +
        • +
        • +
          + +
          +
        • +
        • +
          + +
          +
        • +
        • +
          + +
          +
        • +
        • +
          + + +
          +
        • +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      +
      + + + +
      +
      +
      + +
      +
      \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/public/templates/hidden-operations-windows.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/public/templates/hidden-operations-windows.hbs new file mode 100644 index 000000000..3b5bb3bef --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.edit/public/templates/hidden-operations-windows.hbs @@ -0,0 +1,460 @@ +
      + + +
      + +
      +
      + +
      + + + +
      + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      +
      +
      +
      + + + +
      +
      + +
      + + Un-check following checkbox in case you need to disable camera. +
      +
      +
      + +
      +
      +
      +
      +
      + + + +
      +
      + +
      + + Un-check following checkbox in case you need to disable storage-encryption. +
      +
      +
      + +
      +
      +
      +
      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/public/js/policy-view.js b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/public/js/policy-view.js new file mode 100644 index 000000000..8ca8ff879 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/public/js/policy-view.js @@ -0,0 +1,2256 @@ +/* + * 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. + */ + +var validateStep = {}; +var skipStep = {}; +var stepForwardFrom = {}; +var stepBackFrom = {}; +var policy = {}; +var configuredOperations = []; + +// Constants to define platform types available +var platformTypeConstants = { + "ANDROID": "android", + "IOS": "ios", + "WINDOWS": "windows" +}; + +// Constants to define Android Operation Constants +var androidOperationConstants = { + "PASSCODE_POLICY_OPERATION": "passcode-policy", + "PASSCODE_POLICY_OPERATION_CODE": "PASSCODE_POLICY", + "CAMERA_OPERATION": "camera", + "CAMERA_OPERATION_CODE": "CAMERA", + "ENCRYPT_STORAGE_OPERATION": "encrypt-storage", + "ENCRYPT_STORAGE_OPERATION_CODE": "ENCRYPT_STORAGE", + "WIFI_OPERATION": "wifi", + "WIFI_OPERATION_CODE": "WIFI" +}; + +// Constants to define Android Operation Constants +var windowsOperationConstants = { + "PASSCODE_POLICY_OPERATION": "passcode-policy", + "PASSCODE_POLICY_OPERATION_CODE": "PASSCODE_POLICY", + "CAMERA_OPERATION": "camera", + "CAMERA_OPERATION_CODE": "CAMERA", + "ENCRYPT_STORAGE_OPERATION": "encrypt-storage", + "ENCRYPT_STORAGE_OPERATION_CODE": "ENCRYPT_STORAGE" +}; + +// Constants to define iOS Operation Constants +var iosOperationConstants = { + "PASSCODE_POLICY_OPERATION": "passcode-policy", + "PASSCODE_POLICY_OPERATION_CODE": "PASSCODE_POLICY", + "RESTRICTIONS_OPERATION": "restrictions", + "RESTRICTIONS_OPERATION_CODE": "RESTRICTION", + "WIFI_OPERATION": "wifi", + "WIFI_OPERATION_CODE": "WIFI", + "EMAIL_OPERATION": "email", + "EMAIL_OPERATION_CODE": "EMAIL", + "AIRPLAY_OPERATION": "airplay", + "AIRPLAY_OPERATION_CODE": "AIR_PLAY", + "LDAP_OPERATION": "ldap", + "LDAP_OPERATION_CODE": "LDAP", + "CALENDAR_OPERATION": "calendar", + "CALENDAR_OPERATION_CODE": "CALDAV", + "CALENDAR_SUBSCRIPTION_OPERATION": "calendar-subscription", + "CALENDAR_SUBSCRIPTION_OPERATION_CODE": "CALENDAR_SUBSCRIPTION", + "APN_OPERATION": "apn", + "APN_OPERATION_CODE": "APN", + "CELLULAR_OPERATION": "cellular", + "CELLULAR_OPERATION_CODE": "CELLULAR" +}; + +/** + * Method to update the visibility (i.e. disabled or enabled view) + * of grouped input according to the values + * that they currently possess. + * @param domElement HTML grouped-input element with class name "grouped-input" + */ +var updateGroupedInputVisibility = function (domElement) { + if ($(".parent-input:first", domElement).is(":checked")) { + if ($(".grouped-child-input:first", domElement).hasClass("disabled")) { + $(".grouped-child-input:first", domElement).removeClass("disabled"); + } + $(".child-input", domElement).each(function () { + $(this).prop('disabled', false); + }); + } else { + if (!$(".grouped-child-input:first", domElement).hasClass("disabled")) { + $(".grouped-child-input:first", domElement).addClass("disabled"); + } + $(".child-input", domElement).each(function () { + $(this).prop('disabled', true); + }); + } +}; + +skipStep["policy-platform"] = function (policyPayloadObj) { + policy["name"] = policyPayloadObj["policyName"]; + policy["platform"] = policyPayloadObj["profile"]["deviceType"]["name"]; + policy["platformId"] = policyPayloadObj["profile"]["deviceType"]["id"]; + var userRoleInput = $("#user-roles-input"); + var ownershipInput = $("#ownership-input"); + var userInput = $("#users-select-field"); + var actionInput = $("#action-input"); + var policyNameInput = $("#policy-name-input"); + var policyDescriptionInput = $("#policy-description-input"); + userRoleInput.val(policyPayloadObj.roles); + userInput.val(policyPayloadObj.users); + ownershipInput.val(policyPayloadObj.ownershipType); + actionInput.val(policyPayloadObj.compliance); + policyNameInput.val(policyPayloadObj["policyName"]); + policyDescriptionInput.val(policyPayloadObj["description"]); + // updating next-page wizard title with selected platform + $("#policy-heading").text(policy["platform"].toUpperCase() + " POLICY - " + policy["name"].toUpperCase()); + $("#policy-platform").text(policy["platform"].toUpperCase()); + $("#policy-assignment").text(policyPayloadObj.ownershipType); + $("#policy-action").text(policyPayloadObj.compliance.toUpperCase()); + $("#policy-description").text(policyPayloadObj["description"]); + var policyStatus = "Active"; + if (policyPayloadObj["active"] == true && policyPayloadObj["updated"] == true) { + policyStatus = ' Active/Updated'; + } else if (policyPayloadObj["active"] == true && policyPayloadObj["updated"] == false) { + policyStatus = ' Active'; + } else if (policyPayloadObj["active"] == false && policyPayloadObj["updated"] == true) { + policyStatus = ' Inactive/Updated'; + } else if (policyPayloadObj["active"] == false && policyPayloadObj["updated"] == false) { + policyStatus = ' Inactive'; + } + + $("#policy-status").html(policyStatus); + + if (policyPayloadObj.users.length > 0) { + $("#policy-users").text(policyPayloadObj.users.toString().split(",").join(", ")); + } else { + $("#users-row").addClass("hidden"); + } + + if (policyPayloadObj.roles.length > 0) { + $("#policy-roles").text(policyPayloadObj.roles.toString().split(",").join(", ")); + } else { + $("#roles-row").addClass("hidden"); + } + + var deviceType = policy["platform"]; + var hiddenOperationsByDeviceType = $("#hidden-operations-" + deviceType); + var hiddenOperationsByDeviceTypeCacheKey = deviceType + "HiddenOperations"; + var hiddenOperationsByDeviceTypeSrc = hiddenOperationsByDeviceType.attr("src"); + + setTimeout( + function () { + $.template(hiddenOperationsByDeviceTypeCacheKey, hiddenOperationsByDeviceTypeSrc, function (template) { + var content = template(); + // pushing profile feature input elements + $(".wr-advance-operations").html(content); + // populating values and getting the list of configured features + var configuredOperations = operationModule. + populateProfile(policy["platform"], policyPayloadObj["profile"]["profileFeaturesList"]); + // updating grouped input visibility according to the populated values + $(".wr-advance-operations li.grouped-input").each(function () { + updateGroupedInputVisibility(this); + }); + // enabling previously configured options of last update + for (var i = 0; i < configuredOperations.length; ++i) { + var configuredOperation = configuredOperations[i]; + $(".operation-data").filterByData("operation-code", configuredOperation). + find(".panel-title .wr-input-control.switch input[type=checkbox]").each(function () { + $(this).click(); + }); + } + }); + }, + 250 // time delayed for the execution of above function, 250 milliseconds + ); +}; + +/** + * Checks if provided number is valid against a range. + * + * @param numberInput Number Input + * @param min Minimum Limit + * @param max Maximum Limit + * @returns {boolean} Returns true if input is within the specified range + */ +var inputIsValidAgainstRange = function (numberInput, min, max) { + return (numberInput == min || (numberInput > min && numberInput < max) || numberInput == max); +}; + +/** + * Checks if provided input is valid against RegEx input. + * + * @param regExp Regular expression + * @param input Input string to check + * @returns {boolean} Returns true if input matches RegEx + */ +var inputIsValidAgainstRegExp = function (regExp, input) { + return regExp.test(input); +}; + +validateStep["policy-profile"] = function () { + var validationStatusArray = []; + var validationStatus; + var operation; + + // starting validation process and updating validationStatus + if (policy["platform"] == platformTypeConstants["ANDROID"]) { + if (configuredOperations.length == 0) { + // updating validationStatus + validationStatus = { + "error": true, + "mainErrorMsg": "You cannot continue. Zero configured features." + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } else { + // validating each and every configured Operation + // Validating PASSCODE_POLICY + if ($.inArray(androidOperationConstants["PASSCODE_POLICY_OPERATION_CODE"], configuredOperations) != -1) { + // if PASSCODE_POLICY is configured + operation = androidOperationConstants["PASSCODE_POLICY_OPERATION"]; + // initializing continueToCheckNextInputs to true + var continueToCheckNextInputs = true; + + // validating first input: passcodePolicyMaxPasscodeAgeInDays + var passcodePolicyMaxPasscodeAgeInDays = $("input#passcode-policy-max-passcode-age-in-days").val(); + if (passcodePolicyMaxPasscodeAgeInDays) { + if (!$.isNumeric(passcodePolicyMaxPasscodeAgeInDays)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode age is not a number.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(passcodePolicyMaxPasscodeAgeInDays, 1, 730)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode age is not with in the range of 1-to-730.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + // validating second and last input: passcodePolicyPasscodeHistory + if (continueToCheckNextInputs) { + var passcodePolicyPasscodeHistory = $("input#passcode-policy-passcode-history").val(); + if (passcodePolicyPasscodeHistory) { + if (!$.isNumeric(passcodePolicyPasscodeHistory)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode history is not a number.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(passcodePolicyPasscodeHistory, 1, 50)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode history is not with in the range of 1-to-50.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating CAMERA + if ($.inArray(androidOperationConstants["CAMERA_OPERATION_CODE"], configuredOperations) != -1) { + // if CAMERA is configured + operation = androidOperationConstants["CAMERA_OPERATION"]; + // updating validationStatus + validationStatus = { + "error": false, + "okFeature": operation + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating ENCRYPT_STORAGE + if ($.inArray(androidOperationConstants["ENCRYPT_STORAGE_OPERATION_CODE"], configuredOperations) != -1) { + // if ENCRYPT_STORAGE is configured + operation = androidOperationConstants["ENCRYPT_STORAGE_OPERATION"]; + // updating validationStatus + validationStatus = { + "error": false, + "okFeature": operation + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating WIFI + if ($.inArray(androidOperationConstants["WIFI_OPERATION_CODE"], configuredOperations) != -1) { + // if WIFI is configured + operation = androidOperationConstants["WIFI_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var wifiSSID = $("input#wifi-ssid").val(); + if (!wifiSSID) { + validationStatus = { + "error": true, + "subErrorMsg": "WIFI SSID is not given. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + } + } + if (policy["platform"] == platformTypeConstants["WINDOWS"]) { + if (configuredOperations.length == 0) { + // updating validationStatus + validationStatus = { + "error": true, + "mainErrorMsg": "You cannot continue. Zero configured features." + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } else { + // validating each and every configured Operation + // Validating PASSCODE_POLICY + if ($.inArray(windowsOperationConstants["PASSCODE_POLICY_OPERATION_CODE"], configuredOperations) != -1) { + // if PASSCODE_POLICY is configured + operation = windowsOperationConstants["PASSCODE_POLICY_OPERATION"]; + // initializing continueToCheckNextInputs to true + var continueToCheckNextInputs = true; + + // validating first input: passcodePolicyMaxPasscodeAgeInDays + var passcodePolicyMaxPasscodeAgeInDays = $("input#passcode-policy-max-passcode-age-in-days").val(); + if (passcodePolicyMaxPasscodeAgeInDays) { + if (!$.isNumeric(passcodePolicyMaxPasscodeAgeInDays)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode age is not a number.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(passcodePolicyMaxPasscodeAgeInDays, 1, 730)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode age is not with in the range of 1-to-730.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + // validating second and last input: passcodePolicyPasscodeHistory + if (continueToCheckNextInputs) { + var passcodePolicyPasscodeHistory = $("input#passcode-policy-passcode-history").val(); + if (passcodePolicyPasscodeHistory) { + if (!$.isNumeric(passcodePolicyPasscodeHistory)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode history is not a number.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(passcodePolicyPasscodeHistory, 1, 50)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode history is not with in the range of 1-to-50.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating CAMERA + if ($.inArray(windowsOperationConstants["CAMERA_OPERATION_CODE"], configuredOperations) != -1) { + // if CAMERA is configured + operation = windowsOperationConstants["CAMERA_OPERATION"]; + // updating validationStatus + validationStatus = { + "error": false, + "okFeature": operation + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating ENCRYPT_STORAGE + if ($.inArray(windowsOperationConstants["ENCRYPT_STORAGE_OPERATION_CODE"], configuredOperations) != -1) { + // if ENCRYPT_STORAGE is configured + operation = windowsOperationConstants["ENCRYPT_STORAGE_OPERATION"]; + // updating validationStatus + validationStatus = { + "error": false, + "okFeature": operation + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + + } + } else if (policy["platform"] == platformTypeConstants["IOS"]) { + if (configuredOperations.length == 0) { + // updating validationStatus + validationStatus = { + "error": true, + "mainErrorMsg": "You cannot continue. Zero configured features." + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } else { + // validating each and every configured Operation + // Validating PASSCODE_POLICY + if ($.inArray(iosOperationConstants["PASSCODE_POLICY_OPERATION_CODE"], configuredOperations) != -1) { + // if PASSCODE_POLICY is configured + operation = iosOperationConstants["PASSCODE_POLICY_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + // validating first input: passcodePolicyMaxPasscodeAgeInDays + passcodePolicyMaxPasscodeAgeInDays = $("input#passcode-policy-max-passcode-age-in-days").val(); + if (passcodePolicyMaxPasscodeAgeInDays) { + if (!$.isNumeric(passcodePolicyMaxPasscodeAgeInDays)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode age is not a number.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(passcodePolicyMaxPasscodeAgeInDays, 1, 730)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode age is not with in the range of 1-to-730.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + // validating second and last input: passcodePolicyPasscodeHistory + if (continueToCheckNextInputs) { + passcodePolicyPasscodeHistory = $("input#passcode-policy-passcode-history").val(); + if (passcodePolicyPasscodeHistory) { + if (!$.isNumeric(passcodePolicyPasscodeHistory)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode history is not a number.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(passcodePolicyPasscodeHistory, 1, 50)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode history is not with in the range of 1-to-50.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating RESTRICTIONS + if ($.inArray(iosOperationConstants["RESTRICTIONS_OPERATION_CODE"], configuredOperations) != -1) { + // if RESTRICTION is configured + operation = iosOperationConstants["RESTRICTIONS_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + // getting input values to be validated + var restrictionsAutonomousSingleAppModePermittedAppIDsGridChildInputs = + "div#restrictions-autonomous-single-app-mode-permitted-app-ids .child-input"; + if ($(restrictionsAutonomousSingleAppModePermittedAppIDsGridChildInputs).length > 0) { + var childInput; + var childInputArray = []; + var emptyChildInputCount = 0; + var duplicatesExist = false; + // looping through each child input + $(restrictionsAutonomousSingleAppModePermittedAppIDsGridChildInputs).each(function () { + childInput = $(this).val(); + childInputArray.push(childInput); + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + }); + // checking for duplicates + var initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + var m, poppedChildInput; + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + var n; + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more permitted App ID entries in " + + "Autonomous Single App Mode are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with permitted App ID entries in " + + "Autonomous Single App Mode.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating WIFI + if ($.inArray(iosOperationConstants["WIFI_OPERATION_CODE"], configuredOperations) != -1) { + // if WIFI is configured + operation = iosOperationConstants["WIFI_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + // getting input values to be validated + wifiSSID = $("input#wifi-ssid").val(); + var wifiDomainName = $("input#wifi-domain-name").val(); + if (!wifiSSID && !wifiDomainName) { + validationStatus = { + "error": true, + "subErrorMsg": "Both Wi-Fi SSID and Wi-Fi Domain Name are not given. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + // getting proxy-setup value + var wifiProxyType = $("select#wifi-proxy-type").find("option:selected").attr("value"); + if (wifiProxyType == "Manual") { + // adds up additional fields to be validated + var wifiProxyServer = $("input#wifi-proxy-server").val(); + if (!wifiProxyServer) { + validationStatus = { + "error": true, + "subErrorMsg": "Wi-Fi Proxy Server is required. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + var wifiProxyPort = $("input#wifi-proxy-port").val(); + if (!wifiProxyPort) { + validationStatus = { + "error": true, + "subErrorMsg": "Wi-Fi Proxy Port is required. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!$.isNumeric(wifiProxyPort)) { + validationStatus = { + "error": true, + "subErrorMsg": "Wi-Fi Proxy Port requires a number input.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(wifiProxyPort, 0, 65535)) { + validationStatus = { + "error": true, + "subErrorMsg": "Wi-Fi Proxy Port is not within the range " + + "of valid port numbers.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + } + + if (continueToCheckNextInputs) { + // getting encryption-type value + var wifiEncryptionType = $("select#wifi-encryption-type").find("option:selected").attr("value"); + if (wifiEncryptionType != "None") { + var wifiPayloadCertificateAnchorUUIDsGridChildInputs = + "div#wifi-payload-certificate-anchor-uuids .child-input"; + if ($(wifiPayloadCertificateAnchorUUIDsGridChildInputs).length > 0) { + emptyChildInputCount = 0; + childInputArray = []; + duplicatesExist = false; + // looping through each child input + $(wifiPayloadCertificateAnchorUUIDsGridChildInputs).each(function () { + childInput = $(this).val(); + childInputArray.push(childInput); + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more Payload Certificate " + + "Anchor UUIDs are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist " + + "with Payload Certificate Anchor UUIDs.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + if (continueToCheckNextInputs) { + var wifiTLSTrustedServerNamesGridChildInputs = + "div#wifi-tls-trusted-server-names .child-input"; + if ($(wifiTLSTrustedServerNamesGridChildInputs).length > 0) { + emptyChildInputCount = 0; + childInputArray = []; + duplicatesExist = false; + // looping through each child input + $(wifiTLSTrustedServerNamesGridChildInputs).each(function () { + childInput = $(this).val(); + childInputArray.push(childInput); + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more TLS Trusted Server Names are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist " + + "with TLS Trusted Server Names.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + } + } + + if (continueToCheckNextInputs) { + var wifiRoamingConsortiumOIsGridChildInputs = "div#wifi-roaming-consortium-ois .child-input"; + if ($(wifiRoamingConsortiumOIsGridChildInputs).length > 0) { + emptyChildInputCount = 0; + var outOfAllowedLengthCount = 0; + var invalidAgainstRegExCount = 0; + childInputArray = []; + duplicatesExist = false; + // looping through each child input + $(wifiRoamingConsortiumOIsGridChildInputs).each(function () { + childInput = $(this).val(); + childInputArray.push(childInput); + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } else if (!inputIsValidAgainstLength(childInput, 6, 6) && !inputIsValidAgainstLength(childInput, 10, 10)) { + outOfAllowedLengthCount++; + } else if (!inputIsValidAgainstRegExp(/^[a-fA-F0-9]+$/, childInput)) { + invalidAgainstRegExCount++; + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more Roaming Consortium OIs are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (outOfAllowedLengthCount > 0) { + // if outOfMaxAllowedLength input is present + validationStatus = { + "error": true, + "subErrorMsg": "One or more Roaming Consortium OIs " + + "are out of allowed length.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (invalidAgainstRegExCount > 0) { + // if invalid inputs in terms of hexadecimal format are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more Roaming Consortium OIs " + + "contain non-hexadecimal characters.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with Roaming Consortium OIs.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + if (continueToCheckNextInputs) { + var wifiNAIRealmNamesGridChildInputs = "div#wifi-nai-realm-names .child-input"; + if ($(wifiNAIRealmNamesGridChildInputs).length > 0) { + emptyChildInputCount = 0; + childInputArray = []; + duplicatesExist = false; + // looping through each child input + $(wifiNAIRealmNamesGridChildInputs).each(function () { + childInput = $(this).val(); + childInputArray.push(childInput); + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more NAI Realm Names are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with NAI Realm Names.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + if (continueToCheckNextInputs) { + var wifiMCCAndMNCsGridChildInputs = "div#wifi-mcc-and-mncs .child-input"; + if ($(wifiMCCAndMNCsGridChildInputs).length > 0) { + var childInputCount = 0; + var stringPair; + emptyChildInputCount = 0; + outOfAllowedLengthCount = 0; + var notNumericInputCount = 0; + childInputArray = []; + duplicatesExist = false; + // looping through each child input + $(wifiMCCAndMNCsGridChildInputs).each(function () { + childInput = $(this).val(); + // pushing each string pair to childInputArray + childInputCount++; + if (childInputCount % 2 == 1) { + // initialize stringPair value + stringPair = ""; + // append first part of the string + stringPair += childInput; + } else { + // append second part of the string + stringPair += childInput; + childInputArray.push(stringPair); + } + // updating emptyChildInputCount & outOfAllowedLengthCount + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } else if (!$.isNumeric(childInput)) { + notNumericInputCount++; + } else if (!inputIsValidAgainstLength(childInput, 3, 3)) { + outOfAllowedLengthCount++; + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more MCC/MNC pairs are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (notNumericInputCount > 0) { + // if notNumeric input is present + validationStatus = { + "error": true, + "subErrorMsg": "One or more MCC/MNC pairs are not numeric.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (outOfAllowedLengthCount > 0) { + // if outOfAllowedLength input is present + validationStatus = { + "error": true, + "subErrorMsg": "One or more MCC/MNC pairs " + + "do not fulfill the accepted length of 6 digits.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with MCC/MNC pairs.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating EMAIL + if ($.inArray(iosOperationConstants["EMAIL_OPERATION_CODE"], configuredOperations) != -1) { + // if EMAIL is configured + operation = iosOperationConstants["EMAIL_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var emailAddress = $("input#email-address").val(); + if (emailAddress && !inputIsValidAgainstRegExp(/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/, emailAddress)) { + validationStatus = { + "error": true, + "subErrorMsg": "Email Address is not valid.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + var emailIncomingMailServerHostname = $("input#email-incoming-mail-server-hostname").val(); + if (!emailIncomingMailServerHostname) { + validationStatus = { + "error": true, + "subErrorMsg": "Incoming Mail Server Hostname is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + if (continueToCheckNextInputs) { + var emailIncomingMailServerPort = $("input#email-incoming-mail-server-port").val(); + if (!emailIncomingMailServerPort) { + validationStatus = { + "error": true, + "subErrorMsg": "Incoming Mail Server Port is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!$.isNumeric(emailIncomingMailServerPort)) { + validationStatus = { + "error": true, + "subErrorMsg": "Incoming Mail Server Port requires a number input.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(emailIncomingMailServerPort, 0, 65535)) { + validationStatus = { + "error": true, + "subErrorMsg": "Incoming Mail Server Port is not within the range " + + "of valid port numbers.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + if (continueToCheckNextInputs) { + var emailOutgoingMailServerHostname = $("input#email-outgoing-mail-server-hostname").val(); + if (!emailOutgoingMailServerHostname) { + validationStatus = { + "error": true, + "subErrorMsg": "Outgoing Mail Server Hostname is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + if (continueToCheckNextInputs) { + var emailOutgoingMailServerPort = $("input#email-outgoing-mail-server-port").val(); + if (!emailOutgoingMailServerPort) { + validationStatus = { + "error": true, + "subErrorMsg": "Outgoing Mail Server Port is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!$.isNumeric(emailOutgoingMailServerPort)) { + validationStatus = { + "error": true, + "subErrorMsg": "Outgoing Mail Server Port requires a number input.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(emailOutgoingMailServerPort, 0, 65535)) { + validationStatus = { + "error": true, + "subErrorMsg": "Outgoing Mail Server Port is not within the range " + + "of valid port numbers.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating AIRPLAY + if ($.inArray(iosOperationConstants["AIRPLAY_OPERATION_CODE"], configuredOperations) != -1) { + // if AIRPLAY is configured + operation = iosOperationConstants["AIRPLAY_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var airplayCredentialsGridChildInputs = "div#airplay-credentials .child-input"; + var airplayDestinationsGridChildInputs = "div#airplay-destinations .child-input"; + if ($(airplayCredentialsGridChildInputs).length == 0 && + $(airplayDestinationsGridChildInputs).length == 0) { + validationStatus = { + "error": true, + "subErrorMsg": "AirPlay settings have zero configurations attached.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + if ($(airplayCredentialsGridChildInputs).length > 0) { + childInputCount = 0; + childInputArray = []; + emptyChildInputCount = 0; + duplicatesExist = false; + // looping through each child input + $(airplayCredentialsGridChildInputs).each(function () { + childInputCount++; + if (childInputCount % 2 == 1) { + // if child input is of first column + childInput = $(this).val(); + childInputArray.push(childInput); + // updating emptyChildInputCount + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more Device Names of " + + "AirPlay Credentials are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + // if duplicate input is present + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with " + + "Device Names of AirPlay Credentials.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + if (continueToCheckNextInputs) { + if ($(airplayDestinationsGridChildInputs).length > 0) { + childInputArray = []; + emptyChildInputCount = 0; + invalidAgainstRegExCount = 0; + duplicatesExist = false; + // looping through each child input + $(airplayDestinationsGridChildInputs).each(function () { + childInput = $(this).val(); + childInputArray.push(childInput); + // updating emptyChildInputCount + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } else if (!inputIsValidAgainstRegExp( + /([a-z|A-Z|0-9][a-z|A-Z|0-9][:]){5}([a-z|A-Z|0-9][a-z|A-Z|0-9])$/, childInput)) { + // if child input field is invalid against RegEx + invalidAgainstRegExCount++ + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more AirPlay Destination fields are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (invalidAgainstRegExCount > 0) { + // if invalidAgainstRegEx inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more AirPlay Destination fields " + + "do not fulfill expected format.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + // if duplicate input is present + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with AirPlay Destinations.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating LDAP + if ($.inArray(iosOperationConstants["LDAP_OPERATION_CODE"], configuredOperations) != -1) { + // if LDAP is configured + operation = iosOperationConstants["LDAP_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var ldapAccountHostname = $("input#ldap-account-hostname").val(); + if (!ldapAccountHostname) { + validationStatus = { + "error": true, + "subErrorMsg": "LDAP Account Hostname URL is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + var ldapSearchSettingsGridChildInputs = "div#ldap-search-settings .child-input"; + if ($(ldapSearchSettingsGridChildInputs).length > 0) { + childInputCount = 0; + childInputArray = []; + emptyChildInputCount = 0; + duplicatesExist = false; + // looping through each child input + $(ldapSearchSettingsGridChildInputs).each(function () { + childInputCount++; + if (childInputCount % 3 == 2) { + // if child input is of second column + childInput = $(this).find("option:selected").attr("value"); + stringPair = ""; + stringPair += (childInput + " "); + } else if (childInputCount % 3 == 0) { + // if child input is of third column + childInput = $(this).val(); + stringPair += childInput; + childInputArray.push(stringPair); + // updating emptyChildInputCount + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more Search Setting Scope fields are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + // if duplicate input is present + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with " + + "Search Setting Search Base and Scope pairs.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating CALENDAR + if ($.inArray(iosOperationConstants["CALENDAR_OPERATION_CODE"], configuredOperations) != -1) { + // if CALENDAR is configured + operation = iosOperationConstants["CALENDAR_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var calendarAccountHostname = $("input#calendar-account-hostname").val(); + if (!calendarAccountHostname) { + validationStatus = { + "error": true, + "subErrorMsg": "Account Hostname URL is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + var calendarAccountPort = $("input#calendar-account-port").val(); + if (!calendarAccountPort) { + validationStatus = { + "error": true, + "subErrorMsg": "Account Port is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!$.isNumeric(calendarAccountPort)) { + validationStatus = { + "error": true, + "subErrorMsg": "Account Port requires a number input.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(calendarAccountPort, 0, 65535)) { + validationStatus = { + "error": true, + "subErrorMsg": "Account Port is not within the range " + + "of valid port numbers.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating CALENDAR_SUBSCRIPTION + if ($.inArray(iosOperationConstants["CALENDAR_SUBSCRIPTION_OPERATION_CODE"], configuredOperations) != -1) { + // if CALENDAR_SUBSCRIPTION is configured + operation = iosOperationConstants["CALENDAR_SUBSCRIPTION_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var calendarSubscriptionHostname = $("input#calendar-subscription-hostname").val(); + if (!calendarSubscriptionHostname) { + validationStatus = { + "error": true, + "subErrorMsg": "Account Hostname URL is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating APN + if ($.inArray(iosOperationConstants["APN_OPERATION_CODE"], configuredOperations) != -1) { + // if APN is configured + operation = iosOperationConstants["APN_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var apnConfigurationsGridChildInputs = "div#apn-configurations .child-input"; + if ($(apnConfigurationsGridChildInputs).length == 0) { + validationStatus = { + "error": true, + "subErrorMsg": "APN Settings have zero configurations attached.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if ($(apnConfigurationsGridChildInputs).length > 0) { + childInputCount = 0; + childInputArray = []; + // checking empty APN field count + emptyChildInputCount = 0; + duplicatesExist = false; + // looping through each child input + $(apnConfigurationsGridChildInputs).each(function () { + childInputCount++; + if (childInputCount % 5 == 1) { + // if child input is of first column + childInput = $(this).val(); + childInputArray.push(childInput); + // updating emptyChildInputCount + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more APN fields of Configurations are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + // if duplicate input is present + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with " + + "APN fields of Configurations.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating CELLULAR + if ($.inArray(iosOperationConstants["CELLULAR_OPERATION_CODE"], configuredOperations) != -1) { + // if CELLULAR is configured + operation = iosOperationConstants["CELLULAR_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var cellularAttachAPNName = $("input#cellular-attach-apn-name").val(); + if (!cellularAttachAPNName) { + validationStatus = { + "error": true, + "subErrorMsg": "Cellular Configuration Name is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + var cellularAPNConfigurationsGridChildInputs = "div#cellular-apn-configurations .child-input"; + if ($(cellularAPNConfigurationsGridChildInputs).length > 0) { + childInputCount = 0; + childInputArray = []; + // checking empty APN field count + emptyChildInputCount = 0; + duplicatesExist = false; + // looping through each child input + $(cellularAPNConfigurationsGridChildInputs).each(function () { + childInputCount++; + if (childInputCount % 6 == 1) { + // if child input is of first column + childInput = $(this).val(); + childInputArray.push(childInput); + // updating emptyChildInputCount + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more APN fields of APN Configurations are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + // if duplicate input is present + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with " + + "APN fields of APN Configurations.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + } + } + // ending validation process + + // start taking specific notifying actions upon validation + var wizardIsToBeContinued; + var errorCount = 0; + var mainErrorMsgWrapper, mainErrorMsg, + subErrorMsgWrapper, subErrorMsg, subErrorIcon, subOkIcon, featureConfiguredIcon; + var i; + for (i = 0; i < validationStatusArray.length; i++) { + validationStatus = validationStatusArray[i]; + if (validationStatus["error"]) { + errorCount++; + if (validationStatus["mainErrorMsg"]) { + mainErrorMsgWrapper = "#policy-profile-main-error-msg"; + mainErrorMsg = mainErrorMsgWrapper + " span"; + $(mainErrorMsg).text(validationStatus["mainErrorMsg"]); + $(mainErrorMsgWrapper).removeClass("hidden"); + } else if (validationStatus["subErrorMsg"]) { + subErrorMsgWrapper = "#" + validationStatus["erroneousFeature"] + "-feature-error-msg"; + subErrorMsg = subErrorMsgWrapper + " span"; + subErrorIcon = "#" + validationStatus["erroneousFeature"] + "-error"; + subOkIcon = "#" + validationStatus["erroneousFeature"] + "-ok"; + featureConfiguredIcon = "#" + validationStatus["erroneousFeature"] + "-configured"; + // hiding featureConfiguredState as the first step + if (!$(featureConfiguredIcon).hasClass("hidden")) { + $(featureConfiguredIcon).addClass("hidden"); + } + // updating error state and corresponding messages + $(subErrorMsg).text(validationStatus["subErrorMsg"]); + if ($(subErrorMsgWrapper).hasClass("hidden")) { + $(subErrorMsgWrapper).removeClass("hidden"); + } + if (!$(subOkIcon).hasClass("hidden")) { + $(subOkIcon).addClass("hidden"); + } + if ($(subErrorIcon).hasClass("hidden")) { + $(subErrorIcon).removeClass("hidden"); + } + } + } else { + if (validationStatus["okFeature"]) { + subErrorMsgWrapper = "#" + validationStatus["okFeature"] + "-feature-error-msg"; + subErrorIcon = "#" + validationStatus["okFeature"] + "-error"; + subOkIcon = "#" + validationStatus["okFeature"] + "-ok"; + featureConfiguredIcon = "#" + validationStatus["okFeature"] + "-configured"; + // hiding featureConfiguredState as the first step + if (!$(featureConfiguredIcon).hasClass("hidden")) { + $(featureConfiguredIcon).addClass("hidden"); + } + // updating success state and corresponding messages + if (!$(subErrorMsgWrapper).hasClass("hidden")) { + $(subErrorMsgWrapper).addClass("hidden"); + } + if (!$(subErrorIcon).hasClass("hidden")) { + $(subErrorIcon).addClass("hidden"); + } + if ($(subOkIcon).hasClass("hidden")) { + $(subOkIcon).removeClass("hidden"); + } + } + } + } + + wizardIsToBeContinued = (errorCount == 0); + return wizardIsToBeContinued; +}; + +stepForwardFrom["policy-profile"] = function () { + policy["profile"] = operationModule.generateProfile(policy["platform"], configuredOperations); + // updating next-page wizard title with selected platform + $("#policy-criteria-page-wizard-title").text(policy["platform"] + " POLICY - " + policy["name"]); +}; + +stepForwardFrom["policy-criteria"] = function () { + $("input[type='radio'].select-users-radio").each(function () { + if ($(this).is(':radio')) { + if ($(this).is(":checked")) { + if ($(this).attr("id") == "users-radio-btn") { + policy["selectedUsers"] = $("#users-input").val(); + } else if ($(this).attr("id") == "user-roles-radio-btn") { + policy["selectedUserRoles"] = $("#user-roles-input").val(); + } + } + } + }); + policy["selectedNonCompliantAction"] = $("#action-input").find(":selected").data("action"); + policy["selectedOwnership"] = $("#ownership-input").val(); + // updating next-page wizard title with selected platform + $("#policy-naming-page-wizard-title").text(policy["platform"] + " POLICY - " + policy["name"]); +}; + +/** + * Checks if provided input is valid against provided length range. + * + * @param input Alphanumeric or non-alphanumeric input + * @param minLength Minimum Required Length + * @param maxLength Maximum Required Length + * @returns {boolean} Returns true if input matches the provided minimum length and maximum length + */ +var inputIsValidAgainstLength = function (input, minLength, maxLength) { + var length = input.length; + return (length == minLength || (length > minLength && length < maxLength) || length == maxLength); +}; + +validateStep["policy-naming"] = function () { + var validationStatus = {}; + + // taking values of inputs to be validated + var policyName = $("input#policy-name-input").val(); + // starting validation process and updating validationStatus + if (!policyName) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = "Policy name is empty. You cannot proceed."; + } else if (!inputIsValidAgainstLength(policyName, 1, 30)) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = + "Policy name exceeds maximum allowed length."; + } else { + validationStatus["error"] = false; + } + // ending validation process + + // start taking specific actions upon validation + var wizardIsToBeContinued; + if (validationStatus["error"]) { + wizardIsToBeContinued = false; + var mainErrorMsgWrapper = "#policy-naming-main-error-msg"; + var mainErrorMsg = mainErrorMsgWrapper + " span"; + $(mainErrorMsg).text(validationStatus["mainErrorMsg"]); + $(mainErrorMsgWrapper).removeClass("hidden"); + } else { + wizardIsToBeContinued = true; + } + + return wizardIsToBeContinued; +}; + +validateStep["policy-naming-publish"] = function () { + var validationStatus = {}; + + // taking values of inputs to be validated + var policyName = $("input#policy-name-input").val(); + // starting validation process and updating validationStatus + if (!policyName) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = "Policy name is empty. You cannot proceed."; + } else if (!inputIsValidAgainstLength(policyName, 1, 30)) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = + "Policy name exceeds maximum allowed length."; + } else { + validationStatus["error"] = false; + } + // ending validation process + + // start taking specific actions upon validation + var wizardIsToBeContinued; + if (validationStatus["error"]) { + wizardIsToBeContinued = false; + var mainErrorMsgWrapper = "#policy-naming-main-error-msg"; + var mainErrorMsg = mainErrorMsgWrapper + " span"; + $(mainErrorMsg).text(validationStatus["mainErrorMsg"]); + $(mainErrorMsgWrapper).removeClass("hidden"); + } else { + wizardIsToBeContinued = true; + } + + return wizardIsToBeContinued; +}; + +stepForwardFrom["policy-naming-publish"] = function () { + policy["policyName"] = $("#policy-name-input").val(); + policy["description"] = $("#policy-description-input").val(); + //All data is collected. Policy can now be updated. + updatePolicy(policy, "publish"); +}; +stepForwardFrom["policy-naming"] = function () { + policy["policyName"] = $("#policy-name-input").val(); + policy["description"] = $("#policy-description-input").val(); + //All data is collected. Policy can now be updated. + updatePolicy(policy, "save"); +}; + +var updatePolicy = function (policy, state) { + var profilePayloads = []; + // traverses key by key in policy["profile"] + var key; + for (key in policy["profile"]) { + if (policy["profile"].hasOwnProperty(key)) { + profilePayloads.push({ + "featureCode": key, + "deviceTypeId": policy["platformId"], + "content": policy["profile"][key] + }); + } + } + var payload = { + "policyName": policy["policyName"], + "description": policy["description"], + "compliance": policy["selectedNonCompliantAction"], + "ownershipType": policy["selectedOwnership"], + "profile": { + "profileName": policy["policyName"], + "deviceType": { + "id": policy["platformId"] + }, + "profileFeaturesList": profilePayloads + } + }; + + if (policy["selectedUsers"]) { + payload["users"] = policy["selectedUsers"]; + } else if (policy["selectedUserRoles"]) { + payload["roles"] = policy["selectedUserRoles"]; + } else { + payload["users"] = []; + payload["roles"] = []; + } + var serviceURL = "/devicemgt_admin/policies/" + getParameterByName("id"); + invokerUtil.put( + serviceURL, + payload, + // on success + function () { + if (state == "save") { + var policyList = []; + policyList.push(getParameterByName("id")); + serviceURL = "/devicemgt_admin/policies/inactivate"; + invokerUtil.put( + serviceURL, + policyList, + // on success + function () { + $(".policy-message").removeClass("hidden"); + $(".add-policy").addClass("hidden"); + }, + // on error + function (data) { + console.log(data); + } + ); + } else if (state == "publish") { + var policyList = []; + policyList.push(getParameterByName("id")); + serviceURL = "/devicemgt_admin/policies/activate"; + invokerUtil.put( + serviceURL, + policyList, + // on success + function () { + $(".policy-message").removeClass("hidden"); + $(".add-policy").addClass("hidden"); + }, + // on error + function (data) { + console.log(data); + } + ); + } + }, + // on error + function (data) { + console.log(data); + } + ); +}; + +// Start of HTML embedded invoke methods +var showAdvanceOperation = function (operation, button) { + $(button).addClass('selected'); + $(button).siblings().removeClass('selected'); + var hiddenOperation = ".wr-hidden-operations-content > div"; + $(hiddenOperation + '[data-operation="' + operation + '"]').show(); + $(hiddenOperation + '[data-operation="' + operation + '"]').siblings().hide(); +}; + +/** + * Method to slide down a provided pane upon provided value set. + * + * @param selectElement Select HTML Element to consider + * @param paneID HTML ID of div element to slide down + * @param valueSet Applicable Value Set + */ +var slideDownPaneAgainstValueSet = function (selectElement, paneID, valueSet) { + var selectedValueOnChange = $(selectElement).find("option:selected").val(); + var i, slideDownVotes = 0; + for (i = 0; i < valueSet.length; i++) { + if (selectedValueOnChange == valueSet[i]) { + slideDownVotes++; + } + } + var paneSelector = "#" + paneID; + if (slideDownVotes > 0) { + if (!$(paneSelector).hasClass("expanded")) { + $(paneSelector).addClass("expanded"); + } + $(paneSelector).slideDown(); + } else { + if ($(paneSelector).hasClass("expanded")) { + $(paneSelector).removeClass("expanded"); + } + $(paneSelector).slideUp(); + /* now follows the code to reinitialize all inputs of the slidable pane. + reinitializing input fields into the defaults.*/ + $(paneSelector + " input").each( + function () { + if ($(this).is("input:text")) { + $(this).val($(this).data("default")); + } else if ($(this).is("input:password")) { + $(this).val(""); + } else if ($(this).is("input:checkbox")) { + $(this).prop("checked", $(this).data("default")); + // if this checkbox is the parent input of a grouped-input + if ($(this).hasClass("parent-input")) { + var groupedInput = $(this).parent().parent().parent(); + updateGroupedInputVisibility(groupedInput); + } + } + } + ); + // reinitializing select fields into the defaults + $(paneSelector + " select").each( + function () { + var defaultOption = $(this).data("default"); + $("option:eq(" + defaultOption + ")", this).prop("selected", "selected"); + } + ); + // collapsing expanded-panes (upon the selection of html-select-options) if any + $(paneSelector + " .expanded").each( + function () { + if ($(this).hasClass("expanded")) { + $(this).removeClass("expanded"); + } + $(this).slideUp(); + } + ); + // removing all entries of grid-input elements if exist + $(paneSelector + " .grouped-array-input").each( + function () { + var gridInputs = $(this).find("[data-add-form-clone]"); + if (gridInputs.length > 0) { + gridInputs.remove(); + } + var helpTexts = $(this).find("[data-help-text=add-form]"); + if (helpTexts.length > 0) { + helpTexts.show(); + } + } + ); + } +}; +// End of HTML embedded invoke methods + + +// Start of functions related to grid-input-view + +/** + * Method to set count id to cloned elements. + * @param {object} addFormContainer + */ +var setId = function (addFormContainer) { + $(addFormContainer).find("[data-add-form-clone]").each(function (i) { + $(this).attr("id", $(this).attr("data-add-form-clone").slice(1) + "-" + (i + 1)); + if ($(this).find(".index").length > 0) { + $(this).find(".index").html(i + 1); + } + }); +}; + +/** + * Method to set count id to cloned elements. + * @param {object} addFormContainer + */ +var showHideHelpText = function (addFormContainer) { + var helpText = "[data-help-text=add-form]"; + if ($(addFormContainer).find("[data-add-form-clone]").length > 0) { + $(addFormContainer).find(helpText).hide(); + } else { + $(addFormContainer).find(helpText).show(); + } +}; + +// End of functions related to grid-input-view + +/** + * This method will return query parameter value given its name. + * @param name Query parameter name + * @returns {string} Query parameter value + */ +var getParameterByName = function (name) { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), + results = regex.exec(location.search); + return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); +}; + +$(document).ready(function () { + $('#appbar-btn-apply-changes').addClass('hidden'); + // Adding initial state of wizard-steps. + $("#policy-profile-wizard-steps").html($(".wr-steps").html()); + + var policyPayloadObj; + invokerUtil.get( + "/devicemgt_admin/policies/" + getParameterByName("id"), + // on success + function (data) { + // console.log("success: " + JSON.stringify(data)); + data = JSON.parse(data); + policyPayloadObj = data["responseContent"]; + skipStep["policy-platform"](policyPayloadObj); + }, + // on error + function (data) { + console.log(data); + // should be redirected to an error page + } + ); + + $("select.select2[multiple=multiple]").select2({ + "tags": true + }); + + $("#users-select-field").hide(); + $("#user-roles-select-field").show(); + + $("input[type='radio'].select-users-radio").change(function () { + if ($("#users-radio-btn").is(":checked")) { + $("#user-roles-select-field").hide(); + $("#users-select-field").show(); + } + if ($("#user-roles-radio-btn").is(":checked")) { + $("#users-select-field").hide(); + $("#user-roles-select-field").show(); + } + }); + + // Support for special input type "ANY" on user(s) & user-role(s) selection + $("#users-input, #user-roles-input").select2({ + "tags": true + }).on("select2:select", function (e) { + if (e.params.data.id == "ANY") { + $(this).val("ANY").trigger("change"); + } else { + $("option[value=ANY]", this).prop("selected", false).parent().trigger("change"); + } + }); + + // Maintains an array of configured features of the profile + var advanceOperations = ".wr-advance-operations"; + $(advanceOperations).on("click", ".wr-input-control.switch", function (event) { + var operationCode = $(this).parents(".operation-data").data("operation-code"); + var operation = $(this).parents(".operation-data").data("operation"); + var operationDataWrapper = $(this).data("target"); + // prevents event bubbling by figuring out what element it's being called from. + if (event.target.tagName == "INPUT") { + var featureConfiguredIcon; + if ($("input[type='checkbox']", this).is(":checked")) { + configuredOperations.push(operationCode); + // when a feature is enabled, if "zero-configured-features" msg is available, hide that. + var zeroConfiguredOperationsErrorMsg = "#policy-profile-main-error-msg"; + if (!$(zeroConfiguredOperationsErrorMsg).hasClass("hidden")) { + $(zeroConfiguredOperationsErrorMsg).addClass("hidden"); + } + // add configured-state-icon to the feature + featureConfiguredIcon = "#" + operation + "-configured"; + if ($(featureConfiguredIcon).hasClass("hidden")) { + $(featureConfiguredIcon).removeClass("hidden"); + } + } else { + //splicing the array if operation is present. + var index = $.inArray(operationCode, configuredOperations); + if (index != -1) { + configuredOperations.splice(index, 1); + } + // when a feature is disabled, clearing all its current configured, error or success states + var subErrorMsgWrapper = "#" + operation + "-feature-error-msg"; + var subErrorIcon = "#" + operation + "-error"; + var subOkIcon = "#" + operation + "-ok"; + featureConfiguredIcon = "#" + operation + "-configured"; + + if (!$(subErrorMsgWrapper).hasClass("hidden")) { + $(subErrorMsgWrapper).addClass("hidden"); + } + if (!$(subErrorIcon).hasClass("hidden")) { + $(subErrorIcon).addClass("hidden"); + } + if (!$(subOkIcon).hasClass("hidden")) { + $(subOkIcon).addClass("hidden"); + } + if (!$(featureConfiguredIcon).hasClass("hidden")) { + $(featureConfiguredIcon).addClass("hidden"); + } + // reinitializing input fields into the defaults + $(operationDataWrapper + " input").each( + function () { + if ($(this).is("input:text")) { + $(this).val($(this).data("default")); + } else if ($(this).is("input:password")) { + $(this).val(""); + } else if ($(this).is("input:checkbox")) { + $(this).prop("checked", $(this).data("default")); + // if this checkbox is the parent input of a grouped-input + if ($(this).hasClass("parent-input")) { + var groupedInput = $(this).parent().parent().parent(); + updateGroupedInputVisibility(groupedInput); + } + } + } + ); + // reinitializing select fields into the defaults + $(operationDataWrapper + " select").each( + function () { + var defaultOption = $(this).data("default"); + $("option:eq(" + defaultOption + ")", this).prop("selected", "selected"); + } + ); + // collapsing expanded-panes (upon the selection of html-select-options) if any + $(operationDataWrapper + " .expanded").each( + function () { + if ($(this).hasClass("expanded")) { + $(this).removeClass("expanded"); + } + $(this).slideUp(); + } + ); + // removing all entries of grid-input elements if exist + $(operationDataWrapper + " .grouped-array-input").each( + function () { + var gridInputs = $(this).find("[data-add-form-clone]"); + if (gridInputs.length > 0) { + gridInputs.remove(); + } + var helpTexts = $(this).find("[data-help-text=add-form]"); + if (helpTexts.length > 0) { + helpTexts.show(); + } + } + ); + } + } + }); + + // adding support for cloning multiple profiles per feature with cloneable class definitions + $(advanceOperations).on("click", ".multi-view.add.enabled", function () { + // get a copy of .cloneable and create new .cloned div element + var cloned = "

      " + $(".cloneable", $(this).parent().parent()).html() + "
      "; + // append newly created .cloned div element to panel-body + $(this).parent().parent().append(cloned); + // enable remove action of newly cloned div element + $(".cloned", $(this).parent().parent()).each( + function () { + if ($(".multi-view.remove", this).hasClass("disabled")) { + $(".multi-view.remove", this).removeClass("disabled"); + } + if (!$(".multi-view.remove", this).hasClass("enabled")) { + $(".multi-view.remove", this).addClass("enabled"); + } + } + ); + }); + + $(advanceOperations).on("click", ".multi-view.remove.enabled", function () { + $(this).parent().remove(); + }); + + // enabling or disabling grouped-input based on the status of a parent check-box + $(advanceOperations).on("click", ".grouped-input", function () { + updateGroupedInputVisibility(this); + }); + + // add form entry click function for grid inputs + $(advanceOperations).on("click", "[data-click-event=add-form]", function () { + var addFormContainer = $("[data-add-form-container=" + $(this).attr("href") + "]"); + var clonedForm = $("[data-add-form=" + $(this).attr("href") + "]").clone(). + find("[data-add-form-element=clone]").attr("data-add-form-clone", $(this).attr("href")); + + // adding class .child-input to capture text-input-array-values + $("input, select", clonedForm).addClass("child-input"); + + $(addFormContainer).append(clonedForm); + setId(addFormContainer); + showHideHelpText(addFormContainer); + }); + + // remove form entry click function for grid inputs + $(advanceOperations).on("click", "[data-click-event=remove-form]", function () { + var addFormContainer = $("[data-add-form-container=" + $(this).attr("href") + "]"); + + $(this).closest("[data-add-form-element=clone]").remove(); + setId(addFormContainer); + showHideHelpText(addFormContainer); + }); + + $(".wizard-stepper").click(function () { + // button clicked here can be either a continue button or a back button. + var currentStep = $(this).data("current"); + var validationIsRequired = $(this).data("validate"); + var wizardIsToBeContinued; + + if (validationIsRequired) { + wizardIsToBeContinued = validateStep[currentStep](); + } else { + wizardIsToBeContinued = true; + } + + if (wizardIsToBeContinued) { + // When moving back and forth, following code segment will + // remove if there are any visible error-messages. + var errorMsgWrappers = ".alert.alert-danger"; + $(errorMsgWrappers).each( + function () { + if (!$(this).hasClass("hidden")) { + $(this).addClass("hidden"); + } + } + ); + + var nextStep = $(this).data("next"); + var isBackBtn = $(this).data("is-back-btn"); + + // if current button is a continuation... + if (!isBackBtn) { + // initiate stepForwardFrom[*] functions to gather form data. + if (stepForwardFrom[currentStep]) { + stepForwardFrom[currentStep](this); + } + } else { + // initiate stepBackFrom[*] functions to rollback. + if (stepBackFrom[currentStep]) { + stepBackFrom[currentStep](); + } + } + + // following step occurs only at the last stage of the wizard. + if (!nextStep) { + window.location.href = $(this).data("direct"); + } + + // updating next wizard step as current. + $(".itm-wiz").each(function () { + var step = $(this).data("step"); + if (step == nextStep) { + $(this).addClass("itm-wiz-current"); + } else { + $(this).removeClass("itm-wiz-current"); + } + }); + + // adding next update of wizard-steps. + $("#" + nextStep + "-wizard-steps").html($(".wr-steps").html()); + + // hiding current section of the wizard and showing next section. + $("." + currentStep).addClass("hidden"); + $("." + nextStep).removeClass("hidden"); + } + }); +}); diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/public/templates/hidden-operations-android.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/public/templates/hidden-operations-android.hbs new file mode 100644 index 000000000..80932391a --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/public/templates/hidden-operations-android.hbs @@ -0,0 +1,439 @@ +
      + + +
      + +
      +
      + +
      + + + +
      + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      +
      +
      +
      + + + +
      +
      + +
      + + Un-check following checkbox in case you need to disable camera. +
      +
      +
      + +
      +
      +
      +
      +
      + + + +
      +
      + +
      + + Un-check following checkbox in case you do not need the device to be encrypted. +
      +
      +
      + +
      +
      +
      +
      +
      + + + +
      +
      + +
      + + + + + + + + + + + + + + + + + Please note that * sign represents required fields of data. +
      +
      + +
      + + +
      +
      + + +
      + +
      +
      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/public/templates/hidden-operations-ios.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/public/templates/hidden-operations-ios.hbs new file mode 100644 index 000000000..4aeed0646 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/public/templates/hidden-operations-ios.hbs @@ -0,0 +1,2923 @@ +
      + + +
      + +
      +
      + +
      + + + +
      + +
      + +
      + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      +
      +
      +
      + + + +
      +
      + +
      + + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + +
      + +
      + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + + + +
      + + +
      + + + +
      + +
      + + + + + + + + + + + + + +
      No:Roaming Consortium OI
      + No entries added yet . +
      + + + + + + + + + +
      +
      + +
      + +
      + +
      + + + + + + + + + + + + + +
      No:NAI Realm Name
      + No entries added yet . +
      + + + + + + + + + +
      +
      + +
      + +
      + +
      + + + + + + + + + + + + + + +
      No:Mobile Country Code ( MCC )Mobile Network Code ( MNC )
      + No entries added yet . +
      + + + + + + + + + + +
      +
      +
      +
      +
      + + + +
      +
      + +
      + + + +
      + + +
      + +
      + + +
      + +
      + +
      + + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + +
      + +
      + Incoming Mail Settings : +
      + +
      + + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + Outgoing Mail Settings : +
      + +
      + + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + + +
      + +
      +
      +
      + + + +
      +
      + +
      + + + +
      + +
      + + + + + + + + + + + + + + +
      No:Device NamePassword
      + No entries added yet . +
      + + + + + + + + + + +
      +
      + +
      + +
      + +
      + + + + + + + + + + + + + +
      No:Destination
      + No entries added yet . +
      + + + + + + + + + +
      +
      + +
      +
      +
      + + + +
      +
      + +
      + + + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + +
      + + + + + + + + + + + + + + + +
      No:DescriptionSearch BaseScope
      + No entries added yet . +
      + + + + + + + + + + + +
      +
      + +
      +
      +
      + + + +
      +
      + +
      + + + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      +
      +
      + + + +
      +
      + +
      + + + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      +
      +
      + + + +
      +
      + +
      + + + +
      + +
      + + + + + + + + + + + + + + + + + +
      No:APNUsernamePasswordProxyPort
      + No entries added yet . +
      + + + + + + + + + + + + + +
      +
      + +
      +
      +
      + + + +
      +
      + +
      + + + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + +
      + + + + + + + + + + + + + + + + + + +
      No:APNAuth.TypeUsernamePasswordProxyPort
      + No entries added yet . +
      + + + + + + + + + + + + + + +
      +
      + +
      +
      +
      + + + +
      +
      + +
      + + + + Restrictions on Device Functionality : +
      +
      +
        +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
          +
        • +
          + +
          +
        • +
        • +
          + +
          +
        • +
        • +
          + +
          +
        • +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      +
      +
      + Restrictions on Applications : +
      +
      +
        +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
          +
        • +
          + +
          +
        • +
        • +
          + +
          +
        • +
        • +
          + +
          +
        • +
        • +
          + +
          +
        • +
        • +
          + + +
          +
        • +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      +
      + + + +
      +
      +
      + +
      +
      \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/public/templates/hidden-operations-windows.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/public/templates/hidden-operations-windows.hbs new file mode 100644 index 000000000..5febc3972 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/public/templates/hidden-operations-windows.hbs @@ -0,0 +1,460 @@ +
      + + +
      + +
      +
      + +
      + + + +
      + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      +
      +
      +
      + + + +
      +
      + +
      + + Un-check following checkbox in case you need to disable camera. +
      +
      +
      + +
      +
      +
      +
      +
      + + + +
      +
      + +
      + + Un-check following checkbox in case you need to disable storage-encryption. +
      +
      +
      + +
      +
      +
      +
      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/view.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/view.hbs new file mode 100644 index 000000000..223029164 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/view.hbs @@ -0,0 +1,87 @@ + + {{#defineZone "policy-profile-top"}} +
      +
      + +
      +
      + {{/defineZone}} + + +
      +
      +
      +
      +
      Policy Overview +
      + {{#defineZone "policy-detail-properties"}} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Platform
      Ownership
      Action upon non-compliance
      Status
      Assigned Users
      Assigned Roles
      + {{/defineZone}} +
      Description
      +
      +
      +
      +
      + +
      Profile + Information +
      +
      + +
      +
      +
      + + Loading platform features . . . +
      +
      +
      +
      +
      +
      +
      +
      +
      + +{{#zone "bottomJs"}} + + + + {{js "/js/policy-view.js"}} +{{/zone}} + diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/view.js b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/view.js new file mode 100644 index 000000000..36b130ca0 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/view.js @@ -0,0 +1,25 @@ +/* + * 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. + */ + +function onRequest(context) { + var log = new Log("policy-view-edit-unit backend js"); + log.debug("calling policy-view-edit-unit"); + var userModule = require("/app/modules/user.js").userModule; + context.roles = userModule.getRoles().content; + return context; +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/view.json b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/view.json new file mode 100644 index 000000000..fd2590129 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.view/view.json @@ -0,0 +1,3 @@ +{ + "version" : "1.0.0" +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/public/js/policy-create.js b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/public/js/policy-create.js new file mode 100644 index 000000000..a51ae295d --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/public/js/policy-create.js @@ -0,0 +1,2355 @@ +/* + * 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. + */ + +var validateStep = {}; +var validateInline = {}; +var stepForwardFrom = {}; +var stepBackFrom = {}; +var policy = {}; +var configuredOperations = []; + +// Constants to define platform types available +var platformTypeConstants = { + "ANDROID": "android", + "IOS": "ios", + "WINDOWS": "windows" +}; + +// Constants to define platform types ids. +var platformIds = { + "ANDROID": 1, + "IOS": 3, + "WINDOWS": 2 +}; + +// Constants to define Android Operation Constants +var androidOperationConstants = { + "PASSCODE_POLICY_OPERATION": "passcode-policy", + "PASSCODE_POLICY_OPERATION_CODE": "PASSCODE_POLICY", + "CAMERA_OPERATION": "camera", + "CAMERA_OPERATION_CODE": "CAMERA", + "ENCRYPT_STORAGE_OPERATION": "encrypt-storage", + "ENCRYPT_STORAGE_OPERATION_CODE": "ENCRYPT_STORAGE", + "WIFI_OPERATION": "wifi", + "WIFI_OPERATION_CODE": "WIFI" +}; + +// Constants to define Android Operation Constants +var windowsOperationConstants = { + "PASSCODE_POLICY_OPERATION": "passcode-policy", + "PASSCODE_POLICY_OPERATION_CODE": "PASSCODE_POLICY", + "CAMERA_OPERATION": "camera", + "CAMERA_OPERATION_CODE": "CAMERA", + "ENCRYPT_STORAGE_OPERATION": "encrypt-storage", + "ENCRYPT_STORAGE_OPERATION_CODE": "ENCRYPT_STORAGE" +}; + +// Constants to define iOS Operation Constants +var iosOperationConstants = { + "PASSCODE_POLICY_OPERATION": "passcode-policy", + "PASSCODE_POLICY_OPERATION_CODE": "PASSCODE_POLICY", + "RESTRICTIONS_OPERATION": "restrictions", + "RESTRICTIONS_OPERATION_CODE": "RESTRICTION", + "WIFI_OPERATION": "wifi", + "WIFI_OPERATION_CODE": "WIFI", + "EMAIL_OPERATION": "email", + "EMAIL_OPERATION_CODE": "EMAIL", + "AIRPLAY_OPERATION": "airplay", + "AIRPLAY_OPERATION_CODE": "AIR_PLAY", + "LDAP_OPERATION": "ldap", + "LDAP_OPERATION_CODE": "LDAP", + "CALENDAR_OPERATION": "calendar", + "CALENDAR_OPERATION_CODE": "CALDAV", + "CALENDAR_SUBSCRIPTION_OPERATION": "calendar-subscription", + "CALENDAR_SUBSCRIPTION_OPERATION_CODE": "CALENDAR_SUBSCRIPTION", + "APN_OPERATION": "apn", + "APN_OPERATION_CODE": "APN", + "CELLULAR_OPERATION": "cellular", + "CELLULAR_OPERATION_CODE": "CELLULAR" +}; + +/** + * Method to update the visibility of grouped input. + * @param domElement HTML grouped-input element with class name "grouped-input" + */ +var updateGroupedInputVisibility = function (domElement) { + if ($(".parent-input:first", domElement).is(":checked")) { + if ($(".grouped-child-input:first", domElement).hasClass("disabled")) { + $(".grouped-child-input:first", domElement).removeClass("disabled"); + } + $(".child-input", domElement).each(function () { + $(this).prop('disabled', false); + }); + } else { + if (!$(".grouped-child-input:first", domElement).hasClass("disabled")) { + $(".grouped-child-input:first", domElement).addClass("disabled"); + } + $(".child-input", domElement).each(function () { + $(this).prop('disabled', true); + }); + } +}; + +var validateInline = {}; +var clearInline = {}; + +var enableInlineError = function (inputField, errorMsg, errorSign) { + var fieldIdentifier = "#" + inputField; + var errorMsgIdentifier = "#" + inputField + " ." + errorMsg; + var errorSignIdentifier = "#" + inputField + " ." + errorSign; + + if (inputField) { + $(fieldIdentifier).addClass(" has-error has-feedback"); + } + + if (errorMsg) { + $(errorMsgIdentifier).removeClass(" hidden"); + } + + if (errorSign) { + $(errorSignIdentifier).removeClass(" hidden"); + } +}; + +var disableInlineError = function (inputField, errorMsg, errorSign) { + var fieldIdentifier = "#" + inputField; + var errorMsgIdentifier = "#" + inputField + " ." + errorMsg; + var errorSignIdentifier = "#" + inputField + " ." + errorSign; + + if (inputField) { + $(fieldIdentifier).removeClass(" has-error has-feedback"); + } + + if (errorMsg) { + $(errorMsgIdentifier).addClass(" hidden"); + } + + if (errorSign) { + $(errorSignIdentifier).addClass(" hidden"); + } +}; + +/** + *clear inline validation messages. + */ +clearInline["policy-name"] = function () { + disableInlineError("plicynameField", "nameEmpty", "nameError"); +}; + + +/** + * Validate if provided policy name is valid against RegEx configures. + */ +validateInline["policy-name"] = function () { + var policyName = $("input#policy-name-input").val(); + if (policyName && inputIsValidAgainstLength(policyName, 1, 30)) { + disableInlineError("plicynameField", "nameEmpty", "nameError"); + } else { + enableInlineError("plicynameField", "nameEmpty", "nameError"); + } +}; + +$("#policy-name-input").focus(function () { + clearInline["policy-name"](); +}); + +$("#policy-name-input").blur(function () { + validateInline["policy-name"](); +}); + +/** + * Checks if provided number is valid against a range. + * + * @param numberInput Number Input + * @param min Minimum Limit + * @param max Maximum Limit + * @returns {boolean} Returns true if input is within the specified range + */ +var inputIsValidAgainstRange = function (numberInput, min, max) { + return (numberInput == min || (numberInput > min && numberInput < max) || numberInput == max); +}; + +/** + * Checks if provided input is valid against RegEx input. + * + * @param regExp Regular expression + * @param input Input string to check + * @returns {boolean} Returns true if input matches RegEx + */ +var inputIsValidAgainstRegExp = function (regExp, input) { + return regExp.test(input); +}; + +validateStep["policy-profile"] = function () { + var validationStatusArray = []; + var validationStatus; + var operation; + + // starting validation process and updating validationStatus + if (policy["platform"] == platformTypeConstants["ANDROID"]) { + if (configuredOperations.length == 0) { + // updating validationStatus + validationStatus = { + "error": true, + "mainErrorMsg": "You cannot continue. Zero configured features." + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } else { + // validating each and every configured Operation + // Validating PASSCODE_POLICY + if ($.inArray(androidOperationConstants["PASSCODE_POLICY_OPERATION_CODE"], configuredOperations) != -1) { + // if PASSCODE_POLICY is configured + operation = androidOperationConstants["PASSCODE_POLICY_OPERATION"]; + // initializing continueToCheckNextInputs to true + var continueToCheckNextInputs = true; + + // validating first input: passcodePolicyMaxPasscodeAgeInDays + var passcodePolicyMaxPasscodeAgeInDays = $("input#passcode-policy-max-passcode-age-in-days").val(); + if (passcodePolicyMaxPasscodeAgeInDays) { + if (!$.isNumeric(passcodePolicyMaxPasscodeAgeInDays)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode age is not a number.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(passcodePolicyMaxPasscodeAgeInDays, 1, 730)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode age is not with in the range of 1-to-730.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + // validating second and last input: passcodePolicyPasscodeHistory + if (continueToCheckNextInputs) { + var passcodePolicyPasscodeHistory = $("input#passcode-policy-passcode-history").val(); + if (passcodePolicyPasscodeHistory) { + if (!$.isNumeric(passcodePolicyPasscodeHistory)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode history is not a number.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(passcodePolicyPasscodeHistory, 1, 50)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode history is not with in the range of 1-to-50.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating CAMERA + if ($.inArray(androidOperationConstants["CAMERA_OPERATION_CODE"], configuredOperations) != -1) { + // if CAMERA is configured + operation = androidOperationConstants["CAMERA_OPERATION"]; + // updating validationStatus + validationStatus = { + "error": false, + "okFeature": operation + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating ENCRYPT_STORAGE + if ($.inArray(androidOperationConstants["ENCRYPT_STORAGE_OPERATION_CODE"], configuredOperations) != -1) { + // if ENCRYPT_STORAGE is configured + operation = androidOperationConstants["ENCRYPT_STORAGE_OPERATION"]; + // updating validationStatus + validationStatus = { + "error": false, + "okFeature": operation + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating WIFI + if ($.inArray(androidOperationConstants["WIFI_OPERATION_CODE"], configuredOperations) != -1) { + // if WIFI is configured + operation = androidOperationConstants["WIFI_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var wifiSSID = $("input#wifi-ssid").val(); + if (!wifiSSID) { + validationStatus = { + "error": true, + "subErrorMsg": "WIFI SSID is not given. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + } + } + if (policy["platform"] == platformTypeConstants["WINDOWS"]) { + if (configuredOperations.length == 0) { + // updating validationStatus + validationStatus = { + "error": true, + "mainErrorMsg": "You cannot continue. Zero configured features." + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } else { + // validating each and every configured Operation + // Validating PASSCODE_POLICY + if ($.inArray(windowsOperationConstants["PASSCODE_POLICY_OPERATION_CODE"], configuredOperations) != -1) { + // if PASSCODE_POLICY is configured + operation = windowsOperationConstants["PASSCODE_POLICY_OPERATION"]; + // initializing continueToCheckNextInputs to true + var continueToCheckNextInputs = true; + + // validating first input: passcodePolicyMaxPasscodeAgeInDays + var passcodePolicyMaxPasscodeAgeInDays = $("input#passcode-policy-max-passcode-age-in-days").val(); + if (passcodePolicyMaxPasscodeAgeInDays) { + if (!$.isNumeric(passcodePolicyMaxPasscodeAgeInDays)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode age is not a number.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(passcodePolicyMaxPasscodeAgeInDays, 1, 730)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode age is not with in the range of 1-to-730.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + // validating second and last input: passcodePolicyPasscodeHistory + if (continueToCheckNextInputs) { + var passcodePolicyPasscodeHistory = $("input#passcode-policy-passcode-history").val(); + if (passcodePolicyPasscodeHistory) { + if (!$.isNumeric(passcodePolicyPasscodeHistory)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode history is not a number.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(passcodePolicyPasscodeHistory, 1, 50)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode history is not with in the range of 1-to-50.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating CAMERA + if ($.inArray(windowsOperationConstants["CAMERA_OPERATION_CODE"], configuredOperations) != -1) { + // if CAMERA is configured + operation = windowsOperationConstants["CAMERA_OPERATION"]; + // updating validationStatus + validationStatus = { + "error": false, + "okFeature": operation + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating ENCRYPT_STORAGE + if ($.inArray(windowsOperationConstants["ENCRYPT_STORAGE_OPERATION_CODE"], configuredOperations) != -1) { + // if ENCRYPT_STORAGE is configured + operation = windowsOperationConstants["ENCRYPT_STORAGE_OPERATION"]; + // updating validationStatus + validationStatus = { + "error": false, + "okFeature": operation + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + + } + } else if (policy["platform"] == platformTypeConstants["IOS"]) { + if (configuredOperations.length == 0) { + // updating validationStatus + validationStatus = { + "error": true, + "mainErrorMsg": "You cannot continue. Zero configured features." + }; + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } else { + // validating each and every configured Operation + // Validating PASSCODE_POLICY + if ($.inArray(iosOperationConstants["PASSCODE_POLICY_OPERATION_CODE"], configuredOperations) != -1) { + // if PASSCODE_POLICY is configured + operation = iosOperationConstants["PASSCODE_POLICY_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + // validating first input: passcodePolicyMaxPasscodeAgeInDays + passcodePolicyMaxPasscodeAgeInDays = $("input#passcode-policy-max-passcode-age-in-days").val(); + if (passcodePolicyMaxPasscodeAgeInDays) { + if (!$.isNumeric(passcodePolicyMaxPasscodeAgeInDays)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode age is not a number.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(passcodePolicyMaxPasscodeAgeInDays, 1, 730)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode age is not with in the range of 1-to-730.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + // validating second and last input: passcodePolicyPasscodeHistory + if (continueToCheckNextInputs) { + passcodePolicyPasscodeHistory = $("input#passcode-policy-passcode-history").val(); + if (passcodePolicyPasscodeHistory) { + if (!$.isNumeric(passcodePolicyPasscodeHistory)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode history is not a number.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(passcodePolicyPasscodeHistory, 1, 50)) { + validationStatus = { + "error": true, + "subErrorMsg": "Provided passcode history is not with in the range of 1-to-50.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating RESTRICTIONS + if ($.inArray(iosOperationConstants["RESTRICTIONS_OPERATION_CODE"], configuredOperations) != -1) { + // if RESTRICTION is configured + operation = iosOperationConstants["RESTRICTIONS_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + // getting input values to be validated + var restrictionsAutonomousSingleAppModePermittedAppIDsGridChildInputs = + "div#restrictions-autonomous-single-app-mode-permitted-app-ids .child-input"; + if ($(restrictionsAutonomousSingleAppModePermittedAppIDsGridChildInputs).length > 0) { + var childInput; + var childInputArray = []; + var emptyChildInputCount = 0; + var duplicatesExist = false; + // looping through each child input + $(restrictionsAutonomousSingleAppModePermittedAppIDsGridChildInputs).each(function () { + childInput = $(this).val(); + childInputArray.push(childInput); + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + }); + // checking for duplicates + var initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + var m, poppedChildInput; + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + var n; + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more permitted App ID entries in " + + "Autonomous Single App Mode are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with permitted App ID entries in " + + "Autonomous Single App Mode.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating WIFI + if ($.inArray(iosOperationConstants["WIFI_OPERATION_CODE"], configuredOperations) != -1) { + // if WIFI is configured + operation = iosOperationConstants["WIFI_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + // getting input values to be validated + wifiSSID = $("input#wifi-ssid").val(); + var wifiDomainName = $("input#wifi-domain-name").val(); + if (!wifiSSID && !wifiDomainName) { + validationStatus = { + "error": true, + "subErrorMsg": "Both Wi-Fi SSID and Wi-Fi Domain Name are not given. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + // getting proxy-setup value + var wifiProxyType = $("select#wifi-proxy-type").find("option:selected").attr("value"); + if (wifiProxyType == "Manual") { + // adds up additional fields to be validated + var wifiProxyServer = $("input#wifi-proxy-server").val(); + if (!wifiProxyServer) { + validationStatus = { + "error": true, + "subErrorMsg": "Wi-Fi Proxy Server is required. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + var wifiProxyPort = $("input#wifi-proxy-port").val(); + if (!wifiProxyPort) { + validationStatus = { + "error": true, + "subErrorMsg": "Wi-Fi Proxy Port is required. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!$.isNumeric(wifiProxyPort)) { + validationStatus = { + "error": true, + "subErrorMsg": "Wi-Fi Proxy Port requires a number input.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(wifiProxyPort, 0, 65535)) { + validationStatus = { + "error": true, + "subErrorMsg": "Wi-Fi Proxy Port is not within the range " + + "of valid port numbers.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + } + + if (continueToCheckNextInputs) { + // getting encryption-type value + var wifiEncryptionType = $("select#wifi-encryption-type").find("option:selected").attr("value"); + if (wifiEncryptionType != "None") { + var wifiPayloadCertificateAnchorUUIDsGridChildInputs = + "div#wifi-payload-certificate-anchor-uuids .child-input"; + if ($(wifiPayloadCertificateAnchorUUIDsGridChildInputs).length > 0) { + emptyChildInputCount = 0; + childInputArray = []; + duplicatesExist = false; + // looping through each child input + $(wifiPayloadCertificateAnchorUUIDsGridChildInputs).each(function () { + childInput = $(this).val(); + childInputArray.push(childInput); + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more Payload Certificate " + + "Anchor UUIDs are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist " + + "with Payload Certificate Anchor UUIDs.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + if (continueToCheckNextInputs) { + var wifiTLSTrustedServerNamesGridChildInputs = + "div#wifi-tls-trusted-server-names .child-input"; + if ($(wifiTLSTrustedServerNamesGridChildInputs).length > 0) { + emptyChildInputCount = 0; + childInputArray = []; + duplicatesExist = false; + // looping through each child input + $(wifiTLSTrustedServerNamesGridChildInputs).each(function () { + childInput = $(this).val(); + childInputArray.push(childInput); + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more TLS Trusted Server Names are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist " + + "with TLS Trusted Server Names.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + } + } + + if (continueToCheckNextInputs) { + var wifiRoamingConsortiumOIsGridChildInputs = "div#wifi-roaming-consortium-ois .child-input"; + if ($(wifiRoamingConsortiumOIsGridChildInputs).length > 0) { + emptyChildInputCount = 0; + var outOfAllowedLengthCount = 0; + var invalidAgainstRegExCount = 0; + childInputArray = []; + duplicatesExist = false; + // looping through each child input + $(wifiRoamingConsortiumOIsGridChildInputs).each(function () { + childInput = $(this).val(); + childInputArray.push(childInput); + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } else if (!inputIsValidAgainstLength(childInput, 6, 6) && !inputIsValidAgainstLength(childInput, 10, 10)) { + outOfAllowedLengthCount++; + } else if (!inputIsValidAgainstRegExp(/^[a-fA-F0-9]+$/, childInput)) { + invalidAgainstRegExCount++; + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more Roaming Consortium OIs are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (outOfAllowedLengthCount > 0) { + // if outOfMaxAllowedLength input is present + validationStatus = { + "error": true, + "subErrorMsg": "One or more Roaming Consortium OIs " + + "are out of allowed length.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (invalidAgainstRegExCount > 0) { + // if invalid inputs in terms of hexadecimal format are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more Roaming Consortium OIs " + + "contain non-hexadecimal characters.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with Roaming Consortium OIs.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + if (continueToCheckNextInputs) { + var wifiNAIRealmNamesGridChildInputs = "div#wifi-nai-realm-names .child-input"; + if ($(wifiNAIRealmNamesGridChildInputs).length > 0) { + emptyChildInputCount = 0; + childInputArray = []; + duplicatesExist = false; + // looping through each child input + $(wifiNAIRealmNamesGridChildInputs).each(function () { + childInput = $(this).val(); + childInputArray.push(childInput); + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more NAI Realm Names are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with NAI Realm Names.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + if (continueToCheckNextInputs) { + var wifiMCCAndMNCsGridChildInputs = "div#wifi-mcc-and-mncs .child-input"; + if ($(wifiMCCAndMNCsGridChildInputs).length > 0) { + var childInputCount = 0; + var stringPair; + emptyChildInputCount = 0; + outOfAllowedLengthCount = 0; + var notNumericInputCount = 0; + childInputArray = []; + duplicatesExist = false; + // looping through each child input + $(wifiMCCAndMNCsGridChildInputs).each(function () { + childInput = $(this).val(); + // pushing each string pair to childInputArray + childInputCount++; + if (childInputCount % 2 == 1) { + // initialize stringPair value + stringPair = ""; + // append first part of the string + stringPair += childInput; + } else { + // append second part of the string + stringPair += childInput; + childInputArray.push(stringPair); + } + // updating emptyChildInputCount & outOfAllowedLengthCount + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } else if (!$.isNumeric(childInput)) { + notNumericInputCount++; + } else if (!inputIsValidAgainstLength(childInput, 3, 3)) { + outOfAllowedLengthCount++; + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more MCC/MNC pairs are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (notNumericInputCount > 0) { + // if notNumeric input is present + validationStatus = { + "error": true, + "subErrorMsg": "One or more MCC/MNC pairs are not numeric.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (outOfAllowedLengthCount > 0) { + // if outOfAllowedLength input is present + validationStatus = { + "error": true, + "subErrorMsg": "One or more MCC/MNC pairs " + + "do not fulfill the accepted length of 6 digits.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with MCC/MNC pairs.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating EMAIL + if ($.inArray(iosOperationConstants["EMAIL_OPERATION_CODE"], configuredOperations) != -1) { + // if EMAIL is configured + operation = iosOperationConstants["EMAIL_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var emailAddress = $("input#email-address").val(); + if (emailAddress && !inputIsValidAgainstRegExp(/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/, emailAddress)) { + validationStatus = { + "error": true, + "subErrorMsg": "Email Address is not valid.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + var emailIncomingMailServerHostname = $("input#email-incoming-mail-server-hostname").val(); + if (!emailIncomingMailServerHostname) { + validationStatus = { + "error": true, + "subErrorMsg": "Incoming Mail Server Hostname is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + if (continueToCheckNextInputs) { + var emailIncomingMailServerPort = $("input#email-incoming-mail-server-port").val(); + if (emailIncomingMailServerPort && emailIncomingMailServerPort != '') { + if (!$.isNumeric(emailIncomingMailServerPort)) { + validationStatus = { + "error": true, + "subErrorMsg": "Incoming Mail Server Port requires a number input.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(emailIncomingMailServerPort, 0, 65535)) { + validationStatus = { + "error": true, + "subErrorMsg": "Incoming Mail Server Port is not within the range " + + "of valid port numbers.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + if (continueToCheckNextInputs) { + var emailOutgoingMailServerHostname = $("input#email-outgoing-mail-server-hostname").val(); + if (!emailOutgoingMailServerHostname) { + validationStatus = { + "error": true, + "subErrorMsg": "Outgoing Mail Server Hostname is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + if (continueToCheckNextInputs) { + var emailOutgoingMailServerPort = $("input#email-outgoing-mail-server-port").val(); + if (emailOutgoingMailServerPort) { + if (!$.isNumeric(emailOutgoingMailServerPort)) { + validationStatus = { + "error": true, + "subErrorMsg": "Outgoing Mail Server Port requires a number input.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(emailOutgoingMailServerPort, 0, 65535)) { + validationStatus = { + "error": true, + "subErrorMsg": "Outgoing Mail Server Port is not within the range " + + "of valid port numbers.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating AIRPLAY + if ($.inArray(iosOperationConstants["AIRPLAY_OPERATION_CODE"], configuredOperations) != -1) { + // if AIRPLAY is configured + operation = iosOperationConstants["AIRPLAY_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var airplayCredentialsGridChildInputs = "div#airplay-credentials .child-input"; + var airplayDestinationsGridChildInputs = "div#airplay-destinations .child-input"; + if ($(airplayCredentialsGridChildInputs).length == 0 && + $(airplayDestinationsGridChildInputs).length == 0) { + validationStatus = { + "error": true, + "subErrorMsg": "AirPlay settings have zero configurations attached.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + if ($(airplayCredentialsGridChildInputs).length > 0) { + childInputCount = 0; + childInputArray = []; + emptyChildInputCount = 0; + duplicatesExist = false; + // looping through each child input + $(airplayCredentialsGridChildInputs).each(function () { + childInputCount++; + if (childInputCount % 2 == 1) { + // if child input is of first column + childInput = $(this).val(); + childInputArray.push(childInput); + // updating emptyChildInputCount + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more Device Names of " + + "AirPlay Credentials are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + // if duplicate input is present + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with " + + "Device Names of AirPlay Credentials.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + if (continueToCheckNextInputs) { + if ($(airplayDestinationsGridChildInputs).length > 0) { + childInputArray = []; + emptyChildInputCount = 0; + invalidAgainstRegExCount = 0; + duplicatesExist = false; + // looping through each child input + $(airplayDestinationsGridChildInputs).each(function () { + childInput = $(this).val(); + childInputArray.push(childInput); + // updating emptyChildInputCount + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } else if (!inputIsValidAgainstRegExp( + /([a-z|A-Z|0-9][a-z|A-Z|0-9][:]){5}([a-z|A-Z|0-9][a-z|A-Z|0-9])$/, childInput)) { + // if child input field is invalid against RegEx + invalidAgainstRegExCount++ + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more AirPlay Destination fields are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (invalidAgainstRegExCount > 0) { + // if invalidAgainstRegEx inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more AirPlay Destination fields " + + "do not fulfill expected format.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + // if duplicate input is present + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with AirPlay Destinations.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating LDAP + if ($.inArray(iosOperationConstants["LDAP_OPERATION_CODE"], configuredOperations) != -1) { + // if LDAP is configured + operation = iosOperationConstants["LDAP_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var ldapAccountHostname = $("input#ldap-account-hostname").val(); + if (!ldapAccountHostname) { + validationStatus = { + "error": true, + "subErrorMsg": "LDAP Account Hostname URL is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + var ldapSearchSettingsGridChildInputs = "div#ldap-search-settings .child-input"; + if ($(ldapSearchSettingsGridChildInputs).length > 0) { + childInputCount = 0; + childInputArray = []; + emptyChildInputCount = 0; + duplicatesExist = false; + // looping through each child input + $(ldapSearchSettingsGridChildInputs).each(function () { + childInputCount++; + if (childInputCount % 3 == 2) { + // if child input is of second column + childInput = $(this).find("option:selected").attr("value"); + stringPair = ""; + stringPair += (childInput + " "); + } else if (childInputCount % 3 == 0) { + // if child input is of third column + childInput = $(this).val(); + stringPair += childInput; + childInputArray.push(stringPair); + // updating emptyChildInputCount + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more Search Setting Scope fields are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + // if duplicate input is present + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with " + + "Search Setting Search Base and Scope pairs.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating CALENDAR + if ($.inArray(iosOperationConstants["CALENDAR_OPERATION_CODE"], configuredOperations) != -1) { + // if CALENDAR is configured + operation = iosOperationConstants["CALENDAR_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var calendarAccountHostname = $("input#calendar-account-hostname").val(); + if (!calendarAccountHostname) { + validationStatus = { + "error": true, + "subErrorMsg": "Account Hostname URL is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + var calendarAccountPort = $("input#calendar-account-port").val(); + if (!calendarAccountPort) { + validationStatus = { + "error": true, + "subErrorMsg": "Account Port is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!$.isNumeric(calendarAccountPort)) { + validationStatus = { + "error": true, + "subErrorMsg": "Account Port requires a number input.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (!inputIsValidAgainstRange(calendarAccountPort, 0, 65535)) { + validationStatus = { + "error": true, + "subErrorMsg": "Account Port is not within the range " + + "of valid port numbers.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating CALENDAR_SUBSCRIPTION + if ($.inArray(iosOperationConstants["CALENDAR_SUBSCRIPTION_OPERATION_CODE"], configuredOperations) != -1) { + // if CALENDAR_SUBSCRIPTION is configured + operation = iosOperationConstants["CALENDAR_SUBSCRIPTION_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var calendarSubscriptionHostname = $("input#calendar-subscription-hostname").val(); + if (!calendarSubscriptionHostname) { + validationStatus = { + "error": true, + "subErrorMsg": "Account Hostname URL is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating APN + if ($.inArray(iosOperationConstants["APN_OPERATION_CODE"], configuredOperations) != -1) { + // if APN is configured + operation = iosOperationConstants["APN_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var apnConfigurationsGridChildInputs = "div#apn-configurations .child-input"; + if ($(apnConfigurationsGridChildInputs).length == 0) { + validationStatus = { + "error": true, + "subErrorMsg": "APN Settings have zero configurations attached.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if ($(apnConfigurationsGridChildInputs).length > 0) { + childInputCount = 0; + childInputArray = []; + // checking empty APN field count + emptyChildInputCount = 0; + duplicatesExist = false; + // looping through each child input + $(apnConfigurationsGridChildInputs).each(function () { + childInputCount++; + //if (childInputCount % 5 == 1) { + // if child input is of first column + childInput = $(this).val(); + childInputArray.push(childInput); + // updating emptyChildInputCount + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + //} + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more APN fields of Configurations are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + // if duplicate input is present + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with " + + "APN fields of Configurations.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + // Validating CELLULAR + if ($.inArray(iosOperationConstants["CELLULAR_OPERATION_CODE"], configuredOperations) != -1) { + // if CELLULAR is configured + operation = iosOperationConstants["CELLULAR_OPERATION"]; + // initializing continueToCheckNextInputs to true + continueToCheckNextInputs = true; + + var cellularAttachAPNName = $("input#cellular-attach-apn-name").val(); + if (!cellularAttachAPNName) { + validationStatus = { + "error": true, + "subErrorMsg": "Cellular Configuration Name is empty. You cannot proceed.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + + if (continueToCheckNextInputs) { + var cellularAPNConfigurationsGridChildInputs = "div#cellular-apn-configurations .child-input"; + if ($(cellularAPNConfigurationsGridChildInputs).length > 0) { + childInputCount = 0; + childInputArray = []; + // checking empty APN field count + emptyChildInputCount = 0; + duplicatesExist = false; + // looping through each child input + $(cellularAPNConfigurationsGridChildInputs).each(function () { + childInputCount++; + if (childInputCount % 6 == 1) { + // if child input is of first column + childInput = $(this).val(); + childInputArray.push(childInput); + // updating emptyChildInputCount + if (!childInput) { + // if child input field is empty + emptyChildInputCount++; + } + } + }); + // checking for duplicates + initialChildInputArrayLength = childInputArray.length; + if (emptyChildInputCount == 0 && initialChildInputArrayLength > 1) { + for (m = 0; m < (initialChildInputArrayLength - 1); m++) { + poppedChildInput = childInputArray.pop(); + for (n = 0; n < childInputArray.length; n++) { + if (poppedChildInput == childInputArray[n]) { + duplicatesExist = true; + break; + } + } + if (duplicatesExist) { + break; + } + } + } + // updating validationStatus + if (emptyChildInputCount > 0) { + // if empty child inputs are present + validationStatus = { + "error": true, + "subErrorMsg": "One or more APN fields of APN Configurations are empty.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } else if (duplicatesExist) { + // if duplicate input is present + validationStatus = { + "error": true, + "subErrorMsg": "Duplicate values exist with " + + "APN fields of APN Configurations.", + "erroneousFeature": operation + }; + continueToCheckNextInputs = false; + } + } + } + + // at-last, if the value of continueToCheckNextInputs is still true + // this means that no error is found + if (continueToCheckNextInputs) { + validationStatus = { + "error": false, + "okFeature": operation + }; + } + + // updating validationStatusArray with validationStatus + validationStatusArray.push(validationStatus); + } + } + } + // ending validation process + + // start taking specific notifying actions upon validation + var wizardIsToBeContinued; + var errorCount = 0; + var mainErrorMsgWrapper, mainErrorMsg, + subErrorMsgWrapper, subErrorMsg, subErrorIcon, subOkIcon, featureConfiguredIcon; + var i; + for (i = 0; i < validationStatusArray.length; i++) { + validationStatus = validationStatusArray[i]; + if (validationStatus["error"]) { + errorCount++; + if (validationStatus["mainErrorMsg"]) { + mainErrorMsgWrapper = "#policy-profile-main-error-msg"; + mainErrorMsg = mainErrorMsgWrapper + " span"; + $(mainErrorMsg).text(validationStatus["mainErrorMsg"]); + $(mainErrorMsgWrapper).removeClass("hidden"); + } else if (validationStatus["subErrorMsg"]) { + subErrorMsgWrapper = "#" + validationStatus["erroneousFeature"] + "-feature-error-msg"; + subErrorMsg = subErrorMsgWrapper + " span"; + subErrorIcon = "#" + validationStatus["erroneousFeature"] + "-error"; + subOkIcon = "#" + validationStatus["erroneousFeature"] + "-ok"; + featureConfiguredIcon = "#" + validationStatus["erroneousFeature"] + "-configured"; + // hiding featureConfiguredState as the first step + if (!$(featureConfiguredIcon).hasClass("hidden")) { + $(featureConfiguredIcon).addClass("hidden"); + } + // updating error state and corresponding messages + $(subErrorMsg).text(validationStatus["subErrorMsg"]); + if ($(subErrorMsgWrapper).hasClass("hidden")) { + $(subErrorMsgWrapper).removeClass("hidden"); + } + if (!$(subOkIcon).hasClass("hidden")) { + $(subOkIcon).addClass("hidden"); + } + if ($(subErrorIcon).hasClass("hidden")) { + $(subErrorIcon).removeClass("hidden"); + } + } + } else { + if (validationStatus["okFeature"]) { + subErrorMsgWrapper = "#" + validationStatus["okFeature"] + "-feature-error-msg"; + subErrorIcon = "#" + validationStatus["okFeature"] + "-error"; + subOkIcon = "#" + validationStatus["okFeature"] + "-ok"; + featureConfiguredIcon = "#" + validationStatus["okFeature"] + "-configured"; + // hiding featureConfiguredState as the first step + if (!$(featureConfiguredIcon).hasClass("hidden")) { + $(featureConfiguredIcon).addClass("hidden"); + } + // updating success state and corresponding messages + if (!$(subErrorMsgWrapper).hasClass("hidden")) { + $(subErrorMsgWrapper).addClass("hidden"); + } + if (!$(subErrorIcon).hasClass("hidden")) { + $(subErrorIcon).addClass("hidden"); + } + if ($(subOkIcon).hasClass("hidden")) { + $(subOkIcon).removeClass("hidden"); + } + } + } + } + + wizardIsToBeContinued = (errorCount == 0); + return wizardIsToBeContinued; +}; + +stepForwardFrom["policy-profile"] = function () { + policy["profile"] = operationModule.generateProfile(policy["platform"], configuredOperations); + // updating next-page wizard title with selected platform + $("#policy-criteria-page-wizard-title").text("ADD " + policy["platform"] + " POLICY"); + // updating ownership type options according to platform + if (policy["platform"] == platformTypeConstants["IOS"] || + policy["platform"] == platformTypeConstants["WINDOWS"]) { + var ownershipTypeSelectOptions = $("#ownership-input"); + ownershipTypeSelectOptions.empty(); + ownershipTypeSelectOptions.append($(""). + attr("value", "BYOD").text("BYOD (Bring Your Own Device)")); + ownershipTypeSelectOptions.attr("disabled", true); + } +}; + +stepBackFrom["policy-profile"] = function () { + // reinitialize configuredOperations + configuredOperations = []; + // clearing already-loaded platform specific hidden-operations html content from the relevant div + // so that, the wrong content would not be shown at the first glance, in case + // the user selects a different platform + $(".wr-advance-operations").html( + "
      " + + "
      " + + "" + + "Loading Platform Features . . ." + + "
      " + + "
      " + + "
      " + ); +}; + +stepForwardFrom["policy-criteria"] = function () { + $("input[type='radio'].select-users-radio").each(function () { + if ($(this).is(':radio')) { + if ($(this).is(":checked")) { + if ($(this).attr("id") == "users-radio-btn") { + policy["selectedUsers"] = $("#users-input").val(); + } else if ($(this).attr("id") == "user-roles-radio-btn") { + policy["selectedUserRoles"] = $("#user-roles-input").val(); + } + } + } + }); + policy["selectedNonCompliantAction"] = $("#action-input").find(":selected").data("action"); + policy["selectedOwnership"] = $("#ownership-input").val(); + //updating next-page wizard title with selected platform + $("#policy-naming-page-wizard-title").text("ADD " + policy["platform"] + " POLICY"); +}; + +/** + * Checks if provided input is valid against provided length range. + * + * @param input Alphanumeric or non-alphanumeric input + * @param minLength Minimum Required Length + * @param maxLength Maximum Required Length + * @returns {boolean} Returns true if input matches the provided minimum length and maximum length + */ +var inputIsValidAgainstLength = function (input, minLength, maxLength) { + var length = input.length; + return (length == minLength || (length > minLength && length < maxLength) || length == maxLength); +}; + +validateStep["policy-criteria"] = function () { + var validationStatus = {}; + var selectedAssignees; + var selectedField = "Role(s)"; + + $("input[type='radio'].select-users-radio").each(function () { + if ($(this).is(":checked")) { + if ($(this).attr("id") == "users-radio-btn") { + selectedAssignees = $("#users-input").val(); + selectedField = "User(s)"; + } else if ($(this).attr("id") == "user-roles-radio-btn") { + selectedAssignees = $("#user-roles-input").val(); + } + return false; + } + }); + + if (selectedAssignees) { + validationStatus["error"] = false; + } else { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = selectedField + " is a required field. It cannot be empty"; + } + + var wizardIsToBeContinued; + if (validationStatus["error"]) { + wizardIsToBeContinued = false; + var mainErrorMsgWrapper = "#policy-criteria-main-error-msg"; + var mainErrorMsg = mainErrorMsgWrapper + " span"; + $(mainErrorMsg).text(validationStatus["mainErrorMsg"]); + $(mainErrorMsgWrapper).removeClass("hidden"); + } else { + wizardIsToBeContinued = true; + } + + return wizardIsToBeContinued; +}; + +validateStep["policy-naming"] = function () { + var validationStatus = {}; + + // taking values of inputs to be validated + var policyName = $("input#policy-name-input").val(); + // starting validation process and updating validationStatus + if (!policyName) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = "Policy name is empty. You cannot proceed."; + } else if (!inputIsValidAgainstLength(policyName, 1, 30)) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = + "Policy name exceeds maximum allowed length."; + } else { + validationStatus["error"] = false; + } + // ending validation process + + // start taking specific actions upon validation + var wizardIsToBeContinued; + if (validationStatus["error"]) { + wizardIsToBeContinued = false; + var mainErrorMsgWrapper = "#policy-naming-main-error-msg"; + var mainErrorMsg = mainErrorMsgWrapper + " span"; + $(mainErrorMsg).text(validationStatus["mainErrorMsg"]); + $(mainErrorMsgWrapper).removeClass("hidden"); + } else { + wizardIsToBeContinued = true; + } + + return wizardIsToBeContinued; +}; + +validateStep["policy-platform"] = function () { + return false; +}; + +validateStep["policy-naming-publish"] = function () { + var validationStatus = {}; + + // taking values of inputs to be validated + var policyName = $("input#policy-name-input").val(); + // starting validation process and updating validationStatus + if (!policyName) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = "Policy name is empty. You cannot proceed."; + } else if (!inputIsValidAgainstLength(policyName, 1, 30)) { + validationStatus["error"] = true; + validationStatus["mainErrorMsg"] = + "Policy name exceeds maximum allowed length."; + } else { + validationStatus["error"] = false; + } + // ending validation process + + // start taking specific actions upon validation + var wizardIsToBeContinued; + if (validationStatus["error"]) { + wizardIsToBeContinued = false; + var mainErrorMsgWrapper = "#policy-naming-main-error-msg"; + var mainErrorMsg = mainErrorMsgWrapper + " span"; + $(mainErrorMsg).text(validationStatus["mainErrorMsg"]); + $(mainErrorMsgWrapper).removeClass("hidden"); + } else { + wizardIsToBeContinued = true; + } + + return wizardIsToBeContinued; +}; + +stepForwardFrom["policy-naming-publish"] = function () { + policy["policyName"] = $("#policy-name-input").val(); + policy["description"] = $("#policy-description-input").val(); + //All data is collected. Policy can now be updated. + savePolicy(policy, "/devicemgt_admin/policies/active-policy"); +}; +stepForwardFrom["policy-naming"] = function () { + policy["policyName"] = $("#policy-name-input").val(); + policy["description"] = $("#policy-description-input").val(); + //All data is collected. Policy can now be updated. + savePolicy(policy, "/devicemgt_admin/policies/inactive-policy"); +}; + +var savePolicy = function (policy, serviceURL) { + var profilePayloads = []; + // traverses key by key in policy["profile"] + var key; + for (key in policy["profile"]) { + if (policy["platformId"] == platformIds["WINDOWS"] && + key == windowsOperationConstants["PASSCODE_POLICY_OPERATION_CODE"]) { + policy["profile"][key].enablePassword = true; + } + if (policy["profile"].hasOwnProperty(key)) { + profilePayloads.push({ + "featureCode": key, + "deviceTypeId": policy["platformId"], + "content": policy["profile"][key] + }); + } + } + + $.each(profilePayloads, function (i, item) { + $.each(item.content, function (key, value) { + if (!value) { + item.content[key] = null; + } + }); + }); + + var payload = { + "policyName": policy["policyName"], + "description": policy["description"], + "compliance": policy["selectedNonCompliantAction"], + "ownershipType": policy["selectedOwnership"], + "profile": { + "profileName": policy["policyName"], + "deviceType": { + "id": policy["platformId"] + }, + "profileFeaturesList": profilePayloads + } + }; + + if (policy["selectedUsers"]) { + payload["users"] = policy["selectedUsers"]; + } else if (policy["selectedUserRoles"]) { + payload["roles"] = policy["selectedUserRoles"]; + } else { + payload["users"] = []; + payload["roles"] = []; + } + + invokerUtil.post( + serviceURL, + payload, + function () { + $(".add-policy").addClass("hidden"); + $(".policy-naming").addClass("hidden"); + $(".policy-message").removeClass("hidden"); + }, + function (data) { + } + ); +}; + +// Start of HTML embedded invoke methods +var showAdvanceOperation = function (operation, button) { + $(button).addClass('selected'); + $(button).siblings().removeClass('selected'); + var hiddenOperation = ".wr-hidden-operations-content > div"; + $(hiddenOperation + '[data-operation="' + operation + '"]').show(); + $(hiddenOperation + '[data-operation="' + operation + '"]').siblings().hide(); +}; + +/** + * Method to slide down a provided pane upon provided value set. + * + * @param selectElement Select HTML Element to consider + * @param paneID HTML ID of div element to slide down + * @param valueSet Applicable Value Set + */ +var slideDownPaneAgainstValueSet = function (selectElement, paneID, valueSet) { + var selectedValueOnChange = $(selectElement).find("option:selected").val(); + var i, slideDownVotes = 0; + for (i = 0; i < valueSet.length; i++) { + if (selectedValueOnChange == valueSet[i]) { + slideDownVotes++; + } + } + var paneSelector = "#" + paneID; + if (slideDownVotes > 0) { + if (!$(paneSelector).hasClass("expanded")) { + $(paneSelector).addClass("expanded"); + } + $(paneSelector).slideDown(); + } else { + if ($(paneSelector).hasClass("expanded")) { + $(paneSelector).removeClass("expanded"); + } + $(paneSelector).slideUp(); + /** now follows the code to reinitialize all inputs of the slidable pane */ + // reinitializing input fields into the defaults + $(paneSelector + " input").each( + function () { + if ($(this).is("input:text")) { + $(this).val($(this).data("default")); + } else if ($(this).is("input:password")) { + $(this).val(""); + } else if ($(this).is("input:checkbox")) { + $(this).prop("checked", $(this).data("default")); + // if this checkbox is the parent input of a grouped-input + if ($(this).hasClass("parent-input")) { + var groupedInput = $(this).parent().parent().parent(); + updateGroupedInputVisibility(groupedInput); + } + } + } + ); + // reinitializing select fields into the defaults + $(paneSelector + " select").each( + function () { + var defaultOption = $(this).data("default"); + $("option:eq(" + defaultOption + ")", this).prop("selected", "selected"); + } + ); + // collapsing expanded-panes (upon the selection of html-select-options) if any + $(paneSelector + " .expanded").each( + function () { + if ($(this).hasClass("expanded")) { + $(this).removeClass("expanded"); + } + $(this).slideUp(); + } + ); + // removing all entries of grid-input elements if exist + $(paneSelector + " .grouped-array-input").each( + function () { + var gridInputs = $(this).find("[data-add-form-clone]"); + if (gridInputs.length > 0) { + gridInputs.remove(); + } + var helpTexts = $(this).find("[data-help-text=add-form]"); + if (helpTexts.length > 0) { + helpTexts.show(); + } + } + ); + } +}; +// End of HTML embedded invoke methods + + +// Start of functions related to grid-input-view + +/** + * Method to set count id to cloned elements. + * @param {object} addFormContainer + */ +var setId = function (addFormContainer) { + $(addFormContainer).find("[data-add-form-clone]").each(function (i) { + $(this).attr("id", $(this).attr("data-add-form-clone").slice(1) + "-" + (i + 1)); + if ($(this).find(".index").length > 0) { + $(this).find(".index").html(i + 1); + } + }); +}; + +/** + * Method to set count id to cloned elements. + * @param {object} addFormContainer + */ +var showHideHelpText = function (addFormContainer) { + var helpText = "[data-help-text=add-form]"; + if ($(addFormContainer).find("[data-add-form-clone]").length > 0) { + $(addFormContainer).find(helpText).hide(); + } else { + $(addFormContainer).find(helpText).show(); + } +}; + +function formatRepo(user) { + if (user.loading) { + return user.text; + } + if (!user.username) { + return; + } + var markup = '
      ' + + '
      ' + + '
      ' + + '
      ' + user.username + '
      '; + if (user.firstname) { + markup += '
      ' + user.firstname + '
      '; + } + if (user.emailAddress) { + markup += '
      ' + user.emailAddress + '
      '; + } + markup += '
      '; + return markup; +} + +function formatRepoSelection(user) { + return user.username || user.text; +} + +function promptErrorPolicyPlatform(errorMsg) { + var mainErrorMsgWrapper = "#policy-platform-main-error-msg"; + var mainErrorMsg = mainErrorMsgWrapper + " span"; + $(mainErrorMsg).text(errorMsg); + $(mainErrorMsgWrapper).removeClass("hidden"); +} + +// End of functions related to grid-input-view + + +$(document).ready(function () { + $("#users-input").select2({ + multiple: true, + tags: false, + ajax: { + url: window.location.origin + "/devicemgt/api/invoker/execute/", + method: "POST", + dataType: 'json', + delay: 250, + id: function (user) { + return user.username; + }, + data: function (params) { + var postData = {}; + postData.actionMethod = "GET"; + postData.actionUrl = "/devicemgt_admin/users"; + postData.actionPayload = JSON.stringify({ + q: params.term, // search term + page: params.page + }); + + return JSON.stringify(postData); + }, + processResults: function (data, page) { + var newData = []; + $.each(data.responseContent, function (index, value) { + value.id = value.username; + newData.push(value); + }); + return { + results: newData + }; + }, + cache: true + }, + escapeMarkup: function (markup) { + return markup; + }, // let our custom formatter work + minimumInputLength: 1, + templateResult: formatRepo, // omitted for brevity, see the source of this page + templateSelection: formatRepoSelection // omitted for brevity, see the source of this page + }); + + $("#loading-content").remove(); + $(".policy-platform").removeClass("hidden"); + // Adding initial state of wizard-steps. + $("#policy-profile-wizard-steps").html($(".wr-steps").html()); + + policy["platform"] = $("#platform").data("platform"); + policy["platformId"] = $("#platform").data("platform-id"); + // updating next-page wizard title with selected platform + $("#policy-profile-page-wizard-title").text("ADD " + policy["platform"] + " POLICY"); + + var deviceType = policy["platform"]; + var hiddenOperationsByDeviceType = $("#hidden-operations-" + deviceType); + var hiddenOperationsByDeviceTypeCacheKey = deviceType + "HiddenOperations"; + var hiddenOperationsByDeviceTypeSrc = hiddenOperationsByDeviceType.attr("src"); + + setTimeout( + function () { + $.template(hiddenOperationsByDeviceTypeCacheKey, hiddenOperationsByDeviceTypeSrc, function (template) { + var content = template(); + $(".wr-advance-operations").html(content); + $(".wr-advance-operations li.grouped-input").each(function () { + updateGroupedInputVisibility(this); + }); + }); + }, + 250 // time delayed for the execution of above function, 250 milliseconds + ); + + $("select.select2[multiple=multiple]").select2({ + "tags": false + }); + + $("#users-select-field").hide(); + $("#user-roles-select-field").show(); + + $("input[type='radio'].select-users-radio").change(function () { + if ($("#users-radio-btn").is(":checked")) { + $("#user-roles-select-field").hide(); + $("#users-select-field").show(); + } + if ($("#user-roles-radio-btn").is(":checked")) { + $("#users-select-field").hide(); + $("#user-roles-select-field").show(); + } + }); + + // Support for special input type "ANY" on user(s) & user-role(s) selection + $("#user-roles-input").select2({ + "tags": false + }).on("select2:select", function (e) { + if (e.params.data.id == "ANY") { + $(this).val("ANY").trigger("change"); + } else { + $("option[value=ANY]", this).prop("selected", false).parent().trigger("change"); + } + }); + + // Maintains an array of configured features of the profile + var advanceOperations = ".wr-advance-operations"; + $(advanceOperations).on("click", ".wr-input-control.switch", function (event) { + var operationCode = $(this).parents(".operation-data").data("operation-code"); + var operation = $(this).parents(".operation-data").data("operation"); + var operationDataWrapper = $(this).data("target"); + // prevents event bubbling by figuring out what element it's being called from. + if (event.target.tagName == "INPUT") { + var featureConfiguredIcon; + if ($("input[type='checkbox']", this).is(":checked")) { + configuredOperations.push(operationCode); + // when a feature is enabled, if "zero-configured-features" msg is available, hide that. + var zeroConfiguredOperationsErrorMsg = "#policy-profile-main-error-msg"; + if (!$(zeroConfiguredOperationsErrorMsg).hasClass("hidden")) { + $(zeroConfiguredOperationsErrorMsg).addClass("hidden"); + } + // add configured-state-icon to the feature + featureConfiguredIcon = "#" + operation + "-configured"; + if ($(featureConfiguredIcon).hasClass("hidden")) { + $(featureConfiguredIcon).removeClass("hidden"); + } + } else { + //splicing the array if operation is present. + var index = $.inArray(operationCode, configuredOperations); + if (index != -1) { + configuredOperations.splice(index, 1); + } + // when a feature is disabled, clearing all its current configured, error or success states + var subErrorMsgWrapper = "#" + operation + "-feature-error-msg"; + var subErrorIcon = "#" + operation + "-error"; + var subOkIcon = "#" + operation + "-ok"; + featureConfiguredIcon = "#" + operation + "-configured"; + + if (!$(subErrorMsgWrapper).hasClass("hidden")) { + $(subErrorMsgWrapper).addClass("hidden"); + } + if (!$(subErrorIcon).hasClass("hidden")) { + $(subErrorIcon).addClass("hidden"); + } + if (!$(subOkIcon).hasClass("hidden")) { + $(subOkIcon).addClass("hidden"); + } + if (!$(featureConfiguredIcon).hasClass("hidden")) { + $(featureConfiguredIcon).addClass("hidden"); + } + // reinitializing input fields into the defaults + $(operationDataWrapper + " input").each( + function () { + if ($(this).is("input:text")) { + $(this).val($(this).data("default")); + } else if ($(this).is("input:password")) { + $(this).val(""); + } else if ($(this).is("input:checkbox")) { + $(this).prop("checked", $(this).data("default")); + // if this checkbox is the parent input of a grouped-input + if ($(this).hasClass("parent-input")) { + var groupedInput = $(this).parent().parent().parent(); + updateGroupedInputVisibility(groupedInput); + } + } + } + ); + // reinitializing select fields into the defaults + $(operationDataWrapper + " select").each( + function () { + var defaultOption = $(this).data("default"); + $("option:eq(" + defaultOption + ")", this).prop("selected", "selected"); + } + ); + // collapsing expanded-panes (upon the selection of html-select-options) if any + $(operationDataWrapper + " .expanded").each( + function () { + if ($(this).hasClass("expanded")) { + $(this).removeClass("expanded"); + } + $(this).slideUp(); + } + ); + // removing all entries of grid-input elements if exist + $(operationDataWrapper + " .grouped-array-input").each( + function () { + var gridInputs = $(this).find("[data-add-form-clone]"); + if (gridInputs.length > 0) { + gridInputs.remove(); + } + var helpTexts = $(this).find("[data-help-text=add-form]"); + if (helpTexts.length > 0) { + helpTexts.show(); + } + } + ); + } + } + }); + + // adding support for cloning multiple profiles per feature with cloneable class definitions + $(advanceOperations).on("click", ".multi-view.add.enabled", function () { + // get a copy of .cloneable and create new .cloned div element + var cloned = "

      " + $(".cloneable", $(this).parent().parent()).html() + "
      "; + // append newly created .cloned div element to panel-body + $(this).parent().parent().append(cloned); + // enable remove action of newly cloned div element + $(".cloned", $(this).parent().parent()).each( + function () { + if ($(".multi-view.remove", this).hasClass("disabled")) { + $(".multi-view.remove", this).removeClass("disabled"); + } + if (!$(".multi-view.remove", this).hasClass("enabled")) { + $(".multi-view.remove", this).addClass("enabled"); + } + } + ); + }); + + $(advanceOperations).on("click", ".multi-view.remove.enabled", function () { + $(this).parent().remove(); + }); + + // enabling or disabling grouped-input based on the status of a parent check-box + $(advanceOperations).on("click", ".grouped-input", function () { + updateGroupedInputVisibility(this); + }); + + // add form entry click function for grid inputs + $(advanceOperations).on("click", "[data-click-event=add-form]", function () { + var addFormContainer = $("[data-add-form-container=" + $(this).attr("href") + "]"); + var clonedForm = $("[data-add-form=" + $(this).attr("href") + "]").clone(). + find("[data-add-form-element=clone]").attr("data-add-form-clone", $(this).attr("href")); + + // adding class .child-input to capture text-input-array-values + $("input, select", clonedForm).addClass("child-input"); + + $(addFormContainer).append(clonedForm); + setId(addFormContainer); + showHideHelpText(addFormContainer); + }); + + // remove form entry click function for grid inputs + $(advanceOperations).on("click", "[data-click-event=remove-form]", function () { + var addFormContainer = $("[data-add-form-container=" + $(this).attr("href") + "]"); + + $(this).closest("[data-add-form-element=clone]").remove(); + setId(addFormContainer); + showHideHelpText(addFormContainer); + }); + + $(".wizard-stepper").click(function () { + // button clicked here can be either a continue button or a back button. + var currentStep = $(this).data("current"); + var validationIsRequired = $(this).data("validate"); + var wizardIsToBeContinued; + + if (validationIsRequired) { + wizardIsToBeContinued = validateStep[currentStep](); + } else { + wizardIsToBeContinued = true; + } + + if (wizardIsToBeContinued) { + // When moving back and forth, following code segment will + // remove if there are any visible error-messages. + var errorMsgWrappers = ".alert.alert-danger"; + $(errorMsgWrappers).each( + function () { + if (!$(this).hasClass("hidden")) { + $(this).addClass("hidden"); + } + } + ); + + var nextStep = $(this).data("next"); + var isBackBtn = $(this).data("is-back-btn"); + + // if current button is a continuation... + if (!isBackBtn) { + // initiate stepForwardFrom[*] functions to gather form data. + if (stepForwardFrom[currentStep]) { + stepForwardFrom[currentStep](this); + } + } else { + // initiate stepBackFrom[*] functions to rollback. + if (stepBackFrom[currentStep]) { + stepBackFrom[currentStep](); + } + } + + // following step occurs only at the last stage of the wizard. + if (!nextStep) { + window.location.href = $(this).data("direct"); + } + + // updating next wizard step as current. + $(".itm-wiz").each(function () { + var step = $(this).data("step"); + if (step == nextStep) { + $(this).addClass("itm-wiz-current"); + } else { + $(this).removeClass("itm-wiz-current"); + } + }); + + // adding next update of wizard-steps. + $("#" + nextStep + "-wizard-steps").html($(".wr-steps").html()); + + // hiding current section of the wizard and showing next section. + $("." + currentStep).addClass("hidden"); + $("." + nextStep).removeClass("hidden"); + } + }); +}); \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/public/templates/hidden-operations-android.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/public/templates/hidden-operations-android.hbs new file mode 100644 index 000000000..094f7f73f --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/public/templates/hidden-operations-android.hbs @@ -0,0 +1,458 @@ +
      + + +
      + +
      +
      + +
      + + + +
      + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      +
      +
      +
      + + + +
      +
      + +
      + + Un-check following checkbox in case you need to disable camera. +
      +
      +
      + +
      +
      +
      +
      +
      + + + +
      +
      + +
      + + Un-check following checkbox in case you do not need the device to be encrypted. +
      +
      +
      + +
      +
      +
      +
      +
      + + + +
      +
      + +
      + + + + + + + + + + + + + + + + + Please note that * sign represents required fields of data. +
      +
      + +
      + + + +
      +
      + + +
      + +
      +
      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/public/templates/hidden-operations-ios.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/public/templates/hidden-operations-ios.hbs new file mode 100644 index 000000000..113ce1370 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/public/templates/hidden-operations-ios.hbs @@ -0,0 +1,2923 @@ +
      + + +
      + +
      +
      + +
      + + + +
      + +
      + +
      + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      +
      +
      +
      + + + +
      +
      + +
      + + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + +
      + +
      + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + + + +
      + + +
      + + + +
      + +
      + + + + + + + + + + + + + +
      No:Roaming Consortium OI
      + No entries added yet . +
      + + + + + + + + + +
      +
      + +
      + +
      + +
      + + + + + + + + + + + + + +
      No:NAI Realm Name
      + No entries added yet . +
      + + + + + + + + + +
      +
      + +
      + +
      + +
      + + + + + + + + + + + + + + +
      No:Mobile Country Code ( MCC )Mobile Network Code ( MNC )
      + No entries added yet . +
      + + + + + + + + + + +
      +
      +
      +
      +
      + + + +
      +
      + +
      + + + +
      + + +
      + +
      + + +
      + +
      + +
      + + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + +
      + +
      + Incoming Mail Settings : +
      + +
      + + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + Outgoing Mail Settings : +
      + +
      + + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + + +
      + +
      +
      +
      + + + +
      +
      + +
      + + + +
      + +
      + + + + + + + + + + + + + + +
      No:Device NamePassword
      + No entries added yet . +
      + + + + + + + + + + +
      +
      + +
      + +
      + +
      + + + + + + + + + + + + + +
      No:Destination
      + No entries added yet . +
      + + + + + + + + + +
      +
      + +
      +
      +
      + + + +
      +
      + +
      + + + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + +
      + + + + + + + + + + + + + + + +
      No:DescriptionSearch BaseScope
      + No entries added yet . +
      + + + + + + + + + + + +
      +
      + +
      +
      +
      + + + +
      +
      + +
      + + + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      +
      +
      + + + +
      +
      + +
      + + + +
      + + +
      + +
      + + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      +
      +
      + + + +
      +
      + +
      + + + +
      + +
      + + + + + + + + + + + + + + + + + +
      No:APNUsernamePasswordProxyPort
      + No entries added yet . +
      + + + + + + + + + + + + + +
      +
      + +
      +
      +
      + + + +
      +
      + +
      + + + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + +
      + + + + + + + + + + + + + + + + + + +
      No:APNAuth.TypeUsernamePasswordProxyPort
      + No entries added yet . +
      + + + + + + + + + + + + + + +
      +
      + +
      +
      +
      + + + +
      +
      + +
      + + + + Restrictions on Device Functionality : +
      +
      +
        +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
          +
        • +
          + +
          +
        • +
        • +
          + +
          +
        • +
        • +
          + +
          +
        • +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      +
      +
      + Restrictions on Applications : +
      +
      +
        +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
          +
        • +
          + +
          +
        • +
        • +
          + +
          +
        • +
        • +
          + +
          +
        • +
        • +
          + +
          +
        • +
        • +
          + + +
          +
        • +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      • +
        + +
        +
      • +
      +
      + + + +
      +
      +
      + +
      +
      \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/public/templates/hidden-operations-windows.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/public/templates/hidden-operations-windows.hbs new file mode 100644 index 000000000..3b5bb3bef --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/public/templates/hidden-operations-windows.hbs @@ -0,0 +1,460 @@ +
      + + +
      + +
      +
      + +
      + + + +
      + +
      + +
      + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      + +
      + + +
      +
      +
      +
      + + + +
      +
      + +
      + + Un-check following checkbox in case you need to disable camera. +
      +
      +
      + +
      +
      +
      +
      +
      + + + +
      +
      + +
      + + Un-check following checkbox in case you need to disable storage-encryption. +
      +
      +
      + +
      +
      +
      +
      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      +
      \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/wizard.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/wizard.hbs new file mode 100644 index 000000000..08e9ca77d --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/wizard.hbs @@ -0,0 +1,278 @@ + +
      +
      + + + + + + +
      + + + +
      +
      +

      ADD POLICY

      +
      +
      +
      +
      +
      +

      Step 2: Configure profile

      +
      + + +
      +
      +
      +       + +     + Loading platform features . . . +
      +
      +
      +
      + +
      +
      +
      +
      + + +
      + +{{#zone "bottomJs"}} + + + + {{js "js/policy-create.js"}} +{{/zone}} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/wizard.js b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/wizard.js new file mode 100644 index 000000000..2a2200640 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/wizard.js @@ -0,0 +1,37 @@ +/* + * 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. + */ + +function onRequest(context) { + var userModule = require("/app/modules/user.js")["userModule"]; + var utility = require('/app/modules/utility.js').utility; + var response = userModule.getRoles(); + var wizardPage = {}; + if (response["status"] == "success") { + wizardPage["roles"] = response["content"]; + } + var deviceType = context.uriParams.deviceType; + var typesListResponse = userModule.getPlatforms(); + if (typesListResponse["status"] == "success") { + for (var type in typesListResponse["content"]) { + if (deviceType == typesListResponse["content"][type]["name"]) { + wizardPage["type"] = typesListResponse["content"][type]; + } + } + } + return wizardPage; +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/wizard.json b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/wizard.json new file mode 100644 index 000000000..fd2590129 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.policy.wizard/wizard.json @@ -0,0 +1,3 @@ +{ + "version" : "1.0.0" +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.ui.header.logo/logo.hbs b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.ui.header.logo/logo.hbs new file mode 100644 index 000000000..7e30c1cb7 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.ui.header.logo/logo.hbs @@ -0,0 +1 @@ +{{#zone "productName"}}ENTERPRISE MOBILITY MANAGER{{/zone}} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.ui.header.logo/logo.json b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.ui.header.logo/logo.json new file mode 100644 index 000000000..1299a7b1b --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.ui/src/main/resources/jaggeryapps/devicemgt/app/units/mdm.unit.ui.header.logo/logo.json @@ -0,0 +1,5 @@ +{ + "version": "1.0.0", + "index": 30, + "extends": "cdmf.unit.ui.header.logo" +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.url.printer/pom.xml b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.url.printer/pom.xml new file mode 100644 index 000000000..bbb1c990a --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.url.printer/pom.xml @@ -0,0 +1,81 @@ + + + + + mobile-base-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.mobile.url.printer + bundle + WSO2 Carbon - Startup Handler That Prints MDM End-User Web-App URL + WSO2 Carbon - Startup Handler That Prints MDM End-User Web-App URL + http://wso2.org + + + + + org.apache.felix + maven-scr-plugin + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + ${project.artifactId} + ${project.artifactId} + ${carbon.devicemgt.plugins.version} + Bundle Containing The Startup Handler That Prints MDM End-User Web-App URL + org.wso2.carbon.device.mgt.mobile.url.printer.internal + + org.osgi.framework, + org.osgi.service.component, + org.apache.commons.logging, + org.apache.axis2.*;version="${axis2.osgi.version.range}", + org.wso2.carbon.core, + org.wso2.carbon.utils.*, + + + !org.wso2.carbon.device.mgt.mobile.url.printer.internal, + org.wso2.carbon.device.mgt.mobile.url.printer, + + + + + + + + + + org.eclipse.osgi + org.eclipse.osgi + + + org.eclipse.osgi + org.eclipse.osgi.services + + + org.wso2.carbon + org.wso2.carbon.core + + + org.wso2.carbon + org.wso2.carbon.logging + + + org.wso2.carbon + org.wso2.carbon.utils + + + org.apache.axis2.wso2 + axis2 + + + + \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.url.printer/src/main/java/org/wso2/carbon/device/mgt/mobile/url/printer/URLPrinterStartupHandler.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.url.printer/src/main/java/org/wso2/carbon/device/mgt/mobile/url/printer/URLPrinterStartupHandler.java new file mode 100644 index 000000000..1eaad8ad4 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.url.printer/src/main/java/org/wso2/carbon/device/mgt/mobile/url/printer/URLPrinterStartupHandler.java @@ -0,0 +1,64 @@ +/* + * 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.device.mgt.mobile.url.printer; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.core.ServerStartupObserver; +import org.wso2.carbon.device.mgt.mobile.url.printer.internal.URLPrinterDataHolder; +import org.wso2.carbon.utils.CarbonUtils; +import org.wso2.carbon.utils.ConfigurationContextService; +import org.wso2.carbon.utils.NetworkUtils; + +public class URLPrinterStartupHandler implements ServerStartupObserver { + + private static final Log log = LogFactory.getLog(URLPrinterStartupHandler.class); + + @Override + public void completingServerStartup() { + + } + + @Override + public void completedServerStartup() { + log.info("Device Management Console URL : " + this.getEmmUrl()); + } + + private String getEmmUrl() { + // Hostname + String hostName = "localhost"; + try { + hostName = NetworkUtils.getMgtHostName(); + } catch (Exception ignored) { + } + // HTTPS port + String mgtConsoleTransport = CarbonUtils.getManagementTransport(); + ConfigurationContextService configContextService = + URLPrinterDataHolder.getInstance().getConfigurationContextService(); + int port = CarbonUtils.getTransportPort(configContextService, mgtConsoleTransport); + int httpsProxyPort = + CarbonUtils.getTransportProxyPort(configContextService.getServerConfigContext(), + mgtConsoleTransport); + if (httpsProxyPort > 0) { + port = httpsProxyPort; + } + return "https://" + hostName + ":" + port + "/devicemgt"; + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.url.printer/src/main/java/org/wso2/carbon/device/mgt/mobile/url/printer/internal/URLPrinterDataHolder.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.url.printer/src/main/java/org/wso2/carbon/device/mgt/mobile/url/printer/internal/URLPrinterDataHolder.java new file mode 100644 index 000000000..0dd8a459d --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.url.printer/src/main/java/org/wso2/carbon/device/mgt/mobile/url/printer/internal/URLPrinterDataHolder.java @@ -0,0 +1,42 @@ +/* + * 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.device.mgt.mobile.url.printer.internal; + +import org.wso2.carbon.utils.ConfigurationContextService; + +public class URLPrinterDataHolder { + + private ConfigurationContextService configurationContextService; + private static URLPrinterDataHolder thisInstance = new URLPrinterDataHolder(); + + private URLPrinterDataHolder() {} + + public static URLPrinterDataHolder getInstance() { + return thisInstance; + } + + public ConfigurationContextService getConfigurationContextService() { + return configurationContextService; + } + + public void setConfigurationContextService(ConfigurationContextService configurationContextService) { + this.configurationContextService = configurationContextService; + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.url.printer/src/main/java/org/wso2/carbon/device/mgt/mobile/url/printer/internal/URLPrinterStartupHandlerServiceComponent.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.url.printer/src/main/java/org/wso2/carbon/device/mgt/mobile/url/printer/internal/URLPrinterStartupHandlerServiceComponent.java new file mode 100644 index 000000000..88a2b2dae --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile.url.printer/src/main/java/org/wso2/carbon/device/mgt/mobile/url/printer/internal/URLPrinterStartupHandlerServiceComponent.java @@ -0,0 +1,73 @@ +/* + * 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.device.mgt.mobile.url.printer.internal; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.osgi.framework.BundleContext; +import org.osgi.service.component.ComponentContext; +import org.wso2.carbon.core.ServerStartupObserver; +import org.wso2.carbon.device.mgt.mobile.url.printer.URLPrinterStartupHandler; +import org.wso2.carbon.utils.ConfigurationContextService; + +/** + * @scr.component name="org.wso2.carbon.device.mgt.mobile.url.printer.URLPrinterStartupHandlerServiceComponent" + * immediate="true" + * @scr.reference name="config.context.service" + * interface="org.wso2.carbon.utils.ConfigurationContextService" + * cardinality="0..1" + * policy="dynamic" + * bind="setConfigurationContextService" + * unbind="unsetConfigurationContextService" + */ +public class URLPrinterStartupHandlerServiceComponent { + + private static final Log log = LogFactory.getLog(URLPrinterStartupHandlerServiceComponent.class); + + @SuppressWarnings("unused") + protected void activate(ComponentContext componentContext) { + try { + BundleContext bundleContext = componentContext.getBundleContext(); + /* Registering URL printer start-up handler */ + bundleContext.registerService(ServerStartupObserver.class, new URLPrinterStartupHandler(), null); + } catch (Throwable e) { + log.error("Error occurred while activating URL printer server start-up handler service component", e); + } + } + + @SuppressWarnings("unused") + protected void deactivate(ComponentContext componentContext) { + //do nothing + } + + protected void setConfigurationContextService(ConfigurationContextService configurationContextService) { + if (log.isDebugEnabled()) { + log.debug("Setting ConfigurationContextService"); + } + URLPrinterDataHolder.getInstance().setConfigurationContextService(configurationContextService); + } + + protected void unsetConfigurationContextService(ConfigurationContextService configurationContextService) { + if (log.isDebugEnabled()) { + log.debug("Un-setting ConfigurationContextService"); + } + URLPrinterDataHolder.getInstance().setConfigurationContextService(null); + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/pom.xml b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/pom.xml new file mode 100644 index 000000000..73777a2da --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/pom.xml @@ -0,0 +1,172 @@ + + + + + + + mobile-base-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.mobile + bundle + WSO2 Carbon - Mobile Device Management Impl + WSO2 Carbon - Mobile Device Management Implementation + http://wso2.org + + + + + org.apache.felix + maven-scr-plugin + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + ${project.artifactId} + ${project.artifactId} + ${carbon.devicemgt.plugins.version} + Device Management Mobile Impl Bundle + org.wso2.carbon.device.mgt.mobile.internal + + org.osgi.framework, + org.osgi.service.component, + org.apache.commons.logging, + javax.xml.bind.*, + javax.naming, + javax.sql, + javax.xml.parsers, + org.w3c.dom, + org.wso2.carbon.context, + org.wso2.carbon.utils.*, + org.wso2.carbon.device.mgt.common.*, + org.wso2.carbon.ndatasource.core, + org.wso2.carbon.policy.mgt.common.*, + org.wso2.carbon.registry.core, + org.wso2.carbon.registry.core.service, + org.wso2.carbon.registry.core.session, + org.wso2.carbon.registry.api, + org.wso2.carbon.device.mgt.extensions.license.mgt.registry, + com.google.gson.* + + + !org.wso2.carbon.device.mgt.mobile.internal, + !org.wso2.carbon.device.mgt.mobile.impl, + org.wso2.carbon.device.mgt.mobile.*, + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.18 + + + file:src/test/resources/log4j.properties + + + + src/test/resources/testng.xml + + + + + + + + + org.eclipse.osgi + org.eclipse.osgi + + + org.eclipse.osgi + org.eclipse.osgi.services + + + org.wso2.carbon + org.wso2.carbon.core + + + org.wso2.carbon + org.wso2.carbon.logging + + + org.wso2.carbon + org.wso2.carbon.utils + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.extensions + + + org.wso2.carbon + org.wso2.carbon.ndatasource.core + + + org.wso2.carbon.devicemgt + org.wso2.carbon.policy.mgt.common + + + org.wso2.carbon.devicemgt + org.wso2.carbon.policy.mgt.core + + + org.wso2.carbon + org.wso2.carbon.registry.api + + + org.wso2.carbon + org.wso2.carbon.registry.core + + + org.testng + testng + + + org.apache.tomcat.wso2 + jdbc-pool + + + com.h2database.wso2 + h2-database-engine + test + + + com.google.code.gson + gson + + + com.google.android.gcm + gcm-server + + + diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/AbstractMobileOperationManager.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/AbstractMobileOperationManager.java new file mode 100644 index 000000000..1bf5322da --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/AbstractMobileOperationManager.java @@ -0,0 +1,41 @@ +/* + * 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.device.mgt.mobile; + +import org.wso2.carbon.device.mgt.common.*; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.common.operation.mgt.OperationManagementException; +import org.wso2.carbon.device.mgt.common.operation.mgt.OperationManager; + +import java.util.List; + +public abstract class AbstractMobileOperationManager implements OperationManager { + + @Override + public List getOperations(DeviceIdentifier deviceIdentifier) throws OperationManagementException { + return null; + } + + @Override + public int addOperation(Operation operation, List devices) throws + OperationManagementException { + return 1; + } + +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/DataSourceListener.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/DataSourceListener.java new file mode 100644 index 000000000..649786c43 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/DataSourceListener.java @@ -0,0 +1,25 @@ +/* + * 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.device.mgt.mobile; + +public interface DataSourceListener { + + void notifyObserver(); + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/DataSourceNotAvailableException.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/DataSourceNotAvailableException.java new file mode 100644 index 000000000..5a9e5b8f4 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/DataSourceNotAvailableException.java @@ -0,0 +1,52 @@ +/* + * * + * * 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.device.mgt.mobile; + +public class DataSourceNotAvailableException extends RuntimeException { + + private String message; + private static final long serialVersionUID = 2021891706072918866L; + + public DataSourceNotAvailableException(String message, Exception nestedException) { + super(message, nestedException); + setErrorMessage(message); + } + + public DataSourceNotAvailableException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public DataSourceNotAvailableException(String message) { + super(message); + setErrorMessage(message); + } + + public DataSourceNotAvailableException(Throwable cause) { + super(cause); + } + + public String getMessage() { + return message; + } + + public void setErrorMessage(String errorMessage) { + this.message = errorMessage; + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/common/MobileDeviceMgtPluginException.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/common/MobileDeviceMgtPluginException.java new file mode 100644 index 000000000..ef2871c28 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/common/MobileDeviceMgtPluginException.java @@ -0,0 +1,57 @@ +/* + * 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.device.mgt.mobile.common; + + +public class MobileDeviceMgtPluginException extends Exception{ + + private static final long serialVersionUID = -2297311387874900305L; + private String errorMessage; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public MobileDeviceMgtPluginException(String msg, Exception nestedEx) { + super(msg, nestedEx); + setErrorMessage(msg); + } + + public MobileDeviceMgtPluginException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public MobileDeviceMgtPluginException(String msg) { + super(msg); + setErrorMessage(msg); + } + + public MobileDeviceMgtPluginException() { + super(); + } + + public MobileDeviceMgtPluginException(Throwable cause) { + super(cause); + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/common/MobilePluginConstants.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/common/MobilePluginConstants.java new file mode 100644 index 000000000..67e793fa5 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/common/MobilePluginConstants.java @@ -0,0 +1,29 @@ +/* + * 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.device.mgt.mobile.common; + +public class MobilePluginConstants { + public static final String MOBILE_DB_SCRIPTS_FOLDER = "cdm"; + public static final String MOBILE_CONFIG_REGISTRY_ROOT = "/_system/config"; + + public static final String MEDIA_TYPE_XML = "application/xml"; + public static final String CHARSET_UTF8 = "UTF8"; + public static final String LANGUAGE_CODE_ENGLISH_US = "en_US"; + public static final String LANGUAGE_CODE_ENGLISH_UK = "en_UK"; + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/MobileDeviceConfigurationManager.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/MobileDeviceConfigurationManager.java new file mode 100644 index 000000000..07123434d --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/MobileDeviceConfigurationManager.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 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. + */ + +package org.wso2.carbon.device.mgt.mobile.config; + +import org.w3c.dom.Document; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.mobile.util.MobileDeviceManagementUtil; +import org.wso2.carbon.device.mgt.mobile.config.datasource.MobileDataSourceConfig; +import org.wso2.carbon.utils.CarbonUtils; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.Unmarshaller; +import java.io.File; + +/** + * Class responsible for the mobile device manager configuration initialization. + */ +public class MobileDeviceConfigurationManager { + + private static final String MOBILE_DEVICE_CONFIG_XML_NAME = "mobile-config.xml"; + private static final String MOBILE_DEVICE_PLUGIN_DIRECTORY = "mobile"; + private static final String DEVICE_MGT_PLUGIN_CONFIGS_DIRECTORY = "device-mgt-plugin-configs"; + private MobileDeviceManagementConfig currentMobileDeviceConfig; + private static MobileDeviceConfigurationManager mobileDeviceConfigManager; + + private final String mobileDeviceMgtConfigXMLPath = + CarbonUtils.getEtcCarbonConfigDirPath() + File.separator + + DEVICE_MGT_PLUGIN_CONFIGS_DIRECTORY + + File.separator + + MOBILE_DEVICE_PLUGIN_DIRECTORY + File.separator + MOBILE_DEVICE_CONFIG_XML_NAME; + + public static MobileDeviceConfigurationManager getInstance() { + if (mobileDeviceConfigManager == null) { + synchronized (MobileDeviceConfigurationManager.class) { + if (mobileDeviceConfigManager == null) { + mobileDeviceConfigManager = new MobileDeviceConfigurationManager(); + } + } + } + return mobileDeviceConfigManager; + } + + public synchronized void initConfig() throws DeviceManagementException { + try { + File mobileDeviceMgtConfig = new File(mobileDeviceMgtConfigXMLPath); + Document doc = MobileDeviceManagementUtil.convertToDocument(mobileDeviceMgtConfig); + JAXBContext mobileDeviceMgmtContext = + JAXBContext.newInstance(MobileDeviceManagementConfig.class); + Unmarshaller unmarshaller = mobileDeviceMgmtContext.createUnmarshaller(); + this.currentMobileDeviceConfig = + (MobileDeviceManagementConfig) unmarshaller.unmarshal(doc); + } catch (Exception e) { + throw new DeviceManagementException( + "Error occurred while initializing Mobile Device Management config", e); + } + } + + public MobileDeviceManagementConfig getMobileDeviceManagementConfig() { + return currentMobileDeviceConfig; + } + + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/MobileDeviceManagementConfig.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/MobileDeviceManagementConfig.java new file mode 100644 index 000000000..ced99f955 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/MobileDeviceManagementConfig.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 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. + */ + +package org.wso2.carbon.device.mgt.mobile.config; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Represents Mobile Device Mgt configuration. + */ +@XmlRootElement(name = "MobileDeviceMgtConfiguration") +public final class MobileDeviceManagementConfig { + + private MobileDeviceManagementRepository mobileDeviceMgtRepository; + + @XmlElement(name = "ManagementRepository", nillable = false) + public MobileDeviceManagementRepository getMobileDeviceMgtRepository() { + return mobileDeviceMgtRepository; + } + + public void setMobileDeviceMgtRepository( + MobileDeviceManagementRepository mobileDeviceMgtRepository) { + this.mobileDeviceMgtRepository = mobileDeviceMgtRepository; + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/MobileDeviceManagementRepository.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/MobileDeviceManagementRepository.java new file mode 100644 index 000000000..88369dd84 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/MobileDeviceManagementRepository.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 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. + */ + +package org.wso2.carbon.device.mgt.mobile.config; + +import org.wso2.carbon.device.mgt.mobile.config.datasource.DataSourceConfigAdapter; +import org.wso2.carbon.device.mgt.mobile.config.datasource.MobileDataSourceConfig; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.util.List; +import java.util.Map; + +/** + * Class for holding management repository data. + */ +@XmlRootElement(name = "ManagementRepository") +public class MobileDeviceManagementRepository { + + private Map mobileDataSourceConfigMap; + private List mobileDataSourceConfigs; + + public MobileDataSourceConfig getMobileDataSourceConfig(String provider) { + return mobileDataSourceConfigMap.get(provider); + } + + @XmlElement(name = "DataSourceConfigurations") + @XmlJavaTypeAdapter(DataSourceConfigAdapter.class) + public Map getMobileDataSourceConfigMap() { + return mobileDataSourceConfigMap; + } + + public void setMobileDataSourceConfigMap(Map mobileDataSourceConfigMap) { + this.mobileDataSourceConfigMap = mobileDataSourceConfigMap; + } + + public List getMobileDataSourceConfigs() { + return (List) mobileDataSourceConfigMap.values(); + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/datasource/DataSourceConfigAdapter.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/datasource/DataSourceConfigAdapter.java new file mode 100644 index 000000000..71675785c --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/datasource/DataSourceConfigAdapter.java @@ -0,0 +1,51 @@ +/* + * 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.device.mgt.mobile.config.datasource; + +import javax.xml.bind.annotation.adapters.XmlAdapter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DataSourceConfigAdapter + extends XmlAdapter> { + + @Override + public Map unmarshal(MobileDataSourceConfigurations mobileDataSourceConfigurations) + throws Exception { + + Map mobileDataSourceConfigMap = new HashMap(); + for (MobileDataSourceConfig mobileDataSourceConfig : mobileDataSourceConfigurations + .getMobileDataSourceConfigs()) { + mobileDataSourceConfigMap.put(mobileDataSourceConfig.getType(), mobileDataSourceConfig); + } + return mobileDataSourceConfigMap; + } + + @Override + public MobileDataSourceConfigurations marshal(Map mobileDataSourceConfigMap) + throws Exception { + + MobileDataSourceConfigurations mobileDataSourceConfigurations = new MobileDataSourceConfigurations(); + mobileDataSourceConfigurations.setMobileDataSourceConfigs( + (List) mobileDataSourceConfigMap.values()); + + return mobileDataSourceConfigurations; + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/datasource/JNDILookupDefinition.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/datasource/JNDILookupDefinition.java new file mode 100644 index 000000000..603adcedd --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/datasource/JNDILookupDefinition.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 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. + */ + +package org.wso2.carbon.device.mgt.mobile.config.datasource; + +import javax.xml.bind.annotation.*; +import java.util.List; + +/** + * Class for hold JndiLookupDefinition of mobile-config.xml at parsing with JAXB. + */ +@XmlRootElement(name = "JndiLookupDefinition") +public class JNDILookupDefinition { + + private String jndiName; + private List jndiProperties; + + @XmlElement(name = "Name", nillable = false) + public String getJndiName() { + return jndiName; + } + + public void setJndiName(String jndiName) { + this.jndiName = jndiName; + } + + @XmlElementWrapper(name = "Environment", nillable = false) + @XmlElement(name = "Property", nillable = false) + public List getJndiProperties() { + return jndiProperties; + } + + public void setJndiProperties(List jndiProperties) { + this.jndiProperties = jndiProperties; + } + + @XmlRootElement(name = "Property") + public static class JNDIProperty { + + private String name; + + private String value; + + @XmlAttribute(name = "Name") + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @XmlValue + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + +} + diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/datasource/MobileDataSourceConfig.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/datasource/MobileDataSourceConfig.java new file mode 100644 index 000000000..869aaf12e --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/datasource/MobileDataSourceConfig.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 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. + */ + +package org.wso2.carbon.device.mgt.mobile.config.datasource; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Class for holding data source configuration in mobile-config.xml at parsing with JAXB. + */ +@XmlRootElement(name = "DataSourceConfiguration") +public class MobileDataSourceConfig { + + private JNDILookupDefinition jndiLookupDefinition; + private String type; + + @XmlElement(name = "JndiLookupDefinition", nillable = true) + public JNDILookupDefinition getJndiLookupDefinition() { + return jndiLookupDefinition; + } + + public void setJndiLookupDefinition(JNDILookupDefinition jndiLookupDefinition) { + this.jndiLookupDefinition = jndiLookupDefinition; + } + + @XmlAttribute(name = "type") + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/datasource/MobileDataSourceConfigurations.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/datasource/MobileDataSourceConfigurations.java new file mode 100644 index 000000000..17e80f5bf --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/config/datasource/MobileDataSourceConfigurations.java @@ -0,0 +1,41 @@ +/* + * 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.device.mgt.mobile.config.datasource; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.List; + +@XmlRootElement(name = "DataSourceConfigurations") +@XmlAccessorType(XmlAccessType.FIELD) +public class MobileDataSourceConfigurations { + + @XmlElement(name = "DataSourceConfiguration", nillable = true) + private List mobileDataSourceConfigs; + + public List getMobileDataSourceConfigs() { + return mobileDataSourceConfigs; + } + + public void setMobileDataSourceConfigs(List mobileDataSourceConfigs) { + this.mobileDataSourceConfigs = mobileDataSourceConfigs; + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/AbstractMobileDeviceManagementDAOFactory.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/AbstractMobileDeviceManagementDAOFactory.java new file mode 100644 index 000000000..32bcb8de7 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/AbstractMobileDeviceManagementDAOFactory.java @@ -0,0 +1,102 @@ +/* + * 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.device.mgt.mobile.dao; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.mobile.common.MobileDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.mobile.config.datasource.JNDILookupDefinition; +import org.wso2.carbon.device.mgt.mobile.config.datasource.MobileDataSourceConfig; +import org.wso2.carbon.device.mgt.mobile.dao.util.MobileDeviceManagementDAOUtil; + +import javax.sql.DataSource; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +/** + * Factory class used to create MobileDeviceManagement related DAO objects. + */ +public abstract class AbstractMobileDeviceManagementDAOFactory implements MobileDeviceManagementDAOFactory { + + private static final Log log = LogFactory.getLog(AbstractMobileDeviceManagementDAOFactory.class); + private static Map dataSourceMap = new HashMap<>(); + private static boolean isInitialized; + + public static void init(Map mobileDataSourceConfigMap) + throws MobileDeviceMgtPluginException { + DataSource dataSource; + for (String pluginType : mobileDataSourceConfigMap.keySet()) { + if (dataSourceMap.get(pluginType) == null) { + dataSource = AbstractMobileDeviceManagementDAOFactory.resolveDataSource(mobileDataSourceConfigMap.get + (pluginType)); + dataSourceMap.put(pluginType, dataSource); + } + } + isInitialized = true; + } + + public static void init(String key, MobileDataSourceConfig mobileDataSourceConfig) throws + MobileDeviceMgtPluginException { + DataSource dataSource = AbstractMobileDeviceManagementDAOFactory.resolveDataSource(mobileDataSourceConfig); + dataSourceMap.put(key, dataSource); + } + + /** + * Resolve data source from the data source definition. + * + * @param config Mobile data source configuration + * @return data source resolved from the data source definition + */ + public static DataSource resolveDataSource(MobileDataSourceConfig config) { + DataSource dataSource = null; + if (config == null) { + throw new RuntimeException("Device Management Repository data source configuration " + + "is null and thus, is not initialized"); + } + JNDILookupDefinition jndiConfig = config.getJndiLookupDefinition(); + if (jndiConfig != null) { + if (log.isDebugEnabled()) { + log.debug("Initializing Device Management Repository data source using the JNDI " + + "Lookup Definition"); + } + List jndiPropertyList = + jndiConfig.getJndiProperties(); + if (jndiPropertyList != null) { + Hashtable jndiProperties = new Hashtable(); + for (JNDILookupDefinition.JNDIProperty prop : jndiPropertyList) { + jndiProperties.put(prop.getName(), prop.getValue()); + } + dataSource = + MobileDeviceManagementDAOUtil + .lookupDataSource(jndiConfig.getJndiName(), jndiProperties); + } else { + dataSource = MobileDeviceManagementDAOUtil + .lookupDataSource(jndiConfig.getJndiName(), null); + } + } + return dataSource; + } + + public static Map getDataSourceMap() { + return dataSourceMap; + } + +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/MobileDeviceDAO.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/MobileDeviceDAO.java new file mode 100644 index 000000000..161594273 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/MobileDeviceDAO.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. + */ + +package org.wso2.carbon.device.mgt.mobile.dao; + +import org.wso2.carbon.device.mgt.mobile.dto.MobileDevice; +import java.util.List; + +/** + * This class represents the key operations associated with persisting mobile-device related + * information. + */ +public interface MobileDeviceDAO { + + /** + * Fetches a MobileDevice from MDM database. + * + * @param mblDeviceId Id of the Mobile-Device. + * @return MobileDevice corresponding to given device-id. + * @throws MobileDeviceManagementDAOException + */ + MobileDevice getMobileDevice(String mblDeviceId) throws MobileDeviceManagementDAOException; + + /** + * Adds a new MobileDevice to the MDM database. + * + * @param mobileDevice MobileDevice to be added. + * @return The status of the operation. + * @throws MobileDeviceManagementDAOException + */ + boolean addMobileDevice(MobileDevice mobileDevice) throws MobileDeviceManagementDAOException; + + /** + * Updates MobileDevice information in MDM database. + * + * @param mobileDevice MobileDevice to be updated. + * @return The status of the operation. + * @throws MobileDeviceManagementDAOException + */ + boolean updateMobileDevice(MobileDevice mobileDevice) throws MobileDeviceManagementDAOException; + + /** + * Deletes a given MobileDevice from MDM database. + * + * @param mblDeviceId Id of MobileDevice to be deleted. + * @return The status of the operation. + * @throws MobileDeviceManagementDAOException + */ + boolean deleteMobileDevice(String mblDeviceId) throws MobileDeviceManagementDAOException; + + /** + * Fetches all MobileDevices from MDM database. + * + * @return List of MobileDevices. + * @throws MobileDeviceManagementDAOException + */ + List getAllMobileDevices() throws MobileDeviceManagementDAOException; + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/MobileDeviceManagementDAOException.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/MobileDeviceManagementDAOException.java new file mode 100644 index 000000000..0b04818d9 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/MobileDeviceManagementDAOException.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.device.mgt.mobile.dao; + +/** + * Custom exception class for mobile device specific data access related exceptions. + */ +public class MobileDeviceManagementDAOException extends Exception { + + private String message; + private static final long serialVersionUID = 2021891706072918865L; + + /** + * Constructs a new MobileDeviceManagementDAOException with the specified detail message and + * nested exception. + * + * @param message error message + * @param nestedException exception + */ + public MobileDeviceManagementDAOException(String message, Exception nestedException) { + super(message, nestedException); + setErrorMessage(message); + } + + /** + * Constructs a new MobileDeviceManagementDAOException with the specified detail message + * and cause. + * + * @param message the detail message. + * @param cause the cause of this exception. + */ + public MobileDeviceManagementDAOException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + /** + * Constructs a new MobileDeviceManagementDAOException with the specified detail message. + * + * @param message the detail message. + */ + public MobileDeviceManagementDAOException(String message) { + super(message); + setErrorMessage(message); + } + + /** + * Constructs a new MobileDeviceManagementDAOException with the specified and cause. + * + * @param cause the cause of this exception. + */ + public MobileDeviceManagementDAOException(Throwable cause) { + super(cause); + } + + public String getMessage() { + return message; + } + + public void setErrorMessage(String errorMessage) { + this.message = errorMessage; + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/MobileDeviceManagementDAOFactory.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/MobileDeviceManagementDAOFactory.java new file mode 100644 index 000000000..37a5c311c --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/MobileDeviceManagementDAOFactory.java @@ -0,0 +1,27 @@ +/* + * 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.device.mgt.mobile.dao; + +public interface MobileDeviceManagementDAOFactory { + + MobileDeviceDAO getMobileDeviceDAO(); + + MobileFeatureDAO getMobileFeatureDAO(); + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/MobileFeatureDAO.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/MobileFeatureDAO.java new file mode 100644 index 000000000..55c77424b --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/MobileFeatureDAO.java @@ -0,0 +1,110 @@ +/* + * 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.device.mgt.mobile.dao; + +import org.wso2.carbon.device.mgt.mobile.dto.MobileFeature; + +import java.util.List; + +/** + * This class represents the key operations associated with persisting mobile feature related + * information. + */ +public interface MobileFeatureDAO { + + /** + * Adds a new MobileFeature to Mobile-Feature table. + * + * @param mobileFeature MobileFeature object that holds data related to the feature to be inserted. + * @return boolean status of the operation. + * @throws MobileDeviceManagementDAOException + */ + boolean addFeature(MobileFeature mobileFeature) throws MobileDeviceManagementDAOException; + + /** + * Adda a list of MobileFeatures to Mobile-Feature table. + * + * @param mobileFeatures List of MobileFeature objects. + * @return boolean status of the operation. + * @throws MobileDeviceManagementDAOException + */ + boolean addFeatures(List mobileFeatures) throws MobileDeviceManagementDAOException; + + /** + * Updates a MobileFeature in Mobile-Feature table. + * + * @param mobileFeature MobileFeature object that holds data has to be updated. + * @return The status of the operation. + * @throws MobileDeviceManagementDAOException + */ + boolean updateFeature(MobileFeature mobileFeature) throws MobileDeviceManagementDAOException; + + /** + * Deletes a MobileFeature from Mobile-Feature table when the feature id is given. + * + * @param mblFeatureId MobileFeature id of the MobileFeature to be deleted. + * @return The status of the operation. + * @throws MobileDeviceManagementDAOException + */ + boolean deleteFeatureById(int mblFeatureId) throws MobileDeviceManagementDAOException; + + /** + * Deletes a MobileFeature from Mobile-Feature table when the feature code is given. + * + * @param mblFeatureCode MobileFeature code of the feature to be deleted. + * @return The status of the operation. + * @throws MobileDeviceManagementDAOException + */ + boolean deleteFeatureByCode(String mblFeatureCode) throws MobileDeviceManagementDAOException; + + /** + * Retrieves a given MobileFeature from Mobile-Feature table when the feature id is given. + * + * @param mblFeatureId Feature id of the feature to be retrieved. + * @return MobileFeature object that holds data of the feature represented by featureId. + * @throws MobileDeviceManagementDAOException + */ + MobileFeature getFeatureById(int mblFeatureId) throws MobileDeviceManagementDAOException; + + /** + * Retrieves a given MobileFeature from Mobile-Feature table when the feature code is given. + * + * @param mblFeatureCode Feature code of the feature to be retrieved. + * @return MobileFeature object that holds data of the feature represented by featureCode. + * @throws MobileDeviceManagementDAOException + */ + MobileFeature getFeatureByCode(String mblFeatureCode) throws MobileDeviceManagementDAOException; + + /** + * Retrieves all MobileFeatures of a MobileDevice type from Mobile-Feature table. + * + * @param deviceType MobileDevice type of the MobileFeatures to be retrieved + * @return MobileFeature object list. + * @throws MobileDeviceManagementDAOException + */ + List getFeatureByDeviceType(String deviceType) throws MobileDeviceManagementDAOException; + + /** + * Retrieve all the MobileFeatures from Mobile-Feature table. + * + * @return MobileFeature object list. + * @throws MobileDeviceManagementDAOException + */ + List getAllFeatures() throws MobileDeviceManagementDAOException; +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/impl/MobileFeatureDAOImpl.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/impl/MobileFeatureDAOImpl.java new file mode 100644 index 000000000..86a940fd6 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/impl/MobileFeatureDAOImpl.java @@ -0,0 +1,336 @@ +/* + * Copyright (c) 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. + */ + +package org.wso2.carbon.device.mgt.mobile.dao.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceManagementDAOException; +import org.wso2.carbon.device.mgt.mobile.dao.MobileFeatureDAO; +import org.wso2.carbon.device.mgt.mobile.dao.util.MobileDeviceManagementDAOUtil; +import org.wso2.carbon.device.mgt.mobile.dto.MobileFeature; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * Implementation of MobileFeatureDAO. + */ +public class MobileFeatureDAOImpl implements MobileFeatureDAO { + + private DataSource dataSource; + private static final Log log = LogFactory.getLog(MobileFeatureDAOImpl.class); + + public MobileFeatureDAOImpl(DataSource dataSource) { + this.dataSource = dataSource; + } + + @Override + public boolean addFeature(MobileFeature mobileFeature) + throws MobileDeviceManagementDAOException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = this.getConnection(); + String createDBQuery = + "INSERT INTO AD_FEATURE(CODE, NAME, DESCRIPTION, DEVICE_TYPE) VALUES (?, ?, ?, ?)"; + + stmt = conn.prepareStatement(createDBQuery); + stmt.setString(1, mobileFeature.getCode()); + stmt.setString(2, mobileFeature.getName()); + stmt.setString(3, mobileFeature.getDescription()); + stmt.setString(4, mobileFeature.getDeviceType()); + int rows = stmt.executeUpdate(); + if (rows > 0) { + if (log.isDebugEnabled()) { + log.debug("Added a new MobileFeature " + mobileFeature.getCode() + " to the MDM database."); + } + status = true; + } + } catch (SQLException e) { + String msg = "Error occurred while adding feature code - '" + + mobileFeature.getCode() + "' to feature table"; + log.error(msg, e); + throw new MobileDeviceManagementDAOException(msg, e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(conn, stmt, null); + } + return status; + } + + @Override + public boolean addFeatures(List mobileFeatures) throws MobileDeviceManagementDAOException { + return false; + } + + @Override + public boolean updateFeature(MobileFeature mobileFeature) + throws MobileDeviceManagementDAOException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = this.getConnection(); + String updateDBQuery = + "UPDATE AD_FEATURE SET CODE = ?, NAME = ?, DESCRIPTION = ?, DEVICE_TYPE = ?" + + " WHERE ID = ?"; + stmt = conn.prepareStatement(updateDBQuery); + stmt.setString(1, mobileFeature.getCode()); + stmt.setString(2, mobileFeature.getName()); + stmt.setString(3, mobileFeature.getDescription()); + stmt.setString(4, mobileFeature.getDeviceType()); + stmt.setInt(5, mobileFeature.getId()); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Updated MobileFeature " + mobileFeature.getCode()); + } + } + } catch (SQLException e) { + String msg = "Error occurred while updating the feature with feature code - '" + + mobileFeature.getId() + "'"; + log.error(msg, e); + throw new MobileDeviceManagementDAOException(msg, e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(conn, stmt, null); + } + return status; + } + + @Override + public boolean deleteFeatureByCode(String mblFeatureCode) + throws MobileDeviceManagementDAOException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = this.getConnection(); + String deleteDBQuery = + "DELETE FROM AD_FEATURE WHERE CODE = ?"; + stmt = conn.prepareStatement(deleteDBQuery); + stmt.setString(1, mblFeatureCode); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Deleted MobileFeature code " + mblFeatureCode + " from the MDM database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while deleting feature with code - " + mblFeatureCode; + log.error(msg, e); + throw new MobileDeviceManagementDAOException(msg, e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(conn, stmt, null); + } + return status; + } + + @Override + public boolean deleteFeatureById(int mblFeatureId) + throws MobileDeviceManagementDAOException { + boolean status = false; + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = this.getConnection(); + String deleteDBQuery = + "DELETE FROM AD_FEATURE WHERE ID = ?"; + stmt = conn.prepareStatement(deleteDBQuery); + stmt.setInt(1, mblFeatureId); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Deleted MobileFeature id " + mblFeatureId + " from the MDM database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while deleting feature with id - " + mblFeatureId; + log.error(msg, e); + throw new MobileDeviceManagementDAOException(msg, e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(conn, stmt, null); + } + return status; + } + + @Override + public MobileFeature getFeatureByCode(String mblFeatureCode) + throws MobileDeviceManagementDAOException { + Connection conn = null; + PreparedStatement stmt = null; + MobileFeature mobileFeature = null; + ResultSet resultSet = null; + try { + conn = this.getConnection(); + String selectDBQuery = + "SELECT ID, CODE, NAME, DESCRIPTION, DEVICE_TYPE FROM AD_FEATURE " + + "WHERE CODE = ?"; + stmt = conn.prepareStatement(selectDBQuery); + stmt.setString(1, mblFeatureCode); + resultSet = stmt.executeQuery(); + if (resultSet.next()) { + mobileFeature = new MobileFeature(); + mobileFeature.setId(resultSet.getInt(1)); + mobileFeature.setCode(resultSet.getString(2)); + mobileFeature.setName(resultSet.getString(3)); + mobileFeature.setDescription(resultSet.getString(4)); + mobileFeature.setDeviceType(resultSet.getString(5)); + if (log.isDebugEnabled()) { + log.debug("Fetched MobileFeature " + mblFeatureCode + " from the MDM database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while fetching feature code - '" + mblFeatureCode + "'"; + log.error(msg, e); + throw new MobileDeviceManagementDAOException(msg, e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(conn, stmt, resultSet); + } + return mobileFeature; + } + + @Override + public MobileFeature getFeatureById(int mblFeatureId) + throws MobileDeviceManagementDAOException { + Connection conn = null; + PreparedStatement stmt = null; + MobileFeature mobileFeature = null; + ResultSet resultSet = null; + try { + conn = this.getConnection(); + String selectDBQuery = + "SELECT ID, CODE, NAME, DESCRIPTION, DEVICE_TYPE FROM AD_FEATURE" + + " WHERE ID = ?"; + stmt = conn.prepareStatement(selectDBQuery); + stmt.setInt(1, mblFeatureId); + resultSet = stmt.executeQuery(); + if (resultSet.next()) { + mobileFeature = new MobileFeature(); + mobileFeature.setId(resultSet.getInt(1)); + mobileFeature.setCode(resultSet.getString(2)); + mobileFeature.setName(resultSet.getString(3)); + mobileFeature.setDescription(resultSet.getString(4)); + mobileFeature.setDeviceType(resultSet.getString(5)); + if (log.isDebugEnabled()) { + log.debug("Fetched MobileFeatureId" + mblFeatureId + " from the MDM database."); + } + } + } catch (SQLException e) { + String msg = "Error occurred while fetching feature id - '" + mblFeatureId + "'"; + log.error(msg, e); + throw new MobileDeviceManagementDAOException(msg, e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(conn, stmt, resultSet); + } + return mobileFeature; + } + + @Override + public List getAllFeatures() throws MobileDeviceManagementDAOException { + Connection conn = null; + PreparedStatement stmt = null; + MobileFeature mobileFeature; + List mobileFeatures = new ArrayList(); + ResultSet resultSet = null; + try { + conn = this.getConnection(); + String selectDBQuery = + "SELECT ID, CODE, NAME, DESCRIPTION, DEVICE_TYPE FROM AD_FEATURE"; + stmt = conn.prepareStatement(selectDBQuery); + resultSet = stmt.executeQuery(); + while (resultSet.next()) { + mobileFeature = new MobileFeature(); + mobileFeature.setId(resultSet.getInt(1)); + mobileFeature.setCode(resultSet.getString(2)); + mobileFeature.setName(resultSet.getString(3)); + mobileFeature.setDescription(resultSet.getString(4)); + mobileFeature.setDeviceType(resultSet.getString(5)); + mobileFeatures.add(mobileFeature); + } + if (log.isDebugEnabled()) { + log.debug("Fetched all MobileFeatures from the MDM database."); + } + return mobileFeatures; + } catch (SQLException e) { + String msg = "Error occurred while fetching all features.'"; + log.error(msg, e); + throw new MobileDeviceManagementDAOException(msg, e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(conn, stmt, resultSet); + } + } + + @Override + public List getFeatureByDeviceType(String deviceType) throws MobileDeviceManagementDAOException { + Connection conn = null; + PreparedStatement stmt = null; + MobileFeature mobileFeature; + List mobileFeatures = new ArrayList<>(); + ResultSet resultSet = null; + try { + conn = this.getConnection(); + String selectDBQuery = + "SELECT ID, CODE, NAME, DESCRIPTION, DEVICE_TYPE FROM AD_FEATURE" + + " WHERE DEVICE_TYPE = ?"; + stmt = conn.prepareStatement(selectDBQuery); + stmt.setString(1, deviceType); + resultSet = stmt.executeQuery(); + while (resultSet.next()) { + mobileFeature = new MobileFeature(); + mobileFeature.setId(resultSet.getInt(1)); + mobileFeature.setCode(resultSet.getString(2)); + mobileFeature.setName(resultSet.getString(3)); + mobileFeature.setDescription(resultSet.getString(4)); + mobileFeature.setDeviceType(resultSet.getString(5)); + mobileFeatures.add(mobileFeature); + } + if (log.isDebugEnabled()) { + log.debug("Fetched all MobileFeatures of type " + deviceType + " from the MDM" + + " database."); + } + return mobileFeatures; + } catch (SQLException e) { + String msg = "Error occurred while fetching all features.'"; + log.error(msg, e); + throw new MobileDeviceManagementDAOException(msg, e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(conn, stmt, resultSet); + } + } + + private Connection getConnection() throws MobileDeviceManagementDAOException { + try { + return dataSource.getConnection(); + } catch (SQLException e) { + String msg = "Error occurred while obtaining a connection from the mobile specific " + + "datasource."; + log.error(msg, e); + throw new MobileDeviceManagementDAOException(msg, e); + } + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/util/MobileDeviceManagementDAOUtil.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/util/MobileDeviceManagementDAOUtil.java new file mode 100644 index 000000000..0f72e82e9 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dao/util/MobileDeviceManagementDAOUtil.java @@ -0,0 +1,139 @@ +/* + * 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.device.mgt.mobile.dao.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.mobile.common.MobileDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.mobile.config.datasource.JNDILookupDefinition; +import org.wso2.carbon.device.mgt.mobile.config.datasource.MobileDataSourceConfig; +import org.wso2.carbon.device.mgt.mobile.util.MobileDeviceManagementSchemaInitializer; + +import javax.naming.InitialContext; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Hashtable; +import java.util.List; + +/** + * Utility method required by MobileDeviceManagement DAO classes. + */ +public class MobileDeviceManagementDAOUtil { + + private static final Log log = LogFactory.getLog(MobileDeviceManagementDAOUtil.class); + + public static DataSource lookupDataSource(String dataSourceName, + final Hashtable jndiProperties){ + + try { + if (jndiProperties == null || jndiProperties.isEmpty()) { + return (DataSource) InitialContext.doLookup(dataSourceName); + } + final InitialContext context = new InitialContext(jndiProperties); + return (DataSource) context.lookup(dataSourceName); + } catch (Exception e) { + String msg = "Error in looking up data source: " + e.getMessage(); + log.error(msg, e); + throw new RuntimeException(msg + e.getMessage(), e); + } + } + + public static void cleanupResources(Connection conn, PreparedStatement stmt, ResultSet rs) { + if (rs != null) { + try { + rs.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing result set", e); + } + } + if (stmt != null) { + try { + stmt.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing prepared statement", e); + } + } + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing database connection", e); + } + } + } + + public static void cleanupResources(PreparedStatement stmt, ResultSet rs) { + cleanupResources(null, stmt, rs); + } + + /** + * Creates the mobile device management schema. + * + * @param dataSource Mobile data source + */ + public static void setupMobileDeviceManagementSchema(DataSource dataSource, String pluginType) throws MobileDeviceMgtPluginException { + MobileDeviceManagementSchemaInitializer initializer = + new MobileDeviceManagementSchemaInitializer(dataSource, pluginType); + log.info("Initializing mobile device management repository database schema for : " + pluginType); + try { + initializer.createRegistryDatabase(); + } catch (Exception e) { + throw new MobileDeviceMgtPluginException("Error occurred while initializing Mobile Device " + + "Management database schema", e); + } + } + + + /** + * Resolve data source from the data source definition + * + * @param config data source configuration + * @return data source resolved from the data source definition + */ + private static DataSource resolveDataSource(MobileDataSourceConfig config) { + DataSource dataSource = null; + if (config == null) { + throw new RuntimeException( + "data source configuration " + "is null and " + + "thus, is not initialized"); + } + JNDILookupDefinition jndiConfig = config.getJndiLookupDefinition(); + if (jndiConfig != null) { + if (log.isDebugEnabled()) { + log.debug("Initializing data source using the JNDI " + + "Lookup Definition"); + } + List jndiPropertyList = + jndiConfig.getJndiProperties(); + if (jndiPropertyList != null) { + Hashtable jndiProperties = new Hashtable(); + for (JNDILookupDefinition.JNDIProperty prop : jndiPropertyList) { + jndiProperties.put(prop.getName(), prop.getValue()); + } + dataSource = MobileDeviceManagementDAOUtil.lookupDataSource(jndiConfig.getJndiName(), jndiProperties); + } else { + dataSource = MobileDeviceManagementDAOUtil.lookupDataSource(jndiConfig.getJndiName(), null); + } + } + return dataSource; + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileDevice.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileDevice.java new file mode 100644 index 000000000..185168da4 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileDevice.java @@ -0,0 +1,125 @@ +/* + * 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.device.mgt.mobile.dto; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * DTO of MobileDevice. + */ +public class MobileDevice implements Serializable { + + private String mobileDeviceId; + private String osVersion; + private String model; + private String vendor; + private String latitude; + private String longitude; + private String imei; + private String imsi; + private String serial; + private Map deviceProperties; + + public MobileDevice() { + this.deviceProperties = new HashMap<>(); + } + + public String getMobileDeviceId() { + return mobileDeviceId; + } + + public void setMobileDeviceId(String mobileDeviceId) { + this.mobileDeviceId = mobileDeviceId; + } + + public String getOsVersion() { + return osVersion; + } + + public void setOsVersion(String osVersion) { + this.osVersion = osVersion; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public String getVendor() { + return vendor; + } + + public void setVendor(String vendor) { + this.vendor = vendor; + } + + public String getLatitude() { + return latitude; + } + + public void setLatitude(String latitude) { + this.latitude = latitude; + } + + public String getLongitude() { + return longitude; + } + + public void setLongitude(String longitude) { + this.longitude = longitude; + } + + public String getImei() { + return imei; + } + + public void setImei(String imei) { + this.imei = imei; + } + + public String getImsi() { + return imsi; + } + + public void setImsi(String imsi) { + this.imsi = imsi; + } + + public String getSerial() { + return serial; + } + + public void setSerial(String serial) { + this.serial = serial; + } + + public Map getDeviceProperties() { + return deviceProperties; + } + + public void setDeviceProperties(Map deviceProperties) { + this.deviceProperties = deviceProperties; + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileDeviceOperationMapping.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileDeviceOperationMapping.java new file mode 100644 index 000000000..0ae7e68fa --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileDeviceOperationMapping.java @@ -0,0 +1,86 @@ +/* + * 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.device.mgt.mobile.dto; + +/** + * DTO of Mobile Device Operation Mappings. + */ +public class MobileDeviceOperationMapping { + + private String deviceId; + private int operationId; + private long sentDate; + private long receivedDate; + private Status status; + + public enum Status { + NEW, INPROGRESS, COMPLETED + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public void setStatus(String status) { + if(Status.NEW.name().equals(status)){ + this.status = Status.NEW; + }else if(Status.INPROGRESS.name().equals(status)){ + this.status = Status.INPROGRESS; + }else if(Status.COMPLETED.name().equals(status)){ + this.status = Status.COMPLETED; + } + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public int getOperationId() { + return operationId; + } + + public void setOperationId(int operationId) { + this.operationId = operationId; + } + + public long getSentDate() { + return sentDate; + } + + public void setSentDate(long sentDate) { + this.sentDate = sentDate; + } + + public long getReceivedDate() { + return receivedDate; + } + + public void setReceivedDate(long receivedDate) { + this.receivedDate = receivedDate; + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileFeature.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileFeature.java new file mode 100644 index 000000000..9ec5d30b3 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileFeature.java @@ -0,0 +1,73 @@ +/* + * 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.device.mgt.mobile.dto; + +import java.io.Serializable; + +/** + * DTO of Mobile features. + */ +public class MobileFeature implements Serializable { + + private int id; + private String deviceType; + private String code; + private String name; + private String description; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getDeviceType() { + return deviceType; + } + + public void setDeviceType(String deviceType) { + this.deviceType = deviceType; + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileFeatureProperty.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileFeatureProperty.java new file mode 100644 index 000000000..cc99849de --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileFeatureProperty.java @@ -0,0 +1,45 @@ +/* + * 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.device.mgt.mobile.dto; + +/** + * DTO of Mobile feature property. Represents a property of a mobile feature. + */ +public class MobileFeatureProperty { + + private String property; + private Integer featureID; + + public Integer getFeatureID() { + return featureID; + } + + public void setFeatureID(Integer featureID) { + this.featureID = featureID; + } + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileOperation.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileOperation.java new file mode 100644 index 000000000..7c0b9040c --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileOperation.java @@ -0,0 +1,64 @@ +/* + * 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.device.mgt.mobile.dto; + +import java.util.List; + +/** + * DTO of MobileOperation. + */ +public class MobileOperation { + + private int operationId; + private String featureCode; + private long createdDate; + private List properties; + + public int getOperationId() { + return operationId; + } + + public void setOperationId(int operationId) { + this.operationId = operationId; + } + + public List getProperties() { + return properties; + } + + public void setProperties(List properties) { + this.properties = properties; + } + + public String getFeatureCode() { + return featureCode; + } + + public void setFeatureCode(String featureCode) { + this.featureCode = featureCode; + } + + public long getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(long createdDate) { + this.createdDate = createdDate; + } +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileOperationProperty.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileOperationProperty.java new file mode 100644 index 000000000..d18126aff --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/dto/MobileOperationProperty.java @@ -0,0 +1,54 @@ +/* + * 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.device.mgt.mobile.dto; + +/** + * DTO of Mobile Operation property. + */ +public class MobileOperationProperty { + + private int operationId; + private String property; + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public int getOperationId() { + return operationId; + } + + public void setOperationId(int operationId) { + this.operationId = operationId; + } + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + +} \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/internal/MobileDeviceManagementDataHolder.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/internal/MobileDeviceManagementDataHolder.java new file mode 100644 index 000000000..d8943def1 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/internal/MobileDeviceManagementDataHolder.java @@ -0,0 +1,48 @@ +/* + * 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.device.mgt.mobile.internal; + +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; +import org.wso2.carbon.registry.core.service.RegistryService; + +/** + * DataHolder class of Mobile plugins component. + */ +public class MobileDeviceManagementDataHolder { + + private RegistryService registryService; + + private static MobileDeviceManagementDataHolder thisInstance = new MobileDeviceManagementDataHolder(); + + private MobileDeviceManagementDataHolder() { + } + + public static MobileDeviceManagementDataHolder getInstance() { + return thisInstance; + } + + public RegistryService getRegistryService() { + return registryService; + } + + public void setRegistryService(RegistryService registryService) { + this.registryService = registryService; + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/internal/MobileDeviceManagementServiceComponent.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/internal/MobileDeviceManagementServiceComponent.java new file mode 100644 index 000000000..e08771b3b --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/internal/MobileDeviceManagementServiceComponent.java @@ -0,0 +1,137 @@ +/* + * 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.device.mgt.mobile.internal; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.ComponentContext; +import org.wso2.carbon.device.mgt.mobile.common.MobileDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.mobile.config.MobileDeviceConfigurationManager; +import org.wso2.carbon.device.mgt.mobile.config.MobileDeviceManagementConfig; +import org.wso2.carbon.device.mgt.mobile.config.datasource.MobileDataSourceConfig; +import org.wso2.carbon.device.mgt.mobile.dao.AbstractMobileDeviceManagementDAOFactory; +import org.wso2.carbon.device.mgt.mobile.dao.util.MobileDeviceManagementDAOUtil; +import org.wso2.carbon.ndatasource.core.DataSourceService; +import org.wso2.carbon.registry.core.service.RegistryService; + +import java.util.Map; + +/** + * @scr.component name="org.wso2.carbon.device.mgt.mobile.impl.internal.MobileDeviceManagementServiceComponent" + * immediate="true" + * @scr.reference name="org.wso2.carbon.ndatasource" + * interface="org.wso2.carbon.ndatasource.core.DataSourceService" + * cardinality="1..1" + * policy="dynamic" + * bind="setDataSourceService" + * unbind="unsetDataSourceService" + * @scr.reference name="registry.service" + * interface="org.wso2.carbon.registry.core.service.RegistryService" cardinality="0..1" + * policy="dynamic" bind="setRegistryService" unbind="unsetRegistryService" + *

      + * Adding reference to API Manager Configuration service is an unavoidable hack to get rid of NPEs thrown while + * initializing APIMgtDAOs attempting to register APIs programmatically. APIMgtDAO needs to be proper cleaned up + * to avoid as an ideal fix + */ +public class MobileDeviceManagementServiceComponent { + + private static final Log log = LogFactory.getLog(MobileDeviceManagementServiceComponent.class); + + protected void activate(ComponentContext ctx) { + + if (log.isDebugEnabled()) { + log.debug("Activating Mobile Device Management Service Component"); + } + try { + BundleContext bundleContext = ctx.getBundleContext(); + + /* Initialize the data source configuration */ + MobileDeviceConfigurationManager.getInstance().initConfig(); + MobileDeviceManagementConfig config = MobileDeviceConfigurationManager.getInstance() + .getMobileDeviceManagementConfig(); + Map dsConfigMap = + config.getMobileDeviceMgtRepository().getMobileDataSourceConfigMap(); + + AbstractMobileDeviceManagementDAOFactory.init(dsConfigMap); + + String setupOption = System.getProperty("setup"); + if (setupOption != null) { + if (log.isDebugEnabled()) { + log.debug( + "-Dsetup is enabled. Mobile Device management repository schema initialization is about " + + "to begin"); + } + try { + for (String pluginType : dsConfigMap.keySet()) { + MobileDeviceManagementDAOUtil + .setupMobileDeviceManagementSchema(AbstractMobileDeviceManagementDAOFactory.getDataSourceMap + ().get(pluginType), pluginType); + } + } catch (MobileDeviceMgtPluginException e) { + log.error("Exception occurred while initializing mobile device management database schema", e); + } + } + if (log.isDebugEnabled()) { + log.debug("Mobile Device Management Service Component has been successfully activated"); + } + } catch (Throwable e) { + log.error("Error occurred while activating Mobile Device Management Service Component", e); + } + } + + protected void deactivate(ComponentContext ctx) { + if (log.isDebugEnabled()) { + log.debug("De-activating Mobile Device Management Service Component"); + } + try { + if (log.isDebugEnabled()) { + log.debug( + "Mobile Device Management Service Component has been successfully de-activated"); + } + } catch (Throwable e) { + log.error("Error occurred while de-activating Mobile Device Management bundle", e); + } + } + + protected void setDataSourceService(DataSourceService dataSourceService) { + /* This is to avoid mobile device management component getting initialized before the underlying datasources + are registered */ + if (log.isDebugEnabled()) { + log.debug("Data source service set to mobile service component"); + } + } + + protected void unsetDataSourceService(DataSourceService dataSourceService) { + //do nothing + } + + protected void setRegistryService(RegistryService registryService) { + if (log.isDebugEnabled()) { + log.debug("RegistryService acquired"); + } + MobileDeviceManagementDataHolder.getInstance().setRegistryService(registryService); + } + + protected void unsetRegistryService(RegistryService registryService) { + MobileDeviceManagementDataHolder.getInstance().setRegistryService(null); + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/util/MobileDeviceManagementSchemaInitializer.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/util/MobileDeviceManagementSchemaInitializer.java new file mode 100644 index 000000000..f0071a85f --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/util/MobileDeviceManagementSchemaInitializer.java @@ -0,0 +1,62 @@ +/* + * 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.device.mgt.mobile.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.mobile.common.MobilePluginConstants; +import org.wso2.carbon.utils.CarbonUtils; +import org.wso2.carbon.utils.dbcreator.DatabaseCreator; + +import javax.sql.DataSource; +import java.io.File; + +/** + * + * Provides methods for initializing the database script. + * + */ +public final class MobileDeviceManagementSchemaInitializer extends DatabaseCreator { + + private static final Log log = LogFactory.getLog(MobileDeviceManagementSchemaInitializer.class); + private static final String setupSQLScriptBaseLocation = + CarbonUtils.getCarbonHome() + File.separator + "dbscripts" + File.separator + + MobilePluginConstants.MOBILE_DB_SCRIPTS_FOLDER + + File.separator + "plugins" + File.separator; + private String pluginType; + + public String getPluginType() { + return pluginType; + } + + public MobileDeviceManagementSchemaInitializer(DataSource dataSource, String pType) { + super(dataSource); + this.pluginType = pType; + } + + protected String getDbScriptLocation(String databaseType) { + String scriptName = databaseType + ".sql"; + String scriptLocation = setupSQLScriptBaseLocation + this.getPluginType() + File.separator + scriptName; + if (log.isDebugEnabled()) { + log.debug("Loading database script from :" + scriptLocation); + } + return scriptLocation; + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/util/MobileDeviceManagementUtil.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/util/MobileDeviceManagementUtil.java new file mode 100644 index 000000000..73612a365 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/main/java/org/wso2/carbon/device/mgt/mobile/util/MobileDeviceManagementUtil.java @@ -0,0 +1,320 @@ +/* + * 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.device.mgt.mobile.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.w3c.dom.Document; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.common.DeviceManagementConstants; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.Feature; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.mobile.common.MobileDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.mobile.common.MobilePluginConstants; +import org.wso2.carbon.device.mgt.mobile.dto.*; +import org.wso2.carbon.device.mgt.mobile.internal.MobileDeviceManagementDataHolder; +import org.wso2.carbon.registry.api.RegistryException; +import org.wso2.carbon.registry.api.Resource; +import org.wso2.carbon.registry.core.Registry; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.File; +import java.util.*; + +/** + * Provides utility methods required by the mobile device management bundle. + */ +public class MobileDeviceManagementUtil { + + private static final Log log = LogFactory.getLog(MobileDeviceManagementUtil.class); + private static final String MOBILE_DEVICE_IMEI = "IMEI"; + private static final String MOBILE_DEVICE_IMSI = "IMSI"; + private static final String MOBILE_DEVICE_VENDOR = "VENDOR"; + private static final String MOBILE_DEVICE_OS_VERSION = "OS_VERSION"; + private static final String MOBILE_DEVICE_MODEL = "DEVICE_MODEL"; + private static final String MOBILE_DEVICE_LATITUDE = "LATITUDE"; + private static final String MOBILE_DEVICE_LONGITUDE = "LONGITUDE"; + private static final String MOBILE_DEVICE_SERIAL = "SERIAL"; + + public static Document convertToDocument(File file) throws DeviceManagementException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + try { + DocumentBuilder docBuilder = factory.newDocumentBuilder(); + return docBuilder.parse(file); + } catch (Exception e) { + throw new DeviceManagementException( + "Error occurred while parsing file, while converting " + + "to a org.w3c.dom.Document : " + e.getMessage(), e); + } + } + + private static String getPropertyValue(Device device, String property) { + for (Device.Property prop : device.getProperties()) { + if (property.equals(prop.getName())) { + return prop.getValue(); + } + } + return null; + } + + private static Device.Property getProperty(String property, String value) { + if (property != null) { + Device.Property prop = new Device.Property(); + prop.setName(property); + prop.setValue(value); + return prop; + } + return null; + } + + public static MobileDevice convertToMobileDevice(Device device) { + MobileDevice mobileDevice = null; + if (device != null) { + mobileDevice = new MobileDevice(); + mobileDevice.setMobileDeviceId(device.getDeviceIdentifier()); + mobileDevice.setImei(getPropertyValue(device, MOBILE_DEVICE_IMEI)); + mobileDevice.setImsi(getPropertyValue(device, MOBILE_DEVICE_IMSI)); + mobileDevice.setModel(getPropertyValue(device, MOBILE_DEVICE_MODEL)); + mobileDevice.setOsVersion(getPropertyValue(device, MOBILE_DEVICE_OS_VERSION)); + mobileDevice.setVendor(getPropertyValue(device, MOBILE_DEVICE_VENDOR)); + mobileDevice.setLatitude(getPropertyValue(device, MOBILE_DEVICE_LATITUDE)); + mobileDevice.setLongitude(getPropertyValue(device, MOBILE_DEVICE_LONGITUDE)); + + if (device.getProperties() != null) { + Map deviceProperties = new HashMap(); + for (Device.Property deviceProperty : device.getProperties()) { + deviceProperties.put(deviceProperty.getName(), deviceProperty.getValue()); + } + + mobileDevice.setDeviceProperties(deviceProperties); + } else { + mobileDevice.setDeviceProperties(new HashMap()); + } + } + return mobileDevice; + } + + public static Device convertToDevice(MobileDevice mobileDevice) { + Device device = null; + if (mobileDevice != null) { + device = new Device(); + List propertyList = new ArrayList(); + propertyList.add(getProperty(MOBILE_DEVICE_IMEI, mobileDevice.getImei())); + propertyList.add(getProperty(MOBILE_DEVICE_IMSI, mobileDevice.getImsi())); + propertyList.add(getProperty(MOBILE_DEVICE_MODEL, mobileDevice.getModel())); + propertyList.add(getProperty(MOBILE_DEVICE_OS_VERSION, mobileDevice.getOsVersion())); + propertyList.add(getProperty(MOBILE_DEVICE_VENDOR, mobileDevice.getVendor())); + if(mobileDevice.getLatitude() != null) { + propertyList.add(getProperty(MOBILE_DEVICE_LATITUDE, mobileDevice.getLatitude())); + } + if(mobileDevice.getLongitude() != null) { + propertyList.add(getProperty(MOBILE_DEVICE_LONGITUDE, mobileDevice.getLongitude())); + } + propertyList.add(getProperty(MOBILE_DEVICE_SERIAL, mobileDevice.getSerial())); + + if (mobileDevice.getDeviceProperties() != null) { + for (Map.Entry deviceProperty : mobileDevice.getDeviceProperties() + .entrySet()) { + propertyList + .add(getProperty(deviceProperty.getKey(), deviceProperty.getValue())); + } + } + + device.setProperties(propertyList); + device.setDeviceIdentifier(mobileDevice.getMobileDeviceId()); + } + return device; + } + + public static MobileOperation convertToMobileOperation(Operation operation) { + MobileOperation mobileOperation = new MobileOperation(); + MobileOperationProperty operationProperty; + List properties = new LinkedList(); + mobileOperation.setFeatureCode(operation.getCode()); + mobileOperation.setCreatedDate(new Date().getTime()); + Properties operationProperties = operation.getProperties(); + for (String key : operationProperties.stringPropertyNames()) { + operationProperty = new MobileOperationProperty(); + operationProperty.setProperty(key); + operationProperty.setValue(operationProperties.getProperty(key)); + properties.add(operationProperty); + } + mobileOperation.setProperties(properties); + return mobileOperation; + } + + public static List getMobileOperationIdsFromMobileDeviceOperations( + List mobileDeviceOperationMappings) { + List mobileOperationIds = new ArrayList(mobileDeviceOperationMappings.size()); + for (MobileDeviceOperationMapping mobileDeviceOperationMapping : mobileDeviceOperationMappings) { + mobileOperationIds.add(mobileDeviceOperationMapping.getOperationId()); + } + return mobileOperationIds; + } + + public static Operation convertMobileOperationToOperation(MobileOperation mobileOperation) { + Operation operation = new Operation(); + Properties properties = new Properties(); + operation.setCode(mobileOperation.getFeatureCode()); + for (MobileOperationProperty mobileOperationProperty : mobileOperation.getProperties()) { + properties + .put(mobileOperationProperty.getProperty(), mobileOperationProperty.getValue()); + } + operation.setProperties(properties); + return operation; + } + + public static MobileFeature convertToMobileFeature(Feature feature) { + MobileFeature mobileFeature = new MobileFeature(); + mobileFeature.setName(feature.getName()); + mobileFeature.setCode(feature.getCode()); + mobileFeature.setDescription(feature.getDescription()); + mobileFeature.setDeviceType(feature.getDeviceType()); + return mobileFeature; + } + + public static Feature convertToFeature(MobileFeature mobileFeature) { + Feature feature = new Feature(); + feature.setDescription(mobileFeature.getDescription()); + feature.setDeviceType(mobileFeature.getDeviceType()); + feature.setCode(mobileFeature.getCode()); + feature.setName(mobileFeature.getName()); + return feature; + } + + public static Registry getConfigurationRegistry() throws MobileDeviceMgtPluginException { + try { + int tenantId = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantId(); + return MobileDeviceManagementDataHolder.getInstance().getRegistryService() + .getConfigSystemRegistry( + tenantId); + } catch (RegistryException e) { + throw new MobileDeviceMgtPluginException( + "Error in retrieving conf registry instance: " + + e.getMessage(), e); + } + } + + public static Resource getRegistryResource(String path) throws MobileDeviceMgtPluginException { + try { + if(MobileDeviceManagementUtil.getConfigurationRegistry().resourceExists(path)){ + return MobileDeviceManagementUtil.getConfigurationRegistry().get(path); + } + return null; + } catch (RegistryException e) { + throw new MobileDeviceMgtPluginException("Error in retrieving registry resource : " + + e.getMessage(), e); + } + } + + public static boolean putRegistryResource(String path, + Resource resource) + throws MobileDeviceMgtPluginException { + boolean status; + try { + MobileDeviceManagementUtil.getConfigurationRegistry().beginTransaction(); + MobileDeviceManagementUtil.getConfigurationRegistry().put(path, resource); + MobileDeviceManagementUtil.getConfigurationRegistry().commitTransaction(); + status = true; + } catch (RegistryException e) { + throw new MobileDeviceMgtPluginException( + "Error occurred while persisting registry resource : " + + e.getMessage(), e); + } + return status; + } + + public static String getResourcePath(String resourceName, String platform) { + String regPath = ""; + switch (platform) { + case DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID: + regPath = MobilePluginConstants.MOBILE_CONFIG_REGISTRY_ROOT + "/" + + DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID + + "/" + resourceName; + break; + case DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS: + regPath = MobilePluginConstants.MOBILE_CONFIG_REGISTRY_ROOT + "/" + + DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS + + "/" + resourceName; + break; + case DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_IOS: + regPath = MobilePluginConstants.MOBILE_CONFIG_REGISTRY_ROOT + "/" + + DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_IOS + + "/" + resourceName; + break; + } + return regPath; + } + + public static String getPlatformConfigPath(String platform) { + String regPath = ""; + switch (platform) { + case DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID: + regPath = DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_ANDROID; + break; + case DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS: + regPath = DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS; + break; + case DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_IOS: + regPath = DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_IOS; + break; + } + return regPath; + } + + public static boolean createRegistryCollection(String path) + throws MobileDeviceMgtPluginException { + try { + if (! MobileDeviceManagementUtil.getConfigurationRegistry().resourceExists(path)) { + Resource resource = MobileDeviceManagementUtil.getConfigurationRegistry().newCollection(); + MobileDeviceManagementUtil.getConfigurationRegistry().beginTransaction(); + MobileDeviceManagementUtil.getConfigurationRegistry().put(path, resource); + MobileDeviceManagementUtil.getConfigurationRegistry().commitTransaction(); + } + return true; + } catch (MobileDeviceMgtPluginException e) { + throw new MobileDeviceMgtPluginException( + "Error occurred while creating a registry collection : " + + e.getMessage(), e); + } catch (RegistryException e) { + throw new MobileDeviceMgtPluginException( + "Error occurred while creating a registry collection : " + + e.getMessage(), e); + } + } + + public static List getMissingFeatures(List supportedFeatures, List existingFeatures) { + HashMap featureHashMap = new HashMap(); + for (Feature feature: existingFeatures) { + featureHashMap.put(feature.getCode(),feature); + } + List missingFeatures = new ArrayList(); + for (Feature supportedFeature : supportedFeatures) { + if (featureHashMap.get(supportedFeature.getCode()) != null) { + continue; + } + missingFeatures.add(supportedFeature); + } + return missingFeatures; + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/MobileDeviceManagementConfigTests.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/MobileDeviceManagementConfigTests.java new file mode 100644 index 000000000..461de8a1a --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/MobileDeviceManagementConfigTests.java @@ -0,0 +1,141 @@ +/* + * 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.device.mgt.mobile.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.wso2.carbon.device.mgt.mobile.config.MobileDeviceManagementConfig; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +import javax.xml.XMLConstants; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import java.io.File; + +/** + * Class for holding unit-tests related to MobileDeviceManagementConfig class. + */ + +public class MobileDeviceManagementConfigTests { + + private static final Log log = LogFactory.getLog(MobileDeviceManagementConfigTests.class); + private static final String MALFORMED_TEST_CONFIG_LOCATION_NO_MGT_REPOSITORY = + "./src/test/resources/config/malformed-mobile-config-no-mgt-repo.xml"; + private static final String MALFORMED_TEST_CONFIG_LOCATION_NO_DS_CONFIG = + "./src/test/resources/config/malformed-mobile-config-no-ds-config.xml"; + private static final String MALFORMED_TEST_CONFIG_LOCATION_NO_JNDI_CONFIG = + "./src/test/resources/config/malformed-mobile-config-no-jndi-config.xml"; + private static final String MALFORMED_TEST_CONFIG_LOCATION_NO_APIS_CONFIG = + "./src/test/resources/config/malformed-mobile-config-no-apis-config.xml"; + private static final String MALFORMED_TEST_CONFIG_LOCATION_NO_API_CONFIG = + "./src/test/resources/config/malformed-mobile-config-no-api-config.xml"; + private static final String MALFORMED_TEST_CONFIG_LOCATION_NO_API_PUBLISHER_CONFIG = + "./src/test/resources/config/malformed-mobile-config-no-api-publisher-config.xml"; + private static final String TEST_CONFIG_SCHEMA_LOCATION = + "./src/test/resources/config/schema/MobileDeviceManagementConfigSchema.xsd"; + + private Schema schema; + + @BeforeClass + private void initSchema() { + File deviceManagementSchemaConfig = + new File(MobileDeviceManagementConfigTests.TEST_CONFIG_SCHEMA_LOCATION); + SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + try { + schema = factory.newSchema(deviceManagementSchemaConfig); + } catch (SAXException e) { + Assert.fail("Invalid schema found", e); + } + } + + @Test + public void testMandateManagementRepositoryElement() { + File malformedConfig = + new File( + MobileDeviceManagementConfigTests.MALFORMED_TEST_CONFIG_LOCATION_NO_MGT_REPOSITORY); + this.validateMalformedConfig(malformedConfig); + } + + @Test + public void testMandateDataSourceConfigurationElement() { + File malformedConfig = new File( + MobileDeviceManagementConfigTests.MALFORMED_TEST_CONFIG_LOCATION_NO_DS_CONFIG); + this.validateMalformedConfig(malformedConfig); + } + + @Test + public void testMandateJndiLookupDefinitionElement() { + File malformedConfig = new File( + MobileDeviceManagementConfigTests.MALFORMED_TEST_CONFIG_LOCATION_NO_JNDI_CONFIG); + this.validateMalformedConfig(malformedConfig); + } + + @Test + public void testMandateAPIPublisherElement() { + File malformedConfig = new File( + MobileDeviceManagementConfigTests.MALFORMED_TEST_CONFIG_LOCATION_NO_API_PUBLISHER_CONFIG); + this.validateMalformedConfig(malformedConfig); + } + + @Test + public void testMandateAPIsElement() { + File malformedConfig = new File( + MobileDeviceManagementConfigTests.MALFORMED_TEST_CONFIG_LOCATION_NO_APIS_CONFIG); + this.validateMalformedConfig(malformedConfig); + } + + @Test + public void testMandateAPIElement() { + File malformedConfig = new File( + MobileDeviceManagementConfigTests.MALFORMED_TEST_CONFIG_LOCATION_NO_API_CONFIG); + this.validateMalformedConfig(malformedConfig); + } + + /** + * Validates a given malformed-configuration file. + */ + private void validateMalformedConfig(File malformedConfig) { + try { + JAXBContext ctx = JAXBContext.newInstance(MobileDeviceManagementConfig.class); + Unmarshaller um = ctx.createUnmarshaller(); + um.setSchema(this.getSchema()); + um.unmarshal(malformedConfig); + Assert.assertTrue(false); + } catch (JAXBException e) { + Throwable linkedException = e.getLinkedException(); + if (!(linkedException instanceof SAXParseException)) { + log.error("Unexpected error occurred while unmarshalling mobile device management config", e); + Assert.assertTrue(false); + } + Assert.assertTrue(true); + } + } + + private Schema getSchema() { + return schema; + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/common/DBTypes.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/common/DBTypes.java new file mode 100644 index 000000000..fa263f384 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/common/DBTypes.java @@ -0,0 +1,29 @@ +/* + * 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.device.mgt.mobile.impl.common; + + +public enum DBTypes { + Oracle("Oracle"),H2("H2"),MySql("MySql"); + + String dbName ; + DBTypes(String dbStrName) { + dbName = dbStrName; + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/common/TestDBConfiguration.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/common/TestDBConfiguration.java new file mode 100644 index 000000000..042e105bf --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/common/TestDBConfiguration.java @@ -0,0 +1,90 @@ +/* + * 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.device.mgt.mobile.impl.common; + +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "Type") +public class TestDBConfiguration { + + private String connectionURL; + private String driverClassName; + private String username; + private String password; + + @Override public String toString() { + return "TestDataSourceConfiguration{" + + "ConnectionURL='" + connectionURL + '\'' + + ", DriverClassName='" + driverClassName + '\'' + + ", Username='" + username + '\'' + + ", Password='" + password + '\'' + + ", Type='" + dbType + '\'' + + '}'; + } + + private String dbType; + + @XmlElement(name = "ConnectionURL", nillable = false) + public String getConnectionURL() { + return connectionURL; + } + + public void setConnectionURL(String connectionURL) { + this.connectionURL = connectionURL; + } + + @XmlElement(name = "DriverClassName", nillable = false) + public String getDriverClassName() { + return driverClassName; + } + + public void setDriverClassName(String driverClassName) { + this.driverClassName = driverClassName; + } + + @XmlElement(name = "Username", nillable = false) + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + @XmlElement(name = "Password", nillable = false) + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @XmlAttribute(name = "name") + public String getType() { + return dbType; + } + + public void setType(String type) { + this.dbType = type; + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/common/TestDBConfigurations.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/common/TestDBConfigurations.java new file mode 100644 index 000000000..88092ea41 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/common/TestDBConfigurations.java @@ -0,0 +1,39 @@ +/* + * 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.device.mgt.mobile.impl.common; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.List; + +@XmlRootElement(name = "TestDataSourceConfigurations") +public class TestDBConfigurations { + + private List dbTypesList; + + @XmlElement(name = "Type") + public List getDbTypesList() { + return dbTypesList; + } + + public void setDbTypesList(List dbTypesList) { + this.dbTypesList = dbTypesList; + } + +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/dao/util/MobileDatabaseUtils.java b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/dao/util/MobileDatabaseUtils.java new file mode 100644 index 000000000..f454f6f06 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/java/org/wso2/carbon/device/mgt/mobile/impl/dao/util/MobileDatabaseUtils.java @@ -0,0 +1,111 @@ +/* + * 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.device.mgt.mobile.impl.dao.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.w3c.dom.Document; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceManagementDAOException; +import org.wso2.carbon.device.mgt.mobile.impl.common.DBTypes; +import org.wso2.carbon.device.mgt.mobile.impl.common.TestDBConfiguration; +import org.wso2.carbon.device.mgt.mobile.impl.common.TestDBConfigurations; +import org.wso2.carbon.device.mgt.mobile.util.MobileDeviceManagementUtil; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import java.io.File; +import java.sql.*; + +/** + * This class provides the utility methods needed for DAO related test executions. + */ +public class MobileDatabaseUtils { + + private static final Log log = LogFactory.getLog(MobileDatabaseUtils.class); + public static final String TEST_RESOURCES_DB_CONFIG_FILE = + "src/test/resources/testdbconfig.xml"; + + public static void cleanupResources(Connection conn, Statement stmt, ResultSet rs) { + if (rs != null) { + try { + rs.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing result set", e); + } + } + if (stmt != null) { + try { + stmt.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing prepared statement", e); + } + } + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + log.warn("Error occurred while closing database connection", e); + } + } + } + + public static TestDBConfiguration getTestDBConfiguration(DBTypes dbType) throws + MobileDeviceManagementDAOException, + DeviceManagementException { + File deviceMgtConfig = new File(TEST_RESOURCES_DB_CONFIG_FILE); + Document doc; + TestDBConfiguration testDBConfiguration = null; + TestDBConfigurations testDBConfigurations; + + doc = MobileDeviceManagementUtil.convertToDocument(deviceMgtConfig); + JAXBContext testDBContext; + + try { + testDBContext = JAXBContext.newInstance(TestDBConfigurations.class); + Unmarshaller unmarshaller = testDBContext.createUnmarshaller(); + testDBConfigurations = (TestDBConfigurations) unmarshaller.unmarshal(doc); + } catch (JAXBException e) { + throw new MobileDeviceManagementDAOException("Error parsing test db configurations", e); + } + + for (TestDBConfiguration testDBConfiguration1 : testDBConfigurations.getDbTypesList()) { + testDBConfiguration = testDBConfiguration1; + if (testDBConfiguration.getType().equals(dbType.toString())) { + break; + } + } + + return testDBConfiguration; + } + + public static void createH2DB(TestDBConfiguration testDBConf) throws Exception { + Connection conn = null; + Statement stmt = null; + try { + Class.forName(testDBConf.getDriverClassName()); + conn = DriverManager.getConnection(testDBConf.getConnectionURL()); + stmt = conn.createStatement(); + stmt.executeUpdate("RUNSCRIPT FROM './src/test/resources/sql/h2.sql'"); + } finally { + cleanupResources(conn, stmt, null); + } + } +} diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-api-config.xml b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-api-config.xml new file mode 100644 index 000000000..3fad2eceb --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-api-config.xml @@ -0,0 +1,41 @@ + + + + + + + + + jdbc/MobileIOSDM_DS + + + + + jdbc/MobileAndroidDM_DS + + + + + jdbc/MobileWindowsDM_DS + + + + + + diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-api-publisher-config.xml b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-api-publisher-config.xml new file mode 100644 index 000000000..94fb50462 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-api-publisher-config.xml @@ -0,0 +1,42 @@ + + + + + + + + jdbc/MobileDM_DS + + + + + + + + enrollment + admin + enrollment + 1.0.0 + http://localhost:9763/ + http,https + + + + + diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-apis-config.xml b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-apis-config.xml new file mode 100644 index 000000000..743fe6c24 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-apis-config.xml @@ -0,0 +1,42 @@ + + + + + + + + jdbc/MobileDM_DS + + + + + + + + enrollment + admin + enrollment + 1.0.0 + http://localhost:9763/ + http,https + + + + + diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-ds-config.xml b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-ds-config.xml new file mode 100644 index 000000000..1f1b5ef14 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-ds-config.xml @@ -0,0 +1,42 @@ + + + + + + + + jdbc/MobileDM_DS + + + + + + + + enrollment + admin + enrollment + 1.0.0 + http://localhost:9763/ + http,https + + + + + diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-jndi-config.xml b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-jndi-config.xml new file mode 100644 index 000000000..06dc33761 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-jndi-config.xml @@ -0,0 +1,42 @@ + + + + + + + + jdbc/MobileDM_DS + + + + + + + + enrollment + admin + enrollment + 1.0.0 + http://localhost:9763/ + http,https + + + + + diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-mgt-repo.xml b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-mgt-repo.xml new file mode 100644 index 000000000..617742635 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/malformed-mobile-config-no-mgt-repo.xml @@ -0,0 +1,42 @@ + + + + + + + + jdbc/MobileDM_DS + + + + + + + + enrollment + admin + enrollment + 1.0.0 + http://localhost:9763/ + http,https + + + + + diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/schema/MobileDeviceManagementConfigSchema.xsd b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/schema/MobileDeviceManagementConfigSchema.xsd new file mode 100644 index 000000000..ff6435e50 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/config/schema/MobileDeviceManagementConfigSchema.xsd @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/log4j.properties b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/log4j.properties new file mode 100644 index 000000000..675d63149 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/log4j.properties @@ -0,0 +1,33 @@ +# +# Copyright 2015 WSO2, Inc. (http://wso2.com) +# +# 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. +# + +# +# This is the log4j configuration file used by WSO2 Carbon +# +# IMPORTANT : Please do not remove or change the names of any +# of the Appenders defined here. The layout pattern & log file +# can be changed using the WSO2 Carbon Management Console, and those +# settings will override the settings in this file. +# + +log4j.rootLogger=DEBUG, STD_OUT + +# Redirect log messages to console +log4j.appender.STD_OUT=org.apache.log4j.ConsoleAppender +log4j.appender.STD_OUT.Target=System.out +log4j.appender.STD_OUT.layout=org.apache.log4j.PatternLayout +log4j.appender.STD_OUT.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/sql/h2.sql b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/sql/h2.sql new file mode 100644 index 000000000..78b5b7b8b --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/sql/h2.sql @@ -0,0 +1,54 @@ + +-- ----------------------------------------------------- +-- Table `AD_DEVICE` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `AD_DEVICE` ( + `DEVICE_ID` VARCHAR(45) NOT NULL , + `PUSH_TOKEN` VARCHAR(45) NULL DEFAULT NULL , + `IMEI` VARCHAR(45) NULL DEFAULT NULL , + `IMSI` VARCHAR(45) NULL DEFAULT NULL , + `OS_VERSION` VARCHAR(45) NULL DEFAULT NULL , + `DEVICE_MODEL` VARCHAR(45) NULL DEFAULT NULL , + `VENDOR` VARCHAR(45) NULL DEFAULT NULL , + `LATITUDE` VARCHAR(45) NULL DEFAULT NULL, + `LONGITUDE` VARCHAR(45) NULL DEFAULT NULL, + `CHALLENGE` VARCHAR(45) NULL DEFAULT NULL, + `TOKEN` VARCHAR(500) NULL DEFAULT NULL, + `UNLOCK_TOKEN` VARCHAR(500) NULL DEFAULT NULL, + `SERIAL` VARCHAR(45) NULL DEFAULT NULL, + PRIMARY KEY (`DEVICE_ID`) ); + + +-- ----------------------------------------------------- +-- Table `AD_FEATURE` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `AD_FEATURE` ( + `ID` INT NOT NULL AUTO_INCREMENT , + `CODE` VARCHAR(45) NOT NULL, + `NAME` VARCHAR(100) NULL , + `DESCRIPTION` VARCHAR(200) NULL , + `DEVICE_TYPE` VARCHAR(50) NULL , + PRIMARY KEY (`ID`) ); + +-- ----------------------------------------------------- +-- Table `AD_OPERATION` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `AD_OPERATION` ( + `OPERATION_ID` INT NOT NULL AUTO_INCREMENT , + `FEATURE_CODE` VARCHAR(45) NOT NULL , + `CREATED_DATE` BIGINT NULL , + PRIMARY KEY (`OPERATION_ID`)); + + +-- ----------------------------------------------------- +-- Table `AD_FEATURE_PROPERTY` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `AD_FEATURE_PROPERTY` ( + `PROPERTY` VARCHAR(45) NOT NULL , + `FEATURE_ID` INT NOT NULL , + PRIMARY KEY (`PROPERTY`) , + CONSTRAINT `fk_AD_FEATURE_PROPERTY_AD_FEATURE1` + FOREIGN KEY (`FEATURE_ID` ) + REFERENCES `AD_FEATURE` (`ID` ) + ON DELETE NO ACTION + ON UPDATE NO ACTION); diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/testdbconfig.xml b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/testdbconfig.xml new file mode 100644 index 000000000..eee04a324 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/testdbconfig.xml @@ -0,0 +1,24 @@ + + + + + + jdbc:h2:mem:cdm-mobile-test-db;DB_CLOSE_DELAY=-1 + org.h2.Driver + + + + diff --git a/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/testng.xml b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/testng.xml new file mode 100644 index 000000000..f73ab880b --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/org.wso2.carbon.device.mgt.mobile/src/test/resources/testng.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/components/mobile-plugins/mobile-base-plugin/pom.xml b/components/mobile-plugins/mobile-base-plugin/pom.xml new file mode 100644 index 000000000..b2912b562 --- /dev/null +++ b/components/mobile-plugins/mobile-base-plugin/pom.xml @@ -0,0 +1,62 @@ + + + + + + + org.wso2.carbon.devicemgt-plugins + mobile-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + mobile-base-plugin + pom + WSO2 Carbon - Mobile Plugins + http://wso2.org + + + org.wso2.carbon.device.mgt.mobile + org.wso2.carbon.device.mgt.mobile.api + org.wso2.carbon.device.mgt.mobile.ui + org.wso2.carbon.device.mgt.mobile.url.printer + + + + + + + org.apache.felix + maven-scr-plugin + 1.7.2 + + + generate-scr-scrdescriptor + + scr + + + + + + + + + diff --git a/components/key-mgt/pom.xml b/components/mobile-plugins/pom.xml similarity index 79% rename from components/key-mgt/pom.xml rename to components/mobile-plugins/pom.xml index 8a4e5f619..7c62f920b 100644 --- a/components/key-mgt/pom.xml +++ b/components/mobile-plugins/pom.xml @@ -17,30 +17,28 @@ ~ under the License. --> - + org.wso2.carbon.devicemgt-plugins carbon-device-mgt-plugins-parent - 1.9.0-SNAPSHOT + 2.1.0-SNAPSHOT ../../pom.xml 4.0.0 org.wso2.carbon.devicemgt-plugins - key-mgt - 1.9.0-SNAPSHOT + mobile-plugins pom - WSO2 Carbon - Oauth Key Management Component + WSO2 Carbon - Mobile Plugins http://wso2.org - org.wso2.carbon.key.mgt.handler.valve + android-plugin + windows-plugin + mobile-base-plugin - @@ -60,5 +58,4 @@ - diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/pom.xml b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/pom.xml new file mode 100644 index 000000000..8ee9a2c23 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/pom.xml @@ -0,0 +1,309 @@ + + + + + + windows-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.mobile.windows.agent + WSO2 Carbon - Windows JAX-RS API + Windows JAX-RS API + war + + + + + maven-compiler-plugin + + 1.7 + 1.7 + + 2.3.2 + + + maven-war-plugin + 2.2 + + + ${project.artifactId} + + + + + + + + deploy + + compile + + + org.apache.maven.plugins + maven-antrun-plugin + 1.7 + + + compile + + run + + + + + + + + + + + + + + + + + + + client + + test + + + org.codehaus.mojo + exec-maven-plugin + 1.2.1 + + + test + + java + + + + + + + + + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + + + org.apache.cxf + cxf-rt-frontend-jaxrs + + + org.apache.cxf + cxf-rt-transports-http + + + org.apache.cxf + cxf-rt-bindings-soap + compile + + + org.apache.cxf + cxf-rt-bindings-http + + + org.apache.ws.security + wss4j + + + org.slf4j + slf4j-api + + + compile + + + org.apache.cxf + cxf-rt-rs-extension-providers + compile + + + org.apache.cxf + cxf-rt-ws-security + + + org.slf4j + slf4j-api + + + + + org.apache.wss4j + wss4j-ws-security-common + + + org.slf4j + slf4j-api + + + + + org.apache.ws.commons.axiom + axiom-api + + + org.apache.ws.commons.axiom + axiom-impl + + + log4j + log4j + + + org.springframework + spring-web + + + + com.sun.xml.ws + jaxws-rt + provided + + + com.sun.xml.messaging.saaj + saaj-impl + + + org.bouncycastle + bcpkix-jdk15on + + + org.bouncycastle + bcprov-jdk15on + + + org.codehaus.plexus + plexus-utils + + + com.madgag.spongycastle + pkix + + + com.madgag.spongycastle + prov + + + com.madgag.spongycastle + core + + + commons-codec.wso2 + commons-codec + + + joda-time + joda-time + + + commons-io + commons-io + + + javax.ws.rs + jsr311-api + + + org.testng + testng + + + org.wso2.carbon + org.wso2.carbon.logging + + + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + provided + + + org.wso2.carbon.identity + org.wso2.carbon.identity.oauth.stub + provided + + + org.wso2.carbon.identity + org.wso2.carbon.identity.oauth + provided + + + javax.cache + cache-api + 0.5 + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.core + provided + + + org.wso2.tomcat + tomcat-servlet-api + + + com.google.code.gson + gson + + + org.wso2.carbon.devicemgt + org.wso2.carbon.policy.mgt.core + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.certificate.mgt.core + provided + + + org.codehaus.jettison.wso2 + jettison + 1.1.wso2v1 + + + org.json.wso2 + json + 2.0.0.wso2v1 + + + diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/PluginConstants.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/PluginConstants.java new file mode 100644 index 000000000..d61ee584d --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/PluginConstants.java @@ -0,0 +1,293 @@ +/* + * 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.mdm.mobileservices.windows.common; + +/** + * PluginConstants class for Windows plugin. This class has inner classes for containing constants for + * each service. + */ +public final class PluginConstants { + + //Service endpoints + public static final String DISCOVERY_SERVICE_ENDPOINT = + "org.wso2.carbon.mdm.mobileservices.windows.services.discovery.DiscoveryService"; + public static final String CERTIFICATE_ENROLLMENT_SERVICE_ENDPOINT = + "org.wso2.carbon.mdm.mobileservices.windows.services.wstep" + + ".CertificateEnrollmentService"; + public static final String CERTIFICATE_ENROLLMENT_POLICY_SERVICE_ENDPOINT = + "org.wso2.carbon.mdm.mobileservices.windows.services.xcep" + + ".CertificateEnrollmentPolicyService"; + + //Services' target namespaces + public static final String DISCOVERY_SERVICE_TARGET_NAMESPACE = + "http://schemas.microsoft.com/windows/management/2012/01/enrollment"; + public static final String DEVICE_ENROLLMENT_SERVICE_TARGET_NAMESPACE = + "http://schemas.microsoft.com/windows/pki/2009/01/enrollment/RSTRC"; + public static final String CERTIFICATE_ENROLLMENT_POLICY_SERVICE_TARGET_NAMESPACE = + "http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy"; + + //Certificate enrollment service urls and namespaces + public static final String WS_TRUST_TARGET_NAMESPACE = + "http://docs.oasis-open.org/ws-sx/ws-trust/200512"; + public static final String WS_SECURITY_TARGET_NAMESPACE = + "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"; + public static final String SOAP_AUTHORIZATION_TARGET_NAMESPACE = + "http://schemas.xmlsoap.org/ws/2006/12/authorization"; + + //Certificate enrollment policy service urls and namespaces + public static final String ENROLLMENT_POLICY_TARGET_NAMESPACE = + "http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy"; + public static final String REQUEST_WRAPPER_CLASS_NAME = + "com.microsoft.schemas.windows.pki._2009._01.enrollmentpolicy.GetPolicies"; + public static final String RESPONSE_WRAPPER_CLASS_NAME = + "com.microsoft.schemas.windows.pki._2009._01.enrollmentpolicy.GetPoliciesResponse"; + + //Servlet Context attributes names + public static final String CONTEXT_WAP_PROVISIONING_FILE = "WAP_PROVISIONING_FILE"; + public static final String WINDOWS_PLUGIN_PROPERTIES = "WINDOWS_PLUGIN_PROPERTIES"; + + //Message handler constants + public static final String CONTENT_LENGTH = "Content-Length"; + public static final String SECURITY = "Security"; + public static final String CXF_REQUEST_URI = "org.apache.cxf.request.uri"; + + //Web services media types + public static final String SYNCML_MEDIA_TYPE = "application/vnd.syncml.dm+xml;charset=utf-8"; + + /** + * Discovery service related other constants + */ + public final class Discovery { + private Discovery() { + throw new AssertionError(); + } + + public static final String ENROLL_SUBDOMAIN = "https://EnterpriseEnrollment."; + public static final String CERTIFICATE_ENROLLMENT_POLICY_SERVICE_URL = + "/ENROLLMENTSERVER/PolicyEnrollmentWebservice" + + ".svc"; + public static final String CERTIFICATE_ENROLLMENT_SERVICE_URL = + "/ENROLLMENTSERVER/DeviceEnrollmentWebservice" + + ".svc"; + public static final String ONPREMISE_CERTIFICATE_ENROLLMENT_POLICY = + "/ENROLLMENTSERVER/ONPREMISE/" + + "PolicyEnrollmentWebservice.svc"; + public static final String ONPREMISE_CERTIFICATE_ENROLLMENT_SERVICE_URL = + "/ENROLLMENTSERVER/ONPREMISE/DeviceEnrollmentWebservice.svc"; + public static final String WAB_URL = "/mdm/enrollments/windows/login-agent"; + + } + + /** + * Certificate enrolment policy service related constants + */ + public final class CertificateEnrolmentPolicy { + private CertificateEnrolmentPolicy() { + throw new AssertionError(); + } + + public static final int MINIMAL_KEY_LENGTH = 2048; + public static final int POLICY_SCHEMA = 3; + public static final int HASH_ALGORITHM_OID_REFERENCE = 0; + public static final int OID_REFERENCE = 0; + public static final String OID = "1.3.14.3.2.29"; + public static final String OID_DEFAULT_NAME = "szOID_OIWSEC_sha1RSASign"; + public static final int OID_GROUP = 1; + public static final int OID_REFERENCE_ID = 0; + } + + /** + * Certificate enrollment Service related constants + */ + public final class CertificateEnrolment { + private CertificateEnrolment() { + throw new AssertionError(); + } + + public static final String TOKEN_TYPE = + "http://schemas.microsoft.com/5.0.0" + + ".0/ConfigurationManager/Enrollment/DeviceEnrollmentToken"; + public static final String PARM = "parm"; + public static final String TYPE = "type"; + public static final String VALUE = "value"; + public static final String VALUE_TYPE = + "http://schemas.microsoft.com/5.0.0" + + ".0/ConfigurationManager/Enrollment/DeviceEnrollmentProvisionDoc"; + public static final String ENCODING_TYPE = + "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0" + + ".xsd#base64binary"; + public static final String CA_CERT = "cacert"; + public static final String X_509 = "X.509"; + public static final String PROPERTIES_XML = "properties.xml"; + public static final String WAP_PROVISIONING_XML = "wap-provisioning.xml"; + public static final String PROVIDER = "BC"; + public static final String ALGORITHM = "SHA1withRSA"; + public static final String JKS = "JKS"; + public static final String SECURITY = "Security"; + public static final String WSS_SECURITY_UTILITY = + "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0" + + ".xsd"; + public static final String TIMESTAMP_ID = "Id"; + public static final String TIMESTAMP_U = "u"; + public static final String TIMESTAMP = "Timestamp"; + public static final String TIMESTAMP_0 = "_0"; + public static final String CREATED = "Created"; + public static final String EXPIRES = "Expires"; + public static final String UTF_8 = "utf-8"; + } + + /** + * SynclML service related constants + */ + public final class SyncML { + private SyncML() { + throw new AssertionError(); + } + + public static final String SYNCML_SOURCE = "Source"; + public static final String SYNCML_DATA = "Data"; + public static final String SYNCML_CMD = "Cmd"; + public static final String SYNCML_CHAL = "ChallengeTag"; + public static final String SYNCML_CMD_ID = "CmdID"; + public static final String SYNCML_CMD_REF = "CmdRef"; + public static final String SYNCML_MESSAGE_REF = "MsgRef"; + public static final String SYNCML_LOCATION_URI = "LocURI"; + public static final String SYNCML_TARGET_REF = "TargetRef"; + + public static final int SYNCML_FIRST_MESSAGE_ID = 1; + public static final int SYNCML_SECOND_MESSAGE_ID = 2; + public static final int SYNCML_FIRST_SESSION_ID = 1; + public static final int SYNCML_SECOND_SESSION_ID = 2; + public static final int OSVERSION_POSITION = 0; + public static final int DEVICE_ID_POSITION = 0; + public static final int DEVICE_MODEL_POSITION = 2; + public static final int DEVICE_MAN_POSITION = 1; + public static final int DEVICE_MOD_VER_POSITION = 3; + public static final int DEVICE_LANG_POSITION = 4; + public static final int IMSI_POSITION = 1; + public static final int IMEI_POSITION = 2; + public static final int VENDER_POSITION = 4; + public static final int MODEL_POSITION = 5; + public static final int MACADDRESS_POSITION = 7; + public static final int RESOLUTION_POSITION = 8; + public static final int DEVICE_NAME_POSITION = 9; + public static final String SYNCML_DATA_ONE = "1"; + public static final String SYNCML_DATA_ZERO = "0"; + public static final String OS_VERSION = "OS_VERSION"; + public static final String IMSI = "IMSI"; + public static final String IMEI = "IMEI"; + public static final String VENDOR = "VENDOR"; + public static final String MODEL = "DEVICE_MODEL"; + public static final String MAC_ADDRESS = "MAC_ADDRESS"; + public static final String DEVICE_INFO = "DEVICE_INFO"; + public static final String DEVICE_NAME = "DEVICE_NAME"; + public static final String SOFTWARE_VERSION = "SOFTWARE_VERSION"; + public static final String DEV_ID = "DEV_ID"; + public static final String MANUFACTURER = "MANUFACTURER"; + public static final String LANGUAGE = "LANGUAGE"; + public static final String RESOLUTION = "RESOLUTION"; + } + + /** + * Windows device constants. + */ + public final class DeviceConstants { + private DeviceConstants() { + throw new AssertionError(); + } + + public static final String DEVICE_ID_NOT_FOUND = "Device Id not found for device found at %s"; + public static final String DEVICE_ID_SERVICE_NOT_FOUND = + "Issue in retrieving device management service instance for device found at %s"; + } + + /** + * Device Operation codes. + */ + public final class OperationCodes { + private OperationCodes() { + throw new AssertionError(); + } + + public static final String DEVICE_LOCK = "DEVICE_LOCK"; + public static final String DISENROLL = "DISENROLL"; + public static final String DEVICE_RING = "DEVICE_RING"; + public static final String WIPE_DATA = "WIPE_DATA"; + public static final String ENCRYPT_STORAGE = "ENCRYPT_STORAGE"; + public static final String LOCK_RESET = "LOCK_RESET"; + public static final String PIN_CODE = "LOCK_PIN"; + public static final String CAMERA = "CAMERA"; + public static final String PASSCODE_POLICY = "PASSCODE_POLICY"; + public static final String PASSWORD_EXPIRE = "PASSWORD_EXPIRE"; + public static final String PASSWORD_HISTORY = "PASSWORD_HISTORY"; + public static final String MAX_PASSWORD_INACTIVE_TIME = "MAX_PASSWORD_INACTIVE_TIME"; + public static final String MIN_PASSWORD_COMPLEX_CHARACTERS = "MIN_PASSWORD_COMPLEX_CHARACTERS"; + public static final String ALPHANUMERIC_PASSWORD = "ALPHANUMERIC_PASSWORD"; + public static final String SIMPLE_PASSWORD = "SIMPLE_PASSWORD"; + public static final String MIN_PASSWORD_LENGTH = "MIN_PASSWORD_LENGTH"; + public static final String DEVICE_PASSWORD_ENABLE = "DEVICE_PASSWORD_ENABLE"; + public static final String PASSWORD_MAX_FAIL_ATTEMPTS = "PASSWORD_MAX_FAIL_ATTEMPTS"; + public static final String MONITOR = "MONITOR"; + public static final String CAMERA_STATUS = "CAMERA_STATUS"; + public static final String POLICY_BUNDLE = "POLICY_BUNDLE"; + public static final String ENCRYPT_STORAGE_STATUS = "ENCRYPT_STORAGE_STATUS"; + public static final String DEVICE_PASSWORD_STATUS = "DEVICE_PASSWORD_STATUS"; + public static final String DEVICE_PASSCODE_DELETE = "DEVICE_PASSCODE_DELETE"; + } + + /** + * Plugin response status code constants. + */ + public final class StatusCodes { + private StatusCodes() { + throw new AssertionError(); + } + + public static final int MULTI_STATUS_HTTP_CODE = 207; + } + + /** + * Tenant Configuration related constants. + */ + public final class TenantConfigProperties { + private TenantConfigProperties() { + throw new AssertionError(); + } + + public static final String LICENSE_KEY = "windowsLicense"; + public static final String LANGUAGE_US = "en_US"; + public static final String CONTENT_TYPE_TEXT = "text"; + public static final String NOTIFIER_FREQUENCY = "notifierFrequency"; + public static final String DEFAULT_FREQUENCY = "8"; + + } + + /** + * Policy Configuration related constants. + */ + public final class PolicyConfigProperties { + private PolicyConfigProperties() { + throw new AssertionError(); + } + + public static final String POLICY_ENABLE = "enabled"; + public static final String ENCRYPTED_ENABLE = "encrypted"; + public static final String ENABLE_PASSWORD = "enablePassword"; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/SyncmlCommandType.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/SyncmlCommandType.java new file mode 100644 index 000000000..6f4969231 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/SyncmlCommandType.java @@ -0,0 +1,34 @@ +/* + * 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.mdm.mobileservices.windows.common; + +public enum SyncmlCommandType { + + WIFI("WIFI"); + + private final String commandType; + + SyncmlCommandType(final String commandType) { + this.commandType = commandType; + } + + public String getValue() { + return this.commandType; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/beans/CacheEntry.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/beans/CacheEntry.java new file mode 100644 index 000000000..a160cced1 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/beans/CacheEntry.java @@ -0,0 +1,52 @@ +/* + * 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.mdm.mobileservices.windows.common.beans; + +/** + * Class for java CacheEntry + */ +public class CacheEntry { + private String deviceID; + private String username; + private String ownership; + + public String getOwnership() { + return ownership; + } + + public void setOwnership(String ownership) { + this.ownership = ownership; + } + + public String getDeviceID() { + return deviceID; + } + + public void setDeviceID(String deviceID) { + this.deviceID = deviceID; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/beans/Token.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/beans/Token.java new file mode 100644 index 000000000..cabbbf96d --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/beans/Token.java @@ -0,0 +1,35 @@ +/* + * 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.mdm.mobileservices.windows.common.beans; + +/** + * Class for challenge token + */ +public class Token { + private String challengeToken; + + public String getChallengeToken() { + return challengeToken; + } + + public void setChallengeToken(String challengeToken) { + this.challengeToken = challengeToken; + } +} + diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/beans/WindowsPluginProperties.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/beans/WindowsPluginProperties.java new file mode 100644 index 000000000..d25b1a5ca --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/beans/WindowsPluginProperties.java @@ -0,0 +1,92 @@ +/* + * 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.mdm.mobileservices.windows.common.beans; + +/** + * Bean class for storing Windows plugin properties after reading the property file. + */ +public class WindowsPluginProperties { + + private String keyStorePassword; + private String privateKeyPassword; + private String commonName; + private String authPolicy; + private String domain; + private int notBeforeDays; + private int notAfterDays; + + public String getKeyStorePassword() { + return keyStorePassword; + } + + public String getPrivateKeyPassword() { + return privateKeyPassword; + } + + public String getCommonName() { + return commonName; + } + + public int getNotBeforeDays() { + return notBeforeDays; + } + + public int getNotAfterDays() { + return notAfterDays; + } + + public void setKeyStorePassword(String keyStorePassword) { + this.keyStorePassword = keyStorePassword; + } + + public void setPrivateKeyPassword(String privateKeyPassword) { + this.privateKeyPassword = privateKeyPassword; + } + + public void setCommonName(String commonName) { + this.commonName = commonName; + } + + public void setNotBeforeDays(int notBeforeDays) { + this.notBeforeDays = notBeforeDays; + } + + public void setNotAfterDays(int notAfterDays) { + this.notAfterDays = notAfterDays; + } + + public String getAuthPolicy() { + return authPolicy; + } + + public void setAuthPolicy(String authPolicy) { + this.authPolicy = authPolicy; + } + + public String getDomain() { + return domain; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + +} + diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/AuthenticationException.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/AuthenticationException.java new file mode 100644 index 000000000..8e827ce77 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/AuthenticationException.java @@ -0,0 +1,58 @@ +/* + * 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.mdm.mobileservices.windows.common.exceptions; + +/** + * Exception class for authentication failures in windows device enrollment. + */ +public class AuthenticationException extends Exception { + + private String errorMessage; + + public AuthenticationException(String message) { + super(message); + setErrorMessage(message); + } + + public AuthenticationException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public AuthenticationException(String message, Exception nestedEx) { + super(message, nestedEx); + setErrorMessage(message); + } + + public AuthenticationException(Throwable cause) { + super(cause); + } + + public AuthenticationException() { + super(); + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/BadRequestException.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/BadRequestException.java new file mode 100644 index 000000000..fce28a3b5 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/BadRequestException.java @@ -0,0 +1,36 @@ +/* + * 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.mdm.mobileservices.windows.common.exceptions; + +import org.wso2.carbon.mdm.mobileservices.windows.common.util.Message; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +/** + * Exception class for bad request failures + */ +public class BadRequestException extends WebApplicationException { + + public BadRequestException(Message message, MediaType mediaType) { + super(Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(message). + type(mediaType).build()); + } +} \ No newline at end of file diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/CertificateGenerationException.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/CertificateGenerationException.java new file mode 100644 index 000000000..ce2f135db --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/CertificateGenerationException.java @@ -0,0 +1,58 @@ +/* + * 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.mdm.mobileservices.windows.common.exceptions; + +/** + * Exception class for Certificate generation failures in WSTEP stage. + */ +public class CertificateGenerationException extends Exception { + + private String errorMessage; + + public CertificateGenerationException(String message) { + super(message); + setErrorMessage(message); + } + + public CertificateGenerationException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public CertificateGenerationException(String message, Exception nestedEx) { + super(message, nestedEx); + setErrorMessage(message); + } + + public CertificateGenerationException(Throwable cause) { + super(cause); + } + + public CertificateGenerationException() { + super(); + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/FileOperationException.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/FileOperationException.java new file mode 100644 index 000000000..4fbc04398 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/FileOperationException.java @@ -0,0 +1,58 @@ +/* + * 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.mdm.mobileservices.windows.common.exceptions; + +/** + * Exception class for file reading/writing failures. + */ +public class FileOperationException extends Exception { + + private String errorMessage; + + public FileOperationException(String message) { + super(message); + setErrorMessage(message); + } + + public FileOperationException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public FileOperationException(String message, Exception nestedEx) { + super(message, nestedEx); + setErrorMessage(message); + } + + public FileOperationException(Throwable cause) { + super(cause); + } + + public FileOperationException() { + super(); + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/KeyStoreGenerationException.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/KeyStoreGenerationException.java new file mode 100644 index 000000000..4a783abda --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/KeyStoreGenerationException.java @@ -0,0 +1,58 @@ +/* + * 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.mdm.mobileservices.windows.common.exceptions; + +/** + * Exception class for Keystore generation failures. + */ +public class KeyStoreGenerationException extends Exception { + + private String errorMessage; + + public KeyStoreGenerationException(String message) { + super(message); + setErrorMessage(message); + } + + public KeyStoreGenerationException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public KeyStoreGenerationException(String message, Exception nestedEx) { + super(message, nestedEx); + setErrorMessage(message); + } + + public KeyStoreGenerationException(Throwable cause) { + super(cause); + } + + public KeyStoreGenerationException() { + super(); + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/MDMAPIException.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/MDMAPIException.java new file mode 100644 index 000000000..98553785b --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/MDMAPIException.java @@ -0,0 +1,60 @@ +/* + * 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.mdm.mobileservices.windows.common.exceptions; + +/** + * MDMAPIUtils class provides utility function used by CDM REST-API classes. + */ +public class MDMAPIException extends Exception { + + private static final long serialVersionUID = 7950151650447893900L; + private String errorMessage; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public MDMAPIException(String msg, Exception e) { + super(msg, e); + setErrorMessage(msg); + } + + public MDMAPIException(String msg, Throwable cause) { + super(msg, cause); + setErrorMessage(msg); + } + + public MDMAPIException(String msg) { + super(msg); + setErrorMessage(msg); + } + + public MDMAPIException() { + super(); + } + + public MDMAPIException(Throwable cause) { + super(cause); + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/SyncmlMessageFormatException.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/SyncmlMessageFormatException.java new file mode 100644 index 000000000..e37b8b6b5 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/SyncmlMessageFormatException.java @@ -0,0 +1,57 @@ +/* + * 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.mdm.mobileservices.windows.common.exceptions; + +/** + * Exception class for bad format syncml message exceptions. + */ +public class SyncmlMessageFormatException extends Exception { + private String errorMessage; + + public SyncmlMessageFormatException(String message) { + super(message); + setErrorMessage(message); + } + + public SyncmlMessageFormatException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public SyncmlMessageFormatException(String message, Exception nestedEx) { + super(message, nestedEx); + setErrorMessage(message); + } + + public SyncmlMessageFormatException(Throwable cause) { + super(cause); + } + + public SyncmlMessageFormatException() { + super(); + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/SyncmlOperationException.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/SyncmlOperationException.java new file mode 100644 index 000000000..409ca57e0 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/SyncmlOperationException.java @@ -0,0 +1,58 @@ +/* + * 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.mdm.mobileservices.windows.common.exceptions; + +/** + * Exception class for syncml Operation and policy related errors. + */ +public class SyncmlOperationException extends Exception { + + private String errorMessage; + + public SyncmlOperationException(String message) { + super(message); + setErrorMessage(message); + } + + public SyncmlOperationException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public SyncmlOperationException(String message, Exception nestedEx) { + super(message, nestedEx); + setErrorMessage(message); + } + + public SyncmlOperationException(Throwable cause) { + super(cause); + } + + public SyncmlOperationException() { + super(); + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/WAPProvisioningException.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/WAPProvisioningException.java new file mode 100644 index 000000000..d23368332 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/WAPProvisioningException.java @@ -0,0 +1,58 @@ +/* + * 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.mdm.mobileservices.windows.common.exceptions; + +/** + * Exception class for property/provisioning XML file reading failures. + */ +public class WAPProvisioningException extends Exception { + + private String errorMessage; + + public WAPProvisioningException(String message) { + super(message); + setErrorMessage(message); + } + + public WAPProvisioningException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public WAPProvisioningException(String message, Exception nestedEx) { + super(message, nestedEx); + setErrorMessage(message); + } + + public WAPProvisioningException(Throwable cause) { + super(cause); + } + + public WAPProvisioningException() { + super(); + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/WindowsConfigurationException.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/WindowsConfigurationException.java new file mode 100644 index 000000000..86c49eef2 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/WindowsConfigurationException.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.mdm.mobileservices.windows.common.exceptions; + +/** + * Custom class for windows API configurations. + */ +public class WindowsConfigurationException extends Exception { + + private String errorMessage; + + private static final long serialVersionUID = 7950151650447893900L; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public WindowsConfigurationException(Throwable cause) { + super(cause); + } + + public WindowsConfigurationException() { + super(); + } + + public WindowsConfigurationException(String msg) { + super(msg); + setErrorMessage(msg); + } + + public WindowsConfigurationException(String msg, Throwable cause) { + super(msg, cause); + setErrorMessage(msg); + } + + public WindowsConfigurationException(String msg, Exception exception) { + super(msg, exception); + setErrorMessage(msg); + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/WindowsDeviceEnrolmentException.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/WindowsDeviceEnrolmentException.java new file mode 100644 index 000000000..4722b7621 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/WindowsDeviceEnrolmentException.java @@ -0,0 +1,59 @@ +/* + * 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.mdm.mobileservices.windows.common.exceptions; + +/** + * Exception class for general WSTEP messaging/message generation failures. + */ +public class WindowsDeviceEnrolmentException extends Exception { + + private String errorMessage; + + public WindowsDeviceEnrolmentException(String message) { + super(message); + setErrorMessage(message); + } + + public WindowsDeviceEnrolmentException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + public WindowsDeviceEnrolmentException(String message, Exception nestedEx) { + super(message, nestedEx); + setErrorMessage(message); + } + + public WindowsDeviceEnrolmentException(Throwable cause) { + super(cause); + } + + public WindowsDeviceEnrolmentException() { + super(); + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/WindowsOperationsException.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/WindowsOperationsException.java new file mode 100644 index 000000000..14677391d --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/exceptions/WindowsOperationsException.java @@ -0,0 +1,37 @@ +/* + * 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.mdm.mobileservices.windows.common.exceptions; + +import org.wso2.carbon.mdm.mobileservices.windows.common.util.Message; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +/** + * Exception Class for windows operations related exceptions + */ +public class WindowsOperationsException extends WebApplicationException { + + public WindowsOperationsException(Message message, MediaType mediaType) { + super(Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(message). + type(mediaType).build()); + } + +} \ No newline at end of file diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/AuthenticationInfo.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/AuthenticationInfo.java new file mode 100644 index 000000000..80458a50a --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/AuthenticationInfo.java @@ -0,0 +1,62 @@ +/* + * 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.mdm.mobileservices.windows.common.util; + +/** + * DTO class to hold the information of authenticated user. + */ +public class AuthenticationInfo { + private String message; + private String username; + private String tenantDomain; + private int tenantId = -1; + + public String getUsername() { + return username; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public void setUsername(String username) { + this.username = username; + } + + 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/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/BSTValidator.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/BSTValidator.java new file mode 100644 index 000000000..916f2bf7b --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/BSTValidator.java @@ -0,0 +1,162 @@ +/* + * 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.mdm.mobileservices.windows.common.util; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.ws.security.WSSecurityException; +import org.apache.ws.security.handler.RequestData; +import org.apache.ws.security.message.token.BinarySecurity; +import org.apache.ws.security.validate.Credential; +import org.apache.ws.security.validate.Validator; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.identity.oauth2.dto.OAuth2TokenValidationRequestDTO; +import org.wso2.carbon.identity.oauth2.dto.OAuth2TokenValidationResponseDTO; +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.wso2.carbon.mdm.mobileservices.windows.common.beans.CacheEntry; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.AuthenticationException; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsDeviceEnrolmentException; +import org.wso2.carbon.utils.multitenancy.MultitenantUtils; + +import java.util.HashMap; + +/** + * Validator class for user authentication checking the default carbon user store. + */ +public class BSTValidator implements Validator { + + private static Log log = LogFactory.getLog(BSTValidator.class); + private static final String BEARER_TOKEN_TYPE = "bearer"; + private static final String RESOURCE_KEY = "resource"; + + /** + * This method validates the binary security token in SOAP message coming from the device. + * + * @param credential - binary security token credential object + * @param requestData - Request data associated with the request + * @return - Credential object if authentication is success, or null if not success + * @throws WSSecurityException + */ + @Override + public Credential validate(Credential credential, RequestData requestData) throws WSSecurityException { + String encodedBinarySecurityToken; + String requestedUri; + Credential returnCredentials = null; + + HashMap msgContext = (HashMap) requestData.getMsgContext(); + requestedUri = msgContext.get(PluginConstants.CXF_REQUEST_URI).toString(); + BinarySecurity binarySecurityTokenObject = credential.getBinarySecurityToken(); + String binarySecurityToken = binarySecurityTokenObject.getElement().getFirstChild().getTextContent(); + Base64 base64 = new Base64(); + encodedBinarySecurityToken = new String(base64.decode(binarySecurityToken)); + AuthenticationInfo authenticationInfo; + try { + authenticationInfo = validateRequest(requestedUri, encodedBinarySecurityToken); + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext privilegedCarbonContext = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + privilegedCarbonContext.setTenantId(authenticationInfo.getTenantId()); + privilegedCarbonContext.setTenantDomain(authenticationInfo.getTenantDomain()); + privilegedCarbonContext.setUsername(authenticationInfo.getUsername()); + + if (authenticate(binarySecurityToken)) { + returnCredentials = credential; + } else { + String msg = "Authentication failure due to invalid binary security token."; + log.error(msg); + throw new WindowsDeviceEnrolmentException(msg); + } + } catch (AuthenticationException e) { + String msg = "Failure occurred in the BST validator."; + log.error(msg, e); + throw new WSSecurityException(msg, e); + } catch (WindowsDeviceEnrolmentException e) { + String msg = "Authentication Failure occurred due to binary security token."; + log.error(msg, e); + throw new WSSecurityException(msg, e); + } + return returnCredentials; + } + + /** + * This method authenticates the user checking the binary security token in the user store. + * + * @param binarySecurityToken - Binary security token received in the SOAP message header + * @return - Authentication status + * @throws AuthenticationException + */ + public boolean authenticate(String binarySecurityToken) throws + AuthenticationException { + + CacheEntry cacheentry = (CacheEntry) DeviceUtil.getCacheEntry(binarySecurityToken); + String username = cacheentry.getUsername(); + return username != null; + } + + /** + * Validate SOAP request token. + * + * @param requestedUri- Requested endpoint URI. + * @param encodedBinarySecurityToken-Binary security token comes from the soap request message. + * @return returns authorized user information. + * @throws WindowsDeviceEnrolmentException + */ + public AuthenticationInfo validateRequest(String requestedUri, String encodedBinarySecurityToken) + throws WindowsDeviceEnrolmentException { + + AuthenticationInfo authenticationInfo = new AuthenticationInfo(); + // Create a OAuth2TokenValidationRequestDTO object for validating access token + OAuth2TokenValidationRequestDTO dto = new OAuth2TokenValidationRequestDTO(); + //Set the access token info + OAuth2TokenValidationRequestDTO.OAuth2AccessToken oAuth2AccessToken = dto.new OAuth2AccessToken(); + oAuth2AccessToken.setTokenType(BSTValidator.BEARER_TOKEN_TYPE); + oAuth2AccessToken.setIdentifier(encodedBinarySecurityToken); + dto.setAccessToken(oAuth2AccessToken); + + //Set the resource context param. This will be used in scope validation. + OAuth2TokenValidationRequestDTO.TokenValidationContextParam + resourceContextParam = dto.new TokenValidationContextParam(); + resourceContextParam.setKey(BSTValidator.RESOURCE_KEY); + resourceContextParam.setValue(requestedUri + ":POST"); + + OAuth2TokenValidationRequestDTO.TokenValidationContextParam[] + tokenValidationContextParams = + new OAuth2TokenValidationRequestDTO.TokenValidationContextParam[1]; + tokenValidationContextParams[0] = resourceContextParam; + dto.setContext(tokenValidationContextParams); + try { + OAuth2TokenValidationResponseDTO oAuth2TokenValidationResponseDTO = + WindowsAPIUtils.getOAuth2TokenValidationService().validate(dto); + if (oAuth2TokenValidationResponseDTO.isValid()) { + String username = oAuth2TokenValidationResponseDTO.getAuthorizedUser(); + authenticationInfo.setUsername(username); + authenticationInfo.setTenantDomain(MultitenantUtils.getTenantDomain(username)); + authenticationInfo.setTenantId(WindowsAPIUtils.getTenantIdOFUser(username)); + } else { + authenticationInfo.setMessage(oAuth2TokenValidationResponseDTO.getErrorMsg()); + } + } catch (DeviceManagementException e) { + String msg = "Authentication failure due to invalid binary security token."; + log.error(msg, e); + throw new WindowsDeviceEnrolmentException(msg, e); + } + return authenticationInfo; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/ConfigInitializerContextListener.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/ConfigInitializerContextListener.java new file mode 100644 index 000000000..b2eda4a4f --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/ConfigInitializerContextListener.java @@ -0,0 +1,138 @@ +/* + * 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.mdm.mobileservices.windows.common.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.w3c.dom.Document; +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.wso2.carbon.mdm.mobileservices.windows.common.beans.WindowsPluginProperties; +import org.xml.sax.SAXException; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.File; +import java.io.IOException; + +/** + * This class performs one time operations. + */ +public class ConfigInitializerContextListener implements ServletContextListener { + + public static final int INITIAL_VALUE = 0; + private static Log log = LogFactory.getLog(ConfigInitializerContextListener.class); + + private enum PropertyName { + PROPERTY_SIGNED_CERT_CN("SignedCertCN"), + PROPERTY_SIGNED_CERT_NOT_BEFORE("SignedCertNotBefore"), + PROPERTY_SIGNED_CERT_NOT_AFTER("SignedCertNotAfter"), + PROPERTY_PASSWORD("Password"), + PROPERTY_PRIVATE_KEY_PASSWORD("PrivateKeyPassword"), + AUTH_POLICY("AuthPolicy"), + DOMAIN("domain"); + + private final String propertyName; + + PropertyName(final String propertyName) { + this.propertyName = propertyName; + } + + public String getValue() { + return this.propertyName; + } + } + + /** + * This method loads wap-provisioning file / property file, sets wap-provisioning file and + * extracted properties as attributes in servlet context. + * + * @param servletContextEvent - Uses when servlet communicating with servlet container. + */ + @Override + public void contextInitialized(ServletContextEvent servletContextEvent) { + + ServletContext servletContext = servletContextEvent.getServletContext(); + File propertyFile = new File(getClass().getClassLoader().getResource( + PluginConstants.CertificateEnrolment.PROPERTIES_XML).getFile()); + DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder; + Document document = null; + try { + docBuilder = docBuilderFactory.newDocumentBuilder(); + if (docBuilder != null) { + document = docBuilder.parse(propertyFile); + } + } catch (ParserConfigurationException e) { + log.error("Parser configuration failure while reading properties.xml."); + } catch (SAXException e) { + log.error("Parsing error occurred while reading properties.xml."); + } catch (IOException e) { + log.error("File reading error occurred while accessing properties.xml."); + } + + String password = null; + String privateKeyPassword = null; + String signedCertCommonName = null; + String authPolicy = null; + String domain = null; + int signedCertNotBeforeDate = INITIAL_VALUE; + int signedCertNotAfterDate = INITIAL_VALUE; + + if (document != null) { + password = document.getElementsByTagName(PropertyName.PROPERTY_PASSWORD.getValue()).item(0). + getTextContent(); + privateKeyPassword = document.getElementsByTagName(PropertyName.PROPERTY_PRIVATE_KEY_PASSWORD.getValue()). + item(0).getTextContent(); + signedCertCommonName = + document.getElementsByTagName(PropertyName.PROPERTY_SIGNED_CERT_CN.getValue()).item(0). + getTextContent(); + authPolicy = document.getElementsByTagName(PropertyName.AUTH_POLICY.getValue()).item(0). + getTextContent(); + signedCertNotBeforeDate = Integer.valueOf(document.getElementsByTagName( + PropertyName.PROPERTY_SIGNED_CERT_NOT_BEFORE.getValue()).item(0).getTextContent()); + signedCertNotAfterDate = Integer.valueOf(document.getElementsByTagName( + PropertyName.PROPERTY_SIGNED_CERT_NOT_AFTER.getValue()).item(0).getTextContent()); + domain = document.getElementsByTagName(PropertyName.DOMAIN.getValue()).item(0).getTextContent(); + + } + + WindowsPluginProperties properties = new WindowsPluginProperties(); + properties.setKeyStorePassword(password); + properties.setPrivateKeyPassword(privateKeyPassword); + properties.setCommonName(signedCertCommonName); + properties.setNotBeforeDays(signedCertNotBeforeDate); + properties.setNotAfterDays(signedCertNotAfterDate); + properties.setAuthPolicy(authPolicy); + properties.setDomain(domain); + servletContext.setAttribute(PluginConstants.WINDOWS_PLUGIN_PROPERTIES, properties); + + File wapProvisioningFile = new File(getClass().getClassLoader().getResource( + PluginConstants.CertificateEnrolment.WAP_PROVISIONING_XML).getFile()); + servletContext.setAttribute(PluginConstants.CONTEXT_WAP_PROVISIONING_FILE, wapProvisioningFile); + } + + @Override + public void contextDestroyed(ServletContextEvent servletContextEvent) { + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/DeviceIDHolder.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/DeviceIDHolder.java new file mode 100644 index 000000000..99b0a5d2e --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/DeviceIDHolder.java @@ -0,0 +1,48 @@ +/* + * 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.mdm.mobileservices.windows.common.util; + +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; + +import java.util.List; + +/** + * Class for holding device ids. + */ +public class DeviceIDHolder { + + private List errorDeviceIdList; + private List validDeviceIDList; + + public List getErrorDeviceIdList() { + return errorDeviceIdList; + } + + public void setErrorDeviceIdList(List errorDeviceIdList) { + this.errorDeviceIdList = errorDeviceIdList; + } + + public List getValidDeviceIDList() { + return validDeviceIDList; + } + + public void setValidDeviceIDList(List validDeviceIDList) { + this.validDeviceIDList = validDeviceIDList; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/DeviceUtil.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/DeviceUtil.java new file mode 100644 index 000000000..61d2713b2 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/DeviceUtil.java @@ -0,0 +1,81 @@ +/* + * 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.mdm.mobileservices.windows.common.util; + +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.mdm.mobileservices.windows.common.beans.CacheEntry; + +import javax.cache.Cache; +import javax.cache.CacheConfiguration; +import javax.cache.CacheManager; +import javax.cache.Caching; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * Class for generate random token for XCEP and WSTEP + */ +public class DeviceUtil { + + private static final String TOKEN_CACHE_MANAGER = "TOKEN_CACHE_MANAGER"; + private static final String TOKEN_CACHE = "TOKEN_CACHE"; + private static final long CACHE_DURATION = 15l; + private static boolean isContextCacheInitialized = false; + + public static String generateRandomToken() { + return String.valueOf(UUID.randomUUID()); + } + + public static void persistChallengeToken(String token, String deviceID, String username) throws DeviceManagementException { + + Object objCacheEntry = getCacheEntry(token); + CacheEntry cacheEntry; + if (objCacheEntry == null) { + cacheEntry = new CacheEntry(); + cacheEntry.setUsername(username); + } else { + cacheEntry = (CacheEntry) objCacheEntry; + } + if (deviceID != null) { + cacheEntry.setDeviceID(deviceID); + } + getTokenCache().put(token, cacheEntry); + } + + public static void removeToken(String token) { + getTokenCache().remove(token); + } + + public static Object getCacheEntry(String token) { + return getTokenCache().get(token); + } + + private static Cache getTokenCache() { + CacheManager contextCacheManager = Caching.getCacheManager(TOKEN_CACHE_MANAGER). + getCache(TOKEN_CACHE).getCacheManager(); + if (!isContextCacheInitialized) { + isContextCacheInitialized = true; + return contextCacheManager.createCacheBuilder(TOKEN_CACHE_MANAGER).setExpiry( + CacheConfiguration.ExpiryType.MODIFIED, + new CacheConfiguration.Duration(TimeUnit.MINUTES, CACHE_DURATION)).setStoreByValue(false).build(); + } else { + return Caching.getCacheManager(TOKEN_CACHE_MANAGER).getCache(TOKEN_CACHE); + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/ErrorHandler.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/ErrorHandler.java new file mode 100644 index 000000000..2640302a5 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/ErrorHandler.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 org.wso2.carbon.mdm.mobileservices.windows.common.util; + +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsConfigurationException; + +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +/** + * Class for handle response exceptions. + */ +@Produces({"application/json", "application/xml"}) +public class ErrorHandler implements ExceptionMapper { + + public Response toResponse(WindowsConfigurationException exception) { + ErrorMessage errorMessage = new ErrorMessage(); + errorMessage.setErrorMessage(exception.getErrorMessage()); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(errorMessage).build(); + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/ErrorMessage.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/ErrorMessage.java new file mode 100644 index 000000000..42177b230 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/ErrorMessage.java @@ -0,0 +1,39 @@ +/* + * 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. + */ +/** + * Generate Error Messages for responses. + */ +package org.wso2.carbon.mdm.mobileservices.windows.common.util; + +public class ErrorMessage { + + private String errorMessage; + private String errorCode; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public String getErrorCode() { + return errorCode; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/GsonMessageBodyHandler.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/GsonMessageBodyHandler.java new file mode 100644 index 000000000..eaed2e11a --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/GsonMessageBodyHandler.java @@ -0,0 +1,98 @@ +/* + * 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.mdm.mobileservices.windows.common.util; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import java.io.*; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; + +/** + * Class for parse Gson Messages. + */ +@Provider +@Produces(APPLICATION_JSON) +@Consumes(APPLICATION_JSON) +public class GsonMessageBodyHandler implements MessageBodyWriter, MessageBodyReader { + private Gson gson; + private static final String UTF_8 = "UTF-8"; + + public boolean isReadable(Class aClass, Type type, Annotation[] annotations, MediaType mediaType) { + return true; + } + + private Gson getGson() { + if (gson == null) { + final GsonBuilder gsonBuilder = new GsonBuilder(); + gson = gsonBuilder.create(); + } + return gson; + } + + public Object readFrom(Class objectClass, Type type, Annotation[] annotations, MediaType mediaType, + MultivaluedMap stringStringMultivaluedMap, InputStream entityStream) + throws IOException, WebApplicationException { + + InputStreamReader reader = new InputStreamReader(entityStream, "UTF-8"); + + try { + return getGson().fromJson(reader, type); + } finally { + reader.close(); + } + } + + public boolean isWriteable(Class aClass, Type type, Annotation[] annotations, MediaType mediaType) { + return true; + } + + public long getSize(Object o, Class aClass, Type type, Annotation[] annotations, MediaType mediaType) { + return -1; + } + + public void writeTo(Object object, Class aClass, Type type, Annotation[] annotations, MediaType mediaType, + MultivaluedMap stringObjectMultivaluedMap, OutputStream entityStream) + throws IOException, WebApplicationException { + + OutputStreamWriter writer = new OutputStreamWriter(entityStream, UTF_8); + try { + Type jsonType; + if (type.equals(type)) { + jsonType = type; + } else { + jsonType = type; + } + getGson().toJson(object, jsonType, writer); + } finally { + writer.close(); + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/Message.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/Message.java new file mode 100644 index 000000000..c3f2cc793 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/Message.java @@ -0,0 +1,49 @@ +/* + * 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.mdm.mobileservices.windows.common.util; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Class for generate response Messages. + */ +@XmlRootElement +public class Message { + private String responseCode; + private String responseMessage; + + @XmlElement + public String getResponseMessage() { + return responseMessage; + } + + public void setResponseMessage(String responseMessage) { + this.responseMessage = responseMessage; + } + + @XmlElement + public String getResponseCode() { + return responseCode; + } + + public void setResponseCode(String responseCode) { + this.responseCode = responseCode; + } +} \ No newline at end of file diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/SOAPSecurityHandler.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/SOAPSecurityHandler.java new file mode 100644 index 000000000..679ac010c --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/SOAPSecurityHandler.java @@ -0,0 +1,63 @@ +/* + * 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.mdm.mobileservices.windows.common.util; + + +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; + +import javax.xml.namespace.QName; +import javax.xml.ws.handler.MessageContext; +import javax.xml.ws.handler.soap.SOAPHandler; +import javax.xml.ws.handler.soap.SOAPMessageContext; +import java.util.HashSet; +import java.util.Set; + +/** + * Class for handle SOAP message security. + */ +public class SOAPSecurityHandler implements SOAPHandler { + + /** + * This method resolves the security header coming in the SOAP message. + * + * @return - Security Header + */ + @Override + public Set getHeaders() { + QName securityHeader = new QName(PluginConstants.WS_SECURITY_TARGET_NAMESPACE, PluginConstants.SECURITY); + HashSet headers = new HashSet(); + headers.add(securityHeader); + return headers; + } + + @Override + public boolean handleMessage(SOAPMessageContext context) { + return true; + } + + @Override + public boolean handleFault(SOAPMessageContext context) { + return true; + } + + @Override + public void close(MessageContext context) { + + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/UsernameTokenValidator.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/UsernameTokenValidator.java new file mode 100644 index 000000000..fb9ab5b63 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/UsernameTokenValidator.java @@ -0,0 +1,134 @@ +/* + * 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.mdm.mobileservices.windows.common.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.ws.security.WSSecurityException; +import org.apache.ws.security.handler.RequestData; +import org.apache.ws.security.validate.Credential; +import org.apache.ws.security.validate.Validator; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.AuthenticationException; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsDeviceEnrolmentException; +import org.wso2.carbon.user.api.UserRealm; +import org.wso2.carbon.user.api.UserStoreException; +import org.wso2.carbon.user.core.service.RealmService; +import org.wso2.carbon.utils.multitenancy.MultitenantConstants; + +/** + * Validator class for user authentication checking the default carbon user store. + */ +public class UsernameTokenValidator implements Validator { + + private static final int USER_SEGMENT = 0; + private static final int DOMAIN_SEGMENT = 1; + private static final String DELIMITER = "@"; + private static Log log = LogFactory.getLog(UsernameTokenValidator.class); + + /** + * This method validates the username token in SOAP message coming from the device. + * + * @param credential - Username token credentials coming from device + * @param requestData - Request data associated with the request + * @return - Credential object if authentication is success, or null if not success + * @throws WSSecurityException + */ + @Override + public Credential validate(Credential credential, RequestData requestData) throws + WSSecurityException { + + String domainUser = credential.getUsernametoken().getName(); + String[] domainUserArray = domainUser.split(DELIMITER); + Credential returnCredentials; + String user = domainUserArray[USER_SEGMENT]; + String domain = domainUserArray[DOMAIN_SEGMENT]; + String password = credential.getUsernametoken().getPassword(); + + try { + if (authenticate(user, password, domain)) { + returnCredentials = credential; + } else { + String msg = "Authentication failure due to incorrect credentials."; + log.error(msg); + throw new WindowsDeviceEnrolmentException(msg); + } + //Generic exception is caught here as there is no need of taking different actions for + //different exceptions. + } catch (AuthenticationException e) { + String msg = "Failure occurred in the BST validator."; + log.error(msg, e); + throw new WSSecurityException(msg, e); + } catch (WindowsDeviceEnrolmentException e) { + String msg = "Authentication Failure occurred due to binary security token."; + log.error(msg, e); + throw new WSSecurityException(msg, e); + } + return returnCredentials; + } + + /** + * This method authenticate the user checking the carbon default user store. + * + * @param username - Username in username token + * @param password - Password in username token + * @param tenantDomain - Tenant domain is extracted from the username + * @return - Returns boolean representing authentication result + * @throws AuthenticationException + */ + public boolean authenticate(String username, String password, String tenantDomain) throws + AuthenticationException { + + try { + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + ctx.setTenantDomain(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); + ctx.setTenantId(MultitenantConstants.SUPER_TENANT_ID); + RealmService realmService = (RealmService) ctx.getOSGiService(RealmService.class, null); + + if (realmService == null) { + String msg = "RealmService not initialized."; + log.error(msg); + throw new AuthenticationException(msg); + } + + int tenantId; + if (tenantDomain == null || tenantDomain.trim().isEmpty()) { + tenantId = MultitenantConstants.SUPER_TENANT_ID; + } else { + tenantId = realmService.getTenantManager().getTenantId(tenantDomain); + } + + if (tenantId == MultitenantConstants.INVALID_TENANT_ID) { + String msg = "Invalid tenant domain " + tenantDomain; + log.error(msg); + throw new AuthenticationException(msg); + } + UserRealm userRealm = realmService.getTenantUserRealm(tenantId); + + return userRealm.getUserStoreManager().authenticate(username, password); + } catch (UserStoreException e) { + String msg = "User store is not initialized."; + log.error(msg, e); + throw new AuthenticationException(msg, e); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/WindowsAPIUtils.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/WindowsAPIUtils.java new file mode 100644 index 000000000..e1dfb97e9 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/WindowsAPIUtils.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.mdm.mobileservices.windows.common.util; + + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.base.MultitenantConstants; +import org.wso2.carbon.certificate.mgt.core.service.CertificateManagementService; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementConstants; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.configuration.mgt.TenantConfiguration; +import org.wso2.carbon.device.mgt.common.notification.mgt.NotificationManagementService; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.common.operation.mgt.OperationManagementException; +import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; +import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +import org.wso2.carbon.identity.oauth2.OAuth2TokenValidationService; +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.MDMAPIException; +import org.wso2.carbon.policy.mgt.core.PolicyManagerService; +import org.wso2.carbon.user.api.TenantManager; +import org.wso2.carbon.user.api.UserStoreException; +import org.wso2.carbon.user.api.UserStoreManager; +import org.wso2.carbon.user.core.service.RealmService; +import org.wso2.carbon.utils.multitenancy.MultitenantUtils; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; + +/** + * Class for get Windows API utilities. + */ +public class WindowsAPIUtils { + + private static Log log = LogFactory.getLog(WindowsAPIUtils.class); + + public static DeviceIdentifier convertToDeviceIdentifierObject(String deviceId) { + DeviceIdentifier identifier = new DeviceIdentifier(); + identifier.setId(deviceId); + identifier.setType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS); + return identifier; + } + + public static CertificateManagementService getCertificateManagementService() { + CertificateManagementService cmService; + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + ctx.setTenantDomain(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); + ctx.setTenantId(MultitenantConstants.SUPER_TENANT_ID); + cmService = + (CertificateManagementService)ctx.getOSGiService(DeviceManagementProviderService.class, null); + PrivilegedCarbonContext.endTenantFlow(); + return cmService; + } + + public static DeviceManagementProviderService getDeviceManagementService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + DeviceManagementProviderService deviceManagementProviderService = + (DeviceManagementProviderService) ctx.getOSGiService(DeviceManagementProviderService.class, null); + if (deviceManagementProviderService == null) { + String msg = "Device Management service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return deviceManagementProviderService; + } + + + public static UserStoreManager getUserStoreManager() throws MDMAPIException { + RealmService realmService; + UserStoreManager userStoreManager; + try { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + realmService = (RealmService) ctx.getOSGiService(RealmService.class, null); + if (realmService == null) { + String msg = "Realm service has not initialized."; + throw new IllegalStateException(msg); + } + int tenantId = ctx.getTenantId(); + userStoreManager = realmService.getTenantUserRealm(tenantId).getUserStoreManager(); + } catch (UserStoreException e) { + String msg = "Error occurred while retrieving current user store manager"; + throw new MDMAPIException(msg, e); + } + return userStoreManager; + } + + public static NotificationManagementService getNotificationManagementService() { + NotificationManagementService notificationManagementService; + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + notificationManagementService = (NotificationManagementService) ctx.getOSGiService( + NotificationManagementService.class, null); + if (notificationManagementService == null) { + String msg = "Notification Management service not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return notificationManagementService; + } + + public static MediaType getResponseMediaType(String acceptHeader) { + MediaType responseMediaType; + if (MediaType.WILDCARD.equals(acceptHeader)) { + responseMediaType = MediaType.APPLICATION_JSON_TYPE; + } else { + responseMediaType = MediaType.valueOf(acceptHeader); + } + return responseMediaType; + } + + public static Response getOperationResponse(List deviceIDs, Operation operation, + Message message, MediaType responseMediaType) + throws DeviceManagementException, OperationManagementException { + WindowsDeviceUtils deviceUtils = new WindowsDeviceUtils(); + DeviceIDHolder deviceIDHolder = deviceUtils.validateDeviceIdentifiers(deviceIDs, + message, responseMediaType); + getDeviceManagementService().addOperation(operation, deviceIDHolder.getValidDeviceIDList()); + if (!deviceIDHolder.getErrorDeviceIdList().isEmpty()) { + return javax.ws.rs.core.Response.status(PluginConstants.StatusCodes. + MULTI_STATUS_HTTP_CODE).type( + responseMediaType).entity(deviceUtils. + convertErrorMapIntoErrorMessage(deviceIDHolder.getErrorDeviceIdList())).build(); + } + return javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.CREATED). + type(responseMediaType).build(); + } + + public static PolicyManagerService getPolicyManagerService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + PolicyManagerService policyManagerService = (PolicyManagerService) ctx.getOSGiService( + PolicyManagerService.class, null); + if (policyManagerService == null) { + String msg = "Policy Manager service has not initialized"; + log.error(msg); + throw new IllegalStateException(msg); + } + return policyManagerService; + } + + public static void updateOperation(String deviceId, Operation operation) + throws OperationManagementException { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceId); + deviceIdentifier.setType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS); + getDeviceManagementService().updateOperation(deviceIdentifier, operation); + } + + public static TenantConfiguration getTenantConfiguration() throws DeviceManagementException { + return getDeviceManagementService().getConfiguration( + DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS); + } + + public static int getTenantIdOFUser(String username) throws DeviceManagementException { + int tenantId = 0; + String domainName = MultitenantUtils.getTenantDomain(username); + if (domainName != null) { + try { + TenantManager tenantManager = IdentityTenantUtil.getRealmService().getTenantManager(); + tenantId = tenantManager.getTenantId(domainName); + } catch (UserStoreException e) { + String errorMsg = "Error when getting the tenant id from the tenant domain : " + + domainName; + log.error(errorMsg, e); + throw new DeviceManagementException(errorMsg, e); + } + } + return tenantId; + } + + public static OAuth2TokenValidationService getOAuth2TokenValidationService() { + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + OAuth2TokenValidationService oAuth2TokenValidationService = + (OAuth2TokenValidationService) ctx.getOSGiService(OAuth2TokenValidationService.class, null); + if (oAuth2TokenValidationService == null) { + String msg = "OAuth2TokenValidation service has not initialized."; + log.error(msg); + throw new IllegalStateException(msg); + } + return oAuth2TokenValidationService; + } + + public static void startTenantFlow(String userName) { + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext privilegedCarbonContext = PrivilegedCarbonContext. + getThreadLocalCarbonContext(); + privilegedCarbonContext.setTenantId(MultitenantConstants.SUPER_TENANT_ID); + privilegedCarbonContext.setTenantDomain(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); + privilegedCarbonContext.setUsername(userName); + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/WindowsDeviceUtils.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/WindowsDeviceUtils.java new file mode 100644 index 000000000..2d38ac2cb --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/common/util/WindowsDeviceUtils.java @@ -0,0 +1,85 @@ +/* + * 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.mdm.mobileservices.windows.common.util; + +import org.apache.commons.lang.StringUtils; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementConstants; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.BadRequestException; + +import javax.ws.rs.core.MediaType; +import java.util.ArrayList; +import java.util.List; + +/** + * Class for get windows device utilities. + */ +public class WindowsDeviceUtils { + + private static final String COMMA_SEPARATION_PATTERN = ", "; + + public DeviceIDHolder validateDeviceIdentifiers(List deviceIDs, + Message message, MediaType responseMediaType) { + if (deviceIDs == null) { + message.setResponseMessage("Device identifier list is empty"); + throw new BadRequestException(message, responseMediaType); + } + List errorDeviceIdList = new ArrayList(); + List validDeviceIDList = new ArrayList(); + int deviceIDCounter = 0; + + for (String deviceID : deviceIDs) { + deviceIDCounter++; + if (deviceID == null || deviceID.isEmpty()) { + errorDeviceIdList.add(String.format(PluginConstants.DeviceConstants.DEVICE_ID_NOT_FOUND, + deviceIDCounter)); + continue; + } + try { + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(deviceID); + deviceIdentifier.setType(DeviceManagementConstants.MobileDeviceTypes. + MOBILE_DEVICE_TYPE_WINDOWS); + Device device = WindowsAPIUtils.getDeviceManagementService(). + getDevice(deviceIdentifier); + if (device == null || device.getDeviceIdentifier() == null || + device.getDeviceIdentifier().isEmpty()) { + errorDeviceIdList.add(String.format(PluginConstants.DeviceConstants.DEVICE_ID_NOT_FOUND, + deviceIDCounter)); + continue; + } + validDeviceIDList.add(deviceIdentifier); + } catch (DeviceManagementException e) { + errorDeviceIdList.add(String.format(PluginConstants.DeviceConstants.DEVICE_ID_SERVICE_NOT_FOUND, + deviceIDCounter)); + } + } + DeviceIDHolder deviceIDHolder = new DeviceIDHolder(); + deviceIDHolder.setValidDeviceIDList(validDeviceIDList); + deviceIDHolder.setErrorDeviceIdList(errorDeviceIdList); + return deviceIDHolder; + } + + public String convertErrorMapIntoErrorMessage(List errorDeviceIdList) { + return StringUtils.join(errorDeviceIdList.iterator(), COMMA_SEPARATION_PATTERN); + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/AddTag.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/AddTag.java new file mode 100644 index 000000000..d5adbd9e2 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/AddTag.java @@ -0,0 +1,68 @@ +/* + * 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.mdm.mobileservices.windows.operations; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.Constants; + +import java.util.Iterator; +import java.util.List; + +/** + * Configurations that needs to be added on the device. + */ +public class AddTag { + int commandId = -1; + List items; + + public int getCommandId() { + return commandId; + } + + public void setCommandId(int commandId) { + this.commandId = commandId; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + public void buildAddElement(Document doc, Element rootElement) { + if (getItems() != null) { + Element add = doc.createElement(Constants.ADD); + rootElement.appendChild(add); + if (getCommandId() != -1) { + Element commandId = doc.createElement(Constants.COMMAND_ID); + commandId.appendChild(doc.createTextNode(String.valueOf(getCommandId()))); + add.appendChild(commandId); + } + for (Iterator itemIterator = getItems().iterator(); itemIterator.hasNext(); ) { + Item item = itemIterator.next(); + if (item != null) { + item.buildItemElement(doc, add); + } + } + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Alert.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Alert.java new file mode 100644 index 000000000..0a056669c --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Alert.java @@ -0,0 +1,63 @@ +/* + * 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.mdm.mobileservices.windows.operations; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.Constants; + +/** + * Inform an event occurred from device to server. + */ +public class Alert { + + int commandId = -1; + String data; + + public int getCommandId() { + return commandId; + } + + public void setCommandId(int commandId) { + this.commandId = commandId; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public void buildAlertElement(Document doc, Element rootElement) { + Element alert = doc.createElement(Constants.ALERT); + rootElement.appendChild(alert); + if (getCommandId() != -1) { + Element commandId = doc.createElement(Constants.COMMAND_ID); + commandId.appendChild(doc.createTextNode(String.valueOf(getCommandId()))); + alert.appendChild(commandId); + } + if (getData() != null) { + Element data = doc.createElement(Constants.DATA); + data.appendChild(doc.createTextNode(getData())); + alert.appendChild(data); + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/AtomicTag.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/AtomicTag.java new file mode 100644 index 000000000..75ad00c03 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/AtomicTag.java @@ -0,0 +1,85 @@ +/* + * 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.mdm.mobileservices.windows.operations; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.Constants; + +import java.util.Iterator; +import java.util.List; + +/** + * Wrapper for other SyncML elements. + */ +public class AtomicTag { + int commandId = -1; + List adds; + List replaces; + + public List getReplaces() { + return replaces; + } + + public void setReplaces(List replaces) { + this.replaces = replaces; + } + + public List getAdds() { + return adds; + } + + public void setAdds(List adds) { + this.adds = adds; + } + + public int getCommandId() { + return commandId; + } + + public void setCommandId(int commandId) { + this.commandId = commandId; + } + + public void buildAtomicElement(Document doc, Element rootElement) { + Element atomic = doc.createElement(Constants.ATOMIC); + if (getAdds() != null) { + rootElement.appendChild(atomic); + if (getCommandId() != -1) { + Element commandId = doc.createElement(Constants.COMMAND_ID); + commandId.appendChild(doc.createTextNode(String.valueOf(getCommandId()))); + atomic.appendChild(commandId); + } + for (Iterator addIterator = getAdds().iterator(); addIterator.hasNext(); ) { + AddTag add = addIterator.next(); + if (add != null) { + add.buildAddElement(doc, atomic); + } + } + } + if (getReplaces() != null) { + for (Iterator replaceIterator = getReplaces().iterator(); replaceIterator.hasNext(); ) { + Replace add = replaceIterator.next(); + if (add != null) { + add.buildReplaceElement(doc, atomic); + } + } + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/ChallengeTag.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/ChallengeTag.java new file mode 100644 index 000000000..763032014 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/ChallengeTag.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.mdm.mobileservices.windows.operations; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.Constants; + +/** + * Challenge data pass through the device and Device Management server for the security purpose. + */ +public class ChallengeTag { + MetaTag meta; + + public MetaTag getMeta() { + return meta; + } + + public void setMeta(MetaTag meta) { + this.meta = meta; + } + + public void buildChallElement(Document doc, Element rootElement) { + Element chal = doc.createElement(Constants.CHALLENGE); + rootElement.appendChild(chal); + if (getMeta() != null) { + getMeta().buildMetaElement(doc, chal); + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Credential.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Credential.java new file mode 100644 index 000000000..a595e5ac8 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Credential.java @@ -0,0 +1,60 @@ +/* + * 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.mdm.mobileservices.windows.operations; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.Constants; + +/** + * Credentials passed between the device and the server for security purposes. + */ +public class Credential { + MetaTag meta; + String data; + + public MetaTag getMeta() { + return meta; + } + + public void setMeta(MetaTag meta) { + this.meta = meta; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public void buildCredentialElement(Document doc, Element rootElement) { + Element credentials = doc.createElement(Constants.CREDENTIAL); + rootElement.appendChild(credentials); + if (getMeta() != null) { + getMeta().buildMetaElement(doc, credentials); + } + if (getData() != null) { + Element data = doc.createElement(Constants.DATA); + data.appendChild(doc.createTextNode(getData())); + credentials.appendChild(data); + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/DeleteTag.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/DeleteTag.java new file mode 100644 index 000000000..d899240ea --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/DeleteTag.java @@ -0,0 +1,68 @@ +/* + * 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.mdm.mobileservices.windows.operations; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.Constants; + +import java.util.Iterator; +import java.util.List; + +/** + * Configurations that need to be delete on Device. + */ +public class DeleteTag { + int commandId = -1; + List items; + + public int getCommandId() { + return commandId; + } + + public void setCommandId(int commandId) { + this.commandId = commandId; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + public void buildDeleteElement(Document doc, Element rootElement) { + if (getItems() != null) { + Element delete = doc.createElement(Constants.DELETE); + rootElement.appendChild(delete); + if (getCommandId() != -1) { + Element commandId = doc.createElement(Constants.COMMAND_ID); + commandId.appendChild(doc.createTextNode(String.valueOf(getCommandId()))); + delete.appendChild(commandId); + } + for (Iterator itemIterator = getItems().iterator(); itemIterator.hasNext(); ) { + Item item = itemIterator.next(); + if (item != null) { + item.buildItemElement(doc, delete); + } + } + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/ExecuteTag.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/ExecuteTag.java new file mode 100644 index 000000000..90a95924f --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/ExecuteTag.java @@ -0,0 +1,69 @@ +/* + * 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.mdm.mobileservices.windows.operations; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.Constants; + +import java.util.Iterator; +import java.util.List; + +/** + * Commands that needs to be executed on the device. + */ +public class ExecuteTag { + int commandId = -1; + List items; + + public int getCommandId() { + return commandId; + } + + public void setCommandId(int commandId) { + this.commandId = commandId; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + public void buildExecElement(Document doc, Element rootElement) { + if (getItems() != null) { + Element exec = doc.createElement(Constants.EXECUTE); + rootElement.appendChild(exec); + if (getCommandId() != -1) { + Element commandId = doc.createElement(Constants.COMMAND_ID); + commandId.appendChild(doc.createTextNode(String.valueOf(getCommandId()))); + exec.appendChild(commandId); + } + for (Iterator itemIterator = getItems().iterator(); itemIterator.hasNext(); ) { + Item item = itemIterator.next(); + if (item != null) { + item.buildItemElement(doc, exec); + } + } + } + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Get.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Get.java new file mode 100644 index 000000000..6666e329a --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Get.java @@ -0,0 +1,71 @@ +/* + * 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.mdm.mobileservices.windows.operations; + +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.Constants; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.Iterator; +import java.util.List; + +/** + * Data that needs to be retrieved from the device. + */ +public class Get { + int commandId = -1; + List items; + + public int getCommandId() { + return commandId; + } + + public void setCommandId(int commandId) { + this.commandId = commandId; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + public void buildGetElement(Document doc, Element rootElement) { + if (getItems() != null) { + Element get = doc.createElement(Constants.GET); + rootElement.appendChild(get); + if (getCommandId() != -1) { + Element commandId = doc.createElement(Constants.COMMAND_ID); + commandId.appendChild(doc.createTextNode(String.valueOf(getCommandId()))); + get.appendChild(commandId); + } + if (getItems() != null) { + for (Iterator itemIterator = getItems().iterator(); itemIterator.hasNext(); ) { + Item item = itemIterator.next(); + if (item != null) { + item.buildItemElement(doc, get); + } + } + } + } + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Item.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Item.java new file mode 100644 index 000000000..54cf5f65c --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Item.java @@ -0,0 +1,90 @@ +/* + * 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.mdm.mobileservices.windows.operations; + +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.Constants; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Represents an items that should be retrieved from the device or a command. + */ +public class Item { + + Target target; + Source source; + String data; + MetaTag meta; + + public MetaTag getMeta() { + return meta; + } + + public void setMeta(MetaTag meta) { + this.meta = meta; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public Source getSource() { + return source; + } + + public void setSource(Source source) { + this.source = source; + } + + public Target getTarget() { + return target; + } + + public void setTarget(Target target) { + this.target = target; + } + + public void buildItemElement(Document doc, Element rootElement) { + Element item = doc.createElement(Constants.ITEM); + rootElement.appendChild(item); + + if (getTarget() != null || getSource() != null) { + + if (getTarget() != null) { + getTarget().buildTargetElement(doc, item); + } + if (getSource() != null) { + getSource().buildSourceElement(doc, item); + } + } + if (getData() != null) { + Element data = doc.createElement(Constants.DATA); + data.appendChild(doc.createTextNode(getData())); + item.appendChild(data); + } + if (getMeta() != null) { + getMeta().buildMetaElement(doc, item); + } + + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/MetaTag.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/MetaTag.java new file mode 100644 index 000000000..fd22b710f --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/MetaTag.java @@ -0,0 +1,88 @@ +/* + * 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.mdm.mobileservices.windows.operations; + +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.Constants; + +/** + * MetaTag data related to credentials. + */ +public class MetaTag { + + String format; + String type; + String nextNonce; + + public String getNextNonce() { + return nextNonce; + } + + public void setNextNonce(String nextNonce) { + this.nextNonce = nextNonce; + } + + public String getFormat() { + return format; + } + + public void setFormat(String format) { + this.format = format; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public void buildMetaElement(Document doc, Element rootElement) { + Element meta = doc.createElement(Constants.META); + rootElement.appendChild(meta); + if (getFormat() != null) { + Element format = doc.createElement(Constants.FORMAT); + format.appendChild(doc.createTextNode(getFormat())); + Attr attr = doc.createAttribute(Constants.XMLNS); + attr.setValue(Constants.META_NAMESPACE); + format.setAttributeNode(attr); + meta.appendChild(format); + } + if (getType() != null) { + Element type = doc.createElement(Constants.TYPE); + type.appendChild(doc.createTextNode(getType())); + Attr attr = doc.createAttribute(Constants.XMLNS); + attr.setValue(Constants.META_NAMESPACE); + type.setAttributeNode(attr); + meta.appendChild(type); + } + if (getNextNonce() != null) { + Element nextNonce = doc.createElement(Constants.NEXTNONCE); + nextNonce.appendChild(doc.createTextNode(getNextNonce())); + Attr attr = doc.createAttribute(Constants.XMLNS); + attr.setValue(Constants.META_NAMESPACE); + nextNonce.setAttributeNode(attr); + meta.appendChild(nextNonce); + } + + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Replace.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Replace.java new file mode 100644 index 000000000..4a8058cca --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Replace.java @@ -0,0 +1,70 @@ +/* + * 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.mdm.mobileservices.windows.operations; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.Constants; + +import java.util.Iterator; +import java.util.List; + +/** + * Commands sent from the device. + */ +public class Replace { + int commandId = -1; + List items; + + public int getCommandId() { + return commandId; + } + + public void setCommandId(int commandId) { + this.commandId = commandId; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + public void buildReplaceElement(Document doc, Element rootElement) { + if (getItems() != null) { + Element replace = doc.createElement(Constants.REPLACE); + rootElement.appendChild(replace); + if (getCommandId() != -1) { + Element commandId = doc.createElement(Constants.COMMAND_ID); + commandId.appendChild(doc.createTextNode(String.valueOf(getCommandId()))); + replace.appendChild(commandId); + } + if (getItems() != null) { + for (Iterator itemIterator = getItems().iterator(); itemIterator.hasNext(); ) { + Item item = itemIterator.next(); + if (item != null) { + item.buildItemElement(doc, replace); + } + } + } + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Results.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Results.java new file mode 100644 index 000000000..a310bd125 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Results.java @@ -0,0 +1,96 @@ +/* + * 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.mdm.mobileservices.windows.operations; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.Constants; + +import java.util.Iterator; +import java.util.List; + +/** + * Results sent for the requests made to the device. + */ +public class Results { + int commandId = -1; + int messageReference = -1; + int commandReference = -1; + List item; + + public int getCommandId() { + return commandId; + } + + public void setCommandId(int commandId) { + this.commandId = commandId; + } + + public int getMessageReference() { + return messageReference; + } + + public void setMessageReference(int messageReference) { + this.messageReference = messageReference; + } + + public int getCommandReference() { + return commandReference; + } + + public void setCommandReference(int commandReference) { + this.commandReference = commandReference; + } + + public List getItem() { + return item; + } + + public void setItem(List item) { + this.item = item; + } + + public void buildResultElement(Document doc, Element rootElement) { + Element results = doc.createElement(Constants.RESULTS); + rootElement.appendChild(results); + if (getCommandId() != -1) { + Element commandId = doc.createElement(Constants.COMMAND_ID); + commandId.appendChild(doc.createTextNode(String.valueOf(getCommandId()))); + results.appendChild(commandId); + } + if (getMessageReference() != -1) { + Element messageReference = doc.createElement(Constants.MESSAGE_REFERENCE); + messageReference.appendChild(doc.createTextNode(String.valueOf(getMessageReference()))); + results.appendChild(messageReference); + } + if (getCommandReference() != -1) { + Element messageReference = doc.createElement(Constants.COMMAND_REFERENCE); + messageReference.appendChild(doc.createTextNode(String.valueOf(getCommandReference()))); + results.appendChild(messageReference); + } + if (getItem() != null) { + for (Iterator itemIterator = getItem().iterator(); itemIterator.hasNext(); ) { + Item item = itemIterator.next(); + if (item != null) { + item.buildItemElement(doc, results); + } + } + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/SequenceTag.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/SequenceTag.java new file mode 100644 index 000000000..429512802 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/SequenceTag.java @@ -0,0 +1,117 @@ +/* + * 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.mdm.mobileservices.windows.operations; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.Constants; + +import java.util.Iterator; +import java.util.List; + +/** + * Sequence data that use to execute tag list + */ +public class SequenceTag { + + int commandId; + ExecuteTag exec; + Get get; + DeleteTag deleteTag; + AtomicTag atomicTag; + List replaces; + + public DeleteTag getDeleteTag() { + return deleteTag; + } + + public void setDeleteTag(DeleteTag deleteTag) { + this.deleteTag = deleteTag; + } + + public List getReplaces() { + return replaces; + } + + public void setReplaces(List replaces) { + this.replaces = replaces; + } + + public AtomicTag getAtomicTag() { + return atomicTag; + } + + public void setAtomicTag(AtomicTag atomicTag) { + this.atomicTag = atomicTag; + } + + public ExecuteTag getExec() { + return exec; + } + + public void setExec(ExecuteTag exec) { + this.exec = exec; + } + + public int getCommandId() { + return commandId; + } + + public void setCommandId(int commandId) { + this.commandId = commandId; + } + + public Get getGet() { + return get; + } + + public void setGet(Get get) { + this.get = get; + } + + public void buildSequenceElement(Document doc, Element rootElement) { + Element sequence = doc.createElement(Constants.SEQUENCE); + rootElement.appendChild(sequence); + if (getCommandId() != -1) { + Element commandId = doc.createElement(Constants.COMMAND_ID); + commandId.appendChild(doc.createTextNode(String.valueOf(getCommandId()))); + sequence.appendChild(commandId); + } + if (getExec() != null) { + getExec().buildExecElement(doc, sequence); + } + if (getGet() != null) { + getGet().buildGetElement(doc, sequence); + } + if (getReplaces() != null) { + for (Iterator replaceIterator = getReplaces().iterator(); replaceIterator.hasNext(); ) { + Replace replace = replaceIterator.next(); + if (replace != null) { + replace.buildReplaceElement(doc, sequence); + } + } + } + if (getAtomicTag() != null) { + getAtomicTag().buildAtomicElement(doc, sequence); + } + if (getDeleteTag() != null) { + getDeleteTag().buildDeleteElement(doc, sequence); + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Source.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Source.java new file mode 100644 index 000000000..af0f5cbb3 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Source.java @@ -0,0 +1,63 @@ +/* + * 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.mdm.mobileservices.windows.operations; + +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.Constants; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Source details of syncml header's. + */ +public class Source { + + private String LocURI; + private String LocName; + + public String getLocURI() { + return LocURI; + } + + public void setLocURI(String locURI) { + LocURI = locURI; + } + + public String getLocName() { + return LocName; + } + + public void setLocName(String locName) { + LocName = locName; + } + + public void buildSourceElement(Document doc, Element rootElement) { + Element target = doc.createElement(Constants.SOURCE); + rootElement.appendChild(target); + if (getLocURI() != null) { + Element locURI = doc.createElement(Constants.LOC_URI); + locURI.appendChild(doc.createTextNode(getLocURI())); + target.appendChild(locURI); + } + if (getLocName() != null) { + Element locName = doc.createElement(Constants.LOC_NAME); + locName.appendChild(doc.createTextNode(getLocName())); + target.appendChild(locName); + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Status.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Status.java new file mode 100644 index 000000000..31c209aef --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Status.java @@ -0,0 +1,144 @@ +/* + * 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.mdm.mobileservices.windows.operations; + +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.Constants; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Status of a previously sent message to device; + */ +public class Status { + int commandId = -1; + int messageReference = -1; + int commandReference = -1; + String command; + String targetReference; + String data; + ChallengeTag challenge; + + public Status(int commandId, int messageReference, int commandReference, String command, + String targetReference, String data) { + this.commandId = commandId; + this.messageReference = messageReference; + this.commandReference = commandReference; + this.command = command; + this.targetReference = targetReference; + this.data = data; + } + + public Status() { + } + + public ChallengeTag getChallenge() { + return challenge; + } + + public void setChallenge(ChallengeTag challenge) { + this.challenge = challenge; + } + + public String getTargetReference() { + return targetReference; + } + + public void setTargetReference(String targetReference) { + this.targetReference = targetReference; + } + + public int getCommandId() { + return commandId; + } + + public void setCommandId(int commandId) { + this.commandId = commandId; + } + + public int getMessageReference() { + return messageReference; + } + + public void setMessageReference(int messageReference) { + this.messageReference = messageReference; + } + + public int getCommandReference() { + return commandReference; + } + + public void setCommandReference(int commandReference) { + this.commandReference = commandReference; + } + + public String getCommand() { + return command; + } + + public void setCommand(String command) { + this.command = command; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public void buildStatusElement(Document doc, Element rootElement) { + Element status = doc.createElement(Constants.STATUS); + rootElement.appendChild(status); + if (getCommandId() != -1) { + Element commandId = doc.createElement(Constants.COMMAND_ID); + commandId.appendChild(doc.createTextNode(String.valueOf(getCommandId()))); + status.appendChild(commandId); + } + if (getMessageReference() != -1) { + Element msgReference = doc.createElement(Constants.MESSAGE_REFERENCE); + msgReference.appendChild(doc.createTextNode(String.valueOf(getMessageReference()))); + status.appendChild(msgReference); + } + if (getCommandReference() != -1) { + Element commandReference = doc.createElement(Constants.COMMAND_REFERENCE); + commandReference.appendChild(doc.createTextNode(String.valueOf(getCommandReference()))); + status.appendChild(commandReference); + } + if (getCommand() != null) { + Element command = doc.createElement(Constants.COMMAND); + command.appendChild(doc.createTextNode(getCommand())); + status.appendChild(command); + } + if (getTargetReference() != null) { + Element targetReference = doc.createElement(Constants.TARGET_REFERENCE); + targetReference.appendChild(doc.createTextNode(getTargetReference())); + status.appendChild(targetReference); + } + if (getChallenge() != null) { + getChallenge().buildChallElement(doc, status); + } + if (getData() != null) { + Element data = doc.createElement(Constants.DATA); + data.appendChild(doc.createTextNode(getData())); + status.appendChild(data); + } + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/SyncmlBody.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/SyncmlBody.java new file mode 100644 index 000000000..ac132907a --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/SyncmlBody.java @@ -0,0 +1,144 @@ +/* + * 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.mdm.mobileservices.windows.operations; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.Constants; + +import java.util.Iterator; +import java.util.List; + +/** + * Represents the body details of a syncml. + */ +public class SyncmlBody { + Get getCommands; + List exec; + List status; + Alert alert; + Replace replace; + Results results; + SequenceTag sequence; + AtomicTag atomicTag; + + public AtomicTag getAtomicTag() { + return atomicTag; + } + + public void setAtomicTag(AtomicTag atomicTag) { + this.atomicTag = atomicTag; + } + + public SequenceTag getSequence() { + return sequence; + } + + public void setSequence(SequenceTag sequence) { + this.sequence = sequence; + } + + public List getExec() { + return exec; + } + + public void setExec(List exec) { + this.exec = exec; + } + + public Results getResults() { + return results; + } + + public void setResults(Results results) { + this.results = results; + } + + public Replace getReplace() { + return replace; + } + + public void setReplace(Replace replace) { + this.replace = replace; + } + + public List getStatus() { + return status; + } + + public void setStatus(List status) { + this.status = status; + } + + public Alert getAlert() { + return alert; + } + + public void setAlert(Alert alert) { + this.alert = alert; + } + + public Get getGet() { + return getCommands; + } + + public void setGet(Get get) { + this.getCommands = get; + } + + public void buildBodyElement(Document doc, Element rootElement) { + + Element syncBody = doc.createElement(Constants.SYNC_BODY); + rootElement.appendChild(syncBody); + if (getStatus() != null) { + for (int x = 0; x < getStatus().size(); x++) { + if (getStatus().get(x) != null) { + getStatus().get(x).buildStatusElement(doc, syncBody); + } + } + } + if (getAlert() != null) { + getAlert().buildAlertElement(doc, syncBody); + } + if (getResults() != null) { + getResults().buildResultElement(doc, syncBody); + } + if (getGet() != null) { + getGet().buildGetElement(doc, syncBody); + } + if (getReplace() != null) { + getReplace().buildReplaceElement(doc, syncBody); + } + if (getExec() != null) { + for (Iterator execIterator = getExec().iterator(); execIterator.hasNext(); ) { + ExecuteTag exec = execIterator.next(); + if (exec != null) { + exec.buildExecElement(doc, syncBody); + } + } + } + if (getSequence() != null) { + getSequence().buildSequenceElement(doc, syncBody); + } + if (getAtomicTag() != null) { + getAtomicTag().buildAtomicElement(doc, syncBody); + } + syncBody.appendChild(doc.createElement(Constants.FINAL)); + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/SyncmlDocument.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/SyncmlDocument.java new file mode 100644 index 000000000..284897abe --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/SyncmlDocument.java @@ -0,0 +1,55 @@ +/* + * 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.mdm.mobileservices.windows.operations; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Represents a base format of a syncml document + */ +public class SyncmlDocument { + SyncmlHeader header; + SyncmlBody body; + + public SyncmlHeader getHeader() { + return header; + } + + public void setHeader(SyncmlHeader header) { + this.header = header; + } + + public SyncmlBody getBody() { + return body; + } + + public void setBody(SyncmlBody body) { + this.body = body; + } + + public void buildDocument(Document doc, Element rootElement) { + if (getHeader() != null) { + getHeader().buildSyncmlHeaderElement(doc, rootElement); + } + if (getBody() != null) { + getBody().buildBodyElement(doc, rootElement); + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/SyncmlHeader.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/SyncmlHeader.java new file mode 100644 index 000000000..e0fc9deab --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/SyncmlHeader.java @@ -0,0 +1,114 @@ +/* + * 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.mdm.mobileservices.windows.operations; + +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.Constants; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Represents the header details of a syncml. + */ +public class SyncmlHeader { + private int sessionId = -1; + private int MsgID = -1; + private Target target; + private Source source; + private Credential credential; + private String hexadecimalSessionId; + + public String getHexadecimalSessionId() { + return hexadecimalSessionId; + } + + public void setHexadecimalSessionId(String hexSessionId) { + this.hexadecimalSessionId = hexSessionId; + } + + public Credential getCredential() { + return credential; + } + + public void setCredential(Credential credential) { + this.credential = credential; + } + + public int getSessionId() { + return sessionId; + } + + public void setSessionId(int sessionId) { + this.sessionId = sessionId; + } + + public int getMsgID() { + return MsgID; + } + + public void setMsgID(int msgID) { + this.MsgID = msgID; + } + + public Target getTarget() { + return target; + } + + public void setTarget(Target target) { + this.target = target; + } + + public Source getSource() { + return source; + } + + public void setSource(Source source) { + this.source = source; + } + + public void buildSyncmlHeaderElement(Document doc, Element rootElement) { + Element syncHdr = doc.createElement(Constants.SYNC_HDR); + rootElement.appendChild(syncHdr); + Element verDTD = doc.createElement(Constants.VER_DTD); + verDTD.appendChild(doc.createTextNode(Constants.VER_DTD_VALUE)); + syncHdr.appendChild(verDTD); + + Element verProtocol = doc.createElement(Constants.VER_PROTOCOL); + verProtocol.appendChild(doc.createTextNode(Constants.VER_PROTOCOL_VALUE)); + syncHdr.appendChild(verProtocol); + if (getHexadecimalSessionId() != null) { + Element sessionId = doc.createElement(Constants.SESSION_ID); + sessionId.appendChild(doc.createTextNode(getHexadecimalSessionId())); + syncHdr.appendChild(sessionId); + } + if (getMsgID() != -1) { + Element msgId = doc.createElement(Constants.MESSAGE_ID); + msgId.appendChild(doc.createTextNode(String.valueOf(getMsgID()))); + syncHdr.appendChild(msgId); + } + if (getTarget() != null) { + getTarget().buildTargetElement(doc, syncHdr); + } + if (getSource() != null) { + getSource().buildSourceElement(doc, syncHdr); + } + if (getCredential() != null) { + getCredential().buildCredentialElement(doc, syncHdr); + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Target.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Target.java new file mode 100644 index 000000000..03efb3ee1 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/Target.java @@ -0,0 +1,63 @@ +/* + * 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.mdm.mobileservices.windows.operations; + +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.Constants; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Target details of syncml header's. + */ +public class Target { + + private String LocURI; + private String LocName; + + public String getLocURI() { + return LocURI; + } + + public void setLocURI(String locURI) { + LocURI = locURI; + } + + public String getLocName() { + return LocName; + } + + public void setLocName(String locName) { + LocName = locName; + } + + public void buildTargetElement(Document doc, Element rootElement) { + Element target = doc.createElement(Constants.TARGET); + rootElement.appendChild(target); + if (getLocURI() != null) { + Element locURI = doc.createElement(Constants.LOC_URI); + locURI.appendChild(doc.createTextNode(getLocURI())); + target.appendChild(locURI); + } + if (getLocName() != null) { + Element locName = doc.createElement(Constants.LOC_NAME); + locName.appendChild(doc.createTextNode(getLocName())); + target.appendChild(locName); + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/WindowsOperationException.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/WindowsOperationException.java new file mode 100644 index 000000000..d4202f660 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/WindowsOperationException.java @@ -0,0 +1,58 @@ +/* + * 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.mdm.mobileservices.windows.operations; + +/** + * Exceptions related to operation retrieval and syncml generation + */ +public class WindowsOperationException extends Exception { + + private static final long serialVersionUID = 5435636243242623629L; + private String errorMessage; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public WindowsOperationException(String errorMessage) { + super(errorMessage); + } + + public WindowsOperationException(String errorMessage, Throwable throwable) { + super(errorMessage, throwable); + setErrorMessage(errorMessage); + } + + public WindowsOperationException(String msg, Exception nestedEx) { + super(msg, nestedEx); + setErrorMessage(msg); + } + + public WindowsOperationException() { + super(); + } + + public WindowsOperationException(Throwable throwable) { + super(throwable); + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/Constants.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/Constants.java new file mode 100644 index 000000000..6c4443165 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/Constants.java @@ -0,0 +1,98 @@ +/* + * 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.mdm.mobileservices.windows.operations.util; + +/** + * Constant values used in syncml generator. + */ +public class Constants { + + public static final String PROVIDER_ID = "MobiCDMServer"; + public static final String SERVER_SECRET = "dummy"; + public static final String INITIAL_NONCE = "ZHVtbXk="; + public static final String DISENROLL_ALERT_DATA = "1226"; + public static final String INITIAL_ALERT_DATA = "1201"; + public static final int EMPTY = 0; + + public static final String SYNCML_ROOT_ELEMENT_NAME = "SyncML"; + public static final String XMLNS_SYNCML = "SYNCML:SYNCML1.2"; + public static final String UTF_8 = "UTF-8"; + public static final String MD5 = "MD5"; + public static final String YES = "yes"; + public static final String ERROR = "Error"; + public static final String INFORMATION = "information"; + + public static final String EXECUTE = "Exec"; + public static final String ATOMIC = "Atomic"; + public static final String ADD = "Add"; + public static final String COMMAND_ID = "CmdID"; + public static final String GET = "Get"; + public static final String DELETE = "Delete"; + public static final String ITEM = "Item"; + public static final String SOURCE = "Source"; + public static final String LOC_URI = "LocURI"; + public static final String LOC_NAME = "LocName"; + public static final String MESSAGE_REFERENCE = "MsgRef"; + public static final String COMMAND_REFERENCE = "CmdRef"; + public static final String COMMAND = "Cmd"; + public static final String TARGET_REFERENCE = "TargetRef"; + public static final String DATA = "Data"; + public static final String STATUS = "Status"; + public static final String SYNC_BODY = "SyncBody"; + public static final String SYNC_HDR = "SyncHdr"; + public static final String VER_DTD = "VerDTD"; + public static final String VER_PROTOCOL = "VerProto"; + public static final String SESSION_ID = "SessionID"; + public static final String MESSAGE_ID = "MsgID"; + public static final String TARGET = "Target"; + public static final String VER_DTD_VALUE = "1.2"; + public static final String VER_PROTOCOL_VALUE = "DM/1.2"; + public static final String ALERT = "Alert"; + public static final String FINAL = "Final"; + public static final String REPLACE = "Replace"; + public static final String META = "Meta"; + public static final String CREDENTIAL = "Cred"; + public static final String FORMAT = "Format"; + public static final String TYPE = "Type"; + public static final String NEXTNONCE = "NextNonce"; + public static final String CHALLENGE = "chal"; + public static final String META_NAMESPACE = "syncml:metinf"; + public static final String XMLNS = "xmlns"; + public static final String RESULTS = "Results"; + public static final String CRED_FORMAT = "b64"; + public static final String CRED_TYPE = "syncml:auth-md5"; + public static final String SEQUENCE = "Sequence"; + public static final String META_FORMAT_INT = "int"; + public static final String META_FORMAT_CHARACTER = "chr"; + + /** + * SynclML service related constants + */ + public final class SyncMLResponseCodes { + public static final String AUTHENTICATION_ACCEPTED = "212"; + public static final String ACCEPTED = "200"; + public static final String ACCEPTED_FOR_PROCESSING = "202"; + public static final String PIN_NOTFOUND = "405"; + public static final String CREDENTIALS_MISSING = "407"; + public static final String INVALID_CREDENTIALS = "401"; + public static final String LOCKRESET_NOTIFICATION = "Error occurred in Device Lock Operation. " + + "Please trigger lock-reset operation."; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/DeviceInfo.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/DeviceInfo.java new file mode 100644 index 000000000..052ff48e2 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/DeviceInfo.java @@ -0,0 +1,92 @@ +/* + * 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.mdm.mobileservices.windows.operations.util; + +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class generate Info type operation list. + */ +public class DeviceInfo { + public List getDeviceInfo() { + + List deviceInfoOperations = new ArrayList<>(); + + Operation osVersion = new Operation(); + osVersion.setCode(PluginConstants.SyncML.SOFTWARE_VERSION); + osVersion.setType(Operation.Type.INFO); + deviceInfoOperations.add(osVersion); + + Operation imsi = new Operation(); + imsi.setCode(PluginConstants.SyncML.IMSI); + imsi.setType(Operation.Type.INFO); + deviceInfoOperations.add(imsi); + + Operation imei = new Operation(); + imei.setCode(PluginConstants.SyncML.IMEI); + imei.setType(Operation.Type.INFO); + deviceInfoOperations.add(imei); + + Operation deviceID = new Operation(); + deviceID.setCode(PluginConstants.SyncML.DEV_ID); + deviceID.setType(Operation.Type.INFO); + deviceInfoOperations.add(deviceID); + + Operation manufacturer = new Operation(); + manufacturer.setCode(PluginConstants.SyncML.MANUFACTURER); + manufacturer.setType(Operation.Type.INFO); + deviceInfoOperations.add(manufacturer); + + Operation model = new Operation(); + model.setCode(PluginConstants.SyncML.MODEL); + model.setType(Operation.Type.INFO); + deviceInfoOperations.add(model); + + Operation language = new Operation(); + language.setCode(PluginConstants.SyncML.LANGUAGE); + language.setType(Operation.Type.INFO); + deviceInfoOperations.add(language); + + Operation vendor = new Operation(); + vendor.setCode(PluginConstants.SyncML.VENDOR); + vendor.setType(Operation.Type.INFO); + deviceInfoOperations.add(vendor); + + Operation macaddress = new Operation(); + macaddress.setCode(PluginConstants.SyncML.MAC_ADDRESS); + macaddress.setType(Operation.Type.INFO); + deviceInfoOperations.add(macaddress); + + Operation resolution = new Operation(); + resolution.setCode(PluginConstants.SyncML.RESOLUTION); + resolution.setType(Operation.Type.INFO); + deviceInfoOperations.add(resolution); + + Operation deviceName = new Operation(); + deviceName.setCode(PluginConstants.SyncML.DEVICE_NAME); + deviceName.setType(Operation.Type.INFO); + deviceInfoOperations.add(deviceName); + + return deviceInfoOperations; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/OperationCode.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/OperationCode.java new file mode 100644 index 000000000..c586cf619 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/OperationCode.java @@ -0,0 +1,112 @@ +/* + * 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.mdm.mobileservices.windows.operations.util; + +/** + * Maps operation codes to device specific format. + */ +public class OperationCode { + public static enum Info { + DEV_ID("./DevInfo/DevId"), + MANUFACTURER("./DevInfo/Man"), + DEVICE_MODEL("./DevInfo/Mod"), + DM_VERSION("./DevInfo/DmV"), + LANGUAGE("./DevInfo/Lang"), + IMSI("./Vendor/MSFT/DeviceInstanceService/Identity/Identity1/IMSI"), + IMEI("./Vendor/MSFT/DeviceInstanceService/Identity/Identity1/IMEI"), + SOFTWARE_VERSION("./DevDetail/SwV"), + VENDER("./DevDetail/OEM"), + MAC_ADDRESS("./DevDetail/Ext/WLANMACAddress"), + RESOLUTION("./DevDetail/Ext/Microsoft/Resolution"), + DEVICE_NAME("./DevDetail/Ext/Microsoft/DeviceName"), + CHANNEL_URI("./Vendor/MSFT/DMClient/Provider/MobiCDMServer/Push/ChannelURI"), + LOCK_PIN("./Vendor/MSFT/RemoteLock/NewPINValue"), + LOCK_RESET("./Vendor/MSFT/RemoteLock/LockAndResetPIN"), + CAMERA("./Vendor/MSFT/PolicyManager/My/Camera/AllowCamera"), + CAMERA_STATUS("./Vendor/MSFT/PolicyManager/Device/Camera/AllowCamera"), + ENCRYPT_STORAGE_STATUS("./Vendor/MSFT/PolicyManager/Device/Security/RequireDeviceEncryption"), + DEVICE_PASSWORD_STATUS("./Vendor/MSFT/PolicyManager/Device/DeviceLock/DevicePasswordEnabled"), + DEVICE_PASSCODE_DELETE("./Vendor/MSFT/PolicyManager/My/DeviceLock"), + LONGITUDE("./Vendor/MSFT/RemoteFind/Location/Longitude"), + LATITUDE("./Vendor/MSFT/RemoteFind/Location/Latitude"); + + private final String code; + + Info(String code) { + this.code = code; + } + + public String getCode() { + return this.code; + } + + } + + public static enum Command { + DEVICE_RING("./Vendor/MSFT/RemoteRing/Ring"), + DEVICE_LOCK("./Vendor/MSFT/RemoteLock/Lock"), + WIPE_DATA("./Vendor/MSFT/RemoteWipe/doWipe"), + DISENROLL("./Vendor/MSFT/DMClient/Unenroll"), + LOCK_RESET("./Vendor/MSFT/RemoteLock/LockAndResetPIN"), + CAMERA("./Vendor/MSFT/PolicyManager/My/Camera/AllowCamera"), + ENCRYPT_STORAGE("./Vendor/MSFT/PolicyManager/My/Security/RequireDeviceEncryption"), + CAMERA_STATUS("./Vendor/MSFT/PolicyManager/Device/Camera/AllowCamera"), + ENCRYPT_STORAGE_STATUS("./Vendor/MSFT/PolicyManager/Device/Security/RequireDeviceEncryption"), + DEVICE_PASSWORD_ENABLE("./Vendor/MSFT/PolicyManager/My/DeviceLock/DevicePasswordEnabled"); + + private final String code; + + Command(String code) { + this.code = code; + } + + public String getCode() { + return this.code; + } + + } + + public static enum Configure { + WIFI("./Vendor/MSFT/WiFi/Profile/MyNetwork/WlanXml"), + CAMERA("./Vendor/MSFT/PolicyManager/My/Camera/AllowCamera"), + CAMERA_STATUS("./Vendor/MSFT/PolicyManager/Device/Camera/AllowCamera"), + ENCRYPT_STORAGE("./Vendor/MSFT/PolicyManager/My/Security/RequireDeviceEncryption"), + ENCRYPT_STORAGE_STATUS("./Vendor/MSFT/PolicyManager/Device/Security/RequireDeviceEncryption"), + PASSWORD_MAX_FAIL_ATTEMPTS("./Vendor/MSFT/PolicyManager/My/DeviceLock/MaxDevicePasswordFailedAttempts"), + DEVICE_PASSWORD_ENABLE("./Vendor/MSFT/PolicyManager/My/DeviceLock/DevicePasswordEnabled"), + SIMPLE_PASSWORD("./Vendor/MSFT/PolicyManager/My/DeviceLock/AllowSimpleDevicePassword"), + MIN_PASSWORD_LENGTH("./Vendor/MSFT/PolicyManager/My/DeviceLock/MinDevicePasswordLength"), + Alphanumeric_PASSWORD("./Vendor/MSFT/PolicyManager/My/DeviceLock/AlphanumericDevicePasswordRequired"), + PASSWORD_EXPIRE("./Vendor/MSFT/PolicyManager/My/DeviceLock/DevicePasswordExpiration"), + PASSWORD_HISTORY("./Vendor/MSFT/PolicyManager/My/DeviceLock/DevicePasswordHistory"), + MAX_PASSWORD_INACTIVE_TIME("./Vendor/MSFT/PolicyManager/My/DeviceLock/MaxInactivityTimeDeviceLock"), + MIN_PASSWORD_COMPLEX_CHARACTERS("./Vendor/MSFT/PolicyManager/My/DeviceLock/MinDevicePasswordComplexCharacters"); + + private final String code; + + Configure(String code) { + this.code = code; + } + + public String getCode() { + return this.code; + } + + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/OperationReply.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/OperationReply.java new file mode 100644 index 000000000..289bf3a94 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/OperationReply.java @@ -0,0 +1,772 @@ +/* + * 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.mdm.mobileservices.windows.operations.util; + +import com.google.gson.Gson; +import org.json.JSONException; +import org.json.JSONObject; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.wso2.carbon.mdm.mobileservices.windows.common.SyncmlCommandType; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.SyncmlMessageFormatException; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.SyncmlOperationException; +import org.wso2.carbon.mdm.mobileservices.windows.common.util.WindowsAPIUtils; +import org.wso2.carbon.mdm.mobileservices.windows.operations.*; +import org.wso2.carbon.mdm.mobileservices.windows.services.syncml.beans.PasscodePolicy; +import org.wso2.carbon.mdm.mobileservices.windows.services.syncml.beans.Wifi; +import org.wso2.carbon.policy.mgt.common.FeatureManagementException; +import org.wso2.carbon.policy.mgt.common.PolicyManagementException; +import org.wso2.carbon.policy.mgt.common.ProfileFeature; + +import java.util.ArrayList; +import java.util.List; + +import static org.wso2.carbon.mdm.mobileservices.windows.common.util.WindowsAPIUtils.convertToDeviceIdentifierObject; +import static org.wso2.carbon.mdm.mobileservices.windows.operations.util.OperationCode.*; + +/** + * Used to generate a reply to a receiving syncml from a device. + */ +public class OperationReply { + + private SyncmlDocument syncmlDocument; + private SyncmlDocument replySyncmlDocument; + private int headerCommandId = 1; + private static final int HEADER_STATUS_ID = 0; + private static final String RESULTS_COMMAND_TEXT = "Results"; + private static final String HEADER_COMMAND_TEXT = "SyncHdr"; + private static final String ALERT_COMMAND_TEXT = "Alert"; + private static final String REPLACE_COMMAND_TEXT = "Replace"; + private static final String GET_COMMAND_TEXT = "Get"; + private static final String EXEC_COMMAND_TEXT = "Exec"; + private List operations; + Gson gson = new Gson(); + + public OperationReply(SyncmlDocument syncmlDocument, List operations) { + this.syncmlDocument = syncmlDocument; + replySyncmlDocument = new SyncmlDocument(); + this.operations = operations; + } + + public OperationReply(SyncmlDocument syncmlDocument) { + this.syncmlDocument = syncmlDocument; + replySyncmlDocument = new SyncmlDocument(); + } + + public SyncmlDocument generateReply() throws SyncmlMessageFormatException, SyncmlOperationException { + generateHeader(); + generateBody(); + return replySyncmlDocument; + } + + private void generateHeader() throws SyncmlMessageFormatException { + String nextnonceValue = Constants.INITIAL_NONCE; + SyncmlHeader sourceHeader = syncmlDocument.getHeader(); + SyncmlHeader header = new SyncmlHeader(); + header.setMsgID(sourceHeader.getMsgID()); + header.setHexadecimalSessionId(Integer.toHexString(sourceHeader.getSessionId())); + Target target = new Target(); + target.setLocURI(sourceHeader.getSource().getLocURI()); + header.setTarget(target); + + Source source = new Source(); + source.setLocURI(sourceHeader.getTarget().getLocURI()); + header.setSource(source); + + Credential cred = new Credential(); + if (sourceHeader.getCredential() == null) { + MetaTag meta = new MetaTag(); + meta.setFormat(Constants.CRED_FORMAT); + meta.setType(Constants.CRED_TYPE); + cred.setMeta(meta); + } else { + cred.setMeta(sourceHeader.getCredential().getMeta()); + } + SyncmlBody sourcebody = syncmlDocument.getBody(); + List statuses = sourcebody.getStatus(); + + for (Status status : statuses) { + if (HEADER_COMMAND_TEXT.equals(status.getCommand()) && + status.getChallenge() != null) { + nextnonceValue = status.getChallenge().getMeta().getNextNonce(); + } + } + cred.setData(new SyncmlCredentials().generateCredData(nextnonceValue)); + header.setCredential(cred); + + replySyncmlDocument.setHeader(header); + } + + private void generateBody() throws SyncmlMessageFormatException, SyncmlOperationException { + SyncmlBody syncmlBody = generateStatuses(); + try { + appendOperations(syncmlBody); + } catch (PolicyManagementException e) { + throw new SyncmlOperationException("Error occurred while retrieving policy operations.", e); + } catch (FeatureManagementException e) { + throw new SyncmlOperationException("Error occurred while retrieving effective policy operations.", e); + } catch (JSONException e) { + throw new SyncmlMessageFormatException("Error Occurred while parsing operation object.", e); + } + replySyncmlDocument.setBody(syncmlBody); + } + + private SyncmlBody generateStatuses() { + SyncmlBody sourceSyncmlBody = syncmlDocument.getBody(); + SyncmlHeader sourceHeader = syncmlDocument.getHeader(); + Status headerStatus; + SyncmlBody syncmlBodyReply = new SyncmlBody(); + List statuses = new ArrayList<>(); + List sourceStatuses = sourceSyncmlBody.getStatus(); + if (sourceStatuses.isEmpty()) { + headerStatus = + new Status(headerCommandId, sourceHeader.getMsgID(), HEADER_STATUS_ID, + HEADER_COMMAND_TEXT, sourceHeader.getSource().getLocURI(), + String.valueOf(Constants.SyncMLResponseCodes.AUTHENTICATION_ACCEPTED)); + statuses.add(headerStatus); + } else { + for (Status sourceStatus : sourceStatuses) { + if (sourceStatus.getChallenge() != null && HEADER_COMMAND_TEXT.equals(sourceStatus.getCommand())) { + + headerStatus = + new Status(headerCommandId, sourceHeader.getMsgID(), HEADER_STATUS_ID, + HEADER_COMMAND_TEXT, sourceHeader.getSource().getLocURI(), + String.valueOf(Constants.SyncMLResponseCodes.AUTHENTICATION_ACCEPTED)); + statuses.add(headerStatus); + } + } + } + if (sourceSyncmlBody.getResults() != null) { + int ResultCommandId = ++headerCommandId; + Status resultStatus = new Status(ResultCommandId, sourceHeader.getMsgID(), + sourceSyncmlBody.getResults().getCommandId(), RESULTS_COMMAND_TEXT, null, + String.valueOf(Constants.SyncMLResponseCodes.ACCEPTED)); + statuses.add(resultStatus); + } + if (sourceSyncmlBody.getAlert() != null) { + int alertCommandId = ++headerCommandId; + Status alertStatus = new Status(alertCommandId, + sourceHeader.getMsgID(), + sourceSyncmlBody.getAlert().getCommandId(), + ALERT_COMMAND_TEXT, null, + String.valueOf(Constants.SyncMLResponseCodes.ACCEPTED)); + statuses.add(alertStatus); + } + if (sourceSyncmlBody.getReplace() != null) { + int replaceCommandId = ++headerCommandId; + Status replaceStatus = new Status(replaceCommandId, sourceHeader.getMsgID(), + sourceSyncmlBody.getReplace().getCommandId(), REPLACE_COMMAND_TEXT, null, + String.valueOf(Constants.SyncMLResponseCodes.ACCEPTED) + ); + statuses.add(replaceStatus); + } + if (sourceSyncmlBody.getExec() != null) { + List Executes = sourceSyncmlBody.getExec(); + for (ExecuteTag exec : Executes) { + int execCommandId = ++headerCommandId; + Status execStatus = new Status(execCommandId, sourceHeader.getMsgID(), + exec.getCommandId(), EXEC_COMMAND_TEXT, null, String.valueOf( + Constants.SyncMLResponseCodes.ACCEPTED)); + statuses.add(execStatus); + } + } + if (sourceSyncmlBody.getGet() != null) { + int getCommandId = ++headerCommandId; + Status execStatus = new Status(getCommandId, sourceHeader.getMsgID(), sourceSyncmlBody + .getGet().getCommandId(), GET_COMMAND_TEXT, null, String.valueOf( + Constants.SyncMLResponseCodes.ACCEPTED)); + statuses.add(execStatus); + } + syncmlBodyReply.setStatus(statuses); + return syncmlBodyReply; + } + + private void appendOperations(SyncmlBody syncmlBody) throws PolicyManagementException, + FeatureManagementException, JSONException, SyncmlOperationException { + Get getElement = new Get(); + List getElements = new ArrayList<>(); + List executeElements = new ArrayList<>(); + AtomicTag atomicTagElement = new AtomicTag(); + List addElements = new ArrayList<>(); + Replace replaceElement = new Replace(); + List replaceItems = new ArrayList<>(); + SequenceTag monitorSequence = new SequenceTag(); + + if (operations != null) { + for (Operation operation : operations) { + Operation.Type type = operation.getType(); + switch (type) { + case POLICY: + if (this.syncmlDocument.getBody().getAlert() != null) { + if (this.syncmlDocument.getBody().getAlert().getData().equals + (Constants.INITIAL_ALERT_DATA)) { + SequenceTag policySequence = new SequenceTag(); + policySequence = buildSequence(operation, policySequence); + syncmlBody.setSequence(policySequence); + } + } + break; + case CONFIG: + List addConfigurations = appendAddConfiguration(operation); + for (AddTag addConfiguration : addConfigurations) { + addElements.add(addConfiguration); + } + break; + case MESSAGE: + + break; + case INFO: + Item itemGet = appendGetInfo(operation); + getElements.add(itemGet); + break; + case COMMAND: + if (operation.getCode().equals(PluginConstants + .OperationCodes.DEVICE_LOCK)) { + ExecuteTag execElement = executeCommand(operation); + executeElements.add(execElement); + } + if (operation.getCode().equals(PluginConstants + .OperationCodes.DEVICE_RING)) { + ExecuteTag execElement = executeCommand(operation); + executeElements.add(execElement); + } + if (operation.getCode().equals(PluginConstants + .OperationCodes.DISENROLL)) { + ExecuteTag execElement = executeCommand(operation); + executeElements.add(execElement); + } + if (operation.getCode().equals(PluginConstants + .OperationCodes.WIPE_DATA)) { + ExecuteTag execElement = executeCommand(operation); + executeElements.add(execElement); + } + if (operation.getCode().equals(PluginConstants + .OperationCodes.LOCK_RESET)) { + SequenceTag sequenceElement = new SequenceTag(); + SequenceTag sequence = buildSequence(operation, sequenceElement); + syncmlBody.setSequence(sequence); + } + if (operation.getCode().equals(PluginConstants + .OperationCodes.MONITOR)) { + + Get monitorGetElement = new Get(); + List monitorItems; + List profileFeatures; + + if (this.syncmlDocument.getBody().getAlert() != null) { + if (this.syncmlDocument.getBody().getAlert().getData().equals + (Constants.INITIAL_ALERT_DATA)) { + + monitorSequence.setCommandId(operation.getId()); + DeviceIdentifier deviceIdentifier = convertToDeviceIdentifierObject( + syncmlDocument.getHeader().getSource().getLocURI()); + try { + profileFeatures = WindowsAPIUtils.getPolicyManagerService(). + getEffectiveFeatures(deviceIdentifier); + } catch (FeatureManagementException e) { + throw new SyncmlOperationException("Error in getting effective policy.", e); + } + monitorItems = buildMonitorOperation(profileFeatures); + if (!monitorItems.isEmpty()) { + monitorGetElement.setCommandId(operation.getId()); + monitorGetElement.setItems(monitorItems); + } + monitorSequence.setGet(monitorGetElement); + syncmlBody.setSequence(monitorSequence); + } + } + } + break; + } + } + } + if (!replaceItems.isEmpty()) { + replaceElement.setCommandId(300); + replaceElement.setItems(replaceItems); + } + if (!getElements.isEmpty()) { + getElement.setCommandId(75); + getElement.setItems(getElements); + } + if (!addElements.isEmpty()) { + atomicTagElement.setCommandId(400); + atomicTagElement.setAdds(addElements); + } + syncmlBody.setGet(getElement); + syncmlBody.setExec(executeElements); + syncmlBody.setAtomicTag(atomicTagElement); + syncmlBody.setReplace(replaceElement); + } + + private Item appendExecInfo(Operation operation) { + Item item = new Item(); + String operationCode = operation.getCode(); + for (Command command : Command.values()) { + if (operationCode != null && operationCode.equals(command.name())) { + Target target = new Target(); + target.setLocURI(command.getCode()); + if (operation.getCode().equals(PluginConstants + .OperationCodes.DISENROLL)) { + MetaTag meta = new MetaTag(); + meta.setFormat(Constants.META_FORMAT_CHARACTER); + item.setMeta(meta); + item.setData(Constants.PROVIDER_ID); + } + item.setTarget(target); + } + } + return item; + } + + private Item appendGetInfo(Operation operation) { + Item item = new Item(); + String operationCode = operation.getCode(); + for (Info info : Info.values()) { + if (operationCode != null && operationCode.equals(info.name())) { + Target target = new Target(); + target.setLocURI(info.getCode()); + item.setTarget(target); + } + } + if ((operationCode != null) && operationCode.equals( + PluginConstants.OperationCodes.LOCK_RESET)) { + operation.setCode(PluginConstants.OperationCodes.PIN_CODE); + for (Info getInfo : Info.values()) { + if (operation.getCode().equals(getInfo.name())) { + Target target = new Target(); + target.setLocURI(getInfo.getCode()); + item.setTarget(target); + } + } + } + return item; + } + + private Item appendReplaceInfo(Operation operation) throws JSONException { + String policyAllowData = "1"; + String policyDisallowData = "0"; + Item item = new Item(); + Target target = new Target(); + String operationCode = operation.getCode(); + JSONObject payload = new JSONObject(operation.getPayLoad().toString()); + for (Command command : Command.values()) { + + if (operationCode != null && operationCode.equals(command.name())) { + target.setLocURI(command.getCode()); + + if (operation.getCode().equals(PluginConstants + .OperationCodes.CAMERA)) { + + if (payload.getBoolean("enabled")) { + MetaTag meta = new MetaTag(); + meta.setFormat(Constants.META_FORMAT_INT); + item.setTarget(target); + item.setMeta(meta); + item.setData(policyAllowData); + } else { + MetaTag meta = new MetaTag(); + meta.setFormat(Constants.META_FORMAT_INT); + item.setTarget(target); + item.setMeta(meta); + item.setData(policyDisallowData); + } + } + if (operation.getCode().equals(PluginConstants + .OperationCodes.ENCRYPT_STORAGE)) { + + if (payload.getBoolean("encrypted")) { + MetaTag meta = new MetaTag(); + meta.setFormat(Constants.META_FORMAT_INT); + item.setTarget(target); + item.setMeta(meta); + item.setData(policyAllowData); + } else { + MetaTag meta = new MetaTag(); + meta.setFormat(Constants.META_FORMAT_INT); + item.setTarget(target); + item.setMeta(meta); + item.setData(policyDisallowData); + } + } + } + } + return item; + } + + private List appendAddInfo(Operation operation) throws WindowsOperationException { + + List addList = new ArrayList<>(); + Gson gson = new Gson(); + + if (operation.getCode().equals(PluginConstants.OperationCodes.PASSCODE_POLICY)) { + + PasscodePolicy passcodeObject = gson.fromJson((String) operation.getPayLoad(), PasscodePolicy.class); + + for (Configure configure : Configure.values()) { + + if (operation.getCode() != null && PluginConstants.OperationCodes.PASSWORD_MAX_FAIL_ATTEMPTS. + equals(configure.name())) { + AddTag add = generatePasscodePolicyData(configure, passcodeObject.getMaxFailedAttempts()); + addList.add(add); + } + if (operation.getCode() != null && (PluginConstants.OperationCodes.DEVICE_PASSWORD_ENABLE. + equals(configure.name()) || PluginConstants.OperationCodes.SIMPLE_PASSWORD. + equals(configure.name()) || PluginConstants.OperationCodes.ALPHANUMERIC_PASSWORD. + equals(configure.name()))) { + AddTag add = generatePasscodeBooleanData(operation, configure); + addList.add(add); + } + if (operation.getCode() != null && PluginConstants.OperationCodes.MIN_PASSWORD_LENGTH. + equals(configure.name())) { + AddTag add = generatePasscodePolicyData(configure, passcodeObject.getMinLength()); + addList.add(add); + } + if (operation.getCode() != null && PluginConstants.OperationCodes.PASSWORD_EXPIRE. + equals(configure.name())) { + AddTag add = generatePasscodePolicyData(configure, passcodeObject.getMaxPINAgeInDays()); + addList.add(add); + } + if (operation.getCode() != null && PluginConstants.OperationCodes.PASSWORD_HISTORY. + equals(configure.name())) { + int pinHistory = passcodeObject.getPinHistory(); + AddTag add = generatePasscodePolicyData(configure, pinHistory); + addList.add(add); + } + if (operation.getCode() != null && PluginConstants.OperationCodes.MAX_PASSWORD_INACTIVE_TIME. + equals(configure.name())) { + AddTag add = generatePasscodePolicyData(configure, passcodeObject.getMaxInactiveTime()); + addList.add(add); + } + if (operation.getCode() != null && PluginConstants.OperationCodes.MIN_PASSWORD_COMPLEX_CHARACTERS. + equals(configure.name())) { + int complexChars = passcodeObject.getMinComplexChars(); + AddTag add = generatePasscodePolicyData(configure, complexChars); + addList.add(add); + } + } + } + return addList; + } + + private List appendAddConfiguration(Operation operation) { + + List addList = new ArrayList<>(); + Gson gson = new Gson(); + + if (SyncmlCommandType.WIFI.getValue().equals(operation.getCode())) { + AddTag add = new AddTag(); + String operationCode = operation.getCode(); + Wifi wifiObject = gson.fromJson((String) operation.getPayLoad(), Wifi.class); + String data = "<?xml version="1.0"?><WLANProfile" + + "xmlns="http://www.microsoft.com/networking/WLAN/profile/v1"><name>" + + wifiObject.getNetworkName() + "</name><SSIDConfig><SSID><name>" + + wifiObject.getSsid() + "</name></SSID></SSIDConfig><connectionType>" + + wifiObject.getConnectionType() + "</connectionType><connectionMode>" + + wifiObject.getConnectionMode() + "</connectionMode><MSM><security><" + + "authEncryption><authentication>" + wifiObject.getAuthentication() + + "</authentication><encryption>" + wifiObject.getEncryption() + + "</encryption></authEncryption><sharedKey><keyType>" + + wifiObject.getKeyType() + "</keyType><protected>" + wifiObject.getProtection() + + "</protected><keyMaterial>" + wifiObject.getKeyMaterial() + + "</keyMaterial></sharedKey></security></MSM></WLANProfile>"; + + MetaTag meta = new MetaTag(); + meta.setFormat(Constants.META_FORMAT_CHARACTER); + List items = new ArrayList<>(); + + for (Configure configure : Configure.values()) { + if (operationCode != null && operationCode.equals(configure.name())) { + Target target = new Target(); + target.setLocURI(configure.getCode()); + items.get(0).setTarget(target); + } + } + items.get(0).setMeta(meta); + items.get(0).setData(data); + + add.setCommandId(301); + add.setItems(items); + addList.add(add); + return addList; + } + return null; + } + + public ExecuteTag executeCommand(Operation operation) { + ExecuteTag execElement = new ExecuteTag(); + execElement.setCommandId(operation.getId()); + List itemsExec = new ArrayList<>(); + Item itemExec = appendExecInfo(operation); + itemsExec.add(itemExec); + execElement.setItems(itemsExec); + return execElement; + } + + public SequenceTag buildSequence(Operation operation, SequenceTag sequenceElement) throws + JSONException, SyncmlOperationException { + + sequenceElement.setCommandId(operation.getId()); + List replaceItems = new ArrayList<>(); + + if (operation.getCode().equals(PluginConstants.OperationCodes.LOCK_RESET)) { + ExecuteTag execElement = executeCommand(operation); + Get getElements = new Get(); + getElements.setCommandId(operation.getId()); + List getItems = new ArrayList<>(); + Item itemGets = appendGetInfo(operation); + getItems.add(itemGets); + getElements.setItems(getItems); + + sequenceElement.setExec(execElement); + sequenceElement.setGet(getElements); + return sequenceElement; + + } else if (operation.getCode().equals(PluginConstants.OperationCodes.POLICY_BUNDLE)) { + List policyOperations; + try { + policyOperations = (List) operation.getPayLoad(); + } catch (ClassCastException e) { + throw new ClassCastException(); + } + for (Operation policy : policyOperations) { + if (policy.getCode().equals(PluginConstants.OperationCodes.CAMERA)) { + Replace replaceCameraConfig = new Replace(); + Item cameraItem; + List cameraItems = new ArrayList<>(); + try { + cameraItem = appendReplaceInfo(policy); + cameraItems.add(cameraItem); + } catch (JSONException e) { + throw new SyncmlOperationException("Error occurred while parsing payload object to json.", e); + } + replaceCameraConfig.setCommandId(operation.getId()); + replaceCameraConfig.setItems(cameraItems); + replaceItems.add(replaceCameraConfig); + } + if (policy.getCode().equals(PluginConstants.OperationCodes.ENCRYPT_STORAGE)) { + + Replace replaceStorageConfig = new Replace(); + Item storageItem; + List storageItems = new ArrayList<>(); + try { + storageItem = appendReplaceInfo(policy); + storageItems.add(storageItem); + } catch (JSONException e) { + throw new SyncmlOperationException("Error occurred while parsing payload object to json.", e); + } + replaceStorageConfig.setCommandId(operation.getId()); + replaceStorageConfig.setItems(storageItems); + replaceItems.add(replaceStorageConfig); + + } + if (policy.getCode().equals(PluginConstants.OperationCodes.PASSCODE_POLICY)) { + AtomicTag atomicTagElement = new AtomicTag(); + List addConfig; + try { + addConfig = appendAddInfo(policy); + atomicTagElement.setAdds(addConfig); + atomicTagElement.setCommandId(operation.getId()); + + sequenceElement.setAtomicTag(atomicTagElement); + } catch (WindowsOperationException e) { + throw new SyncmlOperationException("Error occurred while generating operation payload.", e); + } + } + } + if (!replaceItems.isEmpty()) { + sequenceElement.setReplaces(replaceItems); + } + return sequenceElement; + + } else { + return null; + } + } + + public List buildMonitorOperation(List effectiveMonitoringFeature) { + List monitorItems = new ArrayList<>(); + Operation monitorOperation; + for (ProfileFeature profileFeature : effectiveMonitoringFeature) { + + if (profileFeature.getFeatureCode().equals(PluginConstants + .OperationCodes.CAMERA)) { + String cameraStatus = PluginConstants + .OperationCodes.CAMERA_STATUS; + + monitorOperation = new Operation(); + monitorOperation.setCode(cameraStatus); + Item item = appendGetInfo(monitorOperation); + monitorItems.add(item); + } + if (profileFeature.getFeatureCode().equals(PluginConstants + .OperationCodes.ENCRYPT_STORAGE)) { + String encryptStorageStatus = PluginConstants + .OperationCodes.ENCRYPT_STORAGE_STATUS; + + monitorOperation = new Operation(); + monitorOperation.setCode(encryptStorageStatus); + Item item = appendGetInfo(monitorOperation); + monitorItems.add(item); + } + if (profileFeature.getFeatureCode().equals(PluginConstants + .OperationCodes.PASSCODE_POLICY)) { + String passcodeStatus = PluginConstants + .OperationCodes.DEVICE_PASSWORD_STATUS; + + monitorOperation = new Operation(); + monitorOperation.setCode(passcodeStatus); + Item item = appendGetInfo(monitorOperation); + monitorItems.add(item); + } + } + return monitorItems; + } + + public List buildDeleteInfo(Operation operation) { + List deleteItems = new ArrayList<>(); + Item deleteItem = new Item(); + Target target = new Target(); + String operationCode = operation.getCode(); + if (operation.getCode().equals(PluginConstants.OperationCodes.PASSCODE_POLICY)) { + operation.setCode(PluginConstants.OperationCodes.DEVICE_PASSCODE_DELETE); + for (Command command : Command.values()) { + + if (operationCode != null && operationCode.equals(command.name())) { + target.setLocURI(command.getCode()); + deleteItem.setTarget(target); + } + } + } + return deleteItems; + } + + public AddTag generatePasscodePolicyData(Configure configure, int policyData) { + String attempt = String.valueOf(policyData); + AddTag add = new AddTag(); + List itemList = new ArrayList<>(); + Item item = new Item(); + Target target = new Target(); + target.setLocURI(configure.getCode()); + MetaTag meta = new MetaTag(); + meta.setFormat(Constants.META_FORMAT_INT); + item.setTarget(target); + item.setMeta(meta); + item.setData(attempt); + itemList.add(item); + add.setCommandId(90); + add.setItems(itemList); + return add; + } + + public AddTag generatePasscodeBooleanData(Operation operation, Configure configure) { + Target target = new Target(); + MetaTag meta = new MetaTag(); + AddTag add = new AddTag(); + + PasscodePolicy passcodePolicy = gson.fromJson((String) operation.getPayLoad(), PasscodePolicy.class); + if (operation.getCode() != null && (PluginConstants.OperationCodes.DEVICE_PASSWORD_ENABLE. + equals(configure.name()))) { + if (passcodePolicy.isEnablePassword()) { + target.setLocURI(configure.getCode()); + meta.setFormat(Constants.META_FORMAT_INT); + List itemList = new ArrayList<>(); + Item item = new Item(); + item.setTarget(target); + item.setMeta(meta); + item.setData("0"); + itemList.add(item); + + add.setCommandId(operation.getId()); + add.setItems(itemList); + + } else { + target.setLocURI(configure.getCode()); + meta.setFormat(Constants.META_FORMAT_INT); + List itemList = new ArrayList<>(); + Item item = new Item(); + item.setTarget(target); + item.setMeta(meta); + item.setData("1"); + itemList.add(item); + add.setCommandId(operation.getId()); + add.setItems(itemList); + + } + } + if (PluginConstants.OperationCodes.ALPHANUMERIC_PASSWORD. + equals(configure.name())) { + if (passcodePolicy.isRequireAlphanumeric()) { + Item item = new Item(); + target.setLocURI(configure.getCode()); + meta.setFormat(Constants.META_FORMAT_INT); + List itemList = new ArrayList<>(); + item.setTarget(target); + item.setMeta(meta); + item.setData("1"); + itemList.add(item); + add.setCommandId(operation.getId()); + add.setItems(itemList); + } else { + target.setLocURI(configure.getCode()); + meta.setFormat(Constants.META_FORMAT_INT); + List itemList = new ArrayList<>(); + Item item = new Item(); + item.setTarget(target); + item.setMeta(meta); + item.setData("0"); + itemList.add(item); + add.setCommandId(operation.getId()); + add.setItems(itemList); + } + } + if (PluginConstants.OperationCodes.SIMPLE_PASSWORD. + equals(configure.name())) { + if (passcodePolicy.isAllowSimple()) { + Item item = new Item(); + target.setLocURI(configure.getCode()); + meta.setFormat(Constants.META_FORMAT_INT); + List itemList = new ArrayList<>(); + item.setTarget(target); + item.setMeta(meta); + item.setData("1"); + itemList.add(item); + add.setCommandId(operation.getId()); + add.setItems(itemList); + + } else { + Item item = new Item(); + target.setLocURI(configure.getCode()); + meta.setFormat(Constants.META_FORMAT_INT); + List itemList = new ArrayList<>(); + item.setTarget(target); + item.setMeta(meta); + item.setData("0"); + itemList.add(item); + add.setCommandId(operation.getId()); + add.setItems(itemList); + } + } + return add; + } + +} + + + diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/OperationUtils.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/OperationUtils.java new file mode 100644 index 000000000..4f223df0c --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/OperationUtils.java @@ -0,0 +1,505 @@ +/* + * 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.mdm.mobileservices.windows.operations.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.json.JSONObject; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.FeatureManagementException; +import org.wso2.carbon.device.mgt.common.notification.mgt.Notification; +import org.wso2.carbon.device.mgt.common.notification.mgt.NotificationManagementException; +import org.wso2.carbon.device.mgt.common.notification.mgt.NotificationManagementService; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.common.operation.mgt.OperationManagementException; +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsDeviceEnrolmentException; +import org.wso2.carbon.mdm.mobileservices.windows.common.util.WindowsAPIUtils; +import org.wso2.carbon.mdm.mobileservices.windows.operations.*; +import org.wso2.carbon.mdm.mobileservices.windows.services.syncml.beans.Profile; +import org.wso2.carbon.policy.mgt.common.ProfileFeature; +import org.wso2.carbon.policy.mgt.common.monitor.ComplianceFeature; +import org.wso2.carbon.policy.mgt.common.monitor.PolicyComplianceException; + +import java.util.ArrayList; +import java.util.List; + +import static org.wso2.carbon.mdm.mobileservices.windows.common.util.WindowsAPIUtils.convertToDeviceIdentifierObject; + +/** + * Class contains Operation related utilities. + */ +public class OperationUtils { + private static Log log = LogFactory.getLog(OperationUtils.class); + List pendingDataOperations; + + + /** + * Update the operations using device status payload. + * + * @param status Client side status for the specific operations + * @param syncmlDocument syncml payload for operation status which parse through the syncml engine + * @param deviceIdentifier specific device identifier for each device + * @throws OperationManagementException + * @throws DeviceManagementException + */ + public void updateDeviceOperations(Status status, SyncmlDocument syncmlDocument, + DeviceIdentifier deviceIdentifier) + throws OperationManagementException, DeviceManagementException, NotificationManagementException, + WindowsOperationException { + + pendingDataOperations = WindowsAPIUtils.getDeviceManagementService() + .getOperationsByDeviceAndStatus(deviceIdentifier, Operation.Status.PENDING); + if (status.getData().equals(Constants.SyncMLResponseCodes.ACCEPTED) || status.getData().equals + (Constants.SyncMLResponseCodes.ACCEPTED_FOR_PROCESSING)) { + for (Operation operation : pendingDataOperations) { + if (operation.getId() == status.getCommandReference()) { + operation.setStatus(Operation.Status.COMPLETED); + } + } + updateOperations(syncmlDocument.getHeader().getSource().getLocURI(), pendingDataOperations); + } else if (status.getData().equals(Constants.SyncMLResponseCodes.PIN_NOTFOUND)) { + for (Operation operation : pendingDataOperations) { + if (operation.getId() == status.getCommandReference() && operation. + getCode().equals(String.valueOf(OperationCode.Command.DEVICE_LOCK))) { + operation.setStatus(Operation.Status.ERROR); + updateOperations(syncmlDocument.getHeader().getSource().getLocURI(), pendingDataOperations); + try { + NotificationManagementService nmService = WindowsAPIUtils.getNotificationManagementService(); + Notification lockResetNotification = new Notification(); + lockResetNotification.setOperationId(status.getCommandReference()); + lockResetNotification.setStatus(String.valueOf(Notification.Status.NEW)); + lockResetNotification.setDeviceIdentifier(deviceIdentifier); + lockResetNotification.setDescription( + Constants.SyncMLResponseCodes.LOCKRESET_NOTIFICATION); + nmService.addNotification(lockResetNotification); + } catch (NotificationManagementException e) { + throw new WindowsOperationException("Failure occurred in getting notification service", e); + } + } + } + } + } + + /** + * Update operation statuses + * + * @param deviceId specific device Id + * @param operations operation list to be update + * @throws OperationManagementException + */ + public void updateOperations(String deviceId, + List operations) + throws OperationManagementException { + + for (Operation operation : operations) { + WindowsAPIUtils.updateOperation(deviceId, operation); + if (log.isDebugEnabled()) { + log.debug("Updating operation '" + operation.toString() + "'"); + } + } + } + + /** + * Update Status of the lock operation. + * + * @param status Status of the operation. + * @param syncmlDocument parsed syncml payload. + * @param deviceIdentifier Device Id. + * @throws OperationManagementException + * @throws DeviceManagementException + * @throws NotificationManagementException + */ + public void lockOperationUpdate(Status status, SyncmlDocument syncmlDocument, DeviceIdentifier deviceIdentifier) + throws OperationManagementException, DeviceManagementException, NotificationManagementException { + + pendingDataOperations = WindowsAPIUtils.getDeviceManagementService() + .getOperationsByDeviceAndStatus(deviceIdentifier, Operation.Status.PENDING); + if (status.getData().equals(Constants.SyncMLResponseCodes.ACCEPTED)) { + for (Operation operation : pendingDataOperations) { + if (operation.getCode().equals(OperationCode.Command.DEVICE_LOCK.getCode()) + && operation.getId() == status.getCommandReference()) { + operation.setStatus(Operation.Status.COMPLETED); + new OperationUtils().updateOperations(syncmlDocument.getHeader().getSource().getLocURI(), + pendingDataOperations); + } + } + } + if (status.getData().equals(Constants.SyncMLResponseCodes.PIN_NOTFOUND)) { + for (Operation operation : pendingDataOperations) { + + if (operation.getCode().equals(OperationCode.Command.DEVICE_LOCK.getCode()) && + operation.getId() == status.getCommandReference()) { + operation.setStatus(Operation.Status.ERROR); + new OperationUtils().updateOperations(syncmlDocument.getHeader().getSource().getLocURI(), + pendingDataOperations); + try { + NotificationManagementService nmService = WindowsAPIUtils.getNotificationManagementService(); + Notification lockResetNotification = new Notification(); + lockResetNotification.setOperationId(status.getCommandReference()); + lockResetNotification.setStatus(String.valueOf(Notification.Status.NEW)); + lockResetNotification.setDeviceIdentifier(deviceIdentifier); + lockResetNotification.setDescription(Constants.SyncMLResponseCodes.LOCKRESET_NOTIFICATION); + + nmService.addNotification(lockResetNotification); + } catch (NotificationManagementException e) { + String msg = "Failure occurred in getting notification service"; + log.error(msg, e); + throw new NotificationManagementException(msg, e); + } + } + } + } + } + + /*** + * Update status of the ring operation. + * + * @param status Ring status of the device. + * @param syncmlDocument Parsed syncml payload from the syncml engine. + * @param deviceIdentifier specific device id to be update. + * @throws OperationManagementException + * @throws DeviceManagementException + */ + public void ring(Status status, SyncmlDocument syncmlDocument, + DeviceIdentifier deviceIdentifier) + throws OperationManagementException, DeviceManagementException { + + if (status.getData().equals(Constants.SyncMLResponseCodes.ACCEPTED)) { + pendingDataOperations = WindowsAPIUtils.getDeviceManagementService() + .getOperationsByDeviceAndStatus(deviceIdentifier, Operation.Status.PENDING); + for (Operation operation : pendingDataOperations) { + if (operation.getCode().equals(OperationCode.Command.DEVICE_RING) && + (operation.getId() == status.getCommandReference())) { + operation.setStatus(Operation.Status.COMPLETED); + new OperationUtils().updateOperations(syncmlDocument.getHeader().getSource().getLocURI(), + pendingDataOperations); + } + } + } + } + + /*** + * Update the status of the DataWipe operation. + * + * @param status Status of the datawipe. + * @param syncmlDocument Parsed syncml payload from the syncml engine. + * @param deviceIdentifier specific device id to be wiped. + * @throws OperationManagementException + * @throws DeviceManagementException + */ + public void dataWipe(Status status, SyncmlDocument syncmlDocument, + DeviceIdentifier deviceIdentifier) + throws OperationManagementException, DeviceManagementException { + + if (status.getData().equals(Constants.SyncMLResponseCodes.ACCEPTED)) { + pendingDataOperations = WindowsAPIUtils.getDeviceManagementService() + .getOperationsByDeviceAndStatus(deviceIdentifier, Operation.Status.PENDING); + for (Operation operation : pendingDataOperations) { + + if (operation.getCode().equals(OperationCode.Command.WIPE_DATA) && + (operation.getId() == status.getCommandReference())) { + operation.setStatus(Operation.Status.COMPLETED); + updateOperations(syncmlDocument.getHeader().getSource().getLocURI(), + pendingDataOperations); + } + } + } + } + + /** + * Get pending operations. + * + * @param syncmlDocument SyncmlDocument object which creates from the syncml engine using syncml payload + * @return Return list of pending operations. + * @throws OperationManagementException + * @throws DeviceManagementException + * @throws FeatureManagementException + * @throws PolicyComplianceException + * @throws NotificationManagementException + */ + public List getPendingOperations(SyncmlDocument syncmlDocument) + throws OperationManagementException, DeviceManagementException, FeatureManagementException, + PolicyComplianceException, NotificationManagementException, WindowsDeviceEnrolmentException, + WindowsOperationException { + + List pendingOperations; + DeviceIdentifier deviceIdentifier = convertToDeviceIdentifierObject( + syncmlDocument.getHeader().getSource().getLocURI()); + UpdateUriOperations(syncmlDocument); + generateComplianceFeatureStatus(syncmlDocument); + + pendingOperations = WindowsAPIUtils.getDeviceManagementService().getPendingOperations(deviceIdentifier); + return pendingOperations; + } + + /** + * Set compliance of the feature according to the device status for the specific feature. + * + * @param activeFeature + * @param deviceFeature + * @return Returns setting up compliance feature. + */ + public ComplianceFeature setComplianceFeatures(ProfileFeature activeFeature, Profile deviceFeature) { + ComplianceFeature complianceFeature = new ComplianceFeature(); + complianceFeature.setFeature(activeFeature); + complianceFeature.setFeatureCode(activeFeature.getFeatureCode()); + complianceFeature.setCompliance(deviceFeature.isCompliance()); + return complianceFeature; + } + + /** + * Update the completed/Error status of the operation which have the URI of the operation code in the syncml payload. + * + * @param syncmlDocument SyncmlDocument object generated from the the syncml engine. + * @throws DeviceManagementException + * @throws NotificationManagementException + * @throws OperationManagementException + */ + public void UpdateUriOperations(SyncmlDocument syncmlDocument) throws DeviceManagementException, + NotificationManagementException, OperationManagementException, WindowsOperationException { + DeviceIdentifier deviceIdentifier = convertToDeviceIdentifierObject( + syncmlDocument.getHeader().getSource().getLocURI()); + List statuses = syncmlDocument.getBody().getStatus(); + OperationUtils operationUtils = new OperationUtils(); + + for (Status status : statuses) { + + if (status.getCommand().equals(Constants.EXECUTE)) { + if (status.getTargetReference() == null) { + operationUtils.updateDeviceOperations(status, syncmlDocument, deviceIdentifier); + } else { + if (status.getTargetReference().equals(OperationCode.Command.DEVICE_LOCK)) { + operationUtils.lockOperationUpdate(status, syncmlDocument, deviceIdentifier); + } + if (status.getTargetReference().equals(OperationCode.Command.DEVICE_RING)) { + operationUtils.ring(status, syncmlDocument, deviceIdentifier); + } + if (status.getTargetReference().equals(OperationCode.Command.WIPE_DATA)) { + operationUtils.dataWipe(status, syncmlDocument, deviceIdentifier); + } + } + } + if (status.getCommand().equals(Constants.SEQUENCE)) { + if (status.getData().equals(Constants.SyncMLResponseCodes.ACCEPTED)) { + + pendingDataOperations = WindowsAPIUtils.getDeviceManagementService() + .getOperationsByDeviceAndStatus(deviceIdentifier, Operation.Status.PENDING); + for (Operation operation : pendingDataOperations) { + if (operation.getCode().equals(PluginConstants.OperationCodes.POLICY_BUNDLE) && + operation.getId() == status.getCommandReference()) { + operation.setStatus(Operation.Status.COMPLETED); + } + if (operation.getCode().equals(PluginConstants.OperationCodes.MONITOR) && + operation.getId() == status.getCommandReference()) { + operation.setStatus(Operation.Status.COMPLETED); + } + } + operationUtils.updateOperations(syncmlDocument.getHeader().getSource().getLocURI(), + pendingDataOperations); + } else { + pendingDataOperations = WindowsAPIUtils.getDeviceManagementService() + .getOperationsByDeviceAndStatus(deviceIdentifier, Operation.Status.PENDING); + for (Operation operation : pendingDataOperations) { + + if (operation.getCode().equals(PluginConstants.OperationCodes.POLICY_BUNDLE) && + operation.getId() == status.getCommandReference()) { + operation.setStatus(Operation.Status.ERROR); + } + if (operation.getCode().equals(PluginConstants.OperationCodes.MONITOR) && + operation.getId() == status.getCommandReference()) { + operation.setStatus(Operation.Status.ERROR); + } + } + operationUtils.updateOperations(syncmlDocument.getHeader().getSource().getLocURI(), + pendingDataOperations); + } + } + } + } + + /** + * Generate status of the features that have been activated on the device. + * + * @param syncmlDocument syncmlDocument object pasrsed from the syncml engine. + * @return device statuses for the activated features + * @throws NotificationManagementException + */ + public List generateDeviceOperationStatusObject(SyncmlDocument syncmlDocument) throws + NotificationManagementException, WindowsOperationException { + + DeviceIdentifier deviceIdentifier = convertToDeviceIdentifierObject( + syncmlDocument.getHeader().getSource().getLocURI()); + String lockUri = null; + Results result = syncmlDocument.getBody().getResults(); + + List profiles = new ArrayList<>(); + if (result != null) { + List results = result.getItem(); + for (OperationCode.Info info : OperationCode.Info.values()) { + if (PluginConstants.OperationCodes.PIN_CODE.equals(info + .name())) { + lockUri = info.getCode(); + } + } + for (Item item : results) { + for (OperationCode.Info info : OperationCode.Info.values()) { + if (item.getSource().getLocURI().equals(info.getCode()) && info.name().equals( + PluginConstants.OperationCodes.CAMERA_STATUS)) { + Profile cameraProfile = new Profile(); + cameraProfile.setFeatureCode(PluginConstants.OperationCodes.CAMERA); + cameraProfile.setData(item.getData()); + if (item.getData().equals(PluginConstants.SyncML.SYNCML_DATA_ONE)) { + cameraProfile.setEnable(true); + } else { + cameraProfile.setEnable(false); + } + profiles.add(cameraProfile); + } + if (item.getSource().getLocURI().equals(info.getCode()) && info.name().equals( + PluginConstants.OperationCodes.ENCRYPT_STORAGE_STATUS)) { + Profile encryptStorage = new Profile(); + encryptStorage.setFeatureCode(PluginConstants.OperationCodes.ENCRYPT_STORAGE); + encryptStorage.setData(item.getData()); + if (item.getData().equals(PluginConstants.SyncML.SYNCML_DATA_ONE)) { + encryptStorage.setEnable(true); + } else { + encryptStorage.setEnable(false); + } + profiles.add(encryptStorage); + } + if (item.getSource().getLocURI().equals(info.getCode()) && info.name().equals( + PluginConstants.OperationCodes.DEVICE_PASSWORD_STATUS)) { + Profile encryptStorage = new Profile(); + encryptStorage.setFeatureCode(PluginConstants.OperationCodes.PASSCODE_POLICY); + encryptStorage.setData(item.getData()); + if (item.getData().equals(PluginConstants.SyncML.SYNCML_DATA_ZERO)) { + encryptStorage.setEnable(true); + } else { + encryptStorage.setEnable(false); + } + profiles.add(encryptStorage); + } + if (!item.getData().isEmpty() && item.getSource().getLocURI().equals(lockUri)) { + String pinValue = item.getData(); + NotificationManagementService nmService = WindowsAPIUtils.getNotificationManagementService(); + Notification notification = new Notification(); + notification.setDescription("Auto generated DevicePin : " + pinValue); + notification.setOperationId(result.getCommandReference()); + notification.setDeviceIdentifier(deviceIdentifier); + notification.setStatus(String.valueOf(Notification.Status.NEW)); + try { + nmService.addNotification(notification); + } catch (NotificationManagementException e) { + String msg = "Failure Occurred in getting notification service."; + log.error(msg, e); + throw new WindowsOperationException(msg, e); + } + break; + } + } + } + } + return profiles; + } + + /** + * Generate Compliance Features + * + * @param syncmlDocument syncmlDocument object parsed from the syncml engine. + * @throws NotificationManagementException + * @throws FeatureManagementException + * @throws PolicyComplianceException + */ + public void generateComplianceFeatureStatus(SyncmlDocument syncmlDocument) throws NotificationManagementException, + FeatureManagementException, PolicyComplianceException, WindowsDeviceEnrolmentException, + WindowsOperationException { + List profiles = generateDeviceOperationStatusObject(syncmlDocument); + DeviceIdentifier deviceIdentifier = convertToDeviceIdentifierObject( + syncmlDocument.getHeader().getSource().getLocURI()); + boolean isCompliance = false; + if (profiles.size() != Constants.EMPTY) { + try { + List profileFeatures = WindowsAPIUtils.getPolicyManagerService().getEffectiveFeatures( + deviceIdentifier); + List complianceFeatures = new ArrayList<>(); + for (ProfileFeature activeFeature : profileFeatures) { + JSONObject policyContent = new JSONObject(activeFeature.getContent().toString()); + + for (Profile deviceFeature : profiles) { + if (deviceFeature.getFeatureCode().equals(activeFeature.getFeatureCode()) && + deviceFeature.getFeatureCode().equals(PluginConstants.OperationCodes.CAMERA)) { + if (policyContent.getBoolean(PluginConstants.PolicyConfigProperties. + POLICY_ENABLE) == (deviceFeature.isEnable())) { + isCompliance = true; + deviceFeature.setCompliance(isCompliance); + } else { + deviceFeature.setCompliance(isCompliance); + } + ComplianceFeature complianceFeature = setComplianceFeatures(activeFeature, deviceFeature); + complianceFeatures.add(complianceFeature); + } + if (deviceFeature.getFeatureCode().equals(activeFeature.getFeatureCode()) && + deviceFeature.getFeatureCode().equals(PluginConstants.OperationCodes. + ENCRYPT_STORAGE)) { + if (policyContent.getBoolean(PluginConstants.PolicyConfigProperties. + ENCRYPTED_ENABLE) == (deviceFeature.isEnable())) { + isCompliance = true; + deviceFeature.setCompliance(isCompliance); + } else { + deviceFeature.setCompliance(isCompliance); + } + ComplianceFeature complianceFeature = setComplianceFeatures(activeFeature, deviceFeature); + complianceFeatures.add(complianceFeature); + } + if (deviceFeature.getFeatureCode().equals(activeFeature.getFeatureCode()) && + deviceFeature.getFeatureCode().equals(PluginConstants.OperationCodes. + PASSCODE_POLICY)) { + if (policyContent.getBoolean(PluginConstants.PolicyConfigProperties. + ENABLE_PASSWORD) == (deviceFeature.isEnable())) { + isCompliance = true; + deviceFeature.setCompliance(isCompliance); + } else { + deviceFeature.setCompliance(isCompliance); + } + ComplianceFeature complianceFeature = setComplianceFeatures(activeFeature, deviceFeature); + complianceFeatures.add(complianceFeature); + } + } + } + WindowsAPIUtils.getPolicyManagerService().checkPolicyCompliance(deviceIdentifier, complianceFeatures); + } catch (org.wso2.carbon.policy.mgt.common.FeatureManagementException e) { + String msg = "Error occurred while getting effective policy."; + log.error(msg, e); + throw new FeatureManagementException(msg, e); + } catch (JSONException e) { + String msg = "Error occurred while parsing json object."; + log.error(msg); + throw new WindowsDeviceEnrolmentException(msg, e); + } catch (PolicyComplianceException e) { + String msg = "Error occurred while setting up policy compliance."; + log.error(msg, e); + throw new PolicyComplianceException(msg, e); + } + } + + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/SyncmlCredentials.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/SyncmlCredentials.java new file mode 100644 index 000000000..c807a23b1 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/SyncmlCredentials.java @@ -0,0 +1,70 @@ +/* + * 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.mdm.mobileservices.windows.operations.util; + +import org.apache.commons.codec.binary.Base64; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.SyncmlMessageFormatException; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Class for generate security token for client and server. + */ +public class SyncmlCredentials { + + public String generateCredData(String nextNonce) throws SyncmlMessageFormatException { + MessageDigest digest; + String usrPwdNonceHash; + String nonce; + try { + nonce = new String(Base64.decodeBase64(nextNonce), Constants.UTF_8); + digest = MessageDigest.getInstance(Constants.MD5); + String usrPwd = Constants.PROVIDER_ID + ":" + Constants.SERVER_SECRET; + String usrPwdHash = Base64.encodeBase64String(digest.digest(usrPwd.getBytes(Constants.UTF_8))); + String usrPwdNonce = usrPwdHash + ":" + nonce; + usrPwdNonceHash = Base64.encodeBase64String(digest.digest(usrPwdNonce.getBytes(Constants.UTF_8))); + } catch (UnsupportedEncodingException e) { + throw new SyncmlMessageFormatException("Problem occurred in encoding credentials data.", e); + } catch (NoSuchAlgorithmException e) { + throw new SyncmlMessageFormatException("Problem occurred in generating password hash.", e); + } + return usrPwdNonceHash; + } + + public String generateRST(String username, String password) throws SyncmlMessageFormatException { + MessageDigest digest; + String usrPwdNonceHash; + String nonce; + try { + nonce = new String(Base64.decodeBase64(Constants.INITIAL_NONCE), Constants.UTF_8); + digest = MessageDigest.getInstance(Constants.MD5); + String usrPwd = username + ":" + password; + String usrPwdHash = Base64.encodeBase64String(digest.digest(usrPwd.getBytes(Constants.UTF_8))); + String usrPwdNonce = usrPwdHash + ":" + nonce; + usrPwdNonceHash = Base64.encodeBase64String(digest.digest(usrPwdNonce.getBytes(Constants.UTF_8))); + } catch (UnsupportedEncodingException e) { + throw new SyncmlMessageFormatException("Problem occurred in encoding credentials data.", e); + } catch (NoSuchAlgorithmException e) { + throw new SyncmlMessageFormatException("Problem occurred in generating password hash.", e); + } + return usrPwdNonceHash; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/SyncmlGenerator.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/SyncmlGenerator.java new file mode 100644 index 000000000..9591ca673 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/SyncmlGenerator.java @@ -0,0 +1,100 @@ +/* + * 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.mdm.mobileservices.windows.operations.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.SyncmlOperationException; +import org.wso2.carbon.mdm.mobileservices.windows.operations.SyncmlBody; +import org.wso2.carbon.mdm.mobileservices.windows.operations.SyncmlDocument; +import org.wso2.carbon.mdm.mobileservices.windows.operations.SyncmlHeader; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.*; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import java.io.StringWriter; + +/** + * Generates the response syncml xml file that should be sent to the phone. + */ +public class SyncmlGenerator { + + private static Log log = LogFactory.getLog(SyncmlGenerator.class); + + public String generatePayload(SyncmlDocument syncmlDocument) throws SyncmlOperationException { + Document doc = generateDocument(); + Element rootElement = createRootElement(doc); + SyncmlHeader header = syncmlDocument.getHeader(); + header.buildSyncmlHeaderElement(doc, rootElement); + SyncmlBody body = syncmlDocument.getBody(); + body.buildBodyElement(doc, rootElement); + return transformDocument(doc); + } + + private static Document generateDocument() throws SyncmlOperationException { + DocumentBuilderFactory documentFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder; + try { + docBuilder = documentFactory.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + String message = "Error while generating a new document of syncml"; + log.error(message, e); + throw new SyncmlOperationException(message, e); + } + return docBuilder.newDocument(); + } + + private static Element createRootElement(Document document) { + Element rootElement = document.createElementNS(Constants.XMLNS_SYNCML, + Constants.SYNCML_ROOT_ELEMENT_NAME); + document.appendChild(rootElement); + return rootElement; + } + + private String transformDocument(Document document) throws SyncmlOperationException { + DOMSource domSource = new DOMSource(document); + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer; + try { + transformer = transformerFactory.newTransformer(); + } catch (TransformerConfigurationException e) { + String message = "Error while retrieving a new transformer"; + log.error(message, e); + throw new SyncmlOperationException(message, e); + } + transformer.setOutputProperty(OutputKeys.ENCODING, Constants.UTF_8); + transformer.setOutputProperty(OutputKeys.INDENT, Constants.YES); + + StringWriter stringWriter = new StringWriter(); + StreamResult streamResult = new StreamResult(stringWriter); + try { + transformer.transform(domSource, streamResult); + } catch (TransformerException e) { + String message = "Error while transforming document to a string"; + log.error(message, e); + throw new SyncmlOperationException(message, e); + } + return stringWriter.toString(); + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/SyncmlParser.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/SyncmlParser.java new file mode 100644 index 000000000..836aa50be --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/operations/util/SyncmlParser.java @@ -0,0 +1,458 @@ +/* + * 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.mdm.mobileservices.windows.operations.util; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.SyncmlMessageFormatException; +import org.wso2.carbon.mdm.mobileservices.windows.operations.*; + +import java.util.ArrayList; +import java.util.IllegalFormatCodePointException; +import java.util.List; + +/** + * Parses the receiving SyncML payload and generates the SyncML document object from it. + */ +public class SyncmlParser { + + private static String commandId; + private static String messageReference; + private static String commandReference; + private static final String SYNC_HEADER = "SyncHdr"; + private static final String SYNC_BODY = "SyncBody"; + + private enum SyncMLHeaderParameter { + MSG_ID("MsgID"), + SESSION_ID("SessionID"), + TARGET("Target"), + SOURCE("Source"), + CRED("Cred"); + private final String parameterName; + + SyncMLHeaderParameter(final String parameterName) { + this.parameterName = parameterName; + } + + public String getValue() { + return this.parameterName; + } + } + + private enum SycMLCommandType { + ALERT("Alert"), + REPLACE("Replace"), + STATUS("Status"), + RESULTS("Results"); + private final String commandName; + + SycMLCommandType(final String commandName) { + this.commandName = commandName; + } + + public String getValue() { + return this.commandName; + } + } + + + /** + * Parses the raw SyncML payload and generates a SyncmlDocument object using the parsed XML contents. + * + * @param syncmlPayload - Received SyncML XML payload + * @return - SyncmlDocument object generated from the received payload + */ + public static SyncmlDocument parseSyncmlPayload(Document syncmlPayload) throws SyncmlMessageFormatException { + SyncmlDocument syncmlDocument = new SyncmlDocument(); + if (syncmlPayload.getElementsByTagName(SYNC_HEADER) == null) { + throw new SyncmlMessageFormatException(); + } + NodeList syncHeaderList = syncmlPayload.getElementsByTagName(SYNC_HEADER); + Node syncHeader = syncHeaderList.item(0); + SyncmlHeader header = generateSyncmlHeader(syncHeader); + + NodeList syncBodyList = syncmlPayload.getElementsByTagName(SYNC_BODY); + Node syncBody = syncBodyList.item(0); + SyncmlBody body = generateSyncmlBody(syncBody); + + syncmlDocument.setHeader(header); + syncmlDocument.setBody(body); + return syncmlDocument; + } + + /** + * Generates SyncmlHeader object by extracting properties of passed XML node. + * + * @param syncHeader - XML node which represents SyncML header + * @return - SyncmlHeader object + */ + private static SyncmlHeader generateSyncmlHeader(Node syncHeader) { + + String sessionID = null; + String messageID = null; + Target target = null; + Source source = null; + Credential credential = null; + SyncmlHeader header = new SyncmlHeader(); + + NodeList headerElements = syncHeader.getChildNodes(); + for (int i = 0; i < headerElements.getLength(); i++) { + Node node = headerElements.item(i); + + if (node.getNodeType() == Node.ELEMENT_NODE) { + String nodeName = node.getNodeName(); + + if (SyncMLHeaderParameter.MSG_ID.getValue().equals(nodeName)) { + if (node.getTextContent().trim() == null) { + throw new IllegalFormatCodePointException(2); + } else { + messageID = node.getTextContent().trim(); + } + } else if (SyncMLHeaderParameter.SESSION_ID.getValue().equals(nodeName)) { + if (node.getTextContent().trim() == null) { + throw new IllegalFormatCodePointException(2); + } else { + sessionID = node.getTextContent().trim(); + } + } else if (SyncMLHeaderParameter.TARGET.getValue().equals(nodeName)) { + if (node.getTextContent().trim() == null) { + throw new IllegalFormatCodePointException(2); + } else { + target = generateTarget(node); + } + } else if (SyncMLHeaderParameter.SOURCE.getValue().equals(nodeName)) { + if (node.getTextContent().trim() == null) { + throw new IllegalFormatCodePointException(2); + } else { + source = generateSource(node); + } + } else if (SyncMLHeaderParameter.CRED.getValue().equals(nodeName)) { + if (node.getTextContent().trim() == null) { + throw new IllegalFormatCodePointException(2); + } else { + credential = generateCredential(node); + } + } + } + } + header.setMsgID(Integer.valueOf(messageID)); + // Syncml message contains a sessionID which is Hexadecimal value.Hexadecimal sessionID parse as a integer value. + header.setSessionId(Integer.valueOf(sessionID, 16)); + header.setTarget(target); + header.setSource(source); + header.setCredential(credential); + return header; + } + + /** + * Generates SyncmlBody object by extracting properties of passed XML node. + * + * @param syncBody - XML node which represents SyncML body + * @return - SyncmlBody object + */ + private static SyncmlBody generateSyncmlBody(Node syncBody) { + + Alert alert = null; + Replace replace = null; + Results results = null; + List status = new ArrayList<>(); + NodeList bodyElements = syncBody.getChildNodes(); + + for (int i = 0; i < bodyElements.getLength(); i++) { + Node node = bodyElements.item(i); + + if (node.getNodeType() == Node.ELEMENT_NODE) { + String nodeName = node.getNodeName(); + + if (SycMLCommandType.ALERT.getValue().equals(nodeName)) { + alert = generateAlert(node); + } else if (SycMLCommandType.REPLACE.getValue().equals(nodeName)) { + replace = generateReplace(node); + } else if (SycMLCommandType.STATUS.getValue().equals(nodeName)) { + status.add(generateStatus(node)); + } else if (SycMLCommandType.RESULTS.getValue().equals(nodeName)) { + results = generateResults(node); + } + } + } + SyncmlBody body = new SyncmlBody(); + body.setAlert(alert); + body.setReplace(replace); + body.setStatus(status); + body.setResults(results); + return body; + } + + /** + * Generates Source object by extracting properties of passed XML node. + * + * @param node - XML node which represents Source + * @return - Source object + */ + private static Source generateSource(Node node) { + + Source source = new Source(); + Node sourceURIItem = node.getChildNodes().item(0); + Node sourceNameItem = node.getChildNodes().item(1); + String sourceURI = null; + String sourceName = null; + + if (sourceURIItem != null) { + sourceURI = sourceURIItem.getTextContent().trim(); + } + if (sourceNameItem != null) { + sourceName = sourceNameItem.getTextContent().trim(); + } + source.setLocURI(sourceURI); + source.setLocName(sourceName); + return source; + } + + /** + * Generates Target object by extracting properties of passed XML node. + * + * @param node - XML node which represents Target + * @return - Target object + */ + private static Target generateTarget(Node node) { + + Target target = new Target(); + Node targetURIItem = node.getChildNodes().item(0); + Node targetNameItem = node.getChildNodes().item(1); + String targetURI = null; + String targetName = null; + + if (targetURIItem != null) { + targetURI = targetURIItem.getTextContent().trim(); + } + if (targetNameItem != null) { + targetName = targetNameItem.getTextContent().trim(); + } + target.setLocURI(targetURI); + target.setLocName(targetName); + return target; + } + + /** + * Generates Results object by extracting properties of passed XML node. + * + * @param node - XML node which represents Results + * @return - Results object + */ + private static Results generateResults(Node node) { + + Results results = new Results(); + List item = new ArrayList<>(); + + if (node.getNodeType() == Node.ELEMENT_NODE) { + + NodeList nodelist = node.getChildNodes(); + + for (int i = 0; i < nodelist.getLength(); i++) { + String nodeName = nodelist.item(i).getNodeName(); + + switch (nodeName) { + case Constants.COMMAND_ID: + commandId = node.getChildNodes().item(i).getTextContent().trim(); + break; + case Constants.MESSAGE_REFERENCE: + messageReference = node.getChildNodes().item(i).getTextContent().trim(); + break; + case Constants.COMMAND_REFERENCE: + commandReference = node.getChildNodes().item(i).getTextContent().trim(); + break; + case Constants.ITEM: + item.add(generateItem(node.getChildNodes().item(i))); + break; + } + } + results.setCommandId(Integer.valueOf(commandId)); + results.setMessageReference(Integer.valueOf(messageReference)); + results.setCommandReference(Integer.valueOf(commandReference)); + results.setItem(item); + } + return results; + } + + /** + * Generates Status object by extracting properties of passed XML node. + * + * @param node - XML node which represents Status + * @return - Status object + */ + private static Status generateStatus(Node node) { + + Status status = new Status(); + for (int x = 0; x < node.getChildNodes().getLength(); x++) { + String nodeName = node.getChildNodes().item(x).getNodeName(); + switch (nodeName) { + case PluginConstants.SyncML.SYNCML_CMD_ID: + String commandId = node.getChildNodes().item(x).getTextContent().trim(); + status.setCommandId(Integer.valueOf(commandId)); + break; + case PluginConstants.SyncML.SYNCML_MESSAGE_REF: + String messageReference = node.getChildNodes().item(x).getTextContent().trim(); + status.setMessageReference(Integer.valueOf(messageReference)); + break; + case PluginConstants.SyncML.SYNCML_CMD_REF: + String commandReference = node.getChildNodes().item(x).getTextContent().trim(); + status.setCommandReference(Integer.valueOf(commandReference)); + break; + case PluginConstants.SyncML.SYNCML_CMD: + String command = node.getChildNodes().item(x).getTextContent().trim(); + status.setCommand(command); + break; + case PluginConstants.SyncML.SYNCML_CHAL: + NodeList chalNodes = node.getChildNodes().item(x).getChildNodes(); + MetaTag meta = new MetaTag(); + ChallengeTag chal = new ChallengeTag(); + String format = chalNodes.item(0).getFirstChild().getTextContent(); + meta.setFormat(format); + String type = chalNodes.item(0).getFirstChild().getNextSibling().getTextContent(); + meta.setType(type); + String nonce = chalNodes.item(0).getFirstChild().getNextSibling().getNextSibling().getTextContent(); + meta.setNextNonce(nonce); + chal.setMeta(meta); + status.setChallenge(chal); + break; + case PluginConstants.SyncML.SYNCML_DATA: + String data = node.getChildNodes().item(x).getTextContent().trim(); + status.setData(data); + break; + case PluginConstants.SyncML.SYNCML_TARGET_REF: + String targetReference = node.getChildNodes().item(x).getTextContent().trim(); + status.setTargetReference(targetReference); + break; + } + } + return status; + } + + /** + * Generates Replace object by extracting properties of passed XML node. + * + * @param node - XML node which represents Replace + * @return - Replace object + */ + private static Replace generateReplace(Node node) { + + Replace replace = new Replace(); + String commandId = node.getChildNodes().item(0).getTextContent().trim(); + List items = new ArrayList<>(); + for (int i = 0; i < node.getChildNodes().getLength() - 1; i++) { + items.add(generateItem(node.getChildNodes().item(i + 1))); + } + replace.setCommandId(Integer.valueOf(commandId)); + replace.setItems(items); + return replace; + } + + /** + * Generates Alert object by extracting properties of passed XML node. + * + * @param node - XML node which represents Alert + * @return - Alert object + */ + private static Alert generateAlert(Node node) { + Alert alert = new Alert(); + String commandID = node.getChildNodes().item(0).getTextContent().trim(); + String data = node.getChildNodes().item(1).getTextContent().trim(); + alert.setCommandId(Integer.valueOf(commandID)); + alert.setData(data); + return alert; + } + + /** + * Generates Item object by extracting properties of passed XML node. + * + * @param node - XML node which represents Item + * @return - Item object + */ + private static Item generateItem(Node node) { + Item item = new Item(); + Source source = new Source(); + String data; + String nodeName; + String childNodeName; + String locUri; + for (int x = 0; x < node.getChildNodes().getLength(); x++) { + if (node.getChildNodes().item(x).getNodeName() != null) { + nodeName = node.getChildNodes().item(x).getNodeName(); + } else { + throw new IllegalFormatCodePointException(2); + } + if (nodeName == PluginConstants.SyncML.SYNCML_SOURCE) { + if (node.getChildNodes().item(x).getChildNodes().item(x).getNodeName() != null) { + childNodeName = node.getChildNodes().item(x).getChildNodes().item(x).getNodeName(); + } else { + throw new IllegalFormatCodePointException(2); + } + if (childNodeName == PluginConstants.SyncML.SYNCML_LOCATION_URI) { + if (node.getChildNodes().item(x).getChildNodes().item(x).getTextContent().trim() != null) { + locUri = node.getChildNodes().item(x).getChildNodes().item(x).getTextContent().trim(); + } else { + throw new IllegalFormatCodePointException(2); + } + source.setLocURI(locUri); + item.setSource(source); + } + } else if (nodeName == PluginConstants.SyncML.SYNCML_DATA) { + if (node.getChildNodes().item(x).getTextContent().trim() != null) { + data = node.getChildNodes().item(x).getTextContent().trim(); + } else { + throw new IllegalFormatCodePointException(2); + } + item.setData(data); + } + } + return item; + } + + /** + * Generates Credential object by extracting properties of passed XML node. + * + * @param node - XML node which represents Credential + * @return - Credential object + */ + private static Credential generateCredential(Node node) { + Credential credential = new Credential(); + MetaTag meta = generateMeta(node.getChildNodes().item(0)); + String data = node.getChildNodes().item(1).getTextContent().trim(); + credential.setMeta(meta); + credential.setData(data); + return credential; + } + + /** + * Generates MetaTag object by extracting properties of passed XML node. + * + * @param node - XML node which represents MetaTag + * @return - MetaTag object + */ + private static MetaTag generateMeta(Node node) { + MetaTag meta = new MetaTag(); + String format = node.getChildNodes().item(0).getTextContent().trim(); + String type = node.getChildNodes().item(1).getTextContent().trim(); + meta.setFormat(format); + meta.setType(type); + return meta; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/Operations.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/Operations.java new file mode 100644 index 000000000..b96831f4b --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/Operations.java @@ -0,0 +1,57 @@ +/* + * 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.mdm.mobileservices.windows.services.adminoperations; + +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsDeviceEnrolmentException; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; + +/** + * Interface for Admin operations persisting. This interface accepts operations added via UI. + */ +@Path("/operation") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) + +public interface Operations { + + @POST + @Path("/devicelock") + Response lock(@HeaderParam("Accept") String headerParam, List deviceids) throws WindowsDeviceEnrolmentException; + + @POST + @Path("/devicedisenroll") + Response disenroll(@HeaderParam("Accept") String headerParam, List deviceids) throws WindowsDeviceEnrolmentException; + + @POST + @Path("/devicewipe") + Response wipe(@HeaderParam("Accept") String headerParam, List deviceids) throws WindowsDeviceEnrolmentException; + + @POST + @Path("/devicering") + Response ring(@HeaderParam("Accept") String headerParam, List deviceids) throws WindowsDeviceEnrolmentException; + + @POST + @Path("/lockreset") + Response lockReset(@HeaderParam("Accept") String acceptHeader, List deviceids) + throws WindowsDeviceEnrolmentException; +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/Device.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/Device.java new file mode 100644 index 000000000..79a4af79c --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/Device.java @@ -0,0 +1,36 @@ +/* + * 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.mdm.mobileservices.windows.services.adminoperations.beans; + +/** + * Class for get device IDS. + */ +public class Device { + + private String id; + + public String getID() { + return id; + } + + public void setID(String id) { + this.id = id; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/Disenrollment.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/Disenrollment.java new file mode 100644 index 000000000..85054033b --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/Disenrollment.java @@ -0,0 +1,42 @@ +/* + * 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.mdm.mobileservices.windows.services.adminoperations.beans; + +import java.io.Serializable; + +/** + * Class for dis-enrollment operation + */ +public class Disenrollment implements Serializable { + + private boolean enabled; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + private boolean isEnable() { + return enabled; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/OperationRequest.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/OperationRequest.java new file mode 100644 index 000000000..0745aa53a --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/OperationRequest.java @@ -0,0 +1,49 @@ +/* + * 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.mdm.mobileservices.windows.services.adminoperations.beans; + +import org.wso2.carbon.mdm.mobileservices.windows.services.syncml.beans.BasicOperation; + +import java.util.List; + +/** + * Class for set basic operations. + */ +public class OperationRequest { + + private List deviceList; + private BasicOperation basicOperation; + + public BasicOperation getBasicOperation() { + return basicOperation; + } + + public void setBasicOperation(BasicOperation basicOperation) { + this.basicOperation = basicOperation; + } + + public List getDeviceList() { + return deviceList; + } + + public void setDeviceList(List deviceList) { + this.deviceList = deviceList; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/OperationResponse.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/OperationResponse.java new file mode 100644 index 000000000..d1ecce0e6 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/OperationResponse.java @@ -0,0 +1,48 @@ +/* + * 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.mdm.mobileservices.windows.services.adminoperations.beans; + +/** + * This bean class is for credentials coming from wab page at federated authentication step. + */ +public class OperationResponse { + + private String errorCode; + private String statusCode; + + public OperationResponse() { + + } + + public String getErrorCode() { + return errorCode; + } + + public String getStatusCode() { + return statusCode; + } + + public void setErrorCode(String errorCode) { + this.errorCode = errorCode; + } + + public void setStatusCode(String statusCode) { + this.statusCode = statusCode; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/StorageEncryption.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/StorageEncryption.java new file mode 100644 index 000000000..9aaf4ce6f --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/StorageEncryption.java @@ -0,0 +1,36 @@ +/* + * 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.mdm.mobileservices.windows.services.adminoperations.beans; + +import java.io.Serializable; + +/** + * This class represents the information of encrypt operation. + */ +public class StorageEncryption implements Serializable { + private boolean encrypted; + + public boolean isEncrypted() { + return encrypted; + } + + public void setEncrypted(boolean encrypted) { + this.encrypted = encrypted; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/wrapper/DisenrollmentBeanWrapper.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/wrapper/DisenrollmentBeanWrapper.java new file mode 100644 index 000000000..5f683e884 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/wrapper/DisenrollmentBeanWrapper.java @@ -0,0 +1,50 @@ +/* + * 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.mdm.mobileservices.windows.services.adminoperations.beans.wrapper; + +import org.wso2.carbon.mdm.mobileservices.windows.services.adminoperations.beans.Disenrollment; + +import java.util.List; + +/** + * Class is used to wrap dis-enrollment bean with the device + */ +public class DisenrollmentBeanWrapper { + + private Disenrollment operation; + + private List deviceId; + + public Disenrollment getOperation() { + return operation; + } + + public void setOperation(Disenrollment operation) { + this.operation = operation; + } + + public List getDeviceId() { + return deviceId; + } + + public void setDeviceId(List deviceId) { + this.deviceId = deviceId; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/wrapper/EncryptBeanWrapper.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/wrapper/EncryptBeanWrapper.java new file mode 100644 index 000000000..72181661a --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/beans/wrapper/EncryptBeanWrapper.java @@ -0,0 +1,48 @@ +/* + * 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.mdm.mobileservices.windows.services.adminoperations.beans.wrapper; + +import org.wso2.carbon.mdm.mobileservices.windows.services.adminoperations.beans.StorageEncryption; + +import java.util.List; + +/** + * This class is used to wrap the Encrypt bean with devices. + */ +public class EncryptBeanWrapper { + + private StorageEncryption operation; + private List deviceIDs; + + public StorageEncryption getOperation() { + return operation; + } + + public void setOperation(StorageEncryption operation) { + this.operation = operation; + } + + public List getDeviceIDs() { + return deviceIDs; + } + + public void setDeviceIDs(List deviceIDs) { + this.deviceIDs = deviceIDs; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/impl/OperationsImpl.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/impl/OperationsImpl.java new file mode 100644 index 000000000..2a7cd96be --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/impl/OperationsImpl.java @@ -0,0 +1,242 @@ +/* + * 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.mdm.mobileservices.windows.services.adminoperations.impl; + +import com.ibm.wsdl.OperationImpl; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.common.operation.mgt.OperationManagementException; +import org.wso2.carbon.device.mgt.core.operation.mgt.CommandOperation; +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsDeviceEnrolmentException; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsOperationsException; +import org.wso2.carbon.mdm.mobileservices.windows.common.util.Message; +import org.wso2.carbon.mdm.mobileservices.windows.common.util.WindowsAPIUtils; +import org.wso2.carbon.mdm.mobileservices.windows.services.adminoperations.Operations; + +import javax.ws.rs.HeaderParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; + +/** + * Implementation class of operations interface. Each method in this class receives the operations comes via UI + * and persists those in the correct format. + */ +public class OperationsImpl implements Operations { + + private static Log log = LogFactory.getLog(OperationImpl.class); + + /** + * REST endpoint for the Device Lock operation + * + * @param acceptHeader header value of the request POST message. + * @param deviceIDs list of device ids to be add device lockOperationUpdate operation. + * @return Response object for client. + * @throws WindowsDeviceEnrolmentException + */ + @POST + @Path("/lock") + public Response lock(@HeaderParam("Accept") String acceptHeader, List deviceIDs) + throws WindowsDeviceEnrolmentException { + if (log.isDebugEnabled()) { + log.debug("Invoking windows device lockOperationUpdate operation"); + } + MediaType responseMediaType = WindowsAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + try { + CommandOperation operation = new CommandOperation(); + operation.setCode(PluginConstants.OperationCodes.DEVICE_LOCK); + operation.setType(Operation.Type.COMMAND); + operation.setEnabled(true); + return WindowsAPIUtils.getOperationResponse(deviceIDs, operation, message, responseMediaType); + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message.setResponseMessage(errorMessage); + message.setResponseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()); + log.error(errorMessage, e); + throw new WindowsOperationsException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message.setResponseMessage(errorMessage); + message.setResponseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()); + log.error(errorMessage, e); + throw new WindowsOperationsException(message, responseMediaType); + } + } + + /** + * REST end point for device dis-enrollment. + * + * @param acceptHeader POST message header value. + * @param deviceIDs device ids to be dis-enrolled. + * @return Response object to the client. + * @throws WindowsDeviceEnrolmentException + */ + @POST + @Path("/disenroll") + public Response disenroll(@HeaderParam("Accept") String acceptHeader, List deviceIDs) + throws WindowsDeviceEnrolmentException { + + MediaType responseMediaType = WindowsAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + CommandOperation operation = new CommandOperation(); + operation.setCode(PluginConstants.OperationCodes.DISENROLL); + operation.setType(Operation.Type.COMMAND); + operation.setEnabled(true); + try { + return WindowsAPIUtils.getOperationResponse(deviceIDs, operation, message, responseMediaType); + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message.setResponseMessage(errorMessage); + message.setResponseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()); + log.error(errorMessage, e); + throw new WindowsOperationsException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message.setResponseMessage(errorMessage); + message.setResponseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()); + log.error(errorMessage, e); + throw new WindowsOperationsException(message, responseMediaType); + } + } + + /** + * REST Endpoint for the Device wipe. + * + * @param acceptHeader POST message header value. + * @param deviceids device ids to be wiped. + * @return Response object for the client. + * @throws WindowsDeviceEnrolmentException + */ + @POST + @Path("/wipe-data") + public Response wipe(@HeaderParam("Accept") String acceptHeader, List deviceids) + throws WindowsDeviceEnrolmentException { + + if (log.isDebugEnabled()) { + log.debug("Invoking windows wipe-data device operation"); + } + MediaType responseMediaType = WindowsAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + CommandOperation operation = new CommandOperation(); + operation.setCode(PluginConstants.OperationCodes.WIPE_DATA); + operation.setType(Operation.Type.COMMAND); + try { + return WindowsAPIUtils.getOperationResponse(deviceids, operation, message, responseMediaType); + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message.setResponseMessage(errorMessage); + message.setResponseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()); + log.error(errorMessage, e); + throw new WindowsOperationsException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message.setResponseMessage(errorMessage); + message.setResponseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()); + log.error(errorMessage, e); + throw new WindowsOperationsException(message, responseMediaType); + } + } + + /** + * REST end point for the device ring. + * + * @param acceptHeader post message header value. + * @param deviceIDs device ids to be ring. + * @return Response object for the client. + * @throws WindowsDeviceEnrolmentException + */ + @POST + @Path("/ring-device") + public Response ring(@HeaderParam("Accept") String acceptHeader, List deviceIDs) + throws WindowsDeviceEnrolmentException { + + if (log.isDebugEnabled()) { + log.debug("Invoking Windows ring-device device operation"); + } + MediaType responseMediaType = WindowsAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + try { + CommandOperation operation = new CommandOperation(); + operation.setCode(PluginConstants.OperationCodes.DEVICE_RING); + operation.setType(Operation.Type.COMMAND); + return WindowsAPIUtils.getOperationResponse(deviceIDs, operation, message, responseMediaType); + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message.setResponseMessage(errorMessage); + message.setResponseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()); + log.error(errorMessage, e); + throw new WindowsOperationsException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message.setResponseMessage(errorMessage); + message.setResponseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()); + log.error(errorMessage, e); + throw new WindowsOperationsException(message, responseMediaType); + } + } + + /** + * REST endpoint for the device lockOperationUpdate reset. + * Lock reset have to be done, when device user does not set PIN for the lockOperationUpdate screen. + * Admin set lockOperationUpdate operation for the specific device,If the device is in above scenario, + * admin will be notified.since + * admin have to set lockOperationUpdate reset operation to the device so that automatically generate PIN value for the + * lockOperationUpdate screen. + * + * @param acceptHeader POST message header value. + * @param deviceIDs Device ids to be lockOperationUpdate reset. + * @return Response object for the client. + * @throws WindowsDeviceEnrolmentException + */ + @POST + @Path("/lock-reset") + public Response lockReset(@HeaderParam("Accept") String acceptHeader, List deviceIDs) + throws WindowsDeviceEnrolmentException { + + if (log.isDebugEnabled()) { + log.debug("Invoking windows device lockReset storage operation"); + } + MediaType responseMediaType = WindowsAPIUtils.getResponseMediaType(acceptHeader); + Message message = new Message(); + try { + CommandOperation operation = new CommandOperation(); + operation.setCode(PluginConstants.OperationCodes.LOCK_RESET); + operation.setType(Operation.Type.COMMAND); + return WindowsAPIUtils.getOperationResponse(deviceIDs, operation, message, responseMediaType); + } catch (OperationManagementException e) { + String errorMessage = "Issue in retrieving operation management service instance"; + message.setResponseMessage(errorMessage); + message.setResponseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()); + log.error(errorMessage, e); + throw new WindowsOperationsException(message, responseMediaType); + } catch (DeviceManagementException e) { + String errorMessage = "Issue in retrieving device management service instance"; + message.setResponseMessage(errorMessage); + message.setResponseCode(Response.Status.INTERNAL_SERVER_ERROR.toString()); + log.error(errorMessage, e); + throw new WindowsOperationsException(message, responseMediaType); + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/util/OperationStore.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/util/OperationStore.java new file mode 100644 index 000000000..fe009c1f4 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/adminoperations/util/OperationStore.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 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. + */ + +package org.wso2.carbon.mdm.mobileservices.windows.services.adminoperations.util; + +import com.google.gson.Gson; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementConstants; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.common.operation.mgt.OperationManagementException; +import org.wso2.carbon.device.mgt.core.operation.mgt.ConfigOperation; +import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; +import org.wso2.carbon.mdm.mobileservices.windows.common.SyncmlCommandType; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsDeviceEnrolmentException; +import org.wso2.carbon.mdm.mobileservices.windows.services.adminoperations.beans.Device; +import org.wso2.carbon.mdm.mobileservices.windows.services.adminoperations.beans.OperationRequest; +import org.wso2.carbon.mdm.mobileservices.windows.services.syncml.beans.Wifi; + +import java.util.ArrayList; +import java.util.List; + +public class OperationStore { + + private static Log log = LogFactory.getLog(OperationStore.class); + + public static boolean storeOperation(OperationRequest operationRequest, Operation.Type type, + String commandType) throws + WindowsDeviceEnrolmentException { + + List devices = operationRequest.getDeviceList(); + List deviceIdentifiers = new ArrayList(); + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + + Operation operation = transformBasicOperation(operationRequest, type, commandType); + + for (int i = 0; i < devices.size(); i++) { + try { + deviceIdentifier.setType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS); + deviceIdentifier.setId(devices.get(i).getID()); + deviceIdentifiers.add(deviceIdentifier); + getDeviceManagementServiceProvider().getDevice(deviceIdentifier); + + } catch (DeviceManagementException e) { + log.error("Cannot validate device ID: " + devices.get(i).getID()); + deviceIdentifiers.remove(i); + } + } + try { + getDeviceManagementServiceProvider().addOperation(operation, deviceIdentifiers); + } catch (OperationManagementException e) { + String msg = "Failure occurred while storing command operation."; + log.error(msg); + return false; + } + return true; + } + + private static DeviceManagementProviderService getDeviceManagementServiceProvider() { + DeviceManagementProviderService deviceManager; + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + deviceManager = + (DeviceManagementProviderService) ctx.getOSGiService(DeviceManagementProviderService.class, null); + + if (deviceManager == null) { + String msg = "Device management service is not initialized."; + log.error(msg); + } + return deviceManager; + } + + private static Operation transformBasicOperation(OperationRequest operationRequest, Operation.Type type, + String commandType) throws WindowsDeviceEnrolmentException { + + Operation operation = new Operation(); + operation.setCode(commandType); + operation.setType(type); + Gson gson = new Gson(); + + if (commandType == SyncmlCommandType.WIFI.getValue()) { + + operation = new ConfigOperation(); + operation.setCode(commandType); + operation.setType(type); + + Wifi wifiObject = (Wifi) operationRequest.getBasicOperation(); + operation.setPayLoad(gson.toJson(wifiObject)); + } else { + // no operation..... + } + + return operation; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/authbst/BSTProvider.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/authbst/BSTProvider.java new file mode 100644 index 000000000..42660faea --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/authbst/BSTProvider.java @@ -0,0 +1,40 @@ +/* + * 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.mdm.mobileservices.windows.services.authbst; + +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsDeviceEnrolmentException; +import org.wso2.carbon.mdm.mobileservices.windows.services.authbst.beans.Credentials; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +/** + * Interface for handling authentication request comes via MDM login page. + */ +@Path("/bst") +public interface BSTProvider { + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Path("/authentication") + Response getBST(Credentials credentials) throws WindowsDeviceEnrolmentException; +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/authbst/beans/Credentials.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/authbst/beans/Credentials.java new file mode 100644 index 000000000..24be2d06c --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/authbst/beans/Credentials.java @@ -0,0 +1,87 @@ +/* + * 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.mdm.mobileservices.windows.services.authbst.beans; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * This bean class is for credentials coming from wab page at federated authentication step. + */ +@XmlRootElement(name = "credentials") +@XmlAccessorType(XmlAccessType.FIELD) +public class Credentials { + + @XmlElement(required = true, name = "username") + private String username; + @XmlElement(required = true, name = "email") + private String email; + @XmlElement(required = true, name = "password") + private String password; + @XmlElement(required = true, name = "ownership") + private String ownership; + @XmlElement(required = true, name = "token") + private String usertoken; + + public Credentials() { + + } + + public String getUsertoken() { + return usertoken; + } + + public void setUsertoken(String usertoken) { + this.usertoken = usertoken; + } + + public String getOwnership() { + return ownership; + } + + public void setOwnership(String ownership) { + this.ownership = ownership; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/authbst/impl/BSTProviderImpl.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/authbst/impl/BSTProviderImpl.java new file mode 100644 index 000000000..9f8bd7b8a --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/authbst/impl/BSTProviderImpl.java @@ -0,0 +1,131 @@ +/* + * 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.mdm.mobileservices.windows.services.authbst.impl; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.json.JSONObject; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.mdm.mobileservices.windows.common.beans.Token; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.AuthenticationException; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsDeviceEnrolmentException; +import org.wso2.carbon.mdm.mobileservices.windows.common.util.DeviceUtil; +import org.wso2.carbon.mdm.mobileservices.windows.services.authbst.BSTProvider; +import org.wso2.carbon.mdm.mobileservices.windows.services.authbst.beans.Credentials; +import org.wso2.carbon.user.api.UserRealm; +import org.wso2.carbon.user.api.UserStoreException; +import org.wso2.carbon.user.core.service.RealmService; +import org.wso2.carbon.utils.multitenancy.MultitenantConstants; + +import javax.ws.rs.core.Response; + +/** + * Implementation class of BSTProvider interface which authenticates the credentials comes via MDM login page. + */ +public class BSTProviderImpl implements BSTProvider { + + private static Log log = LogFactory.getLog(BSTProviderImpl.class); + private static final String DELIMITER = "@"; + + /** + * This method validates the device user, checking passed credentials and returns the corresponding + * binary security token which is used in XCEP and WSTEP stages for authentication. + * + * @param credentials - Credential object passes from the wab page + * @return - Response with binary security token + */ + @Override + public Response getBST(Credentials credentials) throws WindowsDeviceEnrolmentException { + + String domainUser = credentials.getUsername(); + String userToken = credentials.getUsertoken(); + String encodedToken; + try { + Token tokenBean = new Token(); + tokenBean.setChallengeToken(userToken); + Base64 base64 = new Base64(); + encodedToken = base64.encodeToString(userToken.getBytes()); + DeviceUtil.persistChallengeToken(encodedToken, null, domainUser); + JSONObject tokenContent = new JSONObject(); + tokenContent.put("UserToken", userToken); + return Response.ok().entity(tokenContent.toString()).build(); + } catch (DeviceManagementException e) { + String msg = "Failure occurred in generating challenge token."; + log.error(msg, e); + throw new WindowsDeviceEnrolmentException(msg, e); + } catch (JSONException e) { + String msg = "Failure occurred in generating challenge token Json."; + log.error(msg, e); + throw new WindowsDeviceEnrolmentException(msg, e); + } + } + + /** + * This method authenticate the user checking the carbon default user store. + * + * @param username - Username in username token + * @param password - Password in username token + * @param tenantDomain - Tenant domain is extracted from the username + * @return - Returns boolean representing authentication result + * @throws AuthenticationException + */ + private boolean authenticate(String username, String password, String tenantDomain) throws + AuthenticationException { + + try { + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext ctx = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + ctx.setTenantDomain(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); + ctx.setTenantId(MultitenantConstants.SUPER_TENANT_ID); + RealmService realmService = (RealmService) ctx.getOSGiService(RealmService.class, null); + + if (realmService == null) { + String msg = "RealmService not initialized."; + log.error(msg); + throw new AuthenticationException(msg); + } + + int tenantId; + if (tenantDomain == null || tenantDomain.trim().isEmpty()) { + tenantId = MultitenantConstants.SUPER_TENANT_ID; + } else { + tenantId = realmService.getTenantManager().getTenantId(tenantDomain); + } + + if (tenantId == MultitenantConstants.INVALID_TENANT_ID) { + String msg = "Invalid tenant domain " + tenantDomain; + log.error(msg); + throw new AuthenticationException(msg); + } + UserRealm userRealm = realmService.getTenantUserRealm(tenantId); + + return userRealm.getUserStoreManager().authenticate(username, password); + } catch (UserStoreException e) { + String msg = "User store is not initialized."; + log.error(msg, e); + throw new AuthenticationException(msg, e); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/configurationmgtservice/ConfigurationMgtService.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/configurationmgtservice/ConfigurationMgtService.java new file mode 100644 index 000000000..ae8e5f881 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/configurationmgtservice/ConfigurationMgtService.java @@ -0,0 +1,181 @@ +/* + * 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.mdm.mobileservices.windows.services.configurationmgtservice; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.DeviceManagementConstants; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.configuration.mgt.ConfigurationEntry; +import org.wso2.carbon.device.mgt.common.configuration.mgt.TenantConfiguration; +import org.wso2.carbon.device.mgt.common.license.mgt.License; +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsConfigurationException; +import org.wso2.carbon.mdm.mobileservices.windows.common.util.Message; +import org.wso2.carbon.mdm.mobileservices.windows.common.util.WindowsAPIUtils; + +import javax.jws.WebService; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import java.util.List; + +/** + * Windows Platform Configuration REST-API implementation. + * All end points supports JSON, XMl with content negotiation. + */ +@WebService +@Produces({"application/json", "application/xml"}) +@Consumes({"application/json", "application/xml"}) +public class ConfigurationMgtService { + + private static Log log = LogFactory.getLog(ConfigurationMgtService.class); + + /** + * Save Tenant configurations. + * + * @param configuration Tenant Configurations to be saved. + * @return Message type object for the provide save status. + * @throws WindowsConfigurationException + */ + @POST + public Message ConfigureSettings(TenantConfiguration configuration) throws WindowsConfigurationException { + Message responseMsg = new Message(); + ConfigurationEntry licenseEntry = null; + String message; + + try { + configuration.setType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS); + if (!configuration.getConfiguration().isEmpty()) { + List configs = configuration.getConfiguration(); + for (ConfigurationEntry entry : configs) { + if (PluginConstants.TenantConfigProperties.LICENSE_KEY.equals(entry.getName())) { + License license = new License(); + license.setName(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS); + license.setLanguage(PluginConstants.TenantConfigProperties.LANGUAGE_US); + license.setVersion("1.0.0"); + license.setText(entry.getValue().toString()); + WindowsAPIUtils.getDeviceManagementService().addLicense(DeviceManagementConstants. + MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS, license); + licenseEntry = entry; + } + } + + if (licenseEntry != null) { + configs.remove(licenseEntry); + } + configuration.setConfiguration(configs); + WindowsAPIUtils.getDeviceManagementService().saveConfiguration(configuration); + Response.status(Response.Status.CREATED); + responseMsg.setResponseMessage("Windows platform configuration saved successfully."); + responseMsg.setResponseCode(Response.Status.CREATED.toString()); + return responseMsg; + } + else { + Response.status(Response.Status.BAD_REQUEST); + responseMsg.setResponseMessage("Windows platform configuration can not be saved."); + responseMsg.setResponseCode(Response.Status.CREATED.toString()); + } + } catch (DeviceManagementException e) { + message = "Error Occurred in while configuring Windows Platform."; + log.error(message, e); + throw new WindowsConfigurationException(message, e); + } + return responseMsg; + } + + /** + * Retrieve Tenant configurations according to the device type. + * + * @return Tenant configuration object contains specific tenant configurations. + * @throws WindowsConfigurationException + */ + @GET + public TenantConfiguration getConfiguration() throws WindowsConfigurationException { + String msg; + TenantConfiguration tenantConfiguration = null; + try { + if (WindowsAPIUtils.getDeviceManagementService(). + getConfiguration(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS) != null) { + tenantConfiguration = WindowsAPIUtils.getDeviceManagementService(). + getConfiguration(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS); + List configs = tenantConfiguration.getConfiguration(); + ConfigurationEntry entry = new ConfigurationEntry(); + License license = WindowsAPIUtils.getDeviceManagementService().getLicense( + DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS, PluginConstants. + TenantConfigProperties.LANGUAGE_US); + if(license != null) { + entry.setContentType(PluginConstants.TenantConfigProperties.CONTENT_TYPE_TEXT); + entry.setName(PluginConstants.TenantConfigProperties.LICENSE_KEY); + entry.setValue(license.getText()); + configs.add(entry); + tenantConfiguration.setConfiguration(configs); + } + } + } catch (DeviceManagementException e) { + msg = "Error occurred while retrieving the Windows tenant configuration"; + log.error(msg, e); + throw new WindowsConfigurationException(msg, e); + } + return tenantConfiguration; + } + + /** + * Update Tenant Configurations for the specific Device type. + * + * @param configuration Tenant configurations to be updated. + * @return Response message. + * @throws WindowsConfigurationException + */ + @PUT + public Message updateConfiguration(TenantConfiguration configuration) throws WindowsConfigurationException { + String message; + Message responseMsg = new Message(); + ConfigurationEntry licenseEntry = null; + try { + configuration.setType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS); + List configs = configuration.getConfiguration(); + for (ConfigurationEntry entry : configs) { + if (PluginConstants.TenantConfigProperties.LICENSE_KEY.equals(entry.getName())) { + License license = new License(); + license.setName(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS); + license.setLanguage(PluginConstants.TenantConfigProperties.LANGUAGE_US); + license.setVersion("1.0.0"); + license.setText(entry.getValue().toString()); + WindowsAPIUtils.getDeviceManagementService().addLicense(DeviceManagementConstants. + MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS, license); + licenseEntry = entry; + } + } + + if (licenseEntry != null) { + configs.remove(licenseEntry); + } + configuration.setConfiguration(configs); + WindowsAPIUtils.getDeviceManagementService().saveConfiguration(configuration); + Response.status(Response.Status.CREATED); + responseMsg.setResponseMessage("Windows platform configuration succeeded."); + responseMsg.setResponseCode(Response.Status.CREATED.toString()); + } catch (DeviceManagementException e) { + message = "Error occurred while modifying configuration settings of Windows platform."; + log.error(message, e); + throw new WindowsConfigurationException(message, e); + } + return responseMsg; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/devicemgtservice/DeviceManagementService.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/devicemgtservice/DeviceManagementService.java new file mode 100644 index 000000000..4e702f75e --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/devicemgtservice/DeviceManagementService.java @@ -0,0 +1,158 @@ +/* + * 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.mdm.mobileservices.windows.services.devicemgtservice; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementConstants; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.license.mgt.License; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsConfigurationException; +import org.wso2.carbon.mdm.mobileservices.windows.common.util.Message; +import org.wso2.carbon.mdm.mobileservices.windows.common.util.WindowsAPIUtils; + +import javax.jws.WebService; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import java.util.List; + +/** + * Windows Device Management REST-API implementation. + * All end points supports JSON, XMl with content negotiation. + */ +@WebService +@Produces({"application/json", "application/xml"}) +@Consumes({"application/json", "application/xml"}) +public class DeviceManagementService { + + private static Log log = LogFactory.getLog(DeviceManagementService.class); + + /** + * Get all devices.Returns list of Windows devices registered in MDM. + * + * @return Device List + * @throws WindowsConfigurationException + */ + @GET + public List getAllDevices() + throws WindowsConfigurationException { + String msg; + List devices; + + try { + devices = WindowsAPIUtils.getDeviceManagementService(). + getAllDevices(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS); + } catch (DeviceManagementException e) { + msg = "Error occurred while fetching the device list."; + log.error(msg, e); + throw new WindowsConfigurationException(msg, e); + } + return devices; + } + + /** + * Fetch Windows device details of a given device Id. + * + * @param id Device Id + * @return Device + * @throws WindowsConfigurationException + */ + @GET + @Path("{id}") + public org.wso2.carbon.device.mgt.common.Device getDevice(@PathParam("id") String id) + throws WindowsConfigurationException { + + String msg; + org.wso2.carbon.device.mgt.common.Device device; + + try { + DeviceIdentifier deviceIdentifier = WindowsAPIUtils.convertToDeviceIdentifierObject(id); + device = WindowsAPIUtils.getDeviceManagementService().getDevice(deviceIdentifier); + if (device == null) { + Response.status(Response.Status.NOT_FOUND); + } + } catch (DeviceManagementException deviceMgtEx) { + msg = "Error occurred while fetching the device information."; + log.error(msg, deviceMgtEx); + throw new WindowsConfigurationException(msg, deviceMgtEx); + } + return device; + } + + /** + * Update Windows device details of given device id. + * + * @param id Device Id + * @param device Device Details + * @return Message + * @throws WindowsConfigurationException + */ + @PUT + @Path("{id}") + public Message updateDevice(@PathParam("id") String id, Device device) + throws WindowsConfigurationException { + String msg; + Message responseMessage = new Message(); + DeviceIdentifier deviceIdentifier = new DeviceIdentifier(); + deviceIdentifier.setId(id); + deviceIdentifier + .setType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS); + boolean result; + try { + device.setType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS); + result = WindowsAPIUtils.getDeviceManagementService() + .updateDeviceInfo(deviceIdentifier, device); + if (result) { + Response.status(Response.Status.ACCEPTED); + responseMessage.setResponseMessage("Device information has modified successfully."); + } else { + Response.status(Response.Status.NOT_MODIFIED); + responseMessage.setResponseMessage("Device not found for the update."); + } + } catch (DeviceManagementException e) { + msg = "Error occurred while modifying the device information."; + log.error(msg, e); + throw new WindowsConfigurationException(msg, e); + } + return responseMessage; + } + + @GET + @Path("license") + @Produces("application/json") + public License getLicense() throws WindowsConfigurationException { + License license; + + try { + license = + WindowsAPIUtils.getDeviceManagementService().getLicense( + DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS, + DeviceManagementConstants.LanguageCodes.LANGUAGE_CODE_ENGLISH_US); + } catch (DeviceManagementException e) { + String msg = "Error occurred while retrieving the license configured for Windows device enrollment"; + log.error(msg, e); + throw new WindowsConfigurationException(msg, e); + } + return license; + } + +} + diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/DiscoveryService.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/DiscoveryService.java new file mode 100644 index 000000000..798403331 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/DiscoveryService.java @@ -0,0 +1,62 @@ +/* + * 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.mdm.mobileservices.windows.services.discovery; + +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.wso2.carbon.mdm.mobileservices.windows.services.discovery.beans.DiscoveryRequest; +import org.wso2.carbon.mdm.mobileservices.windows.services.discovery.beans.DiscoveryResponse; + +import javax.jws.WebMethod; +import javax.jws.WebParam; +import javax.jws.WebResult; +import javax.jws.WebService; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.core.Response; +import javax.xml.ws.BindingType; +import javax.xml.ws.RequestWrapper; +import javax.xml.ws.ResponseWrapper; +import javax.xml.ws.soap.SOAPBinding; + +/** + * Interface for Discovery service related operations. + */ +@WebService(targetNamespace = PluginConstants.DISCOVERY_SERVICE_TARGET_NAMESPACE, + name = "IDiscoveryService") +@BindingType(value = SOAPBinding.SOAP12HTTP_BINDING) +public interface DiscoveryService { + + @POST + @RequestWrapper(localName = "Discover", targetNamespace = PluginConstants.DISCOVERY_SERVICE_TARGET_NAMESPACE) + @WebMethod(operationName = "Discover") + @ResponseWrapper(localName = "DiscoverResponse", targetNamespace = PluginConstants.DISCOVERY_SERVICE_TARGET_NAMESPACE) + void discover( + @WebParam(name = "request", targetNamespace = PluginConstants.DISCOVERY_SERVICE_TARGET_NAMESPACE) + DiscoveryRequest request, + @WebParam(mode = WebParam.Mode.OUT, name = "DiscoverResult", + targetNamespace = PluginConstants.DISCOVERY_SERVICE_TARGET_NAMESPACE) + javax.xml.ws.Holder response + ); + + @GET + @WebMethod + @WebResult() + Response discoverGet(); + +} \ No newline at end of file diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/beans/DiscoveryRequest.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/beans/DiscoveryRequest.java new file mode 100644 index 000000000..9acd44f0f --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/beans/DiscoveryRequest.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.mdm.mobileservices.windows.services.discovery.beans; + +import javax.xml.bind.annotation.*; +import java.io.Serializable; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "DiscoveryRequest") +@SuppressWarnings("unused") +public class DiscoveryRequest implements Serializable { + + @XmlElement(name = "EmailAddress", required = true) + private String emailId; + + @XmlElement(name = "RequestVersion") + private String version; + + @XmlElement(name = "DeviceType") + private String deviceType; + + public String getEmailId() { + return emailId; + } + + public String getVersion() { + return version; + } + + public void setEmailId(String emailId) { + this.emailId = emailId; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getDeviceType() { + return deviceType; + } + + public void setDeviceType(String deviceType) { + this.deviceType = deviceType; + } +} \ No newline at end of file diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/beans/DiscoveryResponse.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/beans/DiscoveryResponse.java new file mode 100644 index 000000000..66d597f6a --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/beans/DiscoveryResponse.java @@ -0,0 +1,73 @@ +/* + * 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.mdm.mobileservices.windows.services.discovery.beans; + +import javax.xml.bind.annotation.*; +import java.io.Serializable; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "DiscoveryResponse") +@SuppressWarnings("unused") +public class DiscoveryResponse implements Serializable { + + @XmlElement(name = "AuthPolicy") + private String authPolicy; + + @XmlElement(name = "EnrollmentPolicyServiceUrl") + private String enrollmentPolicyServiceUrl; + + @XmlElement(name = "EnrollmentServiceUrl") + private String enrollmentServiceUrl; + + @XmlElement(name = "AuthenticationServiceUrl") + private String authenticationServiceUrl; + + public void setAuthenticationServiceUrl(String authenticationServiceUrl) { + this.authenticationServiceUrl = authenticationServiceUrl; + } + + public String getAuthenticationServiceUrl() { + return authenticationServiceUrl; + } + + public String getAuthPolicy() { + return authPolicy; + } + + public String getEnrollmentPolicyServiceUrl() { + return enrollmentPolicyServiceUrl; + } + + public String getEnrollmentServiceUrl() { + return enrollmentServiceUrl; + } + + public void setAuthPolicy(String authPolicy) { + this.authPolicy = authPolicy; + } + + public void setEnrollmentPolicyServiceUrl(String enrollmentPolicyServiceUrl) { + this.enrollmentPolicyServiceUrl = enrollmentPolicyServiceUrl; + } + + public void setEnrollmentServiceUrl(String enrollmentServiceUrl) { + this.enrollmentServiceUrl = enrollmentServiceUrl; + } + +} \ No newline at end of file diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/beans/package-info.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/beans/package-info.java new file mode 100644 index 000000000..df5d15392 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/beans/package-info.java @@ -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. +*/ + +@javax.xml.bind.annotation.XmlSchema(namespace = + "http://schemas.microsoft.com/windows/management/2012/01/enrollment", + elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED) +package org.wso2.carbon.mdm.mobileservices.windows.services.discovery.beans; diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/impl/DiscoveryServiceImpl.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/impl/DiscoveryServiceImpl.java new file mode 100644 index 000000000..f15057a66 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/discovery/impl/DiscoveryServiceImpl.java @@ -0,0 +1,112 @@ +/* + * 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.mdm.mobileservices.windows.services.discovery.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.wso2.carbon.mdm.mobileservices.windows.common.beans.WindowsPluginProperties; +import org.wso2.carbon.mdm.mobileservices.windows.services.discovery.beans.DiscoveryRequest; +import org.wso2.carbon.mdm.mobileservices.windows.services.discovery.DiscoveryService; +import org.wso2.carbon.mdm.mobileservices.windows.services.discovery.beans.DiscoveryResponse; + +import javax.annotation.Resource; +import javax.jws.WebService; +import javax.servlet.ServletContext; +import javax.ws.rs.core.Response; +import javax.xml.ws.BindingType; +import javax.xml.ws.Holder; +import javax.xml.ws.WebServiceContext; +import javax.xml.ws.handler.MessageContext; +import javax.xml.ws.soap.Addressing; +import javax.xml.ws.soap.SOAPBinding; + +/** + * Implementation class of Discovery Request. This class implements the first two services + * of device enrolment stage. + */ +@WebService(endpointInterface = PluginConstants.DISCOVERY_SERVICE_ENDPOINT, targetNamespace = PluginConstants + .DISCOVERY_SERVICE_TARGET_NAMESPACE) +@Addressing(enabled = true, required = true) +@BindingType(value = SOAPBinding.SOAP12HTTP_BINDING) +public class DiscoveryServiceImpl implements DiscoveryService { + + public static final String FEDERATED = "Federated"; + private static Log log = LogFactory.getLog(DiscoveryServiceImpl.class); + @Resource + private WebServiceContext context; + + /** + * This method returns the OnPremise AuthPolicy and next two endpoint the mobile device should + * call if this response to received successfully at the device end. This method is called by + * device immediately after the first GET method calling for the same endpoint. + * + * @param discoveryRequest - Request bean comes via mobile phone + * @param response - DiscoveryResponse bean for response + */ + @Override + public void discover(DiscoveryRequest discoveryRequest, Holder response) { + + ServletContext ctx = (ServletContext) context.getMessageContext().get(MessageContext.SERVLET_CONTEXT); + WindowsPluginProperties windowsPluginProperties = (WindowsPluginProperties) ctx.getAttribute( + PluginConstants.WINDOWS_PLUGIN_PROPERTIES); + + DiscoveryResponse discoveryResponse = new DiscoveryResponse(); + if (FEDERATED.equals(windowsPluginProperties.getAuthPolicy())) { + discoveryResponse.setAuthPolicy(windowsPluginProperties.getAuthPolicy()); + discoveryResponse.setEnrollmentPolicyServiceUrl(PluginConstants.Discovery.ENROLL_SUBDOMAIN + + windowsPluginProperties.getDomain() + PluginConstants.Discovery. + CERTIFICATE_ENROLLMENT_POLICY_SERVICE_URL); + discoveryResponse.setEnrollmentServiceUrl(PluginConstants.Discovery.ENROLL_SUBDOMAIN + + windowsPluginProperties.getDomain() + PluginConstants.Discovery. + CERTIFICATE_ENROLLMENT_SERVICE_URL); + discoveryResponse.setAuthenticationServiceUrl(PluginConstants.Discovery.ENROLL_SUBDOMAIN + + windowsPluginProperties.getDomain() + PluginConstants.Discovery.WAB_URL); + } else { + discoveryResponse.setAuthPolicy(windowsPluginProperties.getAuthPolicy()); + discoveryResponse.setEnrollmentPolicyServiceUrl(PluginConstants.Discovery.ENROLL_SUBDOMAIN + + windowsPluginProperties.getDomain() + PluginConstants.Discovery. + ONPREMISE_CERTIFICATE_ENROLLMENT_POLICY); + discoveryResponse.setEnrollmentServiceUrl(PluginConstants.Discovery.ENROLL_SUBDOMAIN + + windowsPluginProperties.getDomain() + PluginConstants.Discovery. + ONPREMISE_CERTIFICATE_ENROLLMENT_SERVICE_URL); + discoveryResponse.setAuthenticationServiceUrl(null); + } + response.value = discoveryResponse; + + if (log.isDebugEnabled()) { + log.debug("Discovery service end point was triggered via POST method"); + } + } + + /** + * This is the first method called through device. The device checks the availability of the + * Service end point by calling this method. + * + * @return - HTTP 200OK message + */ + @Override + public Response discoverGet() { + + if (log.isDebugEnabled()) { + log.debug("Discovery service end point was triggered via GET method."); + } + return Response.ok().build(); + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/policymgtservice/PolicyMgtService.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/policymgtservice/PolicyMgtService.java new file mode 100644 index 000000000..cf013845f --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/policymgtservice/PolicyMgtService.java @@ -0,0 +1,71 @@ +/* + * 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.mdm.mobileservices.windows.services.policymgtservice; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsConfigurationException; +import org.wso2.carbon.mdm.mobileservices.windows.common.util.Message; +import org.wso2.carbon.mdm.mobileservices.windows.common.util.WindowsAPIUtils; +import org.wso2.carbon.policy.mgt.common.Policy; +import org.wso2.carbon.policy.mgt.common.PolicyManagementException; +import org.wso2.carbon.policy.mgt.core.PolicyManagerService; + +import javax.jws.WebService; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; + +/** + * Endpoint for Enforce Effective Policy. + */ +@WebService +@Produces({"application/json", "application/xml"}) +@Consumes({"application/json", "application/xml"}) +public class PolicyMgtService { + private static Log log = LogFactory.getLog(PolicyMgtService.class); + + @GET + @Path("{id}") + public Message getEffectivePolicy(@HeaderParam("Accept") String acceptHeader, + @PathParam("id") String id) throws WindowsConfigurationException { + + DeviceIdentifier deviceIdentifier = WindowsAPIUtils.convertToDeviceIdentifierObject(id); + Message responseMessage = new Message(); + Policy policy; + try { + PolicyManagerService policyManagerService = WindowsAPIUtils.getPolicyManagerService(); + policy = policyManagerService.getEffectivePolicy(deviceIdentifier); + if (policy == null) { + responseMessage.setResponseCode(Response.Status.NO_CONTENT.toString()); + responseMessage.setResponseMessage("No effective policy found"); + return responseMessage; + } else { + responseMessage.setResponseCode(Response.Status.OK.toString()); + responseMessage.setResponseMessage("Effective policy added to operation"); + return responseMessage; + } + + } catch (PolicyManagementException e) { + String msg = "Error occurred while getting the policy."; + log.error(msg, e); + throw new WindowsConfigurationException(msg, e); + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/SyncmlService.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/SyncmlService.java new file mode 100644 index 000000000..9e79626e8 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/SyncmlService.java @@ -0,0 +1,48 @@ +/* + * 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.mdm.mobileservices.windows.services.syncml; + +import org.w3c.dom.Document; +import org.wso2.carbon.device.mgt.common.notification.mgt.NotificationManagementException; +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsConfigurationException; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsDeviceEnrolmentException; +import org.wso2.carbon.mdm.mobileservices.windows.operations.WindowsOperationException; + +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; +import javax.ws.rs.core.Response; + +/** + * Interface for Syncml message flow. + */ +@Path("/devicemanagement") +public interface SyncmlService { + + @Path("/request") + @POST + @Consumes({PluginConstants.SYNCML_MEDIA_TYPE, MediaType.APPLICATION_XML}) + @Produces(PluginConstants.SYNCML_MEDIA_TYPE) + Response getResponse(Document request) throws WindowsDeviceEnrolmentException, WindowsOperationException, + NotificationManagementException, WindowsConfigurationException; + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/beans/BasicOperation.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/beans/BasicOperation.java new file mode 100644 index 000000000..53d265472 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/beans/BasicOperation.java @@ -0,0 +1,35 @@ +/* + * 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.mdm.mobileservices.windows.services.syncml.beans; + +/** + * Bean for get basic operations. + */ +public class BasicOperation { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/beans/PasscodePolicy.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/beans/PasscodePolicy.java new file mode 100644 index 000000000..51fc06cf4 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/beans/PasscodePolicy.java @@ -0,0 +1,107 @@ +/* + * 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.mdm.mobileservices.windows.services.syncml.beans; + +/** + * Bean for device lockOperationUpdate screen passcode policy. + */ +public class PasscodePolicy extends BasicOperation { + + private int maxFailedAttempts; + private int minLength; + private int pinHistory; + private int minComplexChars; + private int maxPINAgeInDays; + private boolean requireAlphanumeric; + private boolean allowSimple; + private boolean enablePassword; + private int maxInactiveTime; + + public int getMaxInactiveTime() { + return maxInactiveTime; + } + + public void setMaxInactiveTime(int maxInactiveTime) { + this.maxInactiveTime = maxInactiveTime; + } + + public boolean isEnablePassword() { + return enablePassword; + } + + public void setEnablePassword(boolean enablePassword) { + this.enablePassword = enablePassword; + } + + public int getMaxFailedAttempts() { + return maxFailedAttempts; + } + + public void setMaxFailedAttempts(int maxFailedAttempts) { + this.maxFailedAttempts = maxFailedAttempts; + } + + public int getMinLength() { + return minLength; + } + + public void setMinLength(int minLength) { + this.minLength = minLength; + } + + public int getPinHistory() { + return pinHistory; + } + + public void setPinHistory(int pinHistory) { + this.pinHistory = pinHistory; + } + + public int getMinComplexChars() { + return minComplexChars; + } + + public void setMinComplexChars(int minComplexChars) { + this.minComplexChars = minComplexChars; + } + + public int getMaxPINAgeInDays() { + return maxPINAgeInDays; + } + + public void setMaxPINAgeInDays(int maxPINAgeInDays) { + this.maxPINAgeInDays = maxPINAgeInDays; + } + + public boolean isRequireAlphanumeric() { + return requireAlphanumeric; + } + + public void setRequireAlphanumeric(boolean requireAlphanumeric) { + this.requireAlphanumeric = requireAlphanumeric; + } + + public boolean isAllowSimple() { + return allowSimple; + } + + public void setAllowSimple(boolean allowSimple) { + this.allowSimple = allowSimple; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/beans/Profile.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/beans/Profile.java new file mode 100644 index 000000000..ad3a702e6 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/beans/Profile.java @@ -0,0 +1,62 @@ +/* + * 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.mdm.mobileservices.windows.services.syncml.beans; + +/** + * Class for contains device active policy. + */ +public class Profile { + + String featureCode; + String data; + boolean enable; + boolean compliance; + + public boolean isCompliance() { + return compliance; + } + + public void setCompliance(boolean compliance) { + this.compliance = compliance; + } + + public boolean isEnable() { + return enable; + } + + public void setEnable(boolean enable) { + this.enable = enable; + } + + public String getFeatureCode() { + return featureCode; + } + + public void setFeatureCode(String featureCode) { + this.featureCode = featureCode; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/beans/Wifi.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/beans/Wifi.java new file mode 100644 index 000000000..99979637f --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/beans/Wifi.java @@ -0,0 +1,107 @@ +/* + * 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.mdm.mobileservices.windows.services.syncml.beans; + +/** + * Bean for WIFI configurations. + */ +public class Wifi extends BasicOperation { + + private String networkName; + private String ssid; + private String connectionType; + private String connectionMode; + private String authentication; + private String encryption; + private String keyType; + private String protection; + private String keyMaterial; + + public String getNetworkName() { + return networkName; + } + + public void setNetworkName(String networkName) { + this.networkName = networkName; + } + + public String getSsid() { + return ssid; + } + + public void setSsid(String ssid) { + this.ssid = ssid; + } + + public String getConnectionType() { + return connectionType; + } + + public void setConnectionType(String connectionType) { + this.connectionType = connectionType; + } + + public String getConnectionMode() { + return connectionMode; + } + + public void setConnectionMode(String connectionMode) { + this.connectionMode = connectionMode; + } + + public String getAuthentication() { + return authentication; + } + + public void setAuthentication(String authentication) { + this.authentication = authentication; + } + + public String getEncryption() { + return encryption; + } + + public void setEncryption(String encryption) { + this.encryption = encryption; + } + + public String getKeyType() { + return keyType; + } + + public void setKeyType(String keyType) { + this.keyType = keyType; + } + + public String getProtection() { + return protection; + } + + public void setProtection(String protection) { + this.protection = protection; + } + + public String getKeyMaterial() { + return keyMaterial; + } + + public void setKeyMaterial(String keyMaterial) { + this.keyMaterial = keyMaterial; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/impl/SyncmlServiceImpl.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/impl/SyncmlServiceImpl.java new file mode 100644 index 000000000..0a260ff75 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/impl/SyncmlServiceImpl.java @@ -0,0 +1,462 @@ +/* + * 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.mdm.mobileservices.windows.services.syncml.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.json.JSONException; +import org.w3c.dom.Document; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.device.mgt.common.*; +import org.wso2.carbon.device.mgt.common.notification.mgt.NotificationManagementException; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.common.operation.mgt.OperationManagementException; +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.wso2.carbon.mdm.mobileservices.windows.common.beans.CacheEntry; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.SyncmlMessageFormatException; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.SyncmlOperationException; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsConfigurationException; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsDeviceEnrolmentException; +import org.wso2.carbon.mdm.mobileservices.windows.common.util.DeviceUtil; +import org.wso2.carbon.mdm.mobileservices.windows.common.util.WindowsAPIUtils; +import org.wso2.carbon.mdm.mobileservices.windows.operations.*; +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.*; +import org.wso2.carbon.mdm.mobileservices.windows.services.syncml.SyncmlService; +import org.wso2.carbon.policy.mgt.common.PolicyManagementException; +import org.wso2.carbon.policy.mgt.common.monitor.PolicyComplianceException; +import org.wso2.carbon.policy.mgt.core.PolicyManagerService; + +import javax.ws.rs.core.Response; +import java.util.ArrayList; +import java.util.List; + +import static org.wso2.carbon.mdm.mobileservices.windows.common.util.WindowsAPIUtils.convertToDeviceIdentifierObject; + +/** + * Implementing class of SyncmlImpl interface. + */ +public class SyncmlServiceImpl implements SyncmlService { + + private static Log log = LogFactory.getLog(SyncmlServiceImpl.class); + + /** + * This method is used to generate and return Device object from the received information at + * the Syncml step. + * + * @param deviceID - Unique device ID received from the Device + * @param osVersion - Device OS version + * @param imsi - Device IMSI + * @param imei - Device IMEI + * @param manufacturer - Device Manufacturer name + * @param model - Device Model + * @return - Generated device object + */ + private Device generateDevice(String type, String deviceID, String osVersion, String imsi, + String imei, String manufacturer, String model, String user) { + + Device generatedDevice = new Device(); + + Device.Property OSVersionProperty = new Device.Property(); + OSVersionProperty.setName(PluginConstants.SyncML.OS_VERSION); + OSVersionProperty.setValue(osVersion); + + Device.Property IMSEIProperty = new Device.Property(); + IMSEIProperty.setName(PluginConstants.SyncML.IMSI); + IMSEIProperty.setValue(imsi); + + Device.Property IMEIProperty = new Device.Property(); + IMEIProperty.setName(PluginConstants.SyncML.IMEI); + IMEIProperty.setValue(imei); + + Device.Property DevManProperty = new Device.Property(); + DevManProperty.setName(PluginConstants.SyncML.VENDOR); + DevManProperty.setValue(manufacturer); + + Device.Property DevModProperty = new Device.Property(); + DevModProperty.setName(PluginConstants.SyncML.MODEL); + DevModProperty.setValue(model); + + List propertyList = new ArrayList<>(); + propertyList.add(OSVersionProperty); + propertyList.add(IMSEIProperty); + propertyList.add(IMEIProperty); + propertyList.add(DevManProperty); + propertyList.add(DevModProperty); + + EnrolmentInfo enrolmentInfo = new EnrolmentInfo(); + enrolmentInfo.setOwner(user); + enrolmentInfo.setOwnership(EnrolmentInfo.OwnerShip.BYOD); + enrolmentInfo.setStatus(EnrolmentInfo.Status.ACTIVE); + + generatedDevice.setEnrolmentInfo(enrolmentInfo); + generatedDevice.setDeviceIdentifier(deviceID); + generatedDevice.setProperties(propertyList); + generatedDevice.setType(type); + + return generatedDevice; + } + + /** + * Method for calling SyncML engine for producing the Syncml response. For the first SyncML message comes from + * the device, this method produces a response to retrieve device information for enrolling the device. + * + * @param request - SyncML request + * @return - SyncML response + * @throws WindowsOperationException + * @throws WindowsDeviceEnrolmentException + */ + @Override + public Response getResponse(Document request) + throws WindowsDeviceEnrolmentException, WindowsOperationException, NotificationManagementException, + WindowsConfigurationException { + int msgId; + int sessionId; + String user; + String token; + String response; + SyncmlDocument syncmlDocument; + List deviceInfoOperations; + List pendingOperations; + OperationUtils operationUtils = new OperationUtils(); + DeviceInfo deviceInfo = new DeviceInfo(); + + try { + if (SyncmlParser.parseSyncmlPayload(request) != null) { + try { + syncmlDocument = SyncmlParser.parseSyncmlPayload(request); + } catch (SyncmlMessageFormatException e) { + String msg = "Error occurred due to bad syncml format."; + log.error(msg, e); + throw new SyncmlMessageFormatException(msg, e); + } + SyncmlHeader syncmlHeader = syncmlDocument.getHeader(); + sessionId = syncmlHeader.getSessionId(); + user = syncmlHeader.getSource().getLocName(); + DeviceIdentifier deviceIdentifier = convertToDeviceIdentifierObject(syncmlHeader.getSource() + .getLocURI()); + msgId = syncmlHeader.getMsgID(); + if ((PluginConstants.SyncML.SYNCML_FIRST_MESSAGE_ID == msgId) && + (PluginConstants.SyncML.SYNCML_FIRST_SESSION_ID == sessionId)) { + token = syncmlHeader.getCredential().getData(); + CacheEntry cacheToken = (CacheEntry) DeviceUtil.getCacheEntry(token); + + if (cacheToken.getUsername().equals(user)) { + + if (enrollDevice(request)) { + deviceInfoOperations = deviceInfo.getDeviceInfo(); + try { + response = generateReply(syncmlDocument, deviceInfoOperations); + PolicyManagerService policyManagerService = WindowsAPIUtils.getPolicyManagerService(); + policyManagerService.getEffectivePolicy(deviceIdentifier); + return Response.status(Response.Status.OK).entity(response).build(); + } catch (PolicyManagementException e) { + String msg = "Error occurred in while getting effective policy."; + log.error(msg, e); + throw new WindowsConfigurationException(msg, e); + } catch (SyncmlOperationException e) { + String msg = "Error occurred in while generating hash value."; + log.error(msg, e); + throw new WindowsOperationException(msg, e); + } + + } else { + String msg = "Error occurred in device enrollment."; + log.error(msg); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(msg).build(); + } + } else { + String msg = "Authentication failure due to incorrect credentials."; + log.error(msg); + return Response.status(Response.Status.UNAUTHORIZED).entity(msg).build(); + } + } else if (PluginConstants.SyncML.SYNCML_SECOND_MESSAGE_ID == msgId && + PluginConstants.SyncML.SYNCML_FIRST_SESSION_ID == sessionId) { + + if (enrollDevice(request)) { + try { + return Response.ok().entity(generateReply(syncmlDocument, null)).build(); + } catch (SyncmlOperationException e) { + String msg = "Error occurred in while getting effective feature"; + log.error(msg, e); + throw new WindowsOperationException(msg, e); + } + } else { + String msg = "Error occurred in modify enrollment."; + log.error(msg); + return Response.status(Response.Status.NOT_MODIFIED).entity(msg).build(); + } + } else if (sessionId >= PluginConstants.SyncML.SYNCML_SECOND_SESSION_ID) { + if ((syncmlDocument.getBody().getAlert() != null)) { + if (!syncmlDocument.getBody().getAlert().getData().equals(Constants.DISENROLL_ALERT_DATA)) { + try { + pendingOperations = operationUtils.getPendingOperations(syncmlDocument); + return Response.ok().entity(generateReply(syncmlDocument, pendingOperations)).build(); + } catch (OperationManagementException e) { + String msg = "Cannot access operation management service."; + log.error(msg, e); + throw new WindowsOperationException(msg, e); + } catch (DeviceManagementException e) { + String msg = "Cannot access Device management service."; + log.error(msg, e); + throw new WindowsOperationException(msg, e); + } catch (FeatureManagementException e) { + String msg = "Error occurred in getting effective features. "; + log.error(msg, e); + throw new WindowsOperationException(msg, e); + } catch (PolicyComplianceException e) { + String msg = "Error occurred in setting policy compliance."; + log.error(msg, e); + throw new WindowsConfigurationException(msg, e); + } catch (NotificationManagementException e) { + String msg = "Error occurred in while getting notification service"; + throw new WindowsOperationException(msg, e); + } catch (SyncmlOperationException e) { + String msg = "Error occurred in while encoding hash value."; + log.error(msg, e); + throw new WindowsOperationException(msg, e); + } + } else { + try { + if (WindowsAPIUtils.getDeviceManagementService().getDevice(deviceIdentifier) != null) { + WindowsAPIUtils.getDeviceManagementService().disenrollDevice(deviceIdentifier); + return Response.ok().entity(generateReply(syncmlDocument, null)).build(); + } else { + String msg = "Enrolled device can not be found in the server."; + log.error(msg); + return Response.status(Response.Status.NOT_FOUND).entity(msg).build(); + } + } catch (DeviceManagementException e) { + String msg = "Failure occurred in dis-enrollment flow."; + log.error(msg, e); + throw new WindowsOperationException(msg, e); + } catch (SyncmlOperationException e) { + String msg = "Error occurred in while generating hash value."; + log.error(msg, e); + throw new WindowsOperationException(msg, e); + } + } + } else { + try { + pendingOperations = operationUtils.getPendingOperations(syncmlDocument); + return Response.ok().entity(generateReply(syncmlDocument, pendingOperations)) + .build(); + } catch (OperationManagementException e) { + String msg = "Cannot access operation management service."; + log.error(msg, e); + throw new WindowsOperationException(msg, e); + } catch (DeviceManagementException e) { + String msg = "Cannot access Device management service."; + log.error(msg, e); + throw new WindowsOperationException(msg, e); + } catch (FeatureManagementException e) { + String msg = "Error occurred in getting effective features. "; + log.error(msg, e); + throw new WindowsConfigurationException(msg, e); + } catch (PolicyComplianceException e) { + String msg = "Error occurred in setting policy compliance."; + log.error(msg, e); + throw new WindowsConfigurationException(msg, e); + } catch (NotificationManagementException e) { + String msg = "Error occurred in while getting notification service."; + log.error(msg, e); + throw new WindowsOperationException(msg, e); + } catch (SyncmlOperationException e) { + String msg = "Error occurred in while getting effective feature."; + log.error(msg, e); + throw new WindowsConfigurationException(msg, e); + } + } + } else { + String msg = "Failure occurred in Device request message."; + log.error(msg); + return Response.status(Response.Status.BAD_REQUEST).entity(msg).build(); + } + } + } catch (SyncmlMessageFormatException e) { + String msg = "Error occurred in parsing syncml request."; + log.error(msg, e); + throw new WindowsOperationException(msg, e); + } + return null; + } + + /** + * Enroll phone device + * + * @param request Device syncml request for the server side. + * @return enroll state + * @throws WindowsDeviceEnrolmentException + * @throws WindowsOperationException + */ + private boolean enrollDevice(Document request) throws WindowsDeviceEnrolmentException, + WindowsOperationException { + + String osVersion; + String imsi = null; + String imei = null; + String devID; + String devMan; + String devMod; + String devLang; + String vender; + String macAddress; + String resolution; + String modVersion; + boolean status = false; + String user; + String deviceName; + int msgID; + SyncmlDocument syncmlDocument; + + try { + syncmlDocument = SyncmlParser.parseSyncmlPayload(request); + msgID = syncmlDocument.getHeader().getMsgID(); + if (msgID == PluginConstants.SyncML.SYNCML_FIRST_MESSAGE_ID) { + Replace replace = syncmlDocument.getBody().getReplace(); + List itemList = replace.getItems(); + devID = itemList.get(PluginConstants.SyncML.DEVICE_ID_POSITION).getData(); + devMan = itemList.get(PluginConstants.SyncML.DEVICE_MAN_POSITION).getData(); + devMod = itemList.get(PluginConstants.SyncML.DEVICE_MODEL_POSITION).getData(); + modVersion = itemList.get(PluginConstants.SyncML.DEVICE_MOD_VER_POSITION).getData(); + devLang = itemList.get(PluginConstants.SyncML.DEVICE_LANG_POSITION).getData(); + user = syncmlDocument.getHeader().getSource().getLocName(); + + if (log.isDebugEnabled()) { + log.debug( + "OS Version:" + modVersion + ", DevID: " + devID + ", DevMan: " + devMan + + ", DevMod: " + devMod + ", DevLang: " + devLang); + } + Device generateDevice = generateDevice(DeviceManagementConstants.MobileDeviceTypes. + MOBILE_DEVICE_TYPE_WINDOWS, devID, modVersion, imsi, imei, devMan, devMod, user); + status = WindowsAPIUtils.getDeviceManagementService().enrollDevice(generateDevice); + WindowsAPIUtils.startTenantFlow(user); + return status; + + } else if (msgID == PluginConstants.SyncML.SYNCML_SECOND_MESSAGE_ID) { + List itemList = syncmlDocument.getBody().getResults().getItem(); + osVersion = itemList.get(PluginConstants.SyncML.OSVERSION_POSITION).getData(); + imsi = itemList.get(PluginConstants.SyncML.IMSI_POSITION).getData(); + imei = itemList.get(PluginConstants.SyncML.IMEI_POSITION).getData(); + vender = itemList.get(PluginConstants.SyncML.VENDER_POSITION).getData(); + devMod = itemList.get(PluginConstants.SyncML.MODEL_POSITION).getData(); + macAddress = itemList.get(PluginConstants.SyncML.MACADDRESS_POSITION).getData(); + resolution = itemList.get(PluginConstants.SyncML.RESOLUTION_POSITION).getData(); + deviceName = itemList.get(PluginConstants.SyncML.DEVICE_NAME_POSITION).getData(); + DeviceIdentifier deviceIdentifier = convertToDeviceIdentifierObject(syncmlDocument. + getHeader().getSource().getLocURI()); + Device existingDevice = WindowsAPIUtils.getDeviceManagementService().getDevice(deviceIdentifier); + + if (!existingDevice.getProperties().isEmpty()) { + List existingProperties = new ArrayList<>(); + + Device.Property imeiProperty = new Device.Property(); + imeiProperty.setName(PluginConstants.SyncML.IMEI); + imeiProperty.setValue(imei); + existingProperties.add(imeiProperty); + + Device.Property osVersionProperty = new Device.Property(); + osVersionProperty.setName(PluginConstants.SyncML.OS_VERSION); + osVersionProperty.setValue(osVersion); + existingProperties.add(osVersionProperty); + + Device.Property imsiProperty = new Device.Property(); + imsiProperty.setName(PluginConstants.SyncML.IMSI); + imsiProperty.setValue(imsi); + existingProperties.add(imsiProperty); + + Device.Property venderProperty = new Device.Property(); + venderProperty.setName(PluginConstants.SyncML.VENDOR); + venderProperty.setValue(vender); + existingProperties.add(venderProperty); + + Device.Property macAddressProperty = new Device.Property(); + macAddressProperty.setName(PluginConstants.SyncML.MAC_ADDRESS); + macAddressProperty.setValue(macAddress); + existingProperties.add(macAddressProperty); + + Device.Property resolutionProperty = new Device.Property(); + resolutionProperty.setName(PluginConstants.SyncML.DEVICE_INFO); + resolutionProperty.setValue(resolution); + existingProperties.add(resolutionProperty); + + Device.Property deviceNameProperty = new Device.Property(); + deviceNameProperty.setName(PluginConstants.SyncML.DEVICE_NAME); + deviceNameProperty.setValue(deviceName); + existingProperties.add(deviceNameProperty); + + Device.Property deviceModelProperty = new Device.Property(); + deviceNameProperty.setName(PluginConstants.SyncML.MODEL); + deviceNameProperty.setValue(devMod); + existingProperties.add(deviceModelProperty); + + existingDevice.setProperties(existingProperties); + existingDevice.setDeviceIdentifier(syncmlDocument.getHeader().getSource().getLocURI()); + existingDevice.setType(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS); + status = WindowsAPIUtils.getDeviceManagementService().modifyEnrollment(existingDevice); + // call effective policy for the enrolling device. + PolicyManagerService policyManagerService = WindowsAPIUtils.getPolicyManagerService(); + policyManagerService.getEffectivePolicy(deviceIdentifier); + return status; + } + } + } catch (DeviceManagementException e) { + String msg = "Failure occurred in enrolling device."; + log.error(msg, e); + throw new WindowsDeviceEnrolmentException(msg, e); + } catch (SyncmlMessageFormatException e) { + String msg = "Error occurred in bad format of the syncml payload."; + log.error(msg, e); + throw new WindowsOperationException(msg, e); + } catch (PolicyManagementException e) { + String msg = "Error occurred in getting effective policy."; + log.error(msg, e); + throw new WindowsOperationException(msg, e); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + return status; + } + + /** + * Generate Device payloads. + * + * @param syncmlDocument parsed syncml payload from the syncml engine. + * @param operations operations for generate payload. + * @return String type syncml payload. + * @throws WindowsOperationException + * @throws JSONException + * @throws PolicyManagementException + * @throws org.wso2.carbon.policy.mgt.common.FeatureManagementException + */ + public String generateReply(SyncmlDocument syncmlDocument, List operations) + throws SyncmlMessageFormatException, SyncmlOperationException { + + OperationReply operationReply; + SyncmlGenerator generator; + SyncmlDocument syncmlResponse; + if (operations == null) { + operationReply = new OperationReply(syncmlDocument); + } else { + operationReply = new OperationReply(syncmlDocument, operations); + } + syncmlResponse = operationReply.generateReply(); + generator = new SyncmlGenerator(); + return generator.generatePayload(syncmlResponse); + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/util/PolicyManager.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/util/PolicyManager.java new file mode 100644 index 000000000..403b44f88 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/util/PolicyManager.java @@ -0,0 +1,54 @@ +/* + * 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.mdm.mobileservices.windows.services.syncml.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsConfigurationException; +import org.wso2.carbon.mdm.mobileservices.windows.common.util.WindowsAPIUtils; +import org.wso2.carbon.policy.mgt.common.Policy; +import org.wso2.carbon.policy.mgt.common.PolicyManagementException; +import org.wso2.carbon.policy.mgt.core.PolicyManagerService; + +/** + * implementation for PolicyManager + */ +public class PolicyManager { + + private static Log log = LogFactory.getLog(PolicyManager.class); + + public Policy getEffectivePolicy(DeviceIdentifier deviceIdentifier) throws WindowsConfigurationException { + Policy policy; + PolicyManagerService policyManagerService = WindowsAPIUtils.getPolicyManagerService(); + try { + policy = policyManagerService.getEffectivePolicy(deviceIdentifier); + if (policy != null) { + return policy; + } else { + return null; + } + } catch (PolicyManagementException e) { + String msg = "Error occurred while getting policy."; + log.error(msg, e); + throw new WindowsConfigurationException(); + + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/util/SyncmlUtils.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/util/SyncmlUtils.java new file mode 100644 index 000000000..90275b3cd --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/syncml/util/SyncmlUtils.java @@ -0,0 +1,49 @@ +/* + * 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.mdm.mobileservices.windows.services.syncml.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.device.mgt.core.service.DeviceManagementProviderService; + +/** + * Class for generate Device object from the received data. + */ +public class SyncmlUtils { + + private static Log log = LogFactory.getLog(SyncmlUtils.class); + + /** + * This method returns Device Management Object for certain tasks such as Device enrollment etc. + * + * @return DeviceManagementServiceObject + */ + public static DeviceManagementProviderService getDeviceManagementService() { + try { + PrivilegedCarbonContext context = PrivilegedCarbonContext.getThreadLocalCarbonContext(); + + return (DeviceManagementProviderService) context.getOSGiService(DeviceManagementProviderService.class, + null); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/CertificateEnrollmentService.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/CertificateEnrollmentService.java new file mode 100644 index 000000000..e517672db --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/CertificateEnrollmentService.java @@ -0,0 +1,62 @@ +/* + * 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.mdm.mobileservices.windows.services.wstep; + +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WAPProvisioningException; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsDeviceEnrolmentException; +import org.wso2.carbon.mdm.mobileservices.windows.services.wstep.beans.AdditionalContext; +import org.wso2.carbon.mdm.mobileservices.windows.services.wstep.beans.RequestSecurityTokenResponse; + +import javax.jws.WebMethod; +import javax.jws.WebParam; +import javax.jws.WebService; +import javax.xml.ws.BindingType; +import javax.xml.ws.RequestWrapper; +import javax.xml.ws.ResponseWrapper; +import javax.xml.ws.soap.SOAPBinding; +import java.io.UnsupportedEncodingException; + +/** + * Interface of WSTEP implementation. + */ +@WebService(targetNamespace = PluginConstants.DEVICE_ENROLLMENT_SERVICE_TARGET_NAMESPACE, name = "wstep") +@BindingType(value = SOAPBinding.SOAP12HTTP_BINDING) +public interface CertificateEnrollmentService { + + @RequestWrapper(localName = "RequestSecurityToken", targetNamespace = PluginConstants + .WS_TRUST_TARGET_NAMESPACE) + @WebMethod(operationName = "RequestSecurityToken") + @ResponseWrapper(localName = "RequestSecurityTokenResponseCollection", targetNamespace = + PluginConstants.WS_TRUST_TARGET_NAMESPACE) + public void requestSecurityToken( + @WebParam(name = "TokenType", targetNamespace = PluginConstants.WS_TRUST_TARGET_NAMESPACE) + String tokenType, + @WebParam(name = "RequestType", targetNamespace = PluginConstants.WS_TRUST_TARGET_NAMESPACE) + String requestType, + @WebParam(name = "BinarySecurityToken", targetNamespace = PluginConstants + .WS_SECURITY_TARGET_NAMESPACE) + String binarySecurityToken, + @WebParam(name = "AdditionalContext", targetNamespace = PluginConstants + .SOAP_AUTHORIZATION_TARGET_NAMESPACE) + AdditionalContext additionalContext, + @WebParam(mode = WebParam.Mode.OUT, name = "RequestSecurityTokenResponse", + targetNamespace = PluginConstants.WS_TRUST_TARGET_NAMESPACE) + javax.xml.ws.Holder response) throws + WindowsDeviceEnrolmentException, UnsupportedEncodingException, WAPProvisioningException; +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/AdditionalContext.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/AdditionalContext.java new file mode 100644 index 000000000..8850d5afd --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/AdditionalContext.java @@ -0,0 +1,48 @@ +/* + * 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.mdm.mobileservices.windows.services.wstep.beans; + +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; +import java.util.ArrayList; +import java.util.List; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "OIDCollection", namespace = PluginConstants.SOAP_AUTHORIZATION_TARGET_NAMESPACE, + propOrder = {"contextitem"}) +@SuppressWarnings("unused") +public class AdditionalContext { + + @XmlElement(name = "ContextItem", required = true, + namespace = PluginConstants.SOAP_AUTHORIZATION_TARGET_NAMESPACE) + + protected List contextitem; + + public List getcontextitem() { + if (contextitem == null) { + contextitem = new ArrayList(); + } + return this.contextitem; + } +} + diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/BinarySecurityToken.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/BinarySecurityToken.java new file mode 100644 index 000000000..544c4cb79 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/BinarySecurityToken.java @@ -0,0 +1,67 @@ +/* + * 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.mdm.mobileservices.windows.services.wstep.beans; + +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlType; +import javax.xml.bind.annotation.XmlValue; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "BinarySecurityToken", namespace = PluginConstants.WS_SECURITY_TARGET_NAMESPACE, + propOrder = {"ValueType", "EncodingType"}) +@SuppressWarnings("unused") +public class BinarySecurityToken { + + @XmlAttribute(name = "ValueType") + protected String ValueType; + @XmlAttribute(name = "EncodingType") + protected String EncodingType; + @XmlValue + protected String Token; + + public void setValueType(String valuetype) { + this.ValueType = valuetype; + } + + public String getValueType() { + return this.ValueType; + } + + public void setEncodingType(String encodingtype) { + this.EncodingType = encodingtype; + } + + public String getEncodingType() { + return this.EncodingType; + } + + public void setToken(String token) { + this.Token = token; + } + + public String getToken() { + return this.Token; + } + +} + diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/ContextItem.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/ContextItem.java new file mode 100644 index 000000000..bfc450e55 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/ContextItem.java @@ -0,0 +1,44 @@ +/* + * 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.mdm.mobileservices.windows.services.wstep.beans; + +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "ContextItem", namespace = PluginConstants.SOAP_AUTHORIZATION_TARGET_NAMESPACE, + propOrder = {"Value"}) +public class ContextItem { + + @XmlElement(required = true, namespace = PluginConstants.SOAP_AUTHORIZATION_TARGET_NAMESPACE) + protected String Value; + + public String getValue() { + return Value; + } + + public void setValue(String value) { + Value = value; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/RequestSecurityToken.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/RequestSecurityToken.java new file mode 100644 index 000000000..74be950d1 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/RequestSecurityToken.java @@ -0,0 +1,42 @@ +/* + * 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.mdm.mobileservices.windows.services.wstep.beans; + +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "RequestedSecurityToken", namespace = PluginConstants.WS_TRUST_TARGET_NAMESPACE, + propOrder = {"binarySecurityToken"}) +@SuppressWarnings("unused") +public class RequestSecurityToken { + + @XmlElement(name = "BinarySecurityToken", required = true, + namespace = PluginConstants.WS_SECURITY_TARGET_NAMESPACE) + + protected BinarySecurityToken binarySecurityToken; + + public void setBinarySecurityToken(BinarySecurityToken binarysecuritytoken) { + this.binarySecurityToken = binarysecuritytoken; + } +} \ No newline at end of file diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/RequestSecurityTokenResponse.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/RequestSecurityTokenResponse.java new file mode 100644 index 000000000..174cb0bc0 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/RequestSecurityTokenResponse.java @@ -0,0 +1,68 @@ +/* + * 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.mdm.mobileservices.windows.services.wstep.beans; + +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; +import java.io.Serializable; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "RequestSecurityTokenResponse", namespace = PluginConstants.WS_TRUST_TARGET_NAMESPACE, + propOrder = {"TokenType", "RequestedSecurityToken", "RequestID"}) +@SuppressWarnings("unused") +public class RequestSecurityTokenResponse implements Serializable { + + @XmlElement(name = "TokenType", namespace = PluginConstants.WS_TRUST_TARGET_NAMESPACE) + private String TokenType; + + @XmlElement(name = "RequestedSecurityToken", required = true, + namespace = PluginConstants.WS_TRUST_TARGET_NAMESPACE) + private RequestedSecurityToken RequestedSecurityToken; + + @XmlElement(name = "RequestID", namespace = PluginConstants.ENROLLMENT_POLICY_TARGET_NAMESPACE) + private int RequestID; + + public String getTokenType() { + return TokenType; + } + + public void setTokenType(String tokenType) { + TokenType = tokenType; + } + + public RequestedSecurityToken getRequestedSecurityToken() { + return RequestedSecurityToken; + } + + public void setRequestedSecurityToken(RequestedSecurityToken requestedSecurityToken) { + RequestedSecurityToken = requestedSecurityToken; + } + + public int getRequestID() { + return RequestID; + } + + public void setRequestID(int requestID) { + RequestID = requestID; + } +} \ No newline at end of file diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/RequestedSecurityToken.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/RequestedSecurityToken.java new file mode 100644 index 000000000..c448c14ce --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/RequestedSecurityToken.java @@ -0,0 +1,42 @@ +/* + * 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.mdm.mobileservices.windows.services.wstep.beans; + +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "RequestedSecurityToken", namespace = PluginConstants.WS_TRUST_TARGET_NAMESPACE, + propOrder = {"binarySecurityToken"}) +public class RequestedSecurityToken { + + @XmlElement(name = "BinarySecurityToken", required = true, + namespace = PluginConstants.WS_SECURITY_TARGET_NAMESPACE) + + protected BinarySecurityToken binarySecurityToken; + + public void setBinarySecurityToken(BinarySecurityToken binarysecuritytoken) { + this.binarySecurityToken = binarysecuritytoken; + } +} + diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/package-info.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/package-info.java new file mode 100644 index 000000000..47dd02949 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/beans/package-info.java @@ -0,0 +1,27 @@ +/* + * 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. + */ + +@XmlSchema(namespace = "http://www.w3.org/2003/05/soap-envelope", + xmlns = { + @XmlNs(prefix = "", namespaceURI = "http://www.w3.org/2003/05/soap-envelope") + }, elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED) + +package org.wso2.carbon.mdm.mobileservices.windows.services.wstep.beans; + +import javax.xml.bind.annotation.XmlNs; +import javax.xml.bind.annotation.XmlSchema; \ No newline at end of file diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/impl/CertificateEnrollmentServiceImpl.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/impl/CertificateEnrollmentServiceImpl.java new file mode 100644 index 000000000..f88600b78 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/impl/CertificateEnrollmentServiceImpl.java @@ -0,0 +1,381 @@ +/* + * 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.mdm.mobileservices.windows.services.wstep.impl; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.cxf.headers.Header; +import org.apache.cxf.helpers.CastUtils; +import org.apache.cxf.jaxws.context.WrappedMessageContext; +import org.apache.cxf.message.Message; +import org.w3c.dom.*; +import org.wso2.carbon.certificate.mgt.core.exception.KeystoreException; +import org.wso2.carbon.certificate.mgt.core.service.CertificateManagementServiceImpl; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.configuration.mgt.ConfigurationEntry; +import org.wso2.carbon.device.mgt.common.configuration.mgt.TenantConfiguration; +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.wso2.carbon.mdm.mobileservices.windows.common.beans.CacheEntry; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.CertificateGenerationException; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.SyncmlMessageFormatException; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WAPProvisioningException; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WindowsDeviceEnrolmentException; +import org.wso2.carbon.mdm.mobileservices.windows.common.util.DeviceUtil; +import org.wso2.carbon.mdm.mobileservices.windows.common.util.WindowsAPIUtils; +import org.wso2.carbon.mdm.mobileservices.windows.operations.util.SyncmlCredentials; +import org.wso2.carbon.mdm.mobileservices.windows.services.wstep.CertificateEnrollmentService; +import org.wso2.carbon.mdm.mobileservices.windows.services.wstep.beans.AdditionalContext; +import org.wso2.carbon.mdm.mobileservices.windows.services.wstep.beans.BinarySecurityToken; +import org.wso2.carbon.mdm.mobileservices.windows.services.wstep.beans.RequestSecurityTokenResponse; +import org.wso2.carbon.mdm.mobileservices.windows.services.wstep.beans.RequestedSecurityToken; +import org.xml.sax.SAXException; + +import javax.annotation.Resource; +import javax.jws.WebService; +import javax.servlet.ServletContext; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.ws.BindingType; +import javax.xml.ws.Holder; +import javax.xml.ws.WebServiceContext; +import javax.xml.ws.handler.MessageContext; +import javax.xml.ws.soap.Addressing; +import javax.xml.ws.soap.SOAPBinding; +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.List; + +/** + * Implementation class of CertificateEnrollmentService interface. This class implements MS-WSTEP + * protocol. + */ +@WebService(endpointInterface = PluginConstants.CERTIFICATE_ENROLLMENT_SERVICE_ENDPOINT, + targetNamespace = PluginConstants.DEVICE_ENROLLMENT_SERVICE_TARGET_NAMESPACE) +@Addressing(enabled = true, required = true) +@BindingType(value = SOAPBinding.SOAP12HTTP_BINDING) +public class CertificateEnrollmentServiceImpl implements CertificateEnrollmentService { + + private static final int REQUEST_ID = 0; + private static final int CA_CERTIFICATE_POSITION = 0; + private static final int SIGNED_CERTIFICATE_POSITION = 1; + private static final int APPAUTH_USERNAME_POSITION = 21; + private static final int APPAUTH_PASSWORD_POSITION = 22; + private static final int POLLING_FREQUENCY_POSITION = 27; + private static Log log = LogFactory.getLog(CertificateEnrollmentServiceImpl.class); + private X509Certificate rootCACertificate; + private String pollingFrequency; + + @Resource + private WebServiceContext context; + + /** + * This method implements MS-WSTEP for Certificate Enrollment Service. + * + * @param tokenType - Device Enrolment Token type is received via device + * @param requestType - WS-Trust request type + * @param binarySecurityToken - CSR from device + * @param additionalContext - Device type and OS version is received + * @param response - Response will include wap-provisioning xml + */ + @Override + public void requestSecurityToken(String tokenType, String requestType, + String binarySecurityToken, + AdditionalContext additionalContext, + Holder response) throws + WindowsDeviceEnrolmentException, UnsupportedEncodingException, WAPProvisioningException { + + String headerBinarySecurityToken = null; + List
      headers = getHeaders(); + for (Header headerElement : headers != null ? headers : null) { + String nodeName = headerElement.getName().getLocalPart(); + if (nodeName.equals(PluginConstants.SECURITY)) { + Element element = (Element) headerElement.getObject(); + headerBinarySecurityToken = element.getFirstChild().getNextSibling().getFirstChild().getTextContent(); + } + } + List tenantConfigurations = null; + try { + if (getTenantConfigurationData() != null) { + tenantConfigurations = getTenantConfigurationData(); + for (ConfigurationEntry configurationEntry : tenantConfigurations) { + if (configurationEntry.getName().equals(PluginConstants.TenantConfigProperties. + NOTIFIER_FREQUENCY)) { + pollingFrequency = configurationEntry.getValue().toString(); + } else { + pollingFrequency = PluginConstants.TenantConfigProperties.DEFAULT_FREQUENCY; + } + } + } else { + pollingFrequency = PluginConstants.TenantConfigProperties.DEFAULT_FREQUENCY; + String msg = "Tenant configurations are not initialized yet."; + log.error(msg); + } + } catch (DeviceManagementException e) { + String msg = "Error occurred in while getting tenant configurations."; + log.error(msg); + throw new WindowsDeviceEnrolmentException(msg, e); + } + ServletContext ctx = + (ServletContext) context.getMessageContext().get(MessageContext.SERVLET_CONTEXT); + File wapProvisioningFile = (File) ctx.getAttribute(PluginConstants.CONTEXT_WAP_PROVISIONING_FILE); + + if (log.isDebugEnabled()) { + log.debug("Received CSR from Device:" + binarySecurityToken); + } + String wapProvisioningFilePath = wapProvisioningFile.getPath(); + RequestSecurityTokenResponse requestSecurityTokenResponse = + new RequestSecurityTokenResponse(); + requestSecurityTokenResponse.setTokenType(PluginConstants.CertificateEnrolment.TOKEN_TYPE); + String encodedWap; + try { + encodedWap = prepareWapProvisioningXML(binarySecurityToken, + wapProvisioningFilePath, headerBinarySecurityToken); + RequestedSecurityToken requestedSecurityToken = new RequestedSecurityToken(); + BinarySecurityToken binarySecToken = new BinarySecurityToken(); + binarySecToken.setValueType(PluginConstants.CertificateEnrolment.VALUE_TYPE); + binarySecToken.setEncodingType(PluginConstants.CertificateEnrolment.ENCODING_TYPE); + binarySecToken.setToken(encodedWap); + requestedSecurityToken.setBinarySecurityToken(binarySecToken); + requestSecurityTokenResponse.setRequestedSecurityToken(requestedSecurityToken); + requestSecurityTokenResponse.setRequestID(REQUEST_ID); + response.value = requestSecurityTokenResponse; + } catch (CertificateGenerationException e) { + String msg = "Problem occurred in generating certificate."; + log.error(msg, e); + throw new WindowsDeviceEnrolmentException(msg, e); + } catch (WAPProvisioningException e) { + String msg = "Problem occurred in generating wap-provisioning file."; + log.error(msg, e); + throw new WAPProvisioningException(msg, e); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + /** + * Method used to Convert the Document object into a String. + * + * @param document - Wap provisioning XML document + * @return - String representation of wap provisioning XML document + * @throws TransformerException + */ + private String convertDocumentToString(Document document) throws TransformerException { + DOMSource DOMSource = new DOMSource(document); + StringWriter stringWriter = new StringWriter(); + StreamResult streamResult = new StreamResult(stringWriter); + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + transformer.transform(DOMSource, streamResult); + + return stringWriter.toString(); + } + + /** + * This method prepares the wap-provisioning file by including relevant certificates etc + * + * @param binarySecurityToken - CSR from device + * @param wapProvisioningFilePath - File path of wap-provisioning file + * @return - base64 encoded final wap-provisioning file as a String + * @throws CertificateGenerationException + * @throws org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WAPProvisioningException + */ + public String prepareWapProvisioningXML( + String binarySecurityToken, + String wapProvisioningFilePath, String headerBst) throws CertificateGenerationException, + WAPProvisioningException, WindowsDeviceEnrolmentException { + + String rootCertEncodedString; + String signedCertEncodedString; + X509Certificate signedCertificate; + + CertificateManagementServiceImpl impl = CertificateManagementServiceImpl.getInstance(); + Base64 base64Encoder = new Base64(); + try { + rootCACertificate = (X509Certificate) impl.getCACertificate(); + rootCertEncodedString = base64Encoder.encodeToString(rootCACertificate.getEncoded()); + } catch (KeystoreException e) { + String msg = "CA certificate cannot be generated"; + log.error(msg, e); + throw new CertificateGenerationException(msg, e); + } catch (CertificateEncodingException e) { + String msg = "CA certificate cannot be encoded."; + log.error(msg, e); + throw new CertificateGenerationException(msg, e); + } + + try { + signedCertificate = impl.getSignedCertificateFromCSR(binarySecurityToken); + signedCertEncodedString = base64Encoder.encodeToString(signedCertificate.getEncoded()); + } catch (CertificateEncodingException e) { + String msg = "Singed certificate cannot be encoded."; + log.error(msg, e); + throw new CertificateGenerationException(msg, e); + } catch (KeystoreException e) { + String msg = "CA certificate cannot be generated"; + log.error(msg, e); + throw new CertificateGenerationException(msg, e); + } + DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder; + String wapProvisioningString = null; + try { + builder = domFactory.newDocumentBuilder(); + + Document document = builder.parse(wapProvisioningFilePath); + NodeList wapParm = document.getElementsByTagName(PluginConstants.CertificateEnrolment.PARM); + Node caCertificatePosition = wapParm.item(CA_CERTIFICATE_POSITION); + + //Adding SHA1 CA certificate finger print to wap-provisioning xml. + caCertificatePosition.getParentNode().getAttributes().getNamedItem(PluginConstants. + CertificateEnrolment.TYPE).setTextContent(String.valueOf( + DigestUtils.sha512Hex(rootCACertificate.getEncoded())).toUpperCase()); + //Adding encoded CA certificate to wap-provisioning file after removing new line + // characters. + NamedNodeMap rootCertAttributes = caCertificatePosition.getAttributes(); + Node rootCertNode = + rootCertAttributes.getNamedItem(PluginConstants.CertificateEnrolment.VALUE); + rootCertEncodedString = rootCertEncodedString.replaceAll("\n", ""); + rootCertNode.setTextContent(rootCertEncodedString); + + if (log.isDebugEnabled()) { + log.debug("Root certificate: " + rootCertEncodedString); + } + + Node signedCertificatePosition = wapParm.item(SIGNED_CERTIFICATE_POSITION); + + //Adding SHA1 signed certificate finger print to wap-provisioning xml. + signedCertificatePosition.getParentNode().getAttributes().getNamedItem(PluginConstants. + CertificateEnrolment.TYPE).setTextContent(String.valueOf( + DigestUtils.sha512Hex(signedCertificate.getEncoded())).toUpperCase()); + + //Adding encoded signed certificate to wap-provisioning file after removing new line + // characters. + NamedNodeMap clientCertAttributes = signedCertificatePosition.getAttributes(); + Node clientEncodedNode = + clientCertAttributes.getNamedItem(PluginConstants.CertificateEnrolment.VALUE); + signedCertEncodedString = signedCertEncodedString.replaceAll("\n", ""); + + clientEncodedNode.setTextContent(signedCertEncodedString); + if (log.isDebugEnabled()) { + log.debug("Signed certificate: " + signedCertEncodedString); + } + + // Adding user name auth token to wap-provisioning xml + Node userNameAuthPosition = wapParm.item(APPAUTH_USERNAME_POSITION); + NamedNodeMap appServerAttribute = userNameAuthPosition.getAttributes(); + Node authNameNode = appServerAttribute.getNamedItem(PluginConstants.CertificateEnrolment.VALUE); + CacheEntry cacheEntry = (CacheEntry) DeviceUtil.getCacheEntry(headerBst); + String userName = cacheEntry.getUsername(); + authNameNode.setTextContent(cacheEntry.getUsername()); + DeviceUtil.removeToken(headerBst); + String password = DeviceUtil.generateRandomToken(); + Node passwordAuthPosition = wapParm.item(APPAUTH_PASSWORD_POSITION); + NamedNodeMap appSrvPasswordAttribute = passwordAuthPosition.getAttributes(); + Node authPasswordNode = appSrvPasswordAttribute.getNamedItem(PluginConstants.CertificateEnrolment.VALUE); + authPasswordNode.setTextContent(password); + String requestSecurityTokenResponse = new SyncmlCredentials().generateRST(userName, password); + DeviceUtil.persistChallengeToken(requestSecurityTokenResponse, null, userName); + + // Get device polling frequency from the tenant Configurations. + Node numberOfFirstRetries = wapParm.item(POLLING_FREQUENCY_POSITION); + NamedNodeMap pollingAttributes = numberOfFirstRetries.getAttributes(); + Node pollValue = pollingAttributes.getNamedItem(PluginConstants.CertificateEnrolment.VALUE); + pollValue.setTextContent(pollingFrequency); + if (log.isDebugEnabled()) { + log.debug("Username: " + userName + "Password: " + requestSecurityTokenResponse); + } + wapProvisioningString = convertDocumentToString(document); + } catch (ParserConfigurationException e) { + String msg = "Problem occurred in parsing wap-provisioning.xml file."; + log.error(msg, e); + throw new WAPProvisioningException(msg, e); + } catch (DeviceManagementException e) { + String msg = "Error occurred in while getting CA and Root certificates."; + log.error(msg, e); + throw new WindowsDeviceEnrolmentException(msg, e); + } catch (CertificateEncodingException e) { + String msg = "Error occurred in while encoding certificates."; + log.error(msg, e); + throw new WindowsDeviceEnrolmentException(msg, e); + } catch (UnsupportedEncodingException e) { + String msg = "Error occurred in while encoding wap-provisioning file."; + log.error(msg, e); + throw new WindowsDeviceEnrolmentException(msg, e); + } catch (SAXException e) { + String msg = "Error occurred in while parsing wap-provisioning.xml file."; + log.error(msg, e); + throw new WAPProvisioningException(msg, e); + } catch (TransformerException e) { + String msg = "Error occurred in while transforming wap-provisioning.xml file."; + log.error(msg, e); + throw new WAPProvisioningException(msg, e); + } catch (IOException e) { + String msg = "Error occurred in while getting wap-provisioning.xml file."; + log.error(msg, e); + throw new WAPProvisioningException(msg, e); + } catch (SyncmlMessageFormatException e) { + String msg = "Error occurred in while getting CA and Root certificates."; + log.error(msg, e); + throw new WindowsDeviceEnrolmentException(msg, e); + } + return base64Encoder.encodeToString(wapProvisioningString.getBytes()); + } + + /** + * This method get the soap request header contents + * + * @return Header object type,soap header tag list + */ + private List
      getHeaders() { + MessageContext messageContext = context.getMessageContext(); + if (messageContext == null || !(messageContext instanceof WrappedMessageContext)) { + return null; + } + Message message = ((WrappedMessageContext) messageContext).getWrappedMessage(); + return CastUtils.cast((List) message.get(Header.HEADER_LIST)); + } + + /** + * This method is used to get tenant configurations. + * + * @return List of Configurations entries. + * @throws DeviceManagementException + */ + private List getTenantConfigurationData() throws DeviceManagementException { + if (WindowsAPIUtils.getTenantConfiguration() != null) { + TenantConfiguration configuration = WindowsAPIUtils.getTenantConfiguration(); + return configuration.getConfiguration(); + } else { + return null; + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/util/CertificateSigningService.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/util/CertificateSigningService.java new file mode 100644 index 000000000..855b96fd9 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/util/CertificateSigningService.java @@ -0,0 +1,143 @@ +/* + * 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.mdm.mobileservices.windows.services.wstep.util; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest; +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.CertificateGenerationException; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WAPProvisioningException; + +import javax.security.auth.x500.X500Principal; +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.List; + +/** + * Class for generating signed certificate for CSR form device. + */ +public class CertificateSigningService { + + private static final long MILLI_SECONDS = 1000L * 60 * 60 * 24; + + private enum PropertyIndex { + COMMON_NAME_INDEX(0), + NOT_BEFORE_DAYS_INDEX(1), + NOT_AFTER_DAYS_INDEX(2); + + private final int itemPosition; + private PropertyIndex(final int itemPosition) { + this.itemPosition = itemPosition; + } + public int getValue() { + return this.itemPosition; + } + } + + private static Log log = LogFactory.getLog(CertificateSigningService.class); + + /** + * Implement certificate signing task using CSR received from the device and the MDM server key + * store. + * @param jcaRequest - CSR from the device + * @param privateKey - Private key of CA certificate in MDM server + * @param caCert - CA certificate in MDM server + * @param certParameterList - Parameter list for Signed certificate generation + * @return - Signed certificate for CSR from device + * @throws CertificateGenerationException + * @throws org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.WAPProvisioningException + */ + public static X509Certificate signCSR(JcaPKCS10CertificationRequest jcaRequest, + PrivateKey privateKey, X509Certificate caCert, + List certParameterList) throws + CertificateGenerationException, WAPProvisioningException { + + String commonName = + (String) certParameterList.get(PropertyIndex.COMMON_NAME_INDEX.getValue()); + int notBeforeDays = + (Integer) certParameterList.get(PropertyIndex.NOT_BEFORE_DAYS_INDEX.getValue()); + int notAfterDays = + (Integer) certParameterList.get(PropertyIndex.NOT_AFTER_DAYS_INDEX.getValue()); + X509v3CertificateBuilder certificateBuilder; + X509Certificate signedCertificate; + + try { + ContentSigner signer; + BigInteger serialNumber = BigInteger.valueOf(new SecureRandom(). + nextInt(Integer.MAX_VALUE)); + Date notBeforeDate = new Date(System.currentTimeMillis() - + (MILLI_SECONDS * notBeforeDays)); + Date notAfterDate = new Date(System.currentTimeMillis() + + (MILLI_SECONDS * notAfterDays)); + certificateBuilder = + new JcaX509v3CertificateBuilder(caCert, serialNumber, notBeforeDate, notAfterDate, + new X500Principal(commonName), + jcaRequest.getPublicKey()); + + //Adding extensions to the signed certificate. + certificateBuilder.addExtension(Extension.keyUsage, true, + new KeyUsage(KeyUsage.digitalSignature)); + certificateBuilder.addExtension(Extension.extendedKeyUsage, false, + new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth)); + certificateBuilder.addExtension(Extension.basicConstraints, true, + new BasicConstraints(false)); + + signer = new JcaContentSignerBuilder(PluginConstants.CertificateEnrolment.ALGORITHM). + setProvider(PluginConstants.CertificateEnrolment.PROVIDER).build(privateKey); + + signedCertificate = new JcaX509CertificateConverter().setProvider( + PluginConstants.CertificateEnrolment.PROVIDER).getCertificate( + certificateBuilder.build(signer)); + } catch (InvalidKeyException e) { + throw new CertificateGenerationException("CSR's public key is invalid", e); + } catch (NoSuchAlgorithmException e) { + throw new CertificateGenerationException("Certificate cannot be generated", e); + } + catch (CertIOException e) { + throw new CertificateGenerationException( + "Cannot add extension(s) to signed certificate", e); + } + catch (OperatorCreationException e) { + throw new CertificateGenerationException("Content signer cannot be created", e); + } + catch (CertificateException e) { + throw new CertificateGenerationException("Signed certificate cannot be generated", e); + } + return signedCertificate; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/util/KeyStoreGenerator.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/util/KeyStoreGenerator.java new file mode 100644 index 000000000..2e02c9faa --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/util/KeyStoreGenerator.java @@ -0,0 +1,93 @@ +/* + * 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.mdm.mobileservices.windows.services.wstep.util; + +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.wso2.carbon.mdm.mobileservices.windows.common.exceptions.KeyStoreGenerationException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.io.FileInputStream; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +/** + * Class for MDM Keystore operations. + */ +public class KeyStoreGenerator { + + private static final Log log = LogFactory.getLog(KeyStoreGenerator.class); + + /** + * This method loads the MDM keystore. + * @param keyStore - MDM Keystore + * @param keyStorePassword - Keystore Password + * @param keyStorePath - Keystore path + * @throws KeyStoreGenerationException + */ + public static void loadToStore(KeyStore keyStore, + char[] keyStorePassword, + String keyStorePath) throws KeyStoreGenerationException { + + FileInputStream fileInputStream = null; + + try { + if (keyStorePath != null) { + fileInputStream = new FileInputStream(keyStorePath); + keyStore.load(fileInputStream, keyStorePassword); + } + } catch (NoSuchAlgorithmException e) { + throw new KeyStoreGenerationException( + "Requested cryptographic algorithm is not available in the environment.", e); + } catch (CertificateException e) { + throw new KeyStoreGenerationException("Error working with certificate related to, " + + keyStorePath, e); + } catch (IOException e) { + throw new KeyStoreGenerationException("File error while working with file, " + + keyStorePath, e); + } finally { + try { + if (fileInputStream != null) { + fileInputStream.close(); + } + } catch (IOException e) { + throw new KeyStoreGenerationException("File error while closing the file, " + + keyStorePath,e); + } + } + } + + /** + * This method is for retrieving instance of Key Store. + * @return Keystore object + * @throws KeyStoreGenerationException + */ + public static KeyStore getKeyStore() throws KeyStoreGenerationException { + try { + return KeyStore.getInstance(PluginConstants.CertificateEnrolment.JKS); + } catch (KeyStoreException e) { + String msg = "KeyStore error while creating new JKS."; + log.error(msg, e); + throw new KeyStoreGenerationException(msg, e); + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/util/MessageHandler.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/util/MessageHandler.java new file mode 100644 index 000000000..9d4ae0118 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/wstep/util/MessageHandler.java @@ -0,0 +1,207 @@ +/* + * 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.mdm.mobileservices.windows.services.wstep.util; + +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.joda.time.DateTime; +import org.joda.time.format.ISODateTimeFormat; + +import javax.ws.rs.core.Response; +import javax.xml.namespace.QName; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import javax.xml.soap.Name; +import javax.xml.soap.SOAPEnvelope; +import javax.xml.soap.SOAPException; +import javax.xml.soap.SOAPFactory; +import javax.xml.soap.SOAPHeader; +import javax.xml.soap.SOAPHeaderElement; +import javax.xml.soap.SOAPMessage; +import javax.xml.ws.handler.MessageContext; +import javax.xml.ws.handler.soap.SOAPHandler; +import javax.xml.ws.handler.soap.SOAPMessageContext; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.*; + +/** + * Class responsible for adding Timestamp security header in SOAP message and adding Content-length + * in the HTTP header for avoiding HTTP chunking. + */ +public class MessageHandler implements SOAPHandler { + + public static final String TIME_ZONE = "Z"; + public static final int VALIDITY_TIME = 5; + public static final int TIMESTAMP_END_INDEX = 6; + public static final int TIMESTAMP_BEGIN_INDEX = 0; + private static Log log = LogFactory.getLog(MessageHandler.class); + + /** + * This method resolves the security header coming in the SOAP message. + * @return - Security Header + */ + @Override + public Set getHeaders() { + QName securityHeader = new QName(PluginConstants.WS_SECURITY_TARGET_NAMESPACE, PluginConstants.SECURITY); + HashSet headers = new HashSet(); + headers.add(securityHeader); + return headers; + } + + /** + * This method adds Timestamp for SOAP header, and adds Content-length for HTTP header for + * avoiding HTTP chunking. + * + * @param context - Context of the SOAP Message + */ + @Override + public boolean handleMessage(SOAPMessageContext context) { + + Boolean outBoundProperty = (Boolean) context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY); + + if (outBoundProperty) { + SOAPMessage message = context.getMessage(); + SOAPHeader header = null; + SOAPEnvelope envelope = null; + try { + header = message.getSOAPHeader(); + envelope = message.getSOAPPart().getEnvelope(); + } catch (SOAPException e) { + Response.serverError().entity("SOAP message content cannot be read.").build(); + } + try { + if ((header == null) && (envelope != null)) { + header = envelope.addHeader(); + } + } catch (SOAPException e) { + Response.serverError().entity("SOAP header cannot be added.").build(); + } + + SOAPFactory soapFactory = null; + try { + soapFactory = SOAPFactory.newInstance(); + } catch (SOAPException e) { + Response.serverError().entity("Cannot get an instance of SOAP factory.").build(); + } + + QName qNamesSecurity = new QName(PluginConstants.WS_SECURITY_TARGET_NAMESPACE, + PluginConstants.CertificateEnrolment.SECURITY); + SOAPHeaderElement Security = null; + Name attributeName = null; + try { + if (header != null) { + Security = header.addHeaderElement(qNamesSecurity); + } + if (soapFactory != null) { + attributeName = + soapFactory.createName(PluginConstants.CertificateEnrolment.TIMESTAMP_ID, + PluginConstants.CertificateEnrolment.TIMESTAMP_U, + PluginConstants.CertificateEnrolment + .WSS_SECURITY_UTILITY); + } + } catch (SOAPException e) { + Response.serverError().entity("Security header cannot be added.").build(); + } + + QName qNameTimestamp = new QName(PluginConstants.CertificateEnrolment.WSS_SECURITY_UTILITY, + PluginConstants.CertificateEnrolment.TIMESTAMP); + SOAPHeaderElement timestamp = null; + try { + if (header != null) { + timestamp = header.addHeaderElement(qNameTimestamp); + timestamp.addAttribute(attributeName, + PluginConstants.CertificateEnrolment.TIMESTAMP_0); + } + } catch (SOAPException e) { + Response.serverError().entity("Exception while adding timestamp header.").build(); + } + DateTime dateTime = new DateTime(); + DateTime expiredDateTime = dateTime.plusMinutes(VALIDITY_TIME); + String createdISOTime = dateTime.toString(ISODateTimeFormat.dateTime()); + String expiredISOTime = expiredDateTime.toString(ISODateTimeFormat.dateTime()); + createdISOTime = createdISOTime.substring(TIMESTAMP_BEGIN_INDEX, + createdISOTime.length() - + TIMESTAMP_END_INDEX); + createdISOTime = createdISOTime + TIME_ZONE; + expiredISOTime = expiredISOTime.substring(TIMESTAMP_BEGIN_INDEX, + expiredISOTime.length() - + TIMESTAMP_END_INDEX); + expiredISOTime = expiredISOTime + TIME_ZONE; + QName qNameCreated = new QName(PluginConstants.CertificateEnrolment.WSS_SECURITY_UTILITY, + PluginConstants.CertificateEnrolment.CREATED); + SOAPHeaderElement SOAPHeaderCreated = null; + + try { + if (header != null) { + SOAPHeaderCreated = header.addHeaderElement(qNameCreated); + SOAPHeaderCreated.addTextNode(createdISOTime); + } + } catch (SOAPException e) { + Response.serverError().entity("Exception while creating SOAP header.").build(); + } + QName qNameExpires = new QName(PluginConstants.CertificateEnrolment.WSS_SECURITY_UTILITY, + PluginConstants.CertificateEnrolment.EXPIRES); + SOAPHeaderElement SOAPHeaderExpires = null; + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + String messageString = null; + try { + if (header != null) { + SOAPHeaderExpires = header.addHeaderElement(qNameExpires); + SOAPHeaderExpires.addTextNode(expiredISOTime); + } + if ((timestamp != null) && (Security != null)) { + timestamp.addChildElement(SOAPHeaderCreated); + timestamp.addChildElement(SOAPHeaderExpires); + Security.addChildElement(timestamp); + } + message.saveChanges(); + message.writeTo(outputStream); + messageString = new String(outputStream.toByteArray(), + PluginConstants.CertificateEnrolment.UTF_8); + } catch (SOAPException e) { + Response.serverError().entity("Exception while creating timestamp SOAP header.") + .build(); + } catch (IOException e) { + Response.serverError().entity("Exception while writing message to output stream.") + .build(); + } + + Map> headers = + (Map>) context.get(MessageContext.HTTP_REQUEST_HEADERS); + headers = new HashMap>(); + if (messageString != null) { + headers.put(PluginConstants.CONTENT_LENGTH, Arrays.asList(String.valueOf( + messageString.length()))); + } + context.put(MessageContext.HTTP_REQUEST_HEADERS, headers); + } + return true; + } + + @Override + public boolean handleFault(SOAPMessageContext context) { + return true; + } + + @Override + public void close(MessageContext context) { + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/CertificateEnrollmentPolicyService.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/CertificateEnrollmentPolicyService.java new file mode 100644 index 000000000..8e08c0529 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/CertificateEnrollmentPolicyService.java @@ -0,0 +1,70 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep; + +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.wso2.carbon.mdm.mobileservices.windows.services.xcep.beans.CACollection; +import org.wso2.carbon.mdm.mobileservices.windows.services.xcep.beans.Client; +import org.wso2.carbon.mdm.mobileservices.windows.services.xcep.beans.OIDCollection; +import org.wso2.carbon.mdm.mobileservices.windows.services.xcep.beans.ObjectFactory; +import org.wso2.carbon.mdm.mobileservices.windows.services.xcep.beans.RequestFilter; +import org.wso2.carbon.mdm.mobileservices.windows.services.xcep.beans.Response; + +import javax.jws.WebMethod; +import javax.jws.WebParam; +import javax.jws.WebService; +import javax.xml.bind.annotation.XmlSeeAlso; +import javax.xml.ws.BindingType; +import javax.xml.ws.Holder; +import javax.xml.ws.RequestWrapper; +import javax.xml.ws.ResponseWrapper; +import javax.xml.ws.soap.SOAPBinding; + +/** + * Interface for MS-XCEP implementation. + */ +@WebService(targetNamespace = PluginConstants.CERTIFICATE_ENROLLMENT_POLICY_SERVICE_TARGET_NAMESPACE, + name = "IPolicy") +@BindingType(value = SOAPBinding.SOAP12HTTP_BINDING) +@XmlSeeAlso({ ObjectFactory.class }) +public interface CertificateEnrollmentPolicyService { + + @RequestWrapper(localName = "GetPolicies", targetNamespace = PluginConstants. + ENROLLMENT_POLICY_TARGET_NAMESPACE, className = PluginConstants.REQUEST_WRAPPER_CLASS_NAME) + @WebMethod(operationName = "GetPolicies") + @ResponseWrapper(localName = "GetPoliciesResponse", targetNamespace = PluginConstants. + ENROLLMENT_POLICY_TARGET_NAMESPACE, className = PluginConstants. + RESPONSE_WRAPPER_CLASS_NAME) void getPolicies( + @WebParam(name = "client", targetNamespace = PluginConstants. + ENROLLMENT_POLICY_TARGET_NAMESPACE) + Client client, + @WebParam(name = "requestFilter", targetNamespace = PluginConstants. + ENROLLMENT_POLICY_TARGET_NAMESPACE) + RequestFilter requestFilter, + @WebParam(mode = WebParam.Mode.OUT, name = "response", targetNamespace = PluginConstants. + ENROLLMENT_POLICY_TARGET_NAMESPACE) + Holder response, + @WebParam(mode = WebParam.Mode.OUT, name = "cAs", targetNamespace = PluginConstants. + ENROLLMENT_POLICY_TARGET_NAMESPACE) + Holder caCollection, + @WebParam(mode = WebParam.Mode.OUT, name = "oIDs", targetNamespace = PluginConstants. + ENROLLMENT_POLICY_TARGET_NAMESPACE) + Holder oidCollection + ); +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Attributes.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Attributes.java new file mode 100644 index 000000000..e39ba5c98 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Attributes.java @@ -0,0 +1,451 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import org.w3c.dom.Element; + +import javax.xml.bind.annotation.*; +import java.util.ArrayList; +import java.util.List; + +/** + *

      Java class for Attributes complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="Attributes">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element ref="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}commonName"/>
      + *         <element name="policySchema" type="{http://www.w3.org/2001/XMLSchema}unsignedInt"/>
      + *         <element name="certificateValidity" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}CertificateValidity"/>
      + *         <element name="permission" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}EnrollmentPermission"/>
      + *         <element name="privateKeyAttributes" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}PrivateKeyAttributes"/>
      + *         <element name="revision" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}Revision"/>
      + *         <element name="supersededPolicies" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}SupersededPolicies"/>
      + *         <element name="privateKeyFlags" type="{http://www.w3.org/2001/XMLSchema}unsignedInt"/>
      + *         <element name="subjectNameFlags" type="{http://www.w3
      + *         .org/2001/XMLSchema}unsignedInt"/>
      + *         <element name="enrollmentFlags" type="{http://www.w3.org/2001/XMLSchema}unsignedInt"/>
      + *         <element name="generalFlags" type="{http://www.w3.org/2001/XMLSchema}unsignedInt"/>
      + *         <element name="hashAlgorithmOIDReference" type="{http://www.w3
      + *         .org/2001/XMLSchema}int"/>
      + *         <element name="rARequirements" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}RARequirements"/>
      + *         <element name="keyArchivalAttributes" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}KeyArchivalAttributes"/>
      + *         <element name="extensions" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}ExtensionCollection"/>
      + *         <any processContents='lax' maxOccurs="unbounded" minOccurs="0"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "Attributes", propOrder = { + "commonName", + "policySchema", + "certificateValidity", + "permission", + "privateKeyAttributes", + "revision", + "supersededPolicies", + "privateKeyFlags", + "subjectNameFlags", + "enrollmentFlags", + "generalFlags", + "hashAlgorithmOIDReference", + "raRequirements", + "keyArchivalAttributes", + "extensions", + "any" +}) +@SuppressWarnings("unused") +public class Attributes { + + @XmlElement(required = true) + protected String commonName; + @XmlSchemaType(name = "unsignedInt") + protected long policySchema; + @XmlElement(required = true) + protected CertificateValidity certificateValidity; + @XmlElement(required = true) + protected EnrollmentPermission permission; + @XmlElement(required = true) + protected PrivateKeyAttributes privateKeyAttributes; + @XmlElement(required = true) + protected Revision revision; + @XmlElement(required = true, nillable = true) + protected SupersededPolicies supersededPolicies; + @XmlElement(required = true, type = Long.class, nillable = true) + @XmlSchemaType(name = "unsignedInt") + protected Long privateKeyFlags; + @XmlElement(required = true, type = Long.class, nillable = true) + @XmlSchemaType(name = "unsignedInt") + protected Long subjectNameFlags; + @XmlElement(required = true, type = Long.class, nillable = true) + @XmlSchemaType(name = "unsignedInt") + protected Long enrollmentFlags; + @XmlElement(required = true, type = Long.class, nillable = true) + @XmlSchemaType(name = "unsignedInt") + protected Long generalFlags; + @XmlElement(required = true, type = Integer.class, nillable = true) + protected Integer hashAlgorithmOIDReference; + @XmlElement(name = "rARequirements", required = true, nillable = true) + protected RARequirements raRequirements; + @XmlElement(required = true, nillable = true) + protected KeyArchivalAttributes keyArchivalAttributes; + @XmlElement(required = true, nillable = true) + protected ExtensionCollection extensions; + @XmlAnyElement(lax = true) + protected List any; + + /** + * Gets the value of the commonName property. + * + * @return possible object is + * {@link String } + */ + public String getCommonName() { + return commonName; + } + + /** + * Sets the value of the commonName property. + * + * @param value allowed object is + * {@link String } + */ + public void setCommonName(String value) { + this.commonName = value; + } + + /** + * Gets the value of the policySchema property. + */ + public long getPolicySchema() { + return policySchema; + } + + /** + * Sets the value of the policySchema property. + */ + public void setPolicySchema(long value) { + this.policySchema = value; + } + + /** + * Gets the value of the certificateValidity property. + * + * @return possible object is + * {@link CertificateValidity } + */ + public CertificateValidity getCertificateValidity() { + return certificateValidity; + } + + /** + * Sets the value of the certificateValidity property. + * + * @param value allowed object is + * {@link CertificateValidity } + */ + public void setCertificateValidity(CertificateValidity value) { + this.certificateValidity = value; + } + + /** + * Gets the value of the permission property. + * + * @return possible object is + * {@link EnrollmentPermission } + */ + public EnrollmentPermission getPermission() { + return permission; + } + + /** + * Sets the value of the permission property. + * + * @param value allowed object is + * {@link EnrollmentPermission } + */ + public void setPermission(EnrollmentPermission value) { + this.permission = value; + } + + /** + * Gets the value of the privateKeyAttributes property. + * + * @return possible object is + * {@link PrivateKeyAttributes } + */ + public PrivateKeyAttributes getPrivateKeyAttributes() { + return privateKeyAttributes; + } + + /** + * Sets the value of the privateKeyAttributes property. + * + * @param value allowed object is + * {@link PrivateKeyAttributes } + */ + public void setPrivateKeyAttributes(PrivateKeyAttributes value) { + this.privateKeyAttributes = value; + } + + /** + * Gets the value of the revision property. + * + * @return possible object is + * {@link Revision } + */ + public Revision getRevision() { + return revision; + } + + /** + * Sets the value of the revision property. + * + * @param value allowed object is + * {@link Revision } + */ + public void setRevision(Revision value) { + this.revision = value; + } + + /** + * Gets the value of the supersededPolicies property. + * + * @return possible object is + * {@link SupersededPolicies } + */ + public SupersededPolicies getSupersededPolicies() { + return supersededPolicies; + } + + /** + * Sets the value of the supersededPolicies property. + * + * @param value allowed object is + * {@link SupersededPolicies } + */ + public void setSupersededPolicies(SupersededPolicies value) { + this.supersededPolicies = value; + } + + /** + * Gets the value of the privateKeyFlags property. + * + * @return possible object is + * {@link Long } + */ + public Long getPrivateKeyFlags() { + return privateKeyFlags; + } + + /** + * Sets the value of the privateKeyFlags property. + * + * @param value allowed object is + * {@link Long } + */ + public void setPrivateKeyFlags(Long value) { + this.privateKeyFlags = value; + } + + /** + * Gets the value of the subjectNameFlags property. + * + * @return possible object is + * {@link Long } + */ + public Long getSubjectNameFlags() { + return subjectNameFlags; + } + + /** + * Sets the value of the subjectNameFlags property. + * + * @param value allowed object is + * {@link Long } + */ + public void setSubjectNameFlags(Long value) { + this.subjectNameFlags = value; + } + + /** + * Gets the value of the enrollmentFlags property. + * + * @return possible object is + * {@link Long } + */ + public Long getEnrollmentFlags() { + return enrollmentFlags; + } + + /** + * Sets the value of the enrollmentFlags property. + * + * @param value allowed object is + * {@link Long } + */ + public void setEnrollmentFlags(Long value) { + this.enrollmentFlags = value; + } + + /** + * Gets the value of the generalFlags property. + * + * @return possible object is + * {@link Long } + */ + public Long getGeneralFlags() { + return generalFlags; + } + + /** + * Sets the value of the generalFlags property. + * + * @param value allowed object is + * {@link Long } + */ + public void setGeneralFlags(Long value) { + this.generalFlags = value; + } + + /** + * Gets the value of the hashAlgorithmOIDReference property. + * + * @return possible object is + * {@link Integer } + */ + public Integer getHashAlgorithmOIDReference() { + return hashAlgorithmOIDReference; + } + + /** + * Sets the value of the hashAlgorithmOIDReference property. + * + * @param value allowed object is + * {@link Integer } + */ + public void setHashAlgorithmOIDReference(Integer value) { + this.hashAlgorithmOIDReference = value; + } + + /** + * Gets the value of the raRequirements property. + * + * @return possible object is + * {@link RARequirements } + */ + public RARequirements getRARequirements() { + return raRequirements; + } + + /** + * Sets the value of the raRequirements property. + * + * @param value allowed object is + * {@link RARequirements } + */ + public void setRARequirements(RARequirements value) { + this.raRequirements = value; + } + + /** + * Gets the value of the keyArchivalAttributes property. + * + * @return possible object is + * {@link KeyArchivalAttributes } + */ + public KeyArchivalAttributes getKeyArchivalAttributes() { + return keyArchivalAttributes; + } + + /** + * Sets the value of the keyArchivalAttributes property. + * + * @param value allowed object is + * {@link KeyArchivalAttributes } + */ + public void setKeyArchivalAttributes(KeyArchivalAttributes value) { + this.keyArchivalAttributes = value; + } + + /** + * Gets the value of the extensions property. + * + * @return possible object is + * {@link ExtensionCollection } + */ + public ExtensionCollection getExtensions() { + return extensions; + } + + /** + * Sets the value of the extensions property. + * + * @param value allowed object is + * {@link ExtensionCollection } + */ + public void setExtensions(ExtensionCollection value) { + this.extensions = value; + } + + /** + * Gets the value of the any property. + *

      + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the any property. + *

      + *

      + * For example, to add a new item, do as follows: + *

      +	 *    getAny().add(newItem);
      +	 * 
      + *

      + *

      + *

      + * Objects of the following type(s) are allowed in the list + * {@link Object } + * {@link Element } + */ + public List getAny() { + if (any == null) { + any = new ArrayList(); + } + return this.any; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CA.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CA.java new file mode 100644 index 000000000..4b643f089 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CA.java @@ -0,0 +1,166 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import org.w3c.dom.Element; + +import javax.xml.bind.annotation.*; +import java.util.ArrayList; +import java.util.List; + +/** + *

      Java class for CA complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="CA">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="uris" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}CAURICollection"/>
      + *         <element name="certificate" type="{http://www.w3.org/2001/XMLSchema}base64Binary"/>
      + *         <element name="enrollPermission" type="{http://www.w3.org/2001/XMLSchema}boolean"/>
      + *         <element name="cAReferenceID" type="{http://www.w3.org/2001/XMLSchema}int"/>
      + *         <any processContents='lax' maxOccurs="unbounded" minOccurs="0"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "CA", propOrder = { + "uris", + "certificate", + "enrollPermission", + "caReferenceID", + "any" +}) +@SuppressWarnings("unused") +public class CA { + + @XmlElement(required = true) + protected CAURICollection uris; + @XmlElement(required = true) + protected byte[] certificate; + protected boolean enrollPermission; + @XmlElement(name = "cAReferenceID") + protected int caReferenceID; + @XmlAnyElement(lax = true) + protected List any; + + /** + * Gets the value of the uris property. + * + * @return possible object is + * {@link CAURICollection } + */ + public CAURICollection getUris() { + return uris; + } + + /** + * Sets the value of the uris property. + * + * @param value allowed object is + * {@link CAURICollection } + */ + public void setUris(CAURICollection value) { + this.uris = value; + } + + /** + * Gets the value of the certificate property. + * + * @return possible object is + * byte[] + */ + public byte[] getCertificate() { + return certificate; + } + + /** + * Sets the value of the certificate property. + * + * @param value allowed object is + * byte[] + */ + public void setCertificate(byte[] value) { + this.certificate = value; + } + + /** + * Gets the value of the enrollPermission property. + */ + public boolean isEnrollPermission() { + return enrollPermission; + } + + /** + * Sets the value of the enrollPermission property. + */ + public void setEnrollPermission(boolean value) { + this.enrollPermission = value; + } + + /** + * Gets the value of the caReferenceID property. + */ + public int getCAReferenceID() { + return caReferenceID; + } + + /** + * Sets the value of the caReferenceID property. + */ + public void setCAReferenceID(int value) { + this.caReferenceID = value; + } + + /** + * Gets the value of the any property. + *

      + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the any property. + *

      + *

      + * For example, to add a new item, do as follows: + *

      +	 *    getAny().add(newItem);
      +	 * 
      + *

      + *

      + *

      + * Objects of the following type(s) are allowed in the list + * {@link Object } + * {@link Element } + */ + public List getAny() { + if (any == null) { + any = new ArrayList(); + } + return this.any; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CACollection.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CACollection.java new file mode 100644 index 000000000..cc94b04db --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CACollection.java @@ -0,0 +1,83 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; +import java.util.ArrayList; +import java.util.List; + +/** + *

      Java class for CACollection complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="CACollection">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="cA" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}CA" maxOccurs="unbounded"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "CACollection", propOrder = { + "ca" +}) +@SuppressWarnings("unused") +public class CACollection { + + @XmlElement(name = "cA", required = true) + protected List ca; + + /** + * Gets the value of the ca property. + *

      + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the ca property. + *

      + *

      + * For example, to add a new item, do as follows: + *

      +	 *    getCA().add(newItem);
      +	 * 
      + *

      + *

      + *

      + * Objects of the following type(s) are allowed in the list + * {@link CA } + */ + public List getCA() { + if (ca == null) { + ca = new ArrayList(); + } + return this.ca; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CAReferenceCollection.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CAReferenceCollection.java new file mode 100644 index 000000000..6199b4ee3 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CAReferenceCollection.java @@ -0,0 +1,83 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; +import java.util.ArrayList; +import java.util.List; + +/** + *

      Java class for CAReferenceCollection complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="CAReferenceCollection">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="cAReference" type="{http://www.w3.org/2001/XMLSchema}int"
      + *         maxOccurs="unbounded"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "CAReferenceCollection", propOrder = { + "caReference" +}) +@SuppressWarnings("unused") +public class CAReferenceCollection { + + @XmlElement(name = "cAReference", type = Integer.class) + protected List caReference; + + /** + * Gets the value of the caReference property. + *

      + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the caReference property. + *

      + *

      + * For example, to add a new item, do as follows: + *

      +	 *    getCAReference().add(newItem);
      +	 * 
      + *

      + *

      + *

      + * Objects of the following type(s) are allowed in the list + * {@link Integer } + */ + public List getCAReference() { + if (caReference == null) { + caReference = new ArrayList(); + } + return this.caReference; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CAURI.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CAURI.java new file mode 100644 index 000000000..ee33ba079 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CAURI.java @@ -0,0 +1,168 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import org.w3c.dom.Element; + +import javax.xml.bind.annotation.*; +import java.util.ArrayList; +import java.util.List; + +/** + *

      Java class for CAURI complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="CAURI">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="clientAuthentication" type="{http://www.w3
      + *         .org/2001/XMLSchema}unsignedInt"/>
      + *         <element name="uri" type="{http://www.w3.org/2001/XMLSchema}anyURI"/>
      + *         <element name="priority" type="{http://www.w3.org/2001/XMLSchema}unsignedInt"/>
      + *         <element name="renewalOnly" type="{http://www.w3.org/2001/XMLSchema}boolean"/>
      + *         <any processContents='lax' maxOccurs="unbounded" minOccurs="0"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "CAURI", propOrder = { + "clientAuthentication", + "uri", + "priority", + "renewalOnly", + "any" +}) +@SuppressWarnings("unused") +public class CAURI { + + @XmlSchemaType(name = "unsignedInt") + protected long clientAuthentication; + @XmlElement(required = true) + @XmlSchemaType(name = "anyURI") + protected String uri; + @XmlElement(required = true, type = Long.class, nillable = true) + @XmlSchemaType(name = "unsignedInt") + protected Long priority; + protected boolean renewalOnly; + @XmlAnyElement(lax = true) + protected List any; + + /** + * Gets the value of the clientAuthentication property. + */ + public long getClientAuthentication() { + return clientAuthentication; + } + + /** + * Sets the value of the clientAuthentication property. + */ + public void setClientAuthentication(long value) { + this.clientAuthentication = value; + } + + /** + * Gets the value of the uri property. + * + * @return possible object is + * {@link String } + */ + public String getUri() { + return uri; + } + + /** + * Sets the value of the uri property. + * + * @param value allowed object is + * {@link String } + */ + public void setUri(String value) { + this.uri = value; + } + + /** + * Gets the value of the priority property. + * + * @return possible object is + * {@link Long } + */ + public Long getPriority() { + return priority; + } + + /** + * Sets the value of the priority property. + * + * @param value allowed object is + * {@link Long } + */ + public void setPriority(Long value) { + this.priority = value; + } + + /** + * Gets the value of the renewalOnly property. + */ + public boolean isRenewalOnly() { + return renewalOnly; + } + + /** + * Sets the value of the renewalOnly property. + */ + public void setRenewalOnly(boolean value) { + this.renewalOnly = value; + } + + /** + * Gets the value of the any property. + *

      + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the any property. + *

      + *

      + * For example, to add a new item, do as follows: + *

      +	 *    getAny().add(newItem);
      +	 * 
      + *

      + *

      + *

      + * Objects of the following type(s) are allowed in the list + * {@link Object } + * {@link Element } + */ + public List getAny() { + if (any == null) { + any = new ArrayList(); + } + return this.any; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CAURICollection.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CAURICollection.java new file mode 100644 index 000000000..6278311da --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CAURICollection.java @@ -0,0 +1,83 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; +import java.util.ArrayList; +import java.util.List; + +/** + *

      Java class for CAURICollection complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="CAURICollection">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="cAURI" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}CAURI" maxOccurs="unbounded"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "CAURICollection", propOrder = { + "cauri" +}) +@SuppressWarnings("unused") +public class CAURICollection { + + @XmlElement(name = "cAURI", required = true) + protected List cauri; + + /** + * Gets the value of the cauri property. + *

      + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the cauri property. + *

      + *

      + * For example, to add a new item, do as follows: + *

      +	 *    getCAURI().add(newItem);
      +	 * 
      + *

      + *

      + *

      + * Objects of the following type(s) are allowed in the list + * {@link CAURI } + */ + public List getCAURI() { + if (cauri == null) { + cauri = new ArrayList(); + } + return this.cauri; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CertificateEnrollmentPolicy.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CertificateEnrollmentPolicy.java new file mode 100644 index 000000000..4ce348190 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CertificateEnrollmentPolicy.java @@ -0,0 +1,149 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import org.w3c.dom.Element; + +import javax.xml.bind.annotation.*; +import java.util.ArrayList; +import java.util.List; + +/** + *

      Java class for CertificateEnrollmentPolicy complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="CertificateEnrollmentPolicy">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="policyOIDReference" type="{http://www.w3.org/2001/XMLSchema}int"/>
      + *         <element name="cAs" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}CAReferenceCollection"/>
      + *         <element name="attributes" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}Attributes"/>
      + *         <any processContents='lax' maxOccurs="unbounded" minOccurs="0"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "CertificateEnrollmentPolicy", propOrder = { + "policyOIDReference", + "cAs", + "attributes", + "any" +}) +@SuppressWarnings("unused") +public class CertificateEnrollmentPolicy { + + protected int policyOIDReference; + @XmlElement(required = true, nillable = true) + protected CAReferenceCollection cAs; + @XmlElement(required = true) + protected Attributes attributes; + @XmlAnyElement(lax = true) + protected List any; + + /** + * Gets the value of the policyOIDReference property. + */ + public int getPolicyOIDReference() { + return policyOIDReference; + } + + /** + * Sets the value of the policyOIDReference property. + */ + public void setPolicyOIDReference(int value) { + this.policyOIDReference = value; + } + + /** + * Gets the value of the cAs property. + * + * @return possible object is + * {@link CAReferenceCollection } + */ + public CAReferenceCollection getCAs() { + return cAs; + } + + /** + * Sets the value of the cAs property. + * + * @param value allowed object is + * {@link CAReferenceCollection } + */ + public void setCAs(CAReferenceCollection value) { + this.cAs = value; + } + + /** + * Gets the value of the attributes property. + * + * @return possible object is + * {@link Attributes } + */ + public Attributes getAttributes() { + return attributes; + } + + /** + * Sets the value of the attributes property. + * + * @param value allowed object is + * {@link Attributes } + */ + public void setAttributes(Attributes value) { + this.attributes = value; + } + + /** + * Gets the value of the any property. + *

      + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the any property. + *

      + *

      + * For example, to add a new item, do as follows: + *

      +	 *    getAny().add(newItem);
      +	 * 
      + *

      + *

      + *

      + * Objects of the following type(s) are allowed in the list + * {@link Object } + * {@link Element } + */ + public List getAny() { + if (any == null) { + any = new ArrayList(); + } + return this.any; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CertificateValidity.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CertificateValidity.java new file mode 100644 index 000000000..41d77542c --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CertificateValidity.java @@ -0,0 +1,99 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.annotation.*; +import java.math.BigInteger; + +/** + *

      Java class for CertificateValidity complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="CertificateValidity">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="validityPeriodSeconds" type="{http://www.w3
      + *         .org/2001/XMLSchema}unsignedLong"/>
      + *         <element name="renewalPeriodSeconds" type="{http://www.w3
      + *         .org/2001/XMLSchema}unsignedLong"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "CertificateValidity", propOrder = { + "validityPeriodSeconds", + "renewalPeriodSeconds" +}) +@SuppressWarnings("unused") +public class CertificateValidity { + + @XmlElement(required = true) + @XmlSchemaType(name = "unsignedLong") + protected BigInteger validityPeriodSeconds; + @XmlElement(required = true) + @XmlSchemaType(name = "unsignedLong") + protected BigInteger renewalPeriodSeconds; + + /** + * Gets the value of the validityPeriodSeconds property. + * + * @return possible object is + * {@link BigInteger } + */ + public BigInteger getValidityPeriodSeconds() { + return validityPeriodSeconds; + } + + /** + * Sets the value of the validityPeriodSeconds property. + * + * @param value allowed object is + * {@link BigInteger } + */ + public void setValidityPeriodSeconds(BigInteger value) { + this.validityPeriodSeconds = value; + } + + /** + * Gets the value of the renewalPeriodSeconds property. + * + * @return possible object is + * {@link BigInteger } + */ + public BigInteger getRenewalPeriodSeconds() { + return renewalPeriodSeconds; + } + + /** + * Sets the value of the renewalPeriodSeconds property. + * + * @param value allowed object is + * {@link BigInteger } + */ + public void setRenewalPeriodSeconds(BigInteger value) { + this.renewalPeriodSeconds = value; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Client.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Client.java new file mode 100644 index 000000000..685bfd522 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Client.java @@ -0,0 +1,136 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import org.w3c.dom.Element; + +import javax.xml.bind.annotation.*; +import javax.xml.bind.annotation.adapters.CollapsedStringAdapter; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import javax.xml.datatype.XMLGregorianCalendar; +import java.util.ArrayList; +import java.util.List; + +/** + *

      Java class for Client complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="Client">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="lastUpdate" type="{http://www.w3.org/2001/XMLSchema}dateTime"/>
      + *         <element name="preferredLanguage" type="{http://www.w3.org/2001/XMLSchema}language"/>
      + *         <any processContents='lax' maxOccurs="unbounded" minOccurs="0"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "Client", propOrder = { + "lastUpdate", + "preferredLanguage", + "any" +}) +@SuppressWarnings("unused") +public class Client { + + @XmlElement(required = true, nillable = true) + @XmlSchemaType(name = "dateTime") + protected XMLGregorianCalendar lastUpdate; + @XmlElement(required = true, nillable = true) + @XmlJavaTypeAdapter(CollapsedStringAdapter.class) + @XmlSchemaType(name = "language") + protected String preferredLanguage; + @XmlAnyElement(lax = true) + protected List any; + + /** + * Gets the value of the lastUpdate property. + * + * @return possible object is + * {@link XMLGregorianCalendar } + */ + public XMLGregorianCalendar getLastUpdate() { + return lastUpdate; + } + + /** + * Sets the value of the lastUpdate property. + * + * @param value allowed object is + * {@link XMLGregorianCalendar } + */ + public void setLastUpdate(XMLGregorianCalendar value) { + this.lastUpdate = value; + } + + /** + * Gets the value of the preferredLanguage property. + * + * @return possible object is + * {@link String } + */ + public String getPreferredLanguage() { + return preferredLanguage; + } + + /** + * Sets the value of the preferredLanguage property. + * + * @param value allowed object is + * {@link String } + */ + public void setPreferredLanguage(String value) { + this.preferredLanguage = value; + } + + /** + * Gets the value of the any property. + *

      + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the any property. + *

      + *

      + * For example, to add a new item, do as follows: + *

      +	 *    getAny().add(newItem);
      +	 * 
      + *

      + *

      + *

      + * Objects of the following type(s) are allowed in the list + * {@link Object } + * {@link Element } + */ + public List getAny() { + if (any == null) { + any = new ArrayList(); + } + return this.any; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CryptoProviders.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CryptoProviders.java new file mode 100644 index 000000000..d483b7c47 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/CryptoProviders.java @@ -0,0 +1,87 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; +import java.util.ArrayList; +import java.util.List; + +/** + *

      Java class for CryptoProviders complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="CryptoProviders">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="provider" type="{http://www.w3.org/2001/XMLSchema}string"
      + *         maxOccurs="unbounded"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "CryptoProviders", propOrder = { + "provider" +}) +@SuppressWarnings("unused") +public class CryptoProviders { + + @XmlElement(required = true) + protected List provider; + + /** + * Instantiate provider list in the constructor + */ + public CryptoProviders() { + this.provider = new ArrayList(); + } + + /** + * Gets the value of the provider property. + *

      + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the provider property. + *

      + *

      + * For example, to add a new item, do as follows: + *

      +	 *    getProvider().add(newItem);
      +	 * 
      + *

      + *

      + *

      + * Objects of the following type(s) are allowed in the list + * {@link String } + */ + public List getProvider() { + return this.provider; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/EnrollmentPermission.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/EnrollmentPermission.java new file mode 100644 index 000000000..3a05b9d28 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/EnrollmentPermission.java @@ -0,0 +1,82 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlType; + +/** + *

      Java class for EnrollmentPermission complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="EnrollmentPermission">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="enroll" type="{http://www.w3.org/2001/XMLSchema}boolean"/>
      + *         <element name="autoEnroll" type="{http://www.w3.org/2001/XMLSchema}boolean"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "EnrollmentPermission", propOrder = { + "enroll", + "autoEnroll" +}) +@SuppressWarnings("unused") +public class EnrollmentPermission { + + protected boolean enroll; + protected boolean autoEnroll; + + /** + * Gets the value of the enroll property. + */ + public boolean isEnroll() { + return enroll; + } + + /** + * Sets the value of the enroll property. + */ + public void setEnroll(boolean value) { + this.enroll = value; + } + + /** + * Gets the value of the autoEnroll property. + */ + public boolean isAutoEnroll() { + return autoEnroll; + } + + /** + * Sets the value of the autoEnroll property. + */ + public void setAutoEnroll(boolean value) { + this.autoEnroll = value; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Extension.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Extension.java new file mode 100644 index 000000000..7647d853a --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Extension.java @@ -0,0 +1,108 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; + +/** + *

      Java class for Extension complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="Extension">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="oIDReference" type="{http://www.w3.org/2001/XMLSchema}int"/>
      + *         <element name="critical" type="{http://www.w3.org/2001/XMLSchema}boolean"/>
      + *         <element name="value" type="{http://www.w3.org/2001/XMLSchema}base64Binary"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "Extension", propOrder = { + "oidReference", + "critical", + "value" +}) +@SuppressWarnings("unused") +public class Extension { + + @XmlElement(name = "oIDReference") + protected int oidReference; + protected boolean critical; + @XmlElement(required = true, nillable = true) + protected byte[] value; + + /** + * Gets the value of the oidReference property. + */ + public int getOIDReference() { + return oidReference; + } + + /** + * Sets the value of the oidReference property. + */ + public void setOIDReference(int value) { + this.oidReference = value; + } + + /** + * Gets the value of the critical property. + */ + public boolean isCritical() { + return critical; + } + + /** + * Sets the value of the critical property. + */ + public void setCritical(boolean value) { + this.critical = value; + } + + /** + * Gets the value of the value property. + * + * @return possible object is + * byte[] + */ + public byte[] getValue() { + return value; + } + + /** + * Sets the value of the value property. + * + * @param value allowed object is + * byte[] + */ + public void setValue(byte[] value) { + this.value = value; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/ExtensionCollection.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/ExtensionCollection.java new file mode 100644 index 000000000..4e5422cbc --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/ExtensionCollection.java @@ -0,0 +1,87 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; +import java.util.ArrayList; +import java.util.List; + +/** + *

      Java class for ExtensionCollection complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="ExtensionCollection">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="extension" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}Extension" maxOccurs="unbounded"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "ExtensionCollection", propOrder = { + "extension" +}) +@SuppressWarnings("unused") +public class ExtensionCollection { + + @XmlElement(required = true) + protected List extension; + + /** + * Instantiate extension list in the constructor + */ + public ExtensionCollection() { + this.extension = new ArrayList(); + } + + /** + * Gets the value of the extension property. + *

      + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the extension property. + *

      + *

      + * For example, to add a new item, do as follows: + *

      +	 *    getExtension().add(newItem);
      +	 * 
      + *

      + *

      + *

      + * Objects of the following type(s) are allowed in the list + * {@link Extension } + */ + public List getExtension() { + return this.extension; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/FilterOIDCollection.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/FilterOIDCollection.java new file mode 100644 index 000000000..a50dc51f2 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/FilterOIDCollection.java @@ -0,0 +1,87 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; +import java.util.ArrayList; +import java.util.List; + +/** + *

      Java class for FilterOIDCollection complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="FilterOIDCollection">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="oid" type="{http://www.w3.org/2001/XMLSchema}string"
      + *         maxOccurs="unbounded"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "FilterOIDCollection", propOrder = { + "oid" +}) +@SuppressWarnings("unused") +public class FilterOIDCollection { + + @XmlElement(required = true) + protected List oid; + + /** + * Instantiate oid list in the constructor + */ + public FilterOIDCollection() { + this.oid = new ArrayList(); + } + + /** + * Gets the value of the oid property. + *

      + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the oid property. + *

      + *

      + * For example, to add a new item, do as follows: + *

      +	 *    getOid().add(newItem);
      +	 * 
      + *

      + *

      + *

      + * Objects of the following type(s) are allowed in the list + * {@link String } + */ + public List getOid() { + return this.oid; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/GetPolicies.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/GetPolicies.java new file mode 100644 index 000000000..c312533f6 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/GetPolicies.java @@ -0,0 +1,101 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + +/** + *

      Java class for anonymous complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType>
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="client" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}Client"/>
      + *         <element name="requestFilter" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}RequestFilter"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "", propOrder = { + "client", + "requestFilter" +}) +@XmlRootElement(name = "GetPolicies") +@SuppressWarnings("unused") +public class GetPolicies { + + @XmlElement(required = true) + protected Client client; + @XmlElement(required = true, nillable = true) + protected RequestFilter requestFilter; + + /** + * Gets the value of the client property. + * + * @return possible object is + * {@link Client } + */ + public Client getClient() { + return client; + } + + /** + * Sets the value of the client property. + * + * @param value allowed object is + * {@link Client } + */ + public void setClient(Client value) { + this.client = value; + } + + /** + * Gets the value of the requestFilter property. + * + * @return possible object is + * {@link RequestFilter } + */ + public RequestFilter getRequestFilter() { + return requestFilter; + } + + /** + * Sets the value of the requestFilter property. + * + * @param value allowed object is + * {@link RequestFilter } + */ + public void setRequestFilter(RequestFilter value) { + this.requestFilter = value; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/GetPoliciesResponse.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/GetPoliciesResponse.java new file mode 100644 index 000000000..d15b4c8b8 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/GetPoliciesResponse.java @@ -0,0 +1,126 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + +/** + *

      Java class for anonymous complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType>
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="response" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}Response"/>
      + *         <element name="cAs" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}CACollection"/>
      + *         <element name="oIDs" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}OIDCollection"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "", propOrder = { + "response", + "cAs", + "oiDs" +}) +@XmlRootElement(name = "GetPoliciesResponse") +@SuppressWarnings("unused") +public class GetPoliciesResponse { + + @XmlElement(required = true, nillable = true) + protected Response response; + @XmlElement(required = true, nillable = true) + protected CACollection cAs; + @XmlElement(name = "oIDs", required = true, nillable = true) + protected OIDCollection oiDs; + + /** + * Gets the value of the response property. + * + * @return possible object is + * {@link Response } + */ + public Response getResponse() { + return response; + } + + /** + * Sets the value of the response property. + * + * @param value allowed object is + * {@link Response } + */ + public void setResponse(Response value) { + this.response = value; + } + + /** + * Gets the value of the cAs property. + * + * @return possible object is + * {@link CACollection } + */ + public CACollection getCAs() { + return cAs; + } + + /** + * Sets the value of the cAs property. + * + * @param value allowed object is + * {@link CACollection } + */ + public void setCAs(CACollection value) { + this.cAs = value; + } + + /** + * Gets the value of the oiDs property. + * + * @return possible object is + * {@link OIDCollection } + */ + public OIDCollection getOIDs() { + return oiDs; + } + + /** + * Sets the value of the oiDs property. + * + * @param value allowed object is + * {@link OIDCollection } + */ + public void setOIDs(OIDCollection value) { + this.oiDs = value; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/KeyArchivalAttributes.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/KeyArchivalAttributes.java new file mode 100644 index 000000000..d48ebb386 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/KeyArchivalAttributes.java @@ -0,0 +1,86 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlSchemaType; +import javax.xml.bind.annotation.XmlType; + +/** + *

      Java class for KeyArchivalAttributes complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="KeyArchivalAttributes">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="symmetricAlgorithmOIDReference" type="{http://www.w3
      + *         .org/2001/XMLSchema}int"/>
      + *         <element name="symmetricAlgorithmKeyLength" type="{http://www.w3
      + *         .org/2001/XMLSchema}unsignedInt"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "KeyArchivalAttributes", propOrder = { + "symmetricAlgorithmOIDReference", + "symmetricAlgorithmKeyLength" +}) +@SuppressWarnings("unused") +public class KeyArchivalAttributes { + + protected int symmetricAlgorithmOIDReference; + @XmlSchemaType(name = "unsignedInt") + protected long symmetricAlgorithmKeyLength; + + /** + * Gets the value of the symmetricAlgorithmOIDReference property. + */ + public int getSymmetricAlgorithmOIDReference() { + return symmetricAlgorithmOIDReference; + } + + /** + * Sets the value of the symmetricAlgorithmOIDReference property. + */ + public void setSymmetricAlgorithmOIDReference(int value) { + this.symmetricAlgorithmOIDReference = value; + } + + /** + * Gets the value of the symmetricAlgorithmKeyLength property. + */ + public long getSymmetricAlgorithmKeyLength() { + return symmetricAlgorithmKeyLength; + } + + /** + * Sets the value of the symmetricAlgorithmKeyLength property. + */ + public void setSymmetricAlgorithmKeyLength(long value) { + this.symmetricAlgorithmKeyLength = value; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/OID.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/OID.java new file mode 100644 index 000000000..7415bc68a --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/OID.java @@ -0,0 +1,166 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import org.w3c.dom.Element; + +import javax.xml.bind.annotation.*; +import java.util.ArrayList; +import java.util.List; + +/** + *

      Java class for OID complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="OID">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="value" type="{http://www.w3.org/2001/XMLSchema}string"/>
      + *         <element name="group" type="{http://www.w3.org/2001/XMLSchema}unsignedInt"/>
      + *         <element name="oIDReferenceID" type="{http://www.w3.org/2001/XMLSchema}int"/>
      + *         <element name="defaultName" type="{http://www.w3.org/2001/XMLSchema}string"/>
      + *         <any processContents='lax' maxOccurs="unbounded" minOccurs="0"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "OID", propOrder = { + "value", + "group", + "oidReferenceID", + "defaultName", + "any" +}) +@SuppressWarnings("unused") +public class OID { + + @XmlElement(required = true) + protected String value; + @XmlSchemaType(name = "unsignedInt") + protected long group; + @XmlElement(name = "oIDReferenceID") + protected int oidReferenceID; + @XmlElement(required = true, nillable = true) + protected String defaultName; + @XmlAnyElement(lax = true) + protected List any; + + /** + * Gets the value of the value property. + * + * @return possible object is + * {@link String } + */ + public String getValue() { + return value; + } + + /** + * Sets the value of the value property. + * + * @param value allowed object is + * {@link String } + */ + public void setValue(String value) { + this.value = value; + } + + /** + * Gets the value of the group property. + */ + public long getGroup() { + return group; + } + + /** + * Sets the value of the group property. + */ + public void setGroup(long value) { + this.group = value; + } + + /** + * Gets the value of the oidReferenceID property. + */ + public int getOIDReferenceID() { + return oidReferenceID; + } + + /** + * Sets the value of the oidReferenceID property. + */ + public void setOIDReferenceID(int value) { + this.oidReferenceID = value; + } + + /** + * Gets the value of the defaultName property. + * + * @return possible object is + * {@link String } + */ + public String getDefaultName() { + return defaultName; + } + + /** + * Sets the value of the defaultName property. + * + * @param value allowed object is + * {@link String } + */ + public void setDefaultName(String value) { + this.defaultName = value; + } + + /** + * Gets the value of the any property. + *

      + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the any property. + *

      + *

      + * For example, to add a new item, do as follows: + *

      +	 *    getAny().add(newItem);
      +	 * 
      + *

      + *

      + *

      + * Objects of the following type(s) are allowed in the list + * {@link Object } + * {@link Element } + */ + public List getAny() { + if (any == null) { + any = new ArrayList(); + } + return this.any; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/OIDCollection.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/OIDCollection.java new file mode 100644 index 000000000..54861d807 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/OIDCollection.java @@ -0,0 +1,83 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; +import java.util.ArrayList; +import java.util.List; + +/** + *

      Java class for OIDCollection complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="OIDCollection">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="oID" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}OID" maxOccurs="unbounded"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "OIDCollection", propOrder = { + "oid" +}) +@SuppressWarnings("unused") +public class OIDCollection { + + @XmlElement(name = "oID", required = true) + protected List oid; + + /** + * Gets the value of the oid property. + *

      + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the oid property. + *

      + *

      + * For example, to add a new item, do as follows: + *

      +	 *    getOID().add(newItem);
      +	 * 
      + *

      + *

      + *

      + * Objects of the following type(s) are allowed in the list + * {@link OID } + */ + public List getOID() { + if (oid == null) { + oid = new ArrayList(); + } + return this.oid; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/OIDReferenceCollection.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/OIDReferenceCollection.java new file mode 100644 index 000000000..9d095532e --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/OIDReferenceCollection.java @@ -0,0 +1,83 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; +import java.util.ArrayList; +import java.util.List; + +/** + *

      Java class for OIDReferenceCollection complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="OIDReferenceCollection">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="oIDReference" type="{http://www.w3.org/2001/XMLSchema}int"
      + *         maxOccurs="unbounded"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "OIDReferenceCollection", propOrder = { + "oidReference" +}) +@SuppressWarnings("unused") +public class OIDReferenceCollection { + + @XmlElement(name = "oIDReference", type = Integer.class) + protected List oidReference; + + /** + * Gets the value of the oidReference property. + *

      + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the oidReference property. + *

      + *

      + * For example, to add a new item, do as follows: + *

      +	 *    getOIDReference().add(newItem);
      +	 * 
      + *

      + *

      + *

      + * Objects of the following type(s) are allowed in the list + * {@link Integer } + */ + public List getOIDReference() { + if (oidReference == null) { + oidReference = new ArrayList(); + } + return this.oidReference; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/ObjectFactory.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/ObjectFactory.java new file mode 100644 index 000000000..83db64243 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/ObjectFactory.java @@ -0,0 +1,253 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.JAXBElement; +import javax.xml.bind.annotation.XmlElementDecl; +import javax.xml.bind.annotation.XmlRegistry; +import javax.xml.namespace.QName; + +/** + * This object contains factory methods for each + * Java content interface and Java element interface + * generated in the com.microsoft.schemas.windows.pki._2009._01.enrollmentpolicy package. + *

      An ObjectFactory allows you to programatically + * construct new instances of the Java representation + * for XML content. The Java representation of XML + * content can consist of schema derived interfaces + * and classes representing the binding of schema + * type definitions, element declarations and model + * groups. Factory methods for each of these are + * provided in this class. + */ +@XmlRegistry +@SuppressWarnings("unused") +public class ObjectFactory { + + private final static QName _CommonName_QNAME = + new QName("http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy", + "commonName"); + + /** + * Create a new ObjectFactory that can be used to create new instances of schema derived + * classes for package: com.microsoft.schemas.windows.pki._2009._01.enrollmentpolicy + */ + public ObjectFactory() { + } + + /** + * Create an instance of {@link GetPolicies } + */ + public GetPolicies createGetPolicies() { + return new GetPolicies(); + } + + /** + * Create an instance of {@link Client } + */ + public Client createClient() { + return new Client(); + } + + /** + * Create an instance of {@link RequestFilter } + */ + public RequestFilter createRequestFilter() { + return new RequestFilter(); + } + + /** + * Create an instance of {@link GetPoliciesResponse } + */ + public GetPoliciesResponse createGetPoliciesResponse() { + return new GetPoliciesResponse(); + } + + /** + * Create an instance of {@link Response } + */ + public Response createResponse() { + return new Response(); + } + + /** + * Create an instance of {@link CACollection } + */ + public CACollection createCACollection() { + return new CACollection(); + } + + /** + * Create an instance of {@link OIDCollection } + */ + public OIDCollection createOIDCollection() { + return new OIDCollection(); + } + + /** + * Create an instance of {@link SupersededPolicies } + */ + public SupersededPolicies createSupersededPolicies() { + return new SupersededPolicies(); + } + + /** + * Create an instance of {@link OID } + */ + public OID createOID() { + return new OID(); + } + + /** + * Create an instance of {@link ExtensionCollection } + */ + public ExtensionCollection createExtensionCollection() { + return new ExtensionCollection(); + } + + /** + * Create an instance of {@link Attributes } + */ + public Attributes createAttributes() { + return new Attributes(); + } + + /** + * Create an instance of {@link EnrollmentPermission } + */ + public EnrollmentPermission createEnrollmentPermission() { + return new EnrollmentPermission(); + } + + /** + * Create an instance of {@link CAReferenceCollection } + */ + public CAReferenceCollection createCAReferenceCollection() { + return new CAReferenceCollection(); + } + + /** + * Create an instance of {@link CertificateValidity } + */ + public CertificateValidity createCertificateValidity() { + return new CertificateValidity(); + } + + /** + * Create an instance of {@link CAURICollection } + */ + public CAURICollection createCAURICollection() { + return new CAURICollection(); + } + + /** + * Create an instance of {@link PolicyCollection } + */ + public PolicyCollection createPolicyCollection() { + return new PolicyCollection(); + } + + /** + * Create an instance of {@link Revision } + */ + public Revision createRevision() { + return new Revision(); + } + + /** + * Create an instance of {@link OIDReferenceCollection } + */ + public OIDReferenceCollection createOIDReferenceCollection() { + return new OIDReferenceCollection(); + } + + /** + * Create an instance of {@link CA } + */ + public CA createCA() { + return new CA(); + } + + /** + * Create an instance of {@link CertificateEnrollmentPolicy } + */ + public CertificateEnrollmentPolicy createCertificateEnrollmentPolicy() { + return new CertificateEnrollmentPolicy(); + } + + /** + * Create an instance of {@link CryptoProviders } + */ + public CryptoProviders createCryptoProviders() { + return new CryptoProviders(); + } + + /** + * Create an instance of {@link Extension } + */ + public Extension createExtension() { + return new Extension(); + } + + /** + * Create an instance of {@link FilterOIDCollection } + */ + public FilterOIDCollection createFilterOIDCollection() { + return new FilterOIDCollection(); + } + + /** + * Create an instance of {@link CAURI } + */ + public CAURI createCAURI() { + return new CAURI(); + } + + /** + * Create an instance of {@link RARequirements } + */ + public RARequirements createRARequirements() { + return new RARequirements(); + } + + /** + * Create an instance of {@link PrivateKeyAttributes } + */ + public PrivateKeyAttributes createPrivateKeyAttributes() { + return new PrivateKeyAttributes(); + } + + /** + * Create an instance of {@link KeyArchivalAttributes } + */ + public KeyArchivalAttributes createKeyArchivalAttributes() { + return new KeyArchivalAttributes(); + } + + /** + * Create an instance of {@link JAXBElement }{@code <}{@link String }{@code >}} + */ + @XmlElementDecl(namespace = + "http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy" + + "", name = "commonName") + public JAXBElement createCommonName(String value) { + return new JAXBElement(_CommonName_QNAME, String.class, null, value); + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/PolicyCollection.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/PolicyCollection.java new file mode 100644 index 000000000..2dbb7f7e0 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/PolicyCollection.java @@ -0,0 +1,84 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; +import java.util.ArrayList; +import java.util.List; + +/** + *

      Java class for PolicyCollection complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="PolicyCollection">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="policy" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}CertificateEnrollmentPolicy"
      + *         maxOccurs="unbounded"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "PolicyCollection", propOrder = { + "policy" +}) +@SuppressWarnings("unused") +public class PolicyCollection { + + @XmlElement(required = true) + protected List policy; + + /** + * Gets the value of the policy property. + *

      + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the policy property. + *

      + *

      + * For example, to add a new item, do as follows: + *

      +	 *    getPolicy().add(newItem);
      +	 * 
      + *

      + *

      + *

      + * Objects of the following type(s) are allowed in the list + * {@link CertificateEnrollmentPolicy } + */ + public List getPolicy() { + if (policy == null) { + policy = new ArrayList(); + } + return this.policy; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/PrivateKeyAttributes.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/PrivateKeyAttributes.java new file mode 100644 index 000000000..9ecd50ecf --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/PrivateKeyAttributes.java @@ -0,0 +1,193 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlSchemaType; +import javax.xml.bind.annotation.XmlType; + +/** + *

      Java class for PrivateKeyAttributes complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="PrivateKeyAttributes">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="minimalKeyLength" type="{http://www.w3
      + *         .org/2001/XMLSchema}unsignedInt"/>
      + *         <element name="keySpec" type="{http://www.w3.org/2001/XMLSchema}unsignedInt"/>
      + *         <element name="keyUsageProperty" type="{http://www.w3
      + *         .org/2001/XMLSchema}unsignedInt"/>
      + *         <element name="permissions" type="{http://www.w3.org/2001/XMLSchema}string"/>
      + *         <element name="algorithmOIDReference" type="{http://www.w3.org/2001/XMLSchema}int"/>
      + *         <element name="cryptoProviders" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}CryptoProviders"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "PrivateKeyAttributes", propOrder = { + "minimalKeyLength", + "keySpec", + "keyUsageProperty", + "permissions", + "algorithmOIDReference", + "cryptoProviders" +}) +@SuppressWarnings("unused") +public class PrivateKeyAttributes { + + @XmlSchemaType(name = "unsignedInt") + protected long minimalKeyLength; + @XmlElement(required = true, type = Long.class, nillable = true) + @XmlSchemaType(name = "unsignedInt") + protected Long keySpec; + @XmlElement(required = true, type = Long.class, nillable = true) + @XmlSchemaType(name = "unsignedInt") + protected Long keyUsageProperty; + @XmlElement(required = true, nillable = true) + protected String permissions; + @XmlElement(required = true, type = Integer.class, nillable = true) + protected Integer algorithmOIDReference; + @XmlElement(required = true, nillable = true) + protected CryptoProviders cryptoProviders; + + /** + * Gets the value of the minimalKeyLength property. + */ + public long getMinimalKeyLength() { + return minimalKeyLength; + } + + /** + * Sets the value of the minimalKeyLength property. + */ + public void setMinimalKeyLength(long value) { + this.minimalKeyLength = value; + } + + /** + * Gets the value of the keySpec property. + * + * @return possible object is + * {@link Long } + */ + public Long getKeySpec() { + return keySpec; + } + + /** + * Sets the value of the keySpec property. + * + * @param value allowed object is + * {@link Long } + */ + public void setKeySpec(Long value) { + this.keySpec = value; + } + + /** + * Gets the value of the keyUsageProperty property. + * + * @return possible object is + * {@link Long } + */ + public Long getKeyUsageProperty() { + return keyUsageProperty; + } + + /** + * Sets the value of the keyUsageProperty property. + * + * @param value allowed object is + * {@link Long } + */ + public void setKeyUsageProperty(Long value) { + this.keyUsageProperty = value; + } + + /** + * Gets the value of the permissions property. + * + * @return possible object is + * {@link String } + */ + public String getPermissions() { + return permissions; + } + + /** + * Sets the value of the permissions property. + * + * @param value allowed object is + * {@link String } + */ + public void setPermissions(String value) { + this.permissions = value; + } + + /** + * Gets the value of the algorithmOIDReference property. + * + * @return possible object is + * {@link Integer } + */ + public Integer getAlgorithmOIDReference() { + return algorithmOIDReference; + } + + /** + * Sets the value of the algorithmOIDReference property. + * + * @param value allowed object is + * {@link Integer } + */ + public void setAlgorithmOIDReference(Integer value) { + this.algorithmOIDReference = value; + } + + /** + * Gets the value of the cryptoProviders property. + * + * @return possible object is + * {@link CryptoProviders } + */ + public CryptoProviders getCryptoProviders() { + return cryptoProviders; + } + + /** + * Sets the value of the cryptoProviders property. + * + * @param value allowed object is + * {@link CryptoProviders } + */ + public void setCryptoProviders(CryptoProviders value) { + this.cryptoProviders = value; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/RARequirements.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/RARequirements.java new file mode 100644 index 000000000..d5ff3788b --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/RARequirements.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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlSchemaType; +import javax.xml.bind.annotation.XmlType; + +/** + *

      Java class for RARequirements complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="RARequirements">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="rASignatures" type="{http://www.w3.org/2001/XMLSchema}unsignedInt"/>
      + *         <element name="rAEKUs" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}OIDReferenceCollection"/>
      + *         <element name="rAPolicies" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}OIDReferenceCollection"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "RARequirements", propOrder = { + "raSignatures", + "raekUs", + "raPolicies" +}) +@SuppressWarnings("unused") +public class RARequirements { + + @XmlElement(name = "rASignatures") + @XmlSchemaType(name = "unsignedInt") + protected long raSignatures; + @XmlElement(name = "rAEKUs", required = true, nillable = true) + protected OIDReferenceCollection raekUs; + @XmlElement(name = "rAPolicies", required = true, nillable = true) + protected OIDReferenceCollection raPolicies; + + /** + * Gets the value of the raSignatures property. + */ + public long getRASignatures() { + return raSignatures; + } + + /** + * Sets the value of the raSignatures property. + */ + public void setRASignatures(long value) { + this.raSignatures = value; + } + + /** + * Gets the value of the raekUs property. + * + * @return possible object is + * {@link OIDReferenceCollection } + */ + public OIDReferenceCollection getRAEKUs() { + return raekUs; + } + + /** + * Sets the value of the raekUs property. + * + * @param value allowed object is + * {@link OIDReferenceCollection } + */ + public void setRAEKUs(OIDReferenceCollection value) { + this.raekUs = value; + } + + /** + * Gets the value of the raPolicies property. + * + * @return possible object is + * {@link OIDReferenceCollection } + */ + public OIDReferenceCollection getRAPolicies() { + return raPolicies; + } + + /** + * Sets the value of the raPolicies property. + * + * @param value allowed object is + * {@link OIDReferenceCollection } + */ + public void setRAPolicies(OIDReferenceCollection value) { + this.raPolicies = value; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/RequestFilter.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/RequestFilter.java new file mode 100644 index 000000000..b1e419806 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/RequestFilter.java @@ -0,0 +1,107 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import org.w3c.dom.Element; + +import javax.xml.bind.annotation.*; +import java.util.ArrayList; +import java.util.List; + +/** + *

      Java class for RequestFilter complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="RequestFilter">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="policyOIDs" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}FilterOIDCollection"/>
      + *         <any processContents='lax' maxOccurs="unbounded" minOccurs="0"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "RequestFilter", propOrder = { + "policyOIDs", + "any" +}) +@SuppressWarnings("unused") +public class RequestFilter { + + @XmlElement(required = true, nillable = true) + protected FilterOIDCollection policyOIDs; + @XmlAnyElement(lax = true) + protected List any; + + /** + * Gets the value of the policyOIDs property. + * + * @return possible object is + * {@link FilterOIDCollection } + */ + public FilterOIDCollection getPolicyOIDs() { + return policyOIDs; + } + + /** + * Sets the value of the policyOIDs property. + * + * @param value allowed object is + * {@link FilterOIDCollection } + */ + public void setPolicyOIDs(FilterOIDCollection value) { + this.policyOIDs = value; + } + + /** + * Gets the value of the any property. + *

      + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the any property. + *

      + *

      + * For example, to add a new item, do as follows: + *

      +	 *    getAny().add(newItem);
      +	 * 
      + *

      + *

      + *

      + * Objects of the following type(s) are allowed in the list + * {@link Object } + * {@link Element } + */ + public List getAny() { + if (any == null) { + any = new ArrayList(); + } + return this.any; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Response.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Response.java new file mode 100644 index 000000000..90246f03c --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Response.java @@ -0,0 +1,204 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import org.w3c.dom.Element; + +import javax.xml.bind.annotation.*; +import java.util.ArrayList; +import java.util.List; + +/** + *

      Java class for Response complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="Response">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="policyID" type="{http://www.w3.org/2001/XMLSchema}string"/>
      + *         <element name="policyFriendlyName" type="{http://www.w3.org/2001/XMLSchema}string"/>
      + *         <element name="nextUpdateHours" type="{http://www.w3.org/2001/XMLSchema}unsignedInt"/>
      + *         <element name="policiesNotChanged" type="{http://www.w3.org/2001/XMLSchema}boolean"/>
      + *         <element name="policies" type="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}PolicyCollection"/>
      + *         <any processContents='lax' maxOccurs="unbounded" minOccurs="0"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "Response", propOrder = { + "policyID", + "policyFriendlyName", + "nextUpdateHours", + "policiesNotChanged", + "policies", + "any" +}) +@SuppressWarnings("unused") +public class Response { + + @XmlElement(required = true) + protected String policyID; + @XmlElement(required = true, nillable = true) + protected String policyFriendlyName; + @XmlElement(required = true, type = Long.class, nillable = true) + @XmlSchemaType(name = "unsignedInt") + protected Long nextUpdateHours; + @XmlElement(required = true, type = Boolean.class, nillable = true) + protected Boolean policiesNotChanged; + @XmlElement(required = true, nillable = true) + protected PolicyCollection policies; + @XmlAnyElement(lax = true) + protected List any; + + /** + * Gets the value of the policyID property. + * + * @return possible object is + * {@link String } + */ + public String getPolicyID() { + return policyID; + } + + /** + * Sets the value of the policyID property. + * + * @param value allowed object is + * {@link String } + */ + public void setPolicyID(String value) { + this.policyID = value; + } + + /** + * Gets the value of the policyFriendlyName property. + * + * @return possible object is + * {@link String } + */ + public String getPolicyFriendlyName() { + return policyFriendlyName; + } + + /** + * Sets the value of the policyFriendlyName property. + * + * @param value allowed object is + * {@link String } + */ + public void setPolicyFriendlyName(String value) { + this.policyFriendlyName = value; + } + + /** + * Gets the value of the nextUpdateHours property. + * + * @return possible object is + * {@link Long } + */ + public Long getNextUpdateHours() { + return nextUpdateHours; + } + + /** + * Sets the value of the nextUpdateHours property. + * + * @param value allowed object is + * {@link Long } + */ + public void setNextUpdateHours(Long value) { + this.nextUpdateHours = value; + } + + /** + * Gets the value of the policiesNotChanged property. + * + * @return possible object is + * {@link Boolean } + */ + public Boolean isPoliciesNotChanged() { + return policiesNotChanged; + } + + /** + * Sets the value of the policiesNotChanged property. + * + * @param value allowed object is + * {@link Boolean } + */ + public void setPoliciesNotChanged(Boolean value) { + this.policiesNotChanged = value; + } + + /** + * Gets the value of the policies property. + * + * @return possible object is + * {@link PolicyCollection } + */ + public PolicyCollection getPolicies() { + return policies; + } + + /** + * Sets the value of the policies property. + * + * @param value allowed object is + * {@link PolicyCollection } + */ + public void setPolicies(PolicyCollection value) { + this.policies = value; + } + + /** + * Gets the value of the any property. + *

      + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the any property. + *

      + *

      + * For example, to add a new item, do as follows: + *

      +	 *    getAny().add(newItem);
      +	 * 
      + *

      + *

      + *

      + * Objects of the following type(s) are allowed in the list + * {@link Object } + * {@link Element } + */ + public List getAny() { + if (any == null) { + any = new ArrayList(); + } + return this.any; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Revision.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Revision.java new file mode 100644 index 000000000..e838b4b26 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/Revision.java @@ -0,0 +1,85 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlSchemaType; +import javax.xml.bind.annotation.XmlType; + +/** + *

      Java class for Revision complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="Revision">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element name="majorRevision" type="{http://www.w3.org/2001/XMLSchema}unsignedInt"/>
      + *         <element name="minorRevision" type="{http://www.w3.org/2001/XMLSchema}unsignedInt"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "Revision", propOrder = { + "majorRevision", + "minorRevision" +}) +@SuppressWarnings("unused") +public class Revision { + + @XmlSchemaType(name = "unsignedInt") + protected long majorRevision; + @XmlSchemaType(name = "unsignedInt") + protected long minorRevision; + + /** + * Gets the value of the majorRevision property. + */ + public long getMajorRevision() { + return majorRevision; + } + + /** + * Sets the value of the majorRevision property. + */ + public void setMajorRevision(long value) { + this.majorRevision = value; + } + + /** + * Gets the value of the minorRevision property. + */ + public long getMinorRevision() { + return minorRevision; + } + + /** + * Sets the value of the minorRevision property. + */ + public void setMinorRevision(long value) { + this.minorRevision = value; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/SupersededPolicies.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/SupersededPolicies.java new file mode 100644 index 000000000..722950cea --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/SupersededPolicies.java @@ -0,0 +1,83 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.beans; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlType; +import java.util.ArrayList; +import java.util.List; + +/** + *

      Java class for SupersededPolicies complex type. + *

      + *

      The following schema fragment specifies the expected content contained within this class. + *

      + *

      + * <complexType name="SupersededPolicies">
      + *   <complexContent>
      + *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
      + *       <sequence>
      + *         <element ref="{http://schemas.microsoft
      + *         .com/windows/pki/2009/01/enrollmentpolicy}commonName" maxOccurs="unbounded"/>
      + *       </sequence>
      + *     </restriction>
      + *   </complexContent>
      + * </complexType>
      + * 
      + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "SupersededPolicies", propOrder = { + "commonName" +}) +@SuppressWarnings("unused") +public class SupersededPolicies { + + @XmlElement(required = true) + protected List commonName; + + /** + * Gets the value of the commonName property. + *

      + *

      + * This accessor method returns a reference to the live list, + * not a snapshot. Therefore any modification you make to the + * returned list will be present inside the JAXB object. + * This is why there is not a set method for the commonName property. + *

      + *

      + * For example, to add a new item, do as follows: + *

      +	 *    getCommonName().add(newItem);
      +	 * 
      + *

      + *

      + *

      + * Objects of the following type(s) are allowed in the list + * {@link String } + */ + public List getCommonName() { + if (commonName == null) { + commonName = new ArrayList(); + } + return this.commonName; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/package-info.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/package-info.java new file mode 100644 index 000000000..f3c0f5b58 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/beans/package-info.java @@ -0,0 +1,23 @@ +/* + * 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. + */ + +@javax.xml.bind.annotation.XmlSchema(namespace = "http://schemas.microsoft" + + ".com/windows/pki/2009/01/enrollmentpolicy", + elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED) + +package org.wso2.carbon.mdm.mobileservices.windows.services.xcep.beans; diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/impl/CertificateEnrollmentPolicyServiceImpl.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/impl/CertificateEnrollmentPolicyServiceImpl.java new file mode 100644 index 000000000..25d909f92 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/java/org/wso2/carbon/mdm/mobileservices/windows/services/xcep/impl/CertificateEnrollmentPolicyServiceImpl.java @@ -0,0 +1,101 @@ +/* + * 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.mdm.mobileservices.windows.services.xcep.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.mdm.mobileservices.windows.common.PluginConstants; +import org.wso2.carbon.mdm.mobileservices.windows.services.xcep.CertificateEnrollmentPolicyService; +import org.wso2.carbon.mdm.mobileservices.windows.services.xcep.beans.*; + +import javax.jws.WebService; +import javax.xml.ws.BindingType; +import javax.xml.ws.Holder; +import javax.xml.ws.soap.Addressing; +import javax.xml.ws.soap.SOAPBinding; + +/** + * Implementation class for CertificateEnrollmentPolicyService. + */ +@WebService(endpointInterface = PluginConstants.CERTIFICATE_ENROLLMENT_POLICY_SERVICE_ENDPOINT, + targetNamespace = PluginConstants.CERTIFICATE_ENROLLMENT_POLICY_SERVICE_TARGET_NAMESPACE) +@Addressing(enabled = true, required = true) +@BindingType(value = SOAPBinding.SOAP12HTTP_BINDING) +public class CertificateEnrollmentPolicyServiceImpl implements CertificateEnrollmentPolicyService { + + private static Log log = LogFactory.getLog(CertificateEnrollmentPolicyServiceImpl.class); + + /** + * This method implements the MS-XCEP protocol for certificate enrollment policy service. + * + * @param client - Included lastUpdate and preferredLanguage tags + * @param requestFilter - Policy constrain tag + * @param response - Response which includes minimal key length, hash algorithm, policy + * schema, policy OID reference + * @param caCollection - Contains the issuers for the certificate enrollment policies + * @param oidCollection - Contains the collection of OIDs for the response + */ + @Override + public void getPolicies(Client client, RequestFilter requestFilter, + Holder response, Holder caCollection, + Holder oidCollection) { + + if (log.isDebugEnabled()) { + log.debug("Enrolment certificate policy end point was triggered by device."); + } + + Response responseElement = new Response(); + OIDCollection oidCollectionElement = new OIDCollection(); + CACollection caCollectionElement = new CACollection(); + + PolicyCollection policyCollectionElement = new PolicyCollection(); + + CertificateEnrollmentPolicy certEnrollmentPolicyElement = new CertificateEnrollmentPolicy(); + Attributes attributeElement = new Attributes(); + PrivateKeyAttributes privateKeyAttributeElement = new PrivateKeyAttributes(); + + privateKeyAttributeElement. + setMinimalKeyLength(PluginConstants.CertificateEnrolmentPolicy.MINIMAL_KEY_LENGTH); + + attributeElement.setPolicySchema(PluginConstants.CertificateEnrolmentPolicy.POLICY_SCHEMA); + attributeElement.setPrivateKeyAttributes(privateKeyAttributeElement); + attributeElement.setHashAlgorithmOIDReference(PluginConstants.CertificateEnrolmentPolicy. + HASH_ALGORITHM_OID_REFERENCE); + certEnrollmentPolicyElement.setPolicyOIDReference(PluginConstants.CertificateEnrolmentPolicy. + OID_REFERENCE); + certEnrollmentPolicyElement.setAttributes(attributeElement); + policyCollectionElement.getPolicy().add(certEnrollmentPolicyElement); + responseElement.setPolicies(policyCollectionElement); + response.value = responseElement; + + OID oidElement = new OID(); + oidElement.setValue(PluginConstants.CertificateEnrolmentPolicy.OID); + oidElement.setGroup(PluginConstants.CertificateEnrolmentPolicy.OID_GROUP); + oidElement.setOIDReferenceID(PluginConstants.CertificateEnrolmentPolicy.OID_REFERENCE_ID); + oidElement.setDefaultName(PluginConstants.CertificateEnrolmentPolicy.OID_DEFAULT_NAME); + + oidCollectionElement.getOID().add(oidElement); + caCollection.value = caCollectionElement; + oidCollection.value = oidCollectionElement; + + PrivilegedCarbonContext.endTenantFlow(); + + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/resources/properties.xml b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/resources/properties.xml new file mode 100644 index 000000000..7df287f68 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/resources/properties.xml @@ -0,0 +1,26 @@ + + + Federated + wso2carbon + cacert + CN=mdmcn + 3 + 300 + wso2.com + \ No newline at end of file diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/resources/wap-provisioning.xml b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/resources/wap-provisioning.xml new file mode 100644 index 000000000..e6729e0f4 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/resources/wap-provisioning.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/resources/wso2mdm.jks b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/resources/wso2mdm.jks new file mode 100644 index 0000000000000000000000000000000000000000..66b68ea395562b9716c8c579b944ae19b5dbf296 GIT binary patch literal 9485 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 W`0v%-2|WuF22km0Do6xx&3^&4V5S-X literal 0 HcmV?d00001 diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/META-INF/permissions.xml b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/META-INF/permissions.xml new file mode 100644 index 000000000..feb34b098 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/META-INF/permissions.xml @@ -0,0 +1,142 @@ + + + + + + + + + Lock + /device-mgt/windows/operation/lock + /services/windows/operation/lock + POST + emm_admin,emm_user + + + + Enterprise Wipe + /device-mgt/windows/operation/wipe-data + /services/windows/operation/wipe-data + POST + emm_admin + + + + Ring + /device-mgt/windows/operation/ring-device + /services/windows/operation/ring-device + POST + emm_admin,emm_user + + + + Disenroll + /device-mgt/windows/operation/disenroll + /services/windows/operation/disenroll + POST + emm_admin,emm_user + + + + LockReset + /device-mgt/windows/operation/lock-reset + /services/windows/operation/lock-reset + POST + emm_admin,emm_user + + + + + Get CSR Policy + /device-mgt/windows/devices/getPolicy + /services/certificatepolicy/xcep + POST + emm_windows_agent + + + Get BST + /device-mgt/windows/devices/getBST + /services/federated/bst/authentication + POST + emm_windows_agent + + + Provide CSR + /device-mgt/windows/devices/requestSecurityToken + /services/deviceenrolment/wstep + POST + emm_windows_agent + + + GetLicense + /device-mgt/windows/devices/license + /services/device/license + GET + emm_windows_agent + + + + View Policies + /device-mgt/windows/policies/view + /services/policy/* + GET + emm_admin + + + + View Policy Features + /device-mgt/windows/policies/view + /services/policy/features/* + GET + emm_admin + + + + View Tenant configuration + /device-mgt/windows/tenant/configuration + /services/configuration + GET + emm_admin + + + + Add Tenant configuration + /device-mgt/windows/tenant/configuration + /services/configuration + POST + emm_admin + + + + Update Tenant configuration + /device-mgt/windows/tenant/configuration + /services/configuration + PUT + emm_admin + + \ No newline at end of file diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/META-INF/webapp-classloading.xml b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/META-INF/webapp-classloading.xml new file mode 100644 index 000000000..0371f7c29 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/META-INF/webapp-classloading.xml @@ -0,0 +1,35 @@ + + + + + + + + + false + + + Carbon + diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/WEB-INF/cxf-servlet.xml b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/WEB-INF/cxf-servlet.xml new file mode 100644 index 000000000..23cd504ba --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/WEB-INF/cxf-servlet.xml @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/WEB-INF/web.xml b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..93b441ed4 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,63 @@ + + + + Windows-Agent-Webapp + + + contextConfigLocation + /WEB-INF/cxf-servlet.xml + + + + org.wso2.carbon.mdm.mobileservices.windows.common.util.ConfigInitializerContextListener + + + + + JAX-WS/JAX-RS-windows Endpoint + JAX-WS/JAX-RS-windows Servlet + JAXServlet-windows + + org.apache.cxf.transport.servlet.CXFServlet + + 1 + + + JAXServlet-windows + /services/* + + + 60 + + + + doAuthentication + true + + + nonSecuredEndPoints + /services/discovery/get,/services/discovery/post,/services/certificatepolicy/xcep, + ,/services/deviceenrolment/wstep,/services/syncml/devicemanagement/request + + \ No newline at end of file diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/servicelist.css b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/servicelist.css new file mode 100644 index 000000000..e6eacadbb --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows.agent/src/main/webapp/servicelist.css @@ -0,0 +1,125 @@ +@CHARSET "ISO-8859-1"; + +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} + +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} + +html { + background: #efefef; +} + +body { + line-height: 1; + width: 960px; + margin: auto; + background: white; + padding: 10px; + box-shadow: 0px 0px 5px #CCC; + font-family: "Lucida Grande", "Lucida Sans", "Microsoft Sans Serif", "Lucida Sans Unicode", "Verdana", "Sans-serif", "trebuchet ms" !important; + +} + +ol, ul { + list-style: none; +} + +blockquote, q { + quotes: none; +} + +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} + +table { + border-collapse: collapse; + border-spacing: 0; + width: 960px; + border: solid 1px #ccc; +} + +table a { + font-size: 12px; + color: #1e90ff; + padding: 7px; + float: left;; +} + +.heading { + font-size: 18px; + margin-top: 20px; + float: left; + color: #0067B1; + margin-bottom: 20px; + padding-top: 20px; +} + +.field { + font-weight: normal; + width: 120px; + font-size: 12px; + float: left; + padding: 7px; + clear: left; +} + +.value { + font-weight: bold; + font-size: 12px; + float: left; + padding: 7px; + clear: right; +} + +.porttypename { + font-weight: bold; + font-size: 14px; +} + +UL { + margin-top: 0; +} + +LI { + font-weight: normal; + font-size: 12px; + margin-top: 10px; +} + +TD { + border: 1px solid #ccc; + vertical-align: text-top; + padding: 5px; +} + + diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/pom.xml b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/pom.xml new file mode 100644 index 000000000..2440f0fff --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/pom.xml @@ -0,0 +1,176 @@ + + + + + + + windows-plugin + org.wso2.carbon.devicemgt-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.device.mgt.mobile.windows + bundle + WSO2 Carbon - Mobile Device Management Windows Impl + WSO2 Carbon - Mobile Device Management Windows Implementation + http://wso2.org + + + + + org.apache.felix + maven-scr-plugin + + + org.apache.felix + maven-bundle-plugin + 1.4.0 + true + + + ${project.artifactId} + ${project.artifactId} + ${carbon.devicemgt.plugins.version} + Device Management Mobile Windows Impl Bundle + org.wso2.carbon.device.mgt.mobile.windows.internal + + org.osgi.framework, + org.osgi.service.component, + org.apache.commons.logging, + javax.xml.bind.*, + javax.naming, + javax.sql, + javax.xml.bind.annotation.*, + javax.xml.parsers.*;resolution:=optional, + org.w3c.dom, + org.wso2.carbon.core, + org.wso2.carbon.context, + org.wso2.carbon.utils.*, + org.wso2.carbon.device.mgt.common.*, + org.wso2.carbon.device.mgt.mobile.*, + org.wso2.carbon.ndatasource.core, + org.wso2.carbon.policy.mgt.common.*, + org.wso2.carbon.policy.mgt.core.*, + org.wso2.carbon.registry.core, + org.wso2.carbon.registry.core.exceptions, + org.wso2.carbon.registry.core.service, + org.wso2.carbon.registry.core.session, + org.wso2.carbon.registry.api, + org.wso2.carbon.device.mgt.extensions.license.mgt.registry, + com.google.gson.* + + + !org.wso2.carbon.device.mgt.mobile.windows.internal, + org.wso2.carbon.device.mgt.mobile.windows.* + + + + + + + + + + + + + + + + + + + + + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.mobile + + + org.eclipse.osgi + org.eclipse.osgi + + + org.eclipse.osgi + org.eclipse.osgi.services + + + org.wso2.carbon + org.wso2.carbon.core + + + org.wso2.carbon + org.wso2.carbon.logging + + + org.wso2.carbon + org.wso2.carbon.utils + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.common + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.extensions + + + org.wso2.carbon + org.wso2.carbon.ndatasource.core + + + org.wso2.carbon.devicemgt + org.wso2.carbon.policy.mgt.common + + + org.wso2.carbon.devicemgt + org.wso2.carbon.policy.mgt.core + + + org.wso2.carbon + org.wso2.carbon.registry.api + + + org.wso2.carbon + org.wso2.carbon.registry.core + + + org.testng + testng + + + org.apache.tomcat.wso2 + jdbc-pool + + + com.h2database.wso2 + h2-database-engine + test + + + com.google.code.gson + gson + + + diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/WindowsDeviceManagementService.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/WindowsDeviceManagementService.java new file mode 100644 index 000000000..36d3bb963 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/WindowsDeviceManagementService.java @@ -0,0 +1,108 @@ +/* + * 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.device.mgt.mobile.windows.impl; + +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.DeviceManager; +import org.wso2.carbon.device.mgt.common.app.mgt.Application; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManagementException; +import org.wso2.carbon.device.mgt.common.app.mgt.ApplicationManager; +import org.wso2.carbon.device.mgt.common.operation.mgt.Operation; +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; + +import java.util.List; + +/** + * This represents the Windows implementation of DeviceManagerService. + */ +public class WindowsDeviceManagementService implements DeviceManagementService { + + private DeviceManager deviceManager; + public static final String DEVICE_TYPE_WINDOWS = "windows"; + public static final String DEVICE_TYPE_TENANT = "carbon.super"; + + @Override + public String getType() { + return WindowsDeviceManagementService.DEVICE_TYPE_WINDOWS; + } + + @Override + public String getProviderTenantDomain() { + return DEVICE_TYPE_TENANT; + } + + @Override + public boolean isSharedWithAllTenants() { + return true; + } + + @Override + public void init() throws DeviceManagementException { + this.deviceManager = new WindowsDeviceManager(); + } + + @Override + public DeviceManager getDeviceManager() { + return deviceManager; + } + + @Override + public ApplicationManager getApplicationManager() { + return null; + } + + @Override public void notifyOperationToDevices(Operation operation, List deviceIdentifiers) + throws DeviceManagementException { + + } + + @Override + public Application[] getApplications(String s, int i, int i2) throws ApplicationManagementException { + return new Application[0]; + } + + @Override + public void updateApplicationStatus(DeviceIdentifier deviceIdentifier, Application application, + String s) throws ApplicationManagementException { + + } + + @Override + public String getApplicationStatus(DeviceIdentifier deviceIdentifier, + Application application) throws ApplicationManagementException { + return null; + } + + @Override public void installApplicationForDevices(Operation operation, List deviceIdentifiers) + throws ApplicationManagementException { + + } + + @Override public void installApplicationForUsers(Operation operation, List strings) + throws ApplicationManagementException { + + } + + @Override public void installApplicationForUserRoles(Operation operation, List strings) + throws ApplicationManagementException { + + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/WindowsDeviceManager.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/WindowsDeviceManager.java new file mode 100644 index 000000000..f3864fe4a --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/WindowsDeviceManager.java @@ -0,0 +1,308 @@ +/* + * 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.device.mgt.mobile.windows.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.*; +import org.wso2.carbon.device.mgt.common.configuration.mgt.TenantConfiguration; +import org.wso2.carbon.device.mgt.common.license.mgt.License; +import org.wso2.carbon.device.mgt.common.license.mgt.LicenseManagementException; +import org.wso2.carbon.device.mgt.common.license.mgt.LicenseManager; +import org.wso2.carbon.device.mgt.extensions.license.mgt.registry.RegistryBasedLicenseManager; +import org.wso2.carbon.device.mgt.mobile.common.MobileDeviceMgtPluginException; +import org.wso2.carbon.device.mgt.mobile.common.MobilePluginConstants; +import org.wso2.carbon.device.mgt.mobile.dao.AbstractMobileDeviceManagementDAOFactory; +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceManagementDAOException; +import org.wso2.carbon.device.mgt.mobile.dto.MobileDevice; +import org.wso2.carbon.device.mgt.mobile.windows.impl.dao.WindowsDAOFactory; +import org.wso2.carbon.device.mgt.mobile.windows.impl.util.WindowsPluginUtils; +import org.wso2.carbon.device.mgt.mobile.util.MobileDeviceManagementUtil; +import org.wso2.carbon.registry.api.RegistryException; +import org.wso2.carbon.registry.api.Resource; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +public class WindowsDeviceManager implements DeviceManager { + + private AbstractMobileDeviceManagementDAOFactory daoFactory; + private LicenseManager licenseManager; + private FeatureManager featureManager = new WindowsFeatureManager(); + private static final Log log = LogFactory.getLog(WindowsDeviceManagementService.class); + + public WindowsDeviceManager() { + this.daoFactory = new WindowsDAOFactory(); + this.licenseManager = new RegistryBasedLicenseManager(); + + License defaultLicense = WindowsPluginUtils.getDefaultLicense(); + + try { + if (licenseManager.getLicense(WindowsDeviceManagementService.DEVICE_TYPE_WINDOWS, + MobilePluginConstants.LANGUAGE_CODE_ENGLISH_US) == null) { + licenseManager.addLicense(WindowsDeviceManagementService.DEVICE_TYPE_WINDOWS, defaultLicense); + } + featureManager.addSupportedFeaturesToDB(); + } catch (LicenseManagementException e) { + log.error("Error occurred while adding default license for Windows devices", e); + } catch (DeviceManagementException e) { + log.error("Error occurred while adding supported device features for Windows platform", e); + } + } + + @Override + public FeatureManager getFeatureManager() { + return featureManager; + } + + @Override + public boolean saveConfiguration(TenantConfiguration tenantConfiguration) + throws DeviceManagementException { + boolean status; + Resource resource; + try { + if (log.isDebugEnabled()) { + log.debug("Persisting windows configurations in Registry"); + } + String resourcePath = MobileDeviceManagementUtil.getPlatformConfigPath( + DeviceManagementConstants. + MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS); + StringWriter writer = new StringWriter(); + JAXBContext context = JAXBContext.newInstance(TenantConfiguration.class); + Marshaller marshaller = context.createMarshaller(); + marshaller.marshal(tenantConfiguration, writer); + + resource = MobileDeviceManagementUtil.getConfigurationRegistry().newResource(); + resource.setContent(writer.toString()); + resource.setMediaType(MobilePluginConstants.MEDIA_TYPE_XML); + MobileDeviceManagementUtil.putRegistryResource(resourcePath, resource); + status = true; + } catch (MobileDeviceMgtPluginException e) { + throw new DeviceManagementException( + "Error occurred while retrieving the Registry instance : " + e.getMessage(), e); + } catch (RegistryException e) { + throw new DeviceManagementException( + "Error occurred while persisting the Registry resource of Windows configuration : " + e.getMessage(), e); + } catch (JAXBException e) { + throw new DeviceManagementException( + "Error occurred while parsing the Windows configuration : " + e.getMessage(), e); + } + return status; + } + + @Override + public TenantConfiguration getConfiguration() throws DeviceManagementException { + Resource resource; + try { + String windowsTenantRegistryPath = + MobileDeviceManagementUtil.getPlatformConfigPath(DeviceManagementConstants. + MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS); + resource = MobileDeviceManagementUtil.getRegistryResource(windowsTenantRegistryPath); + if (resource != null) { + JAXBContext context = JAXBContext.newInstance(TenantConfiguration.class); + Unmarshaller unmarshaller = context.createUnmarshaller(); + return (TenantConfiguration) unmarshaller.unmarshal( + new StringReader(new String((byte[]) resource.getContent(), Charset. + forName(MobilePluginConstants.CHARSET_UTF8)))); + } + return null; + } catch (MobileDeviceMgtPluginException e) { + throw new DeviceManagementException( + "Error occurred while retrieving the Registry instance : " + e.getMessage(), e); + } catch (JAXBException e) { + throw new DeviceManagementException( + "Error occurred while parsing the Windows configuration : " + e.getMessage(), e); + } catch (RegistryException e) { + throw new DeviceManagementException( + "Error occurred while retrieving the Registry resource of Windows configuration : " + e.getMessage(), e); + } + } + + @Override + public boolean modifyEnrollment(Device device) throws DeviceManagementException { + boolean status; + MobileDevice mobileDevice = MobileDeviceManagementUtil.convertToMobileDevice(device); + try { + if (log.isDebugEnabled()) { + log.debug("Modifying the Windows device enrollment data"); + } + WindowsDAOFactory.beginTransaction(); + status = daoFactory.getMobileDeviceDAO().updateMobileDevice(mobileDevice); + WindowsDAOFactory.commitTransaction(); + } catch (MobileDeviceManagementDAOException e) { + WindowsDAOFactory.rollbackTransaction(); + throw new DeviceManagementException("Error while updating the enrollment of the Windows device : " + + device.getDeviceIdentifier(), e); + } finally { + WindowsDAOFactory.closeConnection(); + } + return status; + } + + @Override + public boolean disenrollDevice(DeviceIdentifier deviceId) throws DeviceManagementException { + //Here we don't have anything specific to do. Hence returning. + return true; + } + + @Override + public boolean isEnrolled(DeviceIdentifier deviceId) throws DeviceManagementException { + boolean isEnrolled = false; + try { + if (log.isDebugEnabled()) { + log.debug("Checking the enrollment of Windows device : " + deviceId.getId()); + } + MobileDevice mobileDevice = + daoFactory.getMobileDeviceDAO().getMobileDevice(deviceId.getId()); + if (mobileDevice != null) { + isEnrolled = true; + } + } catch (MobileDeviceManagementDAOException e) { + String msg = "Error while checking the enrollment status of Windows device : " + + deviceId.getId(); + throw new DeviceManagementException(msg, e); + } + return isEnrolled; + } + + @Override + public boolean isActive(DeviceIdentifier deviceId) throws DeviceManagementException { + return true; + } + + @Override + public boolean setActive(DeviceIdentifier deviceId, boolean status) + throws DeviceManagementException { + return true; + } + + public List getAllDevices() throws DeviceManagementException { + List devices = null; + try { + if (log.isDebugEnabled()) { + log.debug("Fetching the details of all Windows devices"); + } + WindowsDAOFactory.openConnection(); + List mobileDevices = daoFactory.getMobileDeviceDAO().getAllMobileDevices(); + if (mobileDevices != null) { + devices = new ArrayList<>(mobileDevices.size()); + for (MobileDevice mobileDevice : mobileDevices) { + devices.add(MobileDeviceManagementUtil.convertToDevice(mobileDevice)); + } + } + } catch (MobileDeviceManagementDAOException e) { + throw new DeviceManagementException("Error occurred while fetching all Windows devices", e); + } finally { + WindowsDAOFactory.closeConnection(); + } + return devices; + } + + @Override + public Device getDevice(DeviceIdentifier deviceId) throws DeviceManagementException { + Device device = null; + try { + if (log.isDebugEnabled()) { + log.debug("Getting the details of Windows device : '" + deviceId.getId() + "'"); + } + WindowsDAOFactory.openConnection(); + MobileDevice mobileDevice = daoFactory.getMobileDeviceDAO(). + getMobileDevice(deviceId.getId()); + device = MobileDeviceManagementUtil.convertToDevice(mobileDevice); + } catch (MobileDeviceManagementDAOException e) { + throw new DeviceManagementException( + "Error occurred while fetching the Windows device: '" + deviceId.getId() + "'", e); + } finally { + WindowsDAOFactory.closeConnection(); + } + return device; + } + + @Override + public boolean setOwnership(DeviceIdentifier deviceId, String ownershipType) + throws DeviceManagementException { + return true; + } + + @Override + public boolean isClaimable(DeviceIdentifier deviceIdentifier) throws DeviceManagementException { + return false; + } + + @Override + public boolean setStatus(DeviceIdentifier deviceIdentifier, String currentUser, + EnrolmentInfo.Status status) throws DeviceManagementException { + return false; + } + + @Override + public License getLicense(String languageCode) throws LicenseManagementException { + return licenseManager.getLicense(WindowsDeviceManagementService.DEVICE_TYPE_WINDOWS, languageCode); + } + + @Override + public void addLicense(License license) throws LicenseManagementException { + licenseManager.addLicense(WindowsDeviceManagementService.DEVICE_TYPE_WINDOWS, license); + } + + @Override + public boolean requireDeviceAuthorization() { + return false; + } + + @Override + public boolean updateDeviceInfo(DeviceIdentifier deviceIdentifier, + Device device) throws DeviceManagementException { + return true; + } + + @Override + public boolean enrollDevice(Device device) throws DeviceManagementException { + boolean status = false; + MobileDevice mobileDevice = MobileDeviceManagementUtil.convertToMobileDevice(device); + try { + if (log.isDebugEnabled()) { + log.debug("Enrolling a new windows device : " + device.getDeviceIdentifier()); + } + boolean isEnrolled = this.isEnrolled( + new DeviceIdentifier(device.getDeviceIdentifier(), device.getType())); + if (isEnrolled) { + this.modifyEnrollment(device); + } else { + WindowsDAOFactory.beginTransaction(); + status = daoFactory.getMobileDeviceDAO().addMobileDevice(mobileDevice); + WindowsDAOFactory.commitTransaction(); + } + } catch (MobileDeviceManagementDAOException e) { + WindowsDAOFactory.rollbackTransaction(); + String msg = + "Error while enrolling the windows device : " + device.getDeviceIdentifier(); + throw new DeviceManagementException(msg, e); + } + return status; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/WindowsFeatureManager.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/WindowsFeatureManager.java new file mode 100644 index 000000000..6b501376a --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/WindowsFeatureManager.java @@ -0,0 +1,196 @@ +/* + * 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.device.mgt.mobile.windows.impl; + +import org.wso2.carbon.device.mgt.common.DeviceManagementException; +import org.wso2.carbon.device.mgt.common.Feature; +import org.wso2.carbon.device.mgt.common.FeatureManager; +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceManagementDAOException; +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceManagementDAOFactory; +import org.wso2.carbon.device.mgt.mobile.dao.MobileFeatureDAO; +import org.wso2.carbon.device.mgt.mobile.dto.MobileFeature; +import org.wso2.carbon.device.mgt.mobile.windows.impl.dao.WindowsDAOFactory; +import org.wso2.carbon.device.mgt.mobile.util.MobileDeviceManagementUtil; + +import java.util.ArrayList; +import java.util.List; + +public class WindowsFeatureManager implements FeatureManager { + + private MobileFeatureDAO featureDAO; + + public WindowsFeatureManager() { + MobileDeviceManagementDAOFactory daoFactory = new WindowsDAOFactory(); + this.featureDAO = daoFactory.getMobileFeatureDAO(); + } + + @Override + public boolean addFeature(Feature feature) throws DeviceManagementException { + try { + WindowsDAOFactory.beginTransaction(); + MobileFeature mobileFeature = MobileDeviceManagementUtil.convertToMobileFeature(feature); + featureDAO.addFeature(mobileFeature); + WindowsDAOFactory.commitTransaction(); + return true; + } catch (MobileDeviceManagementDAOException e) { + WindowsDAOFactory.rollbackTransaction(); + throw new DeviceManagementException("Error occurred while adding the feature", e); + } finally { + WindowsDAOFactory.closeConnection(); + } + } + + @Override + public boolean addFeatures(List features) throws DeviceManagementException { + List mobileFeatures = new ArrayList(features.size()); + for (Feature feature : features) { + mobileFeatures.add(MobileDeviceManagementUtil.convertToMobileFeature(feature)); + } + try { + WindowsDAOFactory.beginTransaction(); + featureDAO.addFeatures(mobileFeatures); + WindowsDAOFactory.commitTransaction(); + return true; + } catch (MobileDeviceManagementDAOException e) { + WindowsDAOFactory.rollbackTransaction(); + throw new DeviceManagementException("Error occurred while adding the features", e); + } finally { + WindowsDAOFactory.closeConnection(); + } + } + + @Override + public Feature getFeature(String name) throws DeviceManagementException { + try { + WindowsDAOFactory.openConnection(); + MobileFeature mobileFeature = featureDAO.getFeatureByCode(name); + Feature feature = MobileDeviceManagementUtil.convertToFeature(mobileFeature); + return feature; + } catch (MobileDeviceManagementDAOException e) { + throw new DeviceManagementException("Error occurred while retrieving the feature", e); + } finally { + WindowsDAOFactory.closeConnection(); + } + } + + @Override + public List getFeatures() throws DeviceManagementException { + + try { + WindowsDAOFactory.openConnection(); + List mobileFeatures = featureDAO.getAllFeatures(); + List featureList = new ArrayList(mobileFeatures.size()); + for (MobileFeature mobileFeature : mobileFeatures) { + featureList.add(MobileDeviceManagementUtil.convertToFeature(mobileFeature)); + } + return featureList; + } catch (MobileDeviceManagementDAOException e) { + throw new DeviceManagementException("Error occurred while retrieving the list of features registered for " + + "Windows platform", e); + } finally { + WindowsDAOFactory.closeConnection(); + } + } + + @Override + public boolean removeFeature(String code) throws DeviceManagementException { + boolean status; + try { + WindowsDAOFactory.beginTransaction(); + featureDAO.deleteFeatureByCode(code); + WindowsDAOFactory.commitTransaction(); + status = true; + } catch (MobileDeviceManagementDAOException e) { + WindowsDAOFactory.rollbackTransaction(); + throw new DeviceManagementException("Error occurred while removing the feature", e); + } finally { + WindowsDAOFactory.closeConnection(); + } + return status; + } + + @Override + public boolean addSupportedFeaturesToDB() throws DeviceManagementException { + synchronized (this) { + List supportedFeatures = getSupportedFeatures(); + List existingFeatures = this.getFeatures(); + List missingFeatures = MobileDeviceManagementUtil. + getMissingFeatures(supportedFeatures, existingFeatures); + if (missingFeatures.size() > 0) { + return this.addFeatures(missingFeatures); + } + return true; + } + } + + /** + * Get supported Windows features. + * + * @return Supported features. + */ + public static List getSupportedFeatures() { + List supportedFeatures = new ArrayList(); + Feature feature = new Feature(); + feature.setCode("DEVICE_LOCK"); + feature.setName("Device Lock"); + feature.setDescription("Lock the device"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("CAMERA"); + feature.setName("camera"); + feature.setDescription("Enable or disable camera"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("DEVICE_INFO"); + feature.setName("Device info"); + feature.setDescription("Request device information"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("WIPE_DATA"); + feature.setName("Wipe Data"); + feature.setDescription("Factory reset the device"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("ENCRYPT_STORAGE"); + feature.setName("Encrypt storage"); + feature.setDescription("Encrypt storage"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("DEVICE_RING"); + feature.setName("Ring"); + feature.setDescription("Ring the device"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("PASSCODE_POLICY"); + feature.setName("Password Policy"); + feature.setDescription("Set passcode policy"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("DISENROLL"); + feature.setName("DisEnroll"); + feature.setDescription("DisEnroll the device"); + supportedFeatures.add(feature); + feature = new Feature(); + feature.setCode("LOCK_RESET"); + feature.setName("LockReset"); + feature.setDescription("Lock Reset device"); + supportedFeatures.add(feature); + return supportedFeatures; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/WindowsPolicyMonitoringService.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/WindowsPolicyMonitoringService.java new file mode 100644 index 000000000..3c9c0f3ab --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/WindowsPolicyMonitoringService.java @@ -0,0 +1,73 @@ +/* + * 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.device.mgt.mobile.windows.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.Device; +import org.wso2.carbon.device.mgt.common.DeviceIdentifier; +import org.wso2.carbon.device.mgt.common.DeviceManagementConstants; +import org.wso2.carbon.policy.mgt.common.Policy; +import org.wso2.carbon.policy.mgt.common.monitor.ComplianceData; +import org.wso2.carbon.policy.mgt.common.monitor.ComplianceFeature; +import org.wso2.carbon.policy.mgt.common.monitor.PolicyComplianceException; +import org.wso2.carbon.policy.mgt.common.spi.PolicyMonitoringService; + +import java.util.ArrayList; +import java.util.List; + +public class WindowsPolicyMonitoringService implements PolicyMonitoringService { + + private static Log log = LogFactory.getLog(WindowsPolicyMonitoringService.class); + + @Override + public void notifyDevices(List list) throws PolicyComplianceException { + + } + + @Override + public ComplianceData checkPolicyCompliance(DeviceIdentifier deviceIdentifier, Policy policy, Object compliancePayload) + throws PolicyComplianceException { + if (log.isDebugEnabled()) { + log.debug("checking policy compliance status of device '" + deviceIdentifier.getId() + "'"); + } + List complianceFeatures = (List) compliancePayload; + List nonComplianceFeatures = new ArrayList<>(); + ComplianceData complianceData = new ComplianceData(); + + if (policy == null || compliancePayload == null) { + return complianceData; + } + + for (ComplianceFeature complianceFeature : complianceFeatures) { + if (!complianceFeature.isCompliant()) { + complianceData.setStatus(false); + nonComplianceFeatures.add(complianceFeature); + break; + } + } + complianceData.setComplianceFeatures(nonComplianceFeatures); + return complianceData; + } + + @Override + public String getType() { + return DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/dao/WindowsDAOFactory.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/dao/WindowsDAOFactory.java new file mode 100644 index 000000000..6e99fc6b9 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/dao/WindowsDAOFactory.java @@ -0,0 +1,131 @@ +/* + * 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.device.mgt.mobile.windows.impl.dao; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.DeviceManagementConstants; +import org.wso2.carbon.device.mgt.mobile.dao.AbstractMobileDeviceManagementDAOFactory; +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceDAO; +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceManagementDAOException; +import org.wso2.carbon.device.mgt.mobile.dao.MobileFeatureDAO; +import org.wso2.carbon.device.mgt.mobile.windows.impl.dao.impl.WindowsDeviceDAOImpl; +import org.wso2.carbon.device.mgt.mobile.windows.impl.dao.impl.WindowsFeatureDAOImpl; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +public class WindowsDAOFactory extends AbstractMobileDeviceManagementDAOFactory { + + private static final Log log = LogFactory.getLog(WindowsDAOFactory.class); + protected static DataSource dataSource; + private static ThreadLocal currentConnection = new ThreadLocal<>(); + + public WindowsDAOFactory() { + this.dataSource = getDataSourceMap().get(DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS); + } + + @Override + public MobileDeviceDAO getMobileDeviceDAO() { + return new WindowsDeviceDAOImpl(); + } + + @Override + public MobileFeatureDAO getMobileFeatureDAO() { + return new WindowsFeatureDAOImpl(); + } + + public static void beginTransaction() throws MobileDeviceManagementDAOException { + try { + Connection conn = dataSource.getConnection(); + conn.setAutoCommit(false); + currentConnection.set(conn); + } catch (SQLException e) { + throw new MobileDeviceManagementDAOException("Error occurred while retrieving datasource connection", e); + } + } + + public static void openConnection() throws MobileDeviceManagementDAOException { + if (currentConnection.get() == null) { + Connection conn; + try { + conn = dataSource.getConnection(); + currentConnection.set(conn); + } catch (SQLException e) { + throw new MobileDeviceManagementDAOException + ("Error occurred while retrieving data source connection", e); + } + } + } + + public static Connection getConnection() throws MobileDeviceManagementDAOException { + if (currentConnection.get() == null) { + try { + currentConnection.set(dataSource.getConnection()); + } catch (SQLException e) { + throw new MobileDeviceManagementDAOException + ("Error occurred while retrieving data source connection", e); + } + } + return currentConnection.get(); + } + + public static void commitTransaction() { + try { + Connection conn = currentConnection.get(); + if (conn != null) { + conn.commit(); + } else { + if (log.isDebugEnabled()) { + log.debug("Datasource connection associated with the current thread is null, hence commit " + + "has not been attempted"); + } + } + } catch (SQLException e) { + log.error("Error occurred while committing the transaction", e); + } + } + + public static void closeConnection() { + Connection con = currentConnection.get(); + try { + con.close(); + } catch (SQLException e) { + log.error("Error occurred while close the connection"); + } + currentConnection.remove(); + } + + public static void rollbackTransaction() { + try { + Connection conn = currentConnection.get(); + if (conn != null) { + conn.rollback(); + } else { + if (log.isDebugEnabled()) { + log.debug("Datasource connection associated with the current thread is null, hence rollback " + + "has not been attempted"); + } + } + } catch (SQLException e) { + log.warn("Error occurred while roll-backing the transaction", e); + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/dao/WindowsFeatureManagementDAOException.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/dao/WindowsFeatureManagementDAOException.java new file mode 100644 index 000000000..1dd62d287 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/dao/WindowsFeatureManagementDAOException.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.device.mgt.mobile.windows.impl.dao; + +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceManagementDAOException; + +/** + * Implement Exception class for Windows Device Features. + */ +public class WindowsFeatureManagementDAOException extends MobileDeviceManagementDAOException { + + private String message; + private static final long serialVersionUID = 2021891706072918865L; + + /** + * Constructs a new MobileDeviceManagementDAOException with the specified detail message and + * nested exception. + * + * @param message error message + * @param nestedException exception + */ + public WindowsFeatureManagementDAOException(String message, Exception nestedException) { + super(message, nestedException); + } + + /** + * Constructs a new MobileDeviceManagementDAOException with the specified detail message + * and cause. + * + * @param message the detail message. + * @param cause the cause of this exception. + */ + public WindowsFeatureManagementDAOException(String message, Throwable cause) { + super(message, cause); + setErrorMessage(message); + } + + /** + * Constructs a new MobileDeviceManagementDAOException with the specified detail message. + * + * @param message the detail message. + */ + public WindowsFeatureManagementDAOException(String message) { + super(message); + setErrorMessage(message); + } + + /** + * Constructs a new MobileDeviceManagementDAOException with the specified and cause. + * + * @param cause the cause of this exception. + */ + public WindowsFeatureManagementDAOException(Throwable cause) { + super(cause); + } + + public String getMessage() { + return message; + } + + public void setErrorMessage(String errorMessage) { + this.message = errorMessage; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/dao/impl/WindowsDeviceDAOImpl.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/dao/impl/WindowsDeviceDAOImpl.java new file mode 100644 index 000000000..f65d42fc7 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/dao/impl/WindowsDeviceDAOImpl.java @@ -0,0 +1,255 @@ +/* + * 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.device.mgt.mobile.windows.impl.dao.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceDAO; +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceManagementDAOException; +import org.wso2.carbon.device.mgt.mobile.dao.util.MobileDeviceManagementDAOUtil; +import org.wso2.carbon.device.mgt.mobile.dto.MobileDevice; +import org.wso2.carbon.device.mgt.mobile.windows.impl.dao.WindowsDAOFactory; +import org.wso2.carbon.device.mgt.mobile.windows.impl.util.WindowsPluginConstants; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Implements MobileDeviceDAO for Windows Devices. + */ +public class WindowsDeviceDAOImpl implements MobileDeviceDAO { + + private static final Log log = LogFactory.getLog(WindowsDeviceDAOImpl.class); + + @Override + public MobileDevice getMobileDevice(String mblDeviceId) throws MobileDeviceManagementDAOException { + Connection conn; + PreparedStatement stmt = null; + ResultSet rs = null; + MobileDevice mobileDevice = null; + try { + conn = WindowsDAOFactory.getConnection(); + String selectDBQuery = + "SELECT DEVICE_ID, CHANNEL_URI, DEVICE_INFO, IMEI, IMSI, " + + "OS_VERSION, DEVICE_MODEL, VENDOR, LATITUDE, LONGITUDE, SERIAL, MAC_ADDRESS," + + " OS_VERSION, DEVICE_NAME " + + "FROM WIN_DEVICE WHERE DEVICE_ID = ?"; + stmt = conn.prepareStatement(selectDBQuery); + stmt.setString(1, mblDeviceId); + rs = stmt.executeQuery(); + + while (rs.next()) { + mobileDevice = new MobileDevice(); + mobileDevice.setMobileDeviceId(rs.getString(WindowsPluginConstants.DEVICE_ID)); + mobileDevice.setImei(rs.getString(WindowsPluginConstants.IMEI)); + mobileDevice.setImsi(rs.getString(WindowsPluginConstants.IMSI)); + mobileDevice.setModel(rs.getString(WindowsPluginConstants.DEVICE_MODEL)); + mobileDevice.setVendor(rs.getString(WindowsPluginConstants.VENDOR)); + mobileDevice.setLatitude(rs.getString(WindowsPluginConstants.LATITUDE)); + mobileDevice.setLongitude(rs.getString(WindowsPluginConstants.LONGITUDE)); + mobileDevice.setSerial(rs.getString(WindowsPluginConstants.SERIAL)); + mobileDevice.setOsVersion(rs.getString(WindowsPluginConstants.LATITUDE)); + + Map propertyMap = new HashMap<>(); + propertyMap.put(WindowsPluginConstants.CHANNEL_URI, rs.getString(WindowsPluginConstants.CHANNEL_URI)); + propertyMap.put(WindowsPluginConstants.DEVICE_INFO, rs.getString(WindowsPluginConstants.DEVICE_INFO)); + propertyMap.put(WindowsPluginConstants.MAC_ADDRESS, rs.getString(WindowsPluginConstants.MAC_ADDRESS)); + propertyMap.put(WindowsPluginConstants.DEVICE_NAME, rs.getString(WindowsPluginConstants.DEVICE_NAME)); + + mobileDevice.setDeviceProperties(propertyMap); + } + if (log.isDebugEnabled()) { + log.debug("All Windows device details have fetched from Windows database."); + } + return mobileDevice; + } catch (SQLException e) { + throw new MobileDeviceManagementDAOException("Error occurred while fetching all Windows device data", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, rs); + } + } + + @Override + public boolean addMobileDevice(MobileDevice mobileDevice) throws MobileDeviceManagementDAOException { + boolean status = false; + Connection conn; + PreparedStatement stmt = null; + try { + conn = WindowsDAOFactory.getConnection(); + String createDBQuery = + "INSERT INTO WIN_DEVICE(DEVICE_ID, CHANNEL_URI, DEVICE_INFO, IMEI, " + + "IMSI, OS_VERSION, DEVICE_MODEL, VENDOR, LATITUDE, LONGITUDE, SERIAL, " + + "MAC_ADDRESS, DEVICE_NAME) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + stmt = conn.prepareStatement(createDBQuery); + stmt.setString(1, mobileDevice.getMobileDeviceId()); + + Map properties = mobileDevice.getDeviceProperties(); + stmt.setString(2, properties.get(WindowsPluginConstants.CHANNEL_URI)); + stmt.setString(3, properties.get(WindowsPluginConstants.DEVICE_INFO)); + stmt.setString(4, mobileDevice.getImei()); + stmt.setString(5, mobileDevice.getImsi()); + stmt.setString(6, mobileDevice.getOsVersion()); + stmt.setString(7, mobileDevice.getModel()); + stmt.setString(8, mobileDevice.getVendor()); + stmt.setString(9, mobileDevice.getLatitude()); + stmt.setString(10, mobileDevice.getLongitude()); + stmt.setString(11, mobileDevice.getSerial()); + stmt.setString(12, properties.get(WindowsPluginConstants.MAC_ADDRESS)); + stmt.setString(13, properties.get(WindowsPluginConstants.DEVICE_NAME)); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Windows device " + mobileDevice.getMobileDeviceId() + " data has been" + + " added to the Windows database."); + } + } + } catch (SQLException e) { + throw new MobileDeviceManagementDAOException("Error occurred while adding the Windows device '" + + mobileDevice.getMobileDeviceId() + "' to the Windows db.", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, null); + } + return status; + } + + @Override + public boolean updateMobileDevice(MobileDevice mobileDevice) throws MobileDeviceManagementDAOException { + boolean status = false; + Connection conn; + PreparedStatement stmt = null; + try { + conn = WindowsDAOFactory.getConnection(); + String updateDBQuery = + "UPDATE WIN_DEVICE SET CHANNEL_URI = ?, DEVICE_INFO = ?, IMEI = ?, IMSI = ?, " + + "OS_VERSION = ?, DEVICE_MODEL = ?, VENDOR = ?, LATITUDE = ?, LONGITUDE = ?, " + + "SERIAL = ?, MAC_ADDRESS = ?, DEVICE_NAME = ? WHERE DEVICE_ID = ?"; + + stmt = conn.prepareStatement(updateDBQuery); + + Map properties = mobileDevice.getDeviceProperties(); + stmt.setString(1, properties.get(WindowsPluginConstants.CHANNEL_URI)); + stmt.setString(2, properties.get(WindowsPluginConstants.DEVICE_INFO)); + stmt.setString(3, mobileDevice.getImei()); + stmt.setString(4, mobileDevice.getImsi()); + stmt.setString(5, mobileDevice.getOsVersion()); + stmt.setString(6, mobileDevice.getModel()); + stmt.setString(7, mobileDevice.getVendor()); + stmt.setString(8, mobileDevice.getLatitude()); + stmt.setString(9, mobileDevice.getLongitude()); + stmt.setString(10, mobileDevice.getSerial()); + stmt.setString(11, properties.get(WindowsPluginConstants.MAC_ADDRESS)); + stmt.setString(12, properties.get(WindowsPluginConstants.DEVICE_NAME)); + stmt.setString(13, mobileDevice.getMobileDeviceId()); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Windows device " + mobileDevice.getMobileDeviceId() + " data has been" + + " modified."); + } + } + } catch (SQLException e) { + throw new MobileDeviceManagementDAOException("Error occurred while modifying the Windows device '" + + mobileDevice.getMobileDeviceId() + "' data.", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, null); + } + return status; + } + + @Override + public boolean deleteMobileDevice(String mblDeviceId) throws MobileDeviceManagementDAOException { + boolean status = false; + Connection conn; + PreparedStatement stmt = null; + try { + conn = WindowsDAOFactory.getConnection(); + String deleteDBQuery = "DELETE FROM WIN_DEVICE WHERE DEVICE_ID = ?"; + stmt = conn.prepareStatement(deleteDBQuery); + stmt.setString(1, mblDeviceId); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Windows device " + mblDeviceId + " data has deleted" + + " from the windows database."); + } + } + } catch (SQLException e) { + throw new MobileDeviceManagementDAOException("Error occurred while deleting windows device '" + + mblDeviceId + "'", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, null); + } + return status; + } + + @Override + public List getAllMobileDevices() throws MobileDeviceManagementDAOException { + Connection conn; + PreparedStatement stmt = null; + ResultSet rs = null; + MobileDevice mobileDevice; + List mobileDevices = new ArrayList<>(); + try { + conn = WindowsDAOFactory.getConnection(); + String selectDBQuery = + "SELECT DEVICE_ID, CHANNEL_URI, DEVICE_INFO, IMEI, IMSI, " + + "OS_VERSION, DEVICE_MODEL, VENDOR, LATITUDE, LONGITUDE, SERIAL, MAC_ADDRESS," + + " OS_VERSION, DEVICE_NAME " + + "FROM WIN_DEVICE"; + stmt = conn.prepareStatement(selectDBQuery); + rs = stmt.executeQuery(); + + while (rs.next()) { + mobileDevice = new MobileDevice(); + mobileDevice.setMobileDeviceId(rs.getString(WindowsPluginConstants.DEVICE_ID)); + mobileDevice.setVendor(rs.getString(WindowsPluginConstants.IMEI)); + mobileDevice.setLatitude(rs.getString(WindowsPluginConstants.IMSI)); + mobileDevice.setLongitude(rs.getString(WindowsPluginConstants.OS_VERSION)); + mobileDevice.setImei(rs.getString(WindowsPluginConstants.DEVICE_MODEL)); + mobileDevice.setImsi(rs.getString(WindowsPluginConstants.VENDOR)); + mobileDevice.setOsVersion(rs.getString(WindowsPluginConstants.LATITUDE)); + + Map propertyMap = new HashMap<>(); + propertyMap.put(WindowsPluginConstants.CHANNEL_URI, rs.getString(WindowsPluginConstants.CHANNEL_URI)); + propertyMap.put(WindowsPluginConstants.DEVICE_INFO, rs.getString(WindowsPluginConstants.DEVICE_INFO)); + propertyMap.put(WindowsPluginConstants.DEVICE_NAME, rs.getString(WindowsPluginConstants.DEVICE_NAME)); + mobileDevice.setDeviceProperties(propertyMap); + mobileDevices.add(mobileDevice); + } + if (log.isDebugEnabled()) { + log.debug("All Windows device details have fetched from Windows database."); + } + return mobileDevices; + } catch (SQLException e) { + throw new MobileDeviceManagementDAOException("Error occurred while fetching all Windows device data", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, rs); + } + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/dao/impl/WindowsFeatureDAOImpl.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/dao/impl/WindowsFeatureDAOImpl.java new file mode 100644 index 000000000..a1e42cdff --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/dao/impl/WindowsFeatureDAOImpl.java @@ -0,0 +1,271 @@ +/* + * 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.device.mgt.mobile.windows.impl.dao.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.device.mgt.common.DeviceManagementConstants; +import org.wso2.carbon.device.mgt.mobile.dao.MobileDeviceManagementDAOException; +import org.wso2.carbon.device.mgt.mobile.dao.MobileFeatureDAO; +import org.wso2.carbon.device.mgt.mobile.dao.util.MobileDeviceManagementDAOUtil; +import org.wso2.carbon.device.mgt.mobile.dto.MobileFeature; +import org.wso2.carbon.device.mgt.mobile.windows.impl.dao.WindowsDAOFactory; +import org.wso2.carbon.device.mgt.mobile.windows.impl.dao.WindowsFeatureManagementDAOException; +import org.wso2.carbon.device.mgt.mobile.windows.impl.util.WindowsPluginConstants; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * Implement MobileFeatureDAO for Windows devices. + */ +public class WindowsFeatureDAOImpl implements MobileFeatureDAO { + + private static final Log log = LogFactory.getLog(WindowsFeatureDAOImpl.class); + + public WindowsFeatureDAOImpl() { + + } + + @Override + public boolean addFeature(MobileFeature mobileFeature) throws MobileDeviceManagementDAOException { + PreparedStatement stmt = null; + boolean status; + Connection conn; + try { + conn = WindowsDAOFactory.getConnection(); + String sql = "INSERT INTO WIN_FEATURE(CODE, NAME, DESCRIPTION) VALUES (?, ?, ?)"; + stmt = conn.prepareStatement(sql); + stmt.setString(1, mobileFeature.getCode()); + stmt.setString(2, mobileFeature.getName()); + stmt.setString(3, mobileFeature.getDescription()); + stmt.executeUpdate(); + status = true; + } catch (SQLException e) { + throw new WindowsFeatureManagementDAOException( + "Error occurred while adding windows feature '" + + mobileFeature.getName() + "' into the metadata repository", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, null); + } + return status; + } + + @Override + public boolean addFeatures(List mobileFeatures) throws MobileDeviceManagementDAOException { + PreparedStatement stmt = null; + boolean status = false; + Connection conn; + try { + conn = WindowsDAOFactory.getConnection(); + stmt = conn.prepareStatement("INSERT INTO WIN_FEATURE(CODE, NAME, DESCRIPTION) VALUES (?, ?, ?)"); + for (MobileFeature mobileFeature : mobileFeatures) { + stmt.setString(1, mobileFeature.getCode()); + stmt.setString(2, mobileFeature.getName()); + stmt.setString(3, mobileFeature.getDescription()); + stmt.addBatch(); + } + stmt.executeBatch(); + status = true; + } catch (SQLException e) { + throw new WindowsFeatureManagementDAOException( + "Error occurred while adding windows features into the metadata repository", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, null); + } + return status; + } + + @Override + public boolean updateFeature(MobileFeature mobileFeature) throws MobileDeviceManagementDAOException { + boolean status = false; + Connection conn; + PreparedStatement stmt = null; + try { + conn = WindowsDAOFactory.getConnection(); + String updateDBQuery = + "UPDATE WIN_FEATURE SET NAME = ?, DESCRIPTION = ?" + + "WHERE CODE = ?"; + stmt = conn.prepareStatement(updateDBQuery); + stmt.setString(1, mobileFeature.getName()); + stmt.setString(2, mobileFeature.getDescription()); + stmt.setString(3, mobileFeature.getCode()); + int rows = stmt.executeUpdate(); + if (rows > 0) { + status = true; + if (log.isDebugEnabled()) { + log.debug("Windows Feature " + mobileFeature.getCode() + " data has been " + + "modified."); + } + } + } catch (SQLException e) { + throw new WindowsFeatureManagementDAOException("Error occurred while updating the Windows Feature '" + + mobileFeature.getCode() + "' to the Windows db.", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, null); + } + return status; + } + + @Override + public boolean deleteFeatureById(int mblFeatureId) throws MobileDeviceManagementDAOException { + PreparedStatement stmt = null; + boolean status = false; + Connection conn; + try { + conn = WindowsDAOFactory.getConnection(); + String sql = "DELETE FROM WIN_FEATURE WHERE ID = ?"; + stmt = conn.prepareStatement(sql); + stmt.setInt(1, mblFeatureId); + stmt.execute(); + status = true; + } catch (SQLException e) { + throw new WindowsFeatureManagementDAOException( + "Error occurred while deleting windows feature '" + + mblFeatureId + "' from Windows database.", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, null); + } + return status; + } + + @Override + public boolean deleteFeatureByCode(String mblFeatureCode) throws MobileDeviceManagementDAOException { + PreparedStatement stmt = null; + boolean status = false; + Connection conn; + try { + conn = WindowsDAOFactory.getConnection(); + String sql = "DELETE FROM WIN_FEATURE WHERE CODE = ?"; + stmt = conn.prepareStatement(sql); + stmt.setString(1, mblFeatureCode); + stmt.execute(); + status = true; + } catch (SQLException e) { + throw new WindowsFeatureManagementDAOException( + "Error occurred while deleting windows feature '" + + mblFeatureCode + "' from Windows database.", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, null); + } + return status; + } + + @Override + public MobileFeature getFeatureById(int mblFeatureId) throws MobileDeviceManagementDAOException { + PreparedStatement stmt = null; + ResultSet rs = null; + Connection conn; + try { + conn = WindowsDAOFactory.getConnection(); + String sql = "SELECT ID, CODE, NAME, DESCRIPTION FROM WIN_FEATURE WHERE ID = ?"; + stmt = conn.prepareStatement(sql); + stmt.setInt(1, mblFeatureId); + rs = stmt.executeQuery(); + MobileFeature mobileFeature = null; + if (rs.next()) { + mobileFeature = new MobileFeature(); + mobileFeature.setId(rs.getInt(WindowsPluginConstants.WINDOWS_FEATURE_ID)); + mobileFeature.setCode(rs.getString(WindowsPluginConstants.WINDOWS_FEATURE_CODE)); + mobileFeature.setName(rs.getString(WindowsPluginConstants.WINDOWS_FEATURE_NAME)); + mobileFeature.setDescription(rs.getString(WindowsPluginConstants.WINDOWS_FEATURE_DESCRIPTION)); + mobileFeature.setDeviceType( + DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS); + } + return mobileFeature; + } catch (SQLException e) { + throw new WindowsFeatureManagementDAOException( + "Error occurred while retrieving windows feature '" + + mblFeatureId + "' from the Windows database.", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, rs); + } + } + + @Override + public MobileFeature getFeatureByCode(String mblFeatureCode) throws MobileDeviceManagementDAOException { + PreparedStatement stmt = null; + ResultSet rs = null; + Connection conn; + try { + conn = WindowsDAOFactory.getConnection(); + String sql = "SELECT ID, CODE, NAME, DESCRIPTION FROM WIN_FEATURE WHERE CODE = ?"; + stmt = conn.prepareStatement(sql); + stmt.setString(1, mblFeatureCode); + rs = stmt.executeQuery(); + MobileFeature mobileFeature = null; + if (rs.next()) { + mobileFeature = new MobileFeature(); + mobileFeature.setId(rs.getInt(WindowsPluginConstants.WINDOWS_FEATURE_ID)); + mobileFeature.setCode(rs.getString(WindowsPluginConstants.WINDOWS_FEATURE_CODE)); + mobileFeature.setName(rs.getString(WindowsPluginConstants.WINDOWS_FEATURE_NAME)); + mobileFeature.setDescription(rs.getString(WindowsPluginConstants.WINDOWS_FEATURE_DESCRIPTION)); + mobileFeature.setDeviceType( + DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS); + } + return mobileFeature; + } catch (SQLException e) { + throw new WindowsFeatureManagementDAOException( + "Error occurred while retrieving windows feature '" + + mblFeatureCode + "' from the Windows database.", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, rs); + } + } + + @Override + public List getFeatureByDeviceType(String deviceType) throws MobileDeviceManagementDAOException { + return this.getAllFeatures(); + } + + @Override + public List getAllFeatures() throws MobileDeviceManagementDAOException { + PreparedStatement stmt = null; + ResultSet rs = null; + Connection conn; + List features = new ArrayList<>(); + try { + conn = WindowsDAOFactory.getConnection(); + String sql = "SELECT ID, CODE, NAME, DESCRIPTION FROM WIN_FEATURE"; + stmt = conn.prepareStatement(sql); + rs = stmt.executeQuery(); + MobileFeature mobileFeature; + while (rs.next()) { + mobileFeature = new MobileFeature(); + mobileFeature.setId(rs.getInt(WindowsPluginConstants.WINDOWS_FEATURE_ID)); + mobileFeature.setCode(rs.getString(WindowsPluginConstants.WINDOWS_FEATURE_CODE)); + mobileFeature.setName(rs.getString(WindowsPluginConstants.WINDOWS_FEATURE_NAME)); + mobileFeature.setDescription(rs.getString(WindowsPluginConstants.WINDOWS_FEATURE_DESCRIPTION)); + mobileFeature.setDeviceType( + DeviceManagementConstants.MobileDeviceTypes.MOBILE_DEVICE_TYPE_WINDOWS); + features.add(mobileFeature); + } + return features; + } catch (SQLException e) { + throw new WindowsFeatureManagementDAOException("Error occurred while retrieving all " + + "windows features from the Windows database.", e); + } finally { + MobileDeviceManagementDAOUtil.cleanupResources(stmt, rs); + } + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/util/WindowsPluginConstants.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/util/WindowsPluginConstants.java new file mode 100644 index 000000000..7596871c9 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/util/WindowsPluginConstants.java @@ -0,0 +1,48 @@ +/* + * 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.device.mgt.mobile.windows.impl.util; + +/** + * Define constance used by Windows plugin. + */ +public class WindowsPluginConstants { + + //properties related to database table WINDOWS_DEVICE + public static final String DEVICE_ID = "DEVICE_ID"; + public static final String CHANNEL_URI = "CHANNEL_URI"; + public static final String DEVICE_INFO = "DEVICE_INFO"; + public static final String IMEI = "IMEI"; + public static final String IMSI = "IMSI"; + public static final String OS_VERSION = "OS_VERSION"; + public static final String DEVICE_MODEL = "DEVICE_MODEL"; + public static final String VENDOR = "VENDOR"; + public static final String LATITUDE = "LATITUDE"; + public static final String LONGITUDE = "LONGITUDE"; + public static final String SERIAL = "SERIAL"; + public static final String MAC_ADDRESS = "MAC_ADDRESS"; + public static final String DEVICE_NAME = "DEVICE_NAME"; + + //Properties related to WIN_FEATURE table + public static final String WINDOWS_FEATURE_ID = "ID"; + public static final String WINDOWS_FEATURE_CODE = "CODE"; + public static final String WINDOWS_FEATURE_NAME = "NAME"; + public static final String WINDOWS_FEATURE_DESCRIPTION = "DESCRIPTION"; + + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/util/WindowsPluginUtils.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/util/WindowsPluginUtils.java new file mode 100644 index 000000000..2c18646ce --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/util/WindowsPluginUtils.java @@ -0,0 +1,58 @@ +/* + * 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.device.mgt.mobile.windows.impl.util; + +import org.wso2.carbon.device.mgt.common.license.mgt.License; +import org.wso2.carbon.device.mgt.mobile.windows.impl.WindowsDeviceManagementService; + +/** + * Contains utility method used by Windows plugin. + */ +public class WindowsPluginUtils { + + public static License getDefaultLicense() { + License license = new License(); + license.setName(WindowsDeviceManagementService.DEVICE_TYPE_WINDOWS); + license.setLanguage("en_US"); + license.setVersion("1.0.0"); + license.setText("This End User License Agreement (\"Agreement\") is a legal agreement between you (\"You\") " + + "and WSO2, Inc., regarding the enrollment of Your personal mobile device (\"Device\") in SoR's " + + "mobile device management program, and the loading to and removal from Your Device and Your use " + + "of certain applications and any associated software and user documentation, whether provided in " + + "\"online\" or electronic format, used in connection with the operation of or provision of services " + + "to WSO2, Inc., BY SELECTING \"I ACCEPT\" DURING INSTALLATION, YOU ARE ENROLLING YOUR DEVICE, AND " + + "THEREBY AUTHORIZING SOR OR ITS AGENTS TO INSTALL, UPDATE AND REMOVE THE APPS FROM YOUR DEVICE AS " + + "DESCRIBED IN THIS AGREEMENT. YOU ARE ALSO EXPLICITLY ACKNOWLEDGING AND AGREEING THAT (1) THIS IS " + + "A BINDING CONTRACT AND (2) YOU HAVE READ AND AGREE TO THE TERMS OF THIS AGREEMENT.\n" + + "\n" + + "IF YOU DO NOT ACCEPT THESE TERMS, DO NOT ENROLL YOUR DEVICE AND DO NOT PROCEED ANY FURTHER.\n" + + "\n" + + "You agree that: (1) You understand and agree to be bound by the terms and conditions contained " + + "in this Agreement, and (2) You are at least 21 years old and have the legal capacity to enter " + + "into this Agreement as defined by the laws of Your jurisdiction. SoR shall have the right, " + + "without prior notice, to terminate or suspend (i) this Agreement, (ii) the enrollment of Your " + + "Device, or (iii) the functioning of the Apps in the event of a violation of this Agreement or " + + "the cessation of Your relationship with SoR (including termination of Your employment if You are " + + "an employee or expiration or termination of Your applicable franchise or supply agreement if You " + + "are a franchisee of or supplier to the WSO2 WSO2, Inc., system). SoR expressly reserves all " + + "rights not expressly granted herein."); + return license; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/util/WindowsUtils.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/util/WindowsUtils.java new file mode 100644 index 000000000..7fba2f5ca --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/impl/util/WindowsUtils.java @@ -0,0 +1,35 @@ +/* + * 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.device.mgt.mobile.windows.impl.util; + +import java.util.Map; + +/** + * Contains utility methods used by Windows plugin. + */ +public class WindowsUtils { + public static String getDeviceProperty(Map deviceProperties, String property) { + + String deviceProperty = deviceProperties.get(property); + if (deviceProperty == null) { + return null; + } + return deviceProperty; + } +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/internal/WindowsDeviceManagementDataHolder.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/internal/WindowsDeviceManagementDataHolder.java new file mode 100644 index 000000000..600aecc1c --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/internal/WindowsDeviceManagementDataHolder.java @@ -0,0 +1,47 @@ +/* + * 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.device.mgt.mobile.windows.internal; + +import org.wso2.carbon.registry.core.service.RegistryService; + +/** + * DataHolder class of Mobile plugins component. + */ +public class WindowsDeviceManagementDataHolder { + + private RegistryService registryService; + + private static WindowsDeviceManagementDataHolder thisInstance = new WindowsDeviceManagementDataHolder(); + + private WindowsDeviceManagementDataHolder() { + } + + public static WindowsDeviceManagementDataHolder getInstance() { + return thisInstance; + } + + public RegistryService getRegistryService() { + return registryService; + } + + public void setRegistryService(RegistryService registryService) { + this.registryService = registryService; + } + +} diff --git a/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/internal/WindowsDeviceManagementServiceComponent.java b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/internal/WindowsDeviceManagementServiceComponent.java new file mode 100644 index 000000000..5fd45c61a --- /dev/null +++ b/components/mobile-plugins/windows-plugin/org.wso2.carbon.device.mgt.mobile.windows/src/main/java/org/wso2/carbon/device/mgt/mobile/windows/internal/WindowsDeviceManagementServiceComponent.java @@ -0,0 +1,121 @@ +/* + * 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.device.mgt.mobile.windows.internal; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.ComponentContext; +import org.wso2.carbon.device.mgt.common.spi.DeviceManagementService; +import org.wso2.carbon.device.mgt.mobile.windows.impl.WindowsDeviceManagementService; +import org.wso2.carbon.device.mgt.mobile.windows.impl.WindowsPolicyMonitoringService; +import org.wso2.carbon.ndatasource.core.DataSourceService; +import org.wso2.carbon.policy.mgt.common.spi.PolicyMonitoringService; +import org.wso2.carbon.registry.core.service.RegistryService; + +/** + * @scr.component name="org.wso2.carbon.device.mgt.mobile.windows.impl.internal.WindowsDeviceManagementServiceComponent" + * immediate="true" + * @scr.reference name="org.wso2.carbon.ndatasource" + * interface="org.wso2.carbon.ndatasource.core.DataSourceService" + * cardinality="1..1" + * policy="dynamic" + * bind="setDataSourceService" + * unbind="unsetDataSourceService" + * @scr.reference name="registry.service" + * interface="org.wso2.carbon.registry.core.service.RegistryService" cardinality="0..1" + * policy="dynamic" bind="setRegistryService" unbind="unsetRegistryService" + *

      + * Adding reference to API Manager Configuration service is an unavoidable hack to get rid of NPEs thrown while + * initializing APIMgtDAOs attempting to register APIs programmatically. APIMgtDAO needs to be proper cleaned up + * to avoid as an ideal fix + */ +public class WindowsDeviceManagementServiceComponent { + + private ServiceRegistration androidServiceRegRef; + + private static final Log log = LogFactory.getLog(WindowsDeviceManagementServiceComponent.class); + + protected void activate(ComponentContext ctx) { + + if (log.isDebugEnabled()) { + log.debug("Activating Android Mobile Device Management Service Component"); + } + try { + BundleContext bundleContext = ctx.getBundleContext(); + + androidServiceRegRef = + bundleContext.registerService(DeviceManagementService.class.getName(), + new WindowsDeviceManagementService(), null); + + // Policy management service + + bundleContext.registerService(PolicyMonitoringService.class, + new WindowsPolicyMonitoringService(), null); + + if (log.isDebugEnabled()) { + log.debug("Android Mobile Device Management Service Component has been successfully activated"); + } + } catch (Throwable e) { + log.error("Error occurred while activating Android Mobile Device Management Service Component", e); + } + } + + protected void deactivate(ComponentContext ctx) { + if (log.isDebugEnabled()) { + log.debug("De-activating Android Mobile Device Management Service Component"); + } + try { + if (androidServiceRegRef != null) { + androidServiceRegRef.unregister(); + } + if (log.isDebugEnabled()) { + log.debug( + "Android Mobile Device Management Service Component has been successfully de-activated"); + } + } catch (Throwable e) { + log.error("Error occurred while de-activating Android Mobile Device Management bundle", e); + } + } + + protected void setDataSourceService(DataSourceService dataSourceService) { + /* This is to avoid mobile device management component getting initialized before the underlying datasources + are registered */ + if (log.isDebugEnabled()) { + log.debug("Data source service set to android mobile service component"); + } + } + + protected void unsetDataSourceService(DataSourceService dataSourceService) { + //do nothing + } + + protected void setRegistryService(RegistryService registryService) { + if (log.isDebugEnabled()) { + log.debug("RegistryService acquired"); + } + WindowsDeviceManagementDataHolder.getInstance().setRegistryService(registryService); + } + + protected void unsetRegistryService(RegistryService registryService) { + WindowsDeviceManagementDataHolder.getInstance().setRegistryService(null); + } + +} diff --git a/components/mobile-plugins/windows-plugin/pom.xml b/components/mobile-plugins/windows-plugin/pom.xml new file mode 100644 index 000000000..e5de76e61 --- /dev/null +++ b/components/mobile-plugins/windows-plugin/pom.xml @@ -0,0 +1,61 @@ + + + + + + + org.wso2.carbon.devicemgt-plugins + mobile-plugins + 2.1.0-SNAPSHOT + ../pom.xml + + + 4.0.0 + org.wso2.carbon.devicemgt-plugins + windows-plugin + pom + WSO2 Carbon - Mobile Plugins + http://wso2.org + + + org.wso2.carbon.device.mgt.mobile.windows + org.wso2.carbon.device.mgt.mobile.windows.agent + + + + + + + org.apache.felix + maven-scr-plugin + 1.7.2 + + + generate-scr-scrdescriptor + + scr + + + + + + + + + diff --git a/pom.xml b/pom.xml index f1e904679..0848df962 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,8 @@ --> - + 4.0.0 org.wso2.carbon.devicemgt-plugins @@ -37,6 +38,8 @@ components/device-mgt + + features/device-mgt @@ -245,6 +248,250 @@ zip ${carbon.device.mgt.version} + + org.wso2.carbon.devicemgt + org.wso2.carbon.certificate.mgt.core + ${carbon.device.mgt.version} + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.analytics + ${carbon.device.mgt.version} + provided + + + org.wso2.carbon.devicemgt + org.wso2.carbon.dynamic.client.registration + ${carbon.device.mgt.version} + + + org.wso2.carbon.devicemgt + org.wso2.carbon.device.mgt.jwt.client.extension + ${carbon.devicemgt.version} + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.application.extension + ${carbon.devicemgt.version} + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.webapp.publisher + ${carbon.devicemgt.version} + + + org.wso2.carbon.devicemgt + org.wso2.carbon.apimgt.annotations + ${carbon.devicemgt.version} + + + + org.wso2.carbon.commons + org.wso2.carbon.databridge.commons + ${carbon.commons.version} + + + org.wso2.carbon.analytics-common + org.wso2.carbon.databridge.core + ${carbon.analytics.common.version} + + + org.wso2.carbon.analytics-common + org.wso2.carbon.databridge.agent + ${carbon.analytics.common.version} + + + commons-codec + commons-codec + + + + + org.wso2.carbon.analytics-common + org.wso2.carbon.databridge.commons + ${carbon.analytics.common.version} + + + + + + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot + ${carbon.devicemgt.plugins.version} + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.api + ${carbon.devicemgt.plugins.version} + war + + + + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.androidsense.plugin + ${carbon.devicemgt.plugins.version} + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.androidsense.controller.api + ${carbon.devicemgt.plugins.version} + war + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.androidsense.manager.api + ${carbon.devicemgt.plugins.version} + war + + + + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.arduino.plugin.impl + ${carbon.devicemgt.plugins.version} + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.arduino.service.impl + ${carbon.devicemgt.plugins.version} + war + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.arduino.manager.service.impl + ${carbon.devicemgt.plugins.version} + war + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.arduino.controller.service.impl + ${carbon.devicemgt.plugins.version} + war + + + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.digitaldisplay.plugin + ${carbon.devicemgt.plugins.version} + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.digitaldisplay.manager.api + ${carbon.devicemgt.plugins.version} + war + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.digitaldisplay.controller.api + ${carbon.devicemgt.plugins.version} + war + + + + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.droneanalyzer.plugin + ${carbon.devicemgt.plugins.version} + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.droneanalyzer.manager.api + ${carbon.devicemgt.plugins.version} + war + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.droneanalyzer.controller.api + ${carbon.devicemgt.plugins.version} + war + + + + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.raspberrypi.plugin.impl + ${carbon.devicemgt.plugins.version} + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.raspberrypi.manager.service.impl + ${carbon.devicemgt.plugins.version} + war + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.raspberrypi.controller.service.impl + ${carbon.devicemgt.plugins.version} + war + + + + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.virtualfirealarm.plugin.impl + ${carbon.devicemgt.plugins.version} + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.virtualfirealarm.manager.service.impl + ${carbon.devicemgt.plugins.version} + war + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.virtualfirealarm.controller.service.impl + ${carbon.devicemgt.plugins.version} + war + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.impl + ${carbon.devicemgt.plugins.version} + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.virtualfirealarm.agent.advanced.impl + ${carbon.devicemgt.plugins.version} + + + + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.iot.camera.plugin.impl + ${carbon.devicemgt.plugins.version} + + + + + + org.wso2.carbon.analytics + org.wso2.carbon.analytics.datasource.commons + ${carbon.analytics.version} + @@ -257,6 +504,16 @@ org.wso2.carbon.device.mgt.mobile.url.printer ${carbon.mobile.device.mgt.version} + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.mobile + ${carbon.mobile.device.mgt.version} + + + org.wso2.carbon.devicemgt-plugins + org.wso2.carbon.device.mgt.mobile.android + ${carbon.mobile.device.mgt.version} + @@ -305,6 +562,11 @@ jdbc-pool ${orbit.tomcat.jdbc.pooling.version} + + org.wso2.tomcat + tomcat-servlet-api + ${orbit.version.tomcat-servlet-api} + + + org.igniterealtime.smack.wso2 + smack + ${smack.wso2.version} + + + org.igniterealtime.smack.wso2 + smackx + ${smackx.wso2.version} + + + + + org.eclipse.jetty + jetty-server + ${jetty.version} + + + + + org.apache.cxf + cxf-rt-frontend-jaxws + ${cxf.version} + + + org.apache.cxf + cxf-rt-frontend-jaxrs + ${cxf.version} + + + org.apache.cxf + cxf-rt-transports-http + ${cxf.version} + + + javax.ws.rs + jsr311-api + ${javax.ws.rs.version} + + + + + commons-httpclient.wso2 + commons-httpclient + ${orbit.version.commons-httpclient} + + + commons-codec.wso2 + commons-codec + ${commons-codec.wso2.version} + + + org.codehaus.jackson + jackson-core-asl + ${jackson.version} + + + org.codehaus.jackson + jackson-jaxrs + ${jackson.version} + + + + + org.apache.cxf + cxf-rt-bindings-soap + ${cxf.version} + + + org.apache.cxf + cxf-rt-bindings-http + ${cxf.bindings.version} + + + org.apache.ws.security + wss4j + ${wss4j.security.version} + + + org.apache.cxf + cxf-rt-rs-extension-providers + ${cxf.version} + + + org.apache.cxf + cxf-rt-ws-security + ${cxf.version} + + + org.apache.wss4j + wss4j-ws-security-common + ${wss4j.security.common.version} + + + org.apache.ws.commons.axiom + axiom-api + ${axiom-api.version} + + + org.apache.ws.commons.axiom + axiom-impl + ${axiom-api.version} + + + log4j + log4j + ${log4j.version} + + + org.springframework + spring-web + ${spring-web.version} + + + org.springframework.ws + spring-ws-security + ${spring-ws-security.version} + + + org.springframework + spring-context + ${spring-web.version} + + + com.sun.xml.ws + jaxws-rt + ${jaxws-rt.version} + + + com.sun.xml.messaging.saaj + saaj-impl + ${saaj-impl.version} + + + org.bouncycastle + bcpkix-jdk15on + ${bcpkix-jdk15on.version} + + + org.bouncycastle + bcprov-jdk15on + ${bcpkix-jdk15on.version} + + + org.codehaus.plexus + plexus-utils + ${plexus-utils.version} + + + com.madgag.spongycastle + pkix + ${spongycastle.version} + + + com.madgag.spongycastle + prov + ${spongycastle.version} + + + com.madgag.spongycastle + core + ${spongycastle.version} + + + joda-time + joda-time + ${joda-time.version} + + + + commons-io + commons-io + ${commons-io.version} + + + + org.wso2.carbon.identity + org.wso2.carbon.identity.oauth.stub + ${carbon.identity.version} + provided + + + org.wso2.carbon.identity + org.wso2.carbon.identity.oauth + ${carbon.identity.version} + provided + + + + commons-collections + commons-collections + ${commons-collections.version} + + + commons-configuration + commons-configuration + ${commons-configuration.version} + + + + org.wso2.carbon.commons + org.wso2.carbon.user.mgt + ${carbon.commons.version} + + + + junit + junit + test + ${junit.version} + + @@ -552,6 +1045,7 @@ 1.3 + 4.8.2 4.3.1 6.8 @@ -566,34 +1060,58 @@ 2.7.16 + 2.5.11 1.9.0 1.1.1 + + 1.1.0-SNAPSHOT + + + 2.1.0-SNAPSHOT + 4.4.8 4.6.0 - + 5.0.7 - + 4.5.0 - + 4.4.8 - + 4.5.8 - + 1.1.0-SNAPSHOT [0.8.0, 2.0.0) + + 5.0.3 + 2.1.0-SNAPSHOT + + 3.0.4.wso2v1 + 3.0.4.wso2v1 + 1.0.2 + + + 8.1.3.v20120416 + + + 5.0.11-SNAPSHOT + [5.0.11,6.0.0) + 1.0.6-SNAPSHOT + [1.0.6,2.0.0] + 1.51.0.0 1.49 @@ -602,8 +1120,33 @@ 2.2.4 1.0.2 - - github-scm + 3.1.0.wso2v2 + 1.4.0.wso2v1 + + 6.0 + + 1.2.14 + 1.2.17 + 3.0.5.RELEASE + 2.1.0.RELEASE + 2.2.8 + 1.3.18 + 1.49 + 3.0.21 + 2.2 + 1.6.17 + 2.0.0 + + 2.4 + 3.0.0.wso2v1 + 3.2.2 + 1.8 + + 7.0.59.wso2v1 + + + + github-scm @@ -715,6 +1258,11 @@ build-helper-maven-plugin 1.8 + + org.apache.maven.plugins + maven-war-plugin + 2.2 + @@ -798,5 +1346,5 @@ GCM Server repository - GitHub https://github.com/slorber/gcm-server-repository/raw/master/releases/ - +

      + +
      + +