thingsboard-aplcache

Details

.gitignore 1(+1 -0)

diff --git a/.gitignore b/.gitignore
index e14c866..6f6dd61 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,4 @@ pom.xml.versionsBackup
 **/target
 **/Californium.properties
 **/.env
+.instance_id
diff --git a/application/build.gradle b/application/build.gradle
index 75a0e52..25ad51d 100644
--- a/application/build.gradle
+++ b/application/build.gradle
@@ -13,6 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+import org.apache.tools.ant.filters.ReplaceTokens
+
 buildscript {
     ext {
         osPackageVersion = "3.8.0"
@@ -56,6 +59,7 @@ ospackage {
 
     // Copy the config files
     from("target/conf") {
+        exclude "${pkgName}.conf"
         fileType CONFIG | NOREPLACE
         fileMode 0754
         into "conf"
@@ -84,6 +88,14 @@ buildRpm {
 
     requires("java-1.8.0")
 
+    from("target/conf") {
+        include "${pkgName}.conf"
+        filter(ReplaceTokens, tokens: ['pkg.platform': 'rpm'])
+        fileType CONFIG | NOREPLACE
+        fileMode 0754
+        into "${pkgInstallFolder}/conf"
+    }
+
     preInstall file("${buildDir}/control/rpm/preinst")
     postInstall file("${buildDir}/control/rpm/postinst")
     preUninstall file("${buildDir}/control/rpm/prerm")
@@ -113,6 +125,14 @@ buildDeb {
 
     requires("openjdk-8-jre").or("java8-runtime").or("oracle-java8-installer").or("openjdk-8-jre-headless")
 
+    from("target/conf") {
+        include "${pkgName}.conf"
+        filter(ReplaceTokens, tokens: ['pkg.platform': 'deb'])
+        fileType CONFIG | NOREPLACE
+        fileMode 0754
+        into "${pkgInstallFolder}/conf"
+    }
+
     configurationFile("${pkgInstallFolder}/conf/${pkgName}.conf")
     configurationFile("${pkgInstallFolder}/conf/${pkgName}.yml")
     configurationFile("${pkgInstallFolder}/conf/logback.xml")
diff --git a/application/src/main/conf/thingsboard.conf b/application/src/main/conf/thingsboard.conf
index 328054f..ef977ee 100644
--- a/application/src/main/conf/thingsboard.conf
+++ b/application/src/main/conf/thingsboard.conf
@@ -14,6 +14,6 @@
 # limitations under the License.
 #
 
-export JAVA_OPTS="$JAVA_OPTS"
+export JAVA_OPTS="$JAVA_OPTS -Dplatform=@pkg.platform@"
 export LOG_FILENAME=${pkg.name}.out
 export LOADER_PATH=${pkg.installFolder}/conf,${pkg.installFolder}/extensions
diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
index b17c394..bc3c47e 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java
@@ -22,6 +22,8 @@ import org.thingsboard.server.common.data.AdminSettings;
 import org.thingsboard.server.dao.settings.AdminSettingsService;
 import org.thingsboard.server.exception.ThingsboardException;
 import org.thingsboard.server.service.mail.MailService;
+import org.thingsboard.server.service.update.UpdateService;
+import org.thingsboard.server.service.update.model.UpdateMessage;
 
 @RestController
 @RequestMapping("/api/admin")
@@ -33,6 +35,9 @@ public class AdminController extends BaseController {
     @Autowired
     private AdminSettingsService adminSettingsService;
 
+    @Autowired
+    private UpdateService updateService;
+
     @PreAuthorize("hasAuthority('SYS_ADMIN')")
     @RequestMapping(value = "/settings/{key}", method = RequestMethod.GET)
     @ResponseBody
@@ -72,4 +77,16 @@ public class AdminController extends BaseController {
             throw handleException(e);
         }
     }
+
+    @PreAuthorize("hasAuthority('SYS_ADMIN')")
+    @RequestMapping(value = "/updates", method = RequestMethod.GET)
+    @ResponseBody
+    public UpdateMessage checkUpdates() throws ThingsboardException {
+        try {
+            return updateService.checkUpdates();
+        } catch (Exception e) {
+            throw handleException(e);
+        }
+    }
+
 }
diff --git a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java
new file mode 100644
index 0000000..0856b8a
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java
@@ -0,0 +1,128 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * 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.thingsboard.server.service.update;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import org.thingsboard.server.service.update.model.UpdateMessage;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.UUID;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+@Service
+@Slf4j
+public class DefaultUpdateService implements UpdateService {
+
+    private static final String INSTANCE_ID_FILE = ".instance_id";
+    private static final String UPDATE_SERVER_BASE_URL = "https://updates.thingsboard.io";
+
+    private static final String PLATFORM_PARAM = "platform";
+    private static final String VERSION_PARAM = "version";
+    private static final String INSTANCE_ID_PARAM = "instanceId";
+
+    @Value("${updates.enabled}")
+    private boolean updatesEnabled;
+
+    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
+
+    private ScheduledFuture checkUpdatesFuture = null;
+    private RestTemplate restClient = new RestTemplate();
+
+    private UpdateMessage updateMessage;
+
+    private String platform;
+    private String version;
+    private UUID instanceId = null;
+
+    @PostConstruct
+    private void init() {
+        updateMessage = new UpdateMessage("", false);
+        if (updatesEnabled) {
+            try {
+                platform = System.getProperty("platform", "unknown");
+                version = getClass().getPackage().getImplementationVersion();
+                if (version == null) {
+                    version = "unknown";
+                }
+                Path instanceIdPath = Paths.get(INSTANCE_ID_FILE);
+                if (Files.exists(instanceIdPath)) {
+                    byte[] data = Files.readAllBytes(instanceIdPath);
+                    if (data != null && data.length > 0) {
+                        try {
+                            instanceId = UUID.fromString(new String(data));
+                        } catch (IllegalArgumentException e) {
+                        }
+                    }
+                }
+                if (instanceId == null) {
+                    instanceId = UUID.randomUUID();
+                    Files.write(instanceIdPath, instanceId.toString().getBytes());
+                }
+                checkUpdatesFuture = scheduler.scheduleAtFixedRate(checkUpdatesRunnable, 0, 1, TimeUnit.HOURS);
+            } catch (Exception e) {}
+        }
+    }
+
+    @PreDestroy
+    private void destroy() {
+        try {
+            if (checkUpdatesFuture != null) {
+                checkUpdatesFuture.cancel(true);
+            }
+            scheduler.shutdownNow();
+        } catch (Exception e) {}
+    }
+
+    Runnable checkUpdatesRunnable = new Runnable() {
+        @Override
+        public void run() {
+            try {
+                log.trace("Executing check update method for instanceId [{}], platform [{}] and version [{}]", instanceId, platform, version);
+                ObjectNode request = new ObjectMapper().createObjectNode();
+                request.put(PLATFORM_PARAM, platform);
+                request.put(VERSION_PARAM, version);
+                request.put(INSTANCE_ID_PARAM, instanceId.toString());
+                JsonNode response = restClient.postForObject(UPDATE_SERVER_BASE_URL+"/api/thingsboard/updates", request, JsonNode.class);
+                updateMessage = new UpdateMessage(
+                        response.get("message").asText(),
+                        response.get("updateAvailable").asBoolean()
+                );
+            } catch (Exception e) {
+                log.trace(e.getMessage());
+            }
+        }
+    };
+
+    @Override
+    public UpdateMessage checkUpdates() {
+        return updateMessage;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/update/model/UpdateMessage.java b/application/src/main/java/org/thingsboard/server/service/update/model/UpdateMessage.java
new file mode 100644
index 0000000..bbb8511
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/update/model/UpdateMessage.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * 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.thingsboard.server.service.update.model;
+
+import lombok.Data;
+
+@Data
+public class UpdateMessage {
+
+    private final String message;
+    private final boolean isUpdateAvailable;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/update/UpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/UpdateService.java
new file mode 100644
index 0000000..18bfb2f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/update/UpdateService.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016-2017 The Thingsboard Authors
+ *
+ * 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.thingsboard.server.service.update;
+
+import org.thingsboard.server.service.update.model.UpdateMessage;
+
+public interface UpdateService {
+
+    UpdateMessage checkUpdates();
+
+}
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 42b37db..778406a 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -184,3 +184,7 @@ cache:
       policy: "${CACHE_DEVICE_CREDENTIAL_MAX_SIZE_POLICY:PER_NODE}"
       size: "${CACHE_DEVICE_CREDENTIAL_MAX_SIZE_SIZE:1000000}"
 
+# Check new version updates parameters
+updates:
+  # Enable/disable updates checking.
+  enabled: "${UPDATES_ENABLED:true}"
diff --git a/application/src/main/scripts/windows/service.xml b/application/src/main/scripts/windows/service.xml
index b2acc45..becbcdc 100644
--- a/application/src/main/scripts/windows/service.xml
+++ b/application/src/main/scripts/windows/service.xml
@@ -7,6 +7,7 @@
     <logmode>rotate</logmode>
     <env name="LOADER_PATH" value="%BASE%\conf,%BASE%\extensions" />
     <executable>java</executable>
+    <startargument>-Dplatform=windows</startargument>
     <startargument>-jar</startargument>
     <startargument>%BASE%\lib\${pkg.name}.jar</startargument>
 </service>

ui/package.json 1(+1 -0)

diff --git a/ui/package.json b/ui/package.json
index ed4b5b1..a36bb09 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -64,6 +64,7 @@
     "ngreact": "^0.3.0",
     "objectpath": "^1.2.1",
     "oclazyload": "^1.0.9",
+    "raphael": "^2.2.7",
     "rc-select": "^6.6.1",
     "react": "^15.4.1",
     "react-ace": "^4.1.0",
diff --git a/ui/src/app/api/admin.service.js b/ui/src/app/api/admin.service.js
index cd41966..1591397 100644
--- a/ui/src/app/api/admin.service.js
+++ b/ui/src/app/api/admin.service.js
@@ -23,7 +23,8 @@ function AdminService($http, $q) {
     var service = {
         getAdminSettings: getAdminSettings,
         saveAdminSettings: saveAdminSettings,
-        sendTestMail: sendTestMail
+        sendTestMail: sendTestMail,
+        checkUpdates: checkUpdates
     }
 
     return service;
@@ -60,4 +61,15 @@ function AdminService($http, $q) {
         });
         return deferred.promise;
     }
+
+    function checkUpdates() {
+        var deferred = $q.defer();
+        var url = '/api/admin/updates';
+        $http.get(url, null).then(function success(response) {
+            deferred.resolve(response.data);
+        }, function fail() {
+            deferred.reject();
+        });
+        return deferred.promise;
+    }
 }
diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js
index ea86cd6..516d823 100644
--- a/ui/src/app/api/user.service.js
+++ b/ui/src/app/api/user.service.js
@@ -22,7 +22,7 @@ export default angular.module('thingsboard.api.user', [thingsboardApiLogin,
     .name;
 
 /*@ngInject*/
-function UserService($http, $q, $rootScope, store, jwtHelper, $translate, $state) {
+function UserService($http, $q, $rootScope, adminService, toast, store, jwtHelper, $translate, $state) {
     var currentUser = null,
         currentUserDetails = null,
         userLoaded = false;
@@ -382,6 +382,14 @@ function UserService($http, $q, $rootScope, store, jwtHelper, $translate, $state
                     place = 'home.dashboards.dashboard';
                     params = {dashboardId: currentUserDetails.additionalInfo.defaultDashboardId};
                 }
+            } else if (currentUser.authority === 'SYS_ADMIN') {
+                adminService.checkUpdates().then(
+                    function (updateMessage) {
+                        if (updateMessage && updateMessage.updateAvailable) {
+                            toast.showInfo(updateMessage.message, 0, null, 'bottom right');
+                        }
+                    }
+                );
             }
             $state.go(place, params);
         } else {
diff --git a/ui/src/app/services/info-toast.tpl.html b/ui/src/app/services/info-toast.tpl.html
new file mode 100644
index 0000000..7a1f29d
--- /dev/null
+++ b/ui/src/app/services/info-toast.tpl.html
@@ -0,0 +1,25 @@
+<!--
+
+    Copyright © 2016-2017 The Thingsboard Authors
+
+    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.
+
+-->
+<md-toast class="tb-info-toast">
+    <div class="md-toast-content">
+        <div class="md-toast-text" ng-bind-html="vm.message"></div>
+        <md-button class="md-action md-highlight md-accent" ng-click="vm.closeToast()">
+            {{ 'action.close' | translate }}
+        </md-button>
+    </div>
+</md-toast>
diff --git a/ui/src/app/services/toast.scss b/ui/src/app/services/toast.scss
index b1740f1..5730d88 100644
--- a/ui/src/app/services/toast.scss
+++ b/ui/src/app/services/toast.scss
@@ -13,6 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+md-toast.tb-info-toast .md-toast-content {
+  font-size: 18px;
+  padding: 18px;
+  height: 100%;
+}
+
 md-toast.tb-success-toast .md-toast-content {
   font-size: 18px !important;
   background-color: green;
diff --git a/ui/src/app/services/toast.service.js b/ui/src/app/services/toast.service.js
index d9cd7b0..b18abda 100644
--- a/ui/src/app/services/toast.service.js
+++ b/ui/src/app/services/toast.service.js
@@ -15,6 +15,7 @@
  */
 /* eslint-disable import/no-unresolved, import/default */
 
+import infoToast from './info-toast.tpl.html';
 import successToast from './success-toast.tpl.html';
 import errorToast from './error-toast.tpl.html';
 
@@ -26,6 +27,7 @@ export default function Toast($mdToast, $document) {
     var showing = false;
 
     var service = {
+        showInfo: showInfo,
         showSuccess: showSuccess,
         showError: showError,
         hide: hide
@@ -33,7 +35,15 @@ export default function Toast($mdToast, $document) {
 
     return service;
 
+    function showInfo(infoMessage, delay, toastParent, position) {
+        showMessage(infoToast, infoMessage, delay, toastParent, position);
+    }
+
     function showSuccess(successMessage, delay, toastParent, position) {
+        showMessage(successToast, successMessage, delay, toastParent, position);
+    }
+
+    function showMessage(templateUrl, message, delay, toastParent, position) {
         if (!toastParent) {
             toastParent = angular.element($document[0].getElementById('toast-parent'));
         }
@@ -45,8 +55,8 @@ export default function Toast($mdToast, $document) {
             position: position,
             controller: 'ToastController',
             controllerAs: 'vm',
-            templateUrl: successToast,
-            locals: {message: successMessage},
+            templateUrl: templateUrl,
+            locals: {message: message},
             parent: toastParent
         });
     }