Commit 6b63d336dd58c3707ae7a704cc5ba86d8c056aee

Authored by Vladyslav_Prykhodko
2 parents ce1a3ea5 ecb2efda

Merge remote-tracking branch 'upstream/master' into improvement/ota-updates/select

Showing 80 changed files with 1450 additions and 432 deletions
@@ -67,6 +67,7 @@ CREATE TABLE IF NOT EXISTS ota_package ( @@ -67,6 +67,7 @@ CREATE TABLE IF NOT EXISTS ota_package (
67 type varchar(32) NOT NULL, 67 type varchar(32) NOT NULL,
68 title varchar(255) NOT NULL, 68 title varchar(255) NOT NULL,
69 version varchar(255) NOT NULL, 69 version varchar(255) NOT NULL,
  70 + url varchar(255),
70 file_name varchar(255), 71 file_name varchar(255),
71 content_type varchar(255), 72 content_type varchar(255),
72 checksum_algorithm varchar(32), 73 checksum_algorithm varchar(32),
@@ -60,7 +60,9 @@ import org.thingsboard.server.dao.edge.EdgeService; @@ -60,7 +60,9 @@ import org.thingsboard.server.dao.edge.EdgeService;
60 import org.thingsboard.server.dao.entityview.EntityViewService; 60 import org.thingsboard.server.dao.entityview.EntityViewService;
61 import org.thingsboard.server.dao.event.EventService; 61 import org.thingsboard.server.dao.event.EventService;
62 import org.thingsboard.server.dao.nosql.CassandraBufferedRateExecutor; 62 import org.thingsboard.server.dao.nosql.CassandraBufferedRateExecutor;
  63 +import org.thingsboard.server.dao.ota.OtaPackageService;
63 import org.thingsboard.server.dao.relation.RelationService; 64 import org.thingsboard.server.dao.relation.RelationService;
  65 +import org.thingsboard.server.dao.resource.ResourceService;
64 import org.thingsboard.server.dao.rule.RuleChainService; 66 import org.thingsboard.server.dao.rule.RuleChainService;
65 import org.thingsboard.server.dao.rule.RuleNodeStateService; 67 import org.thingsboard.server.dao.rule.RuleNodeStateService;
66 import org.thingsboard.server.dao.tenant.TenantProfileService; 68 import org.thingsboard.server.dao.tenant.TenantProfileService;
@@ -311,6 +313,14 @@ public class ActorSystemContext { @@ -311,6 +313,14 @@ public class ActorSystemContext {
311 @Autowired(required = false) 313 @Autowired(required = false)
312 @Getter private EdgeRpcService edgeRpcService; 314 @Getter private EdgeRpcService edgeRpcService;
313 315
  316 + @Lazy
  317 + @Autowired(required = false)
  318 + @Getter private ResourceService resourceService;
  319 +
  320 + @Lazy
  321 + @Autowired(required = false)
  322 + @Getter private OtaPackageService otaPackageService;
  323 +
314 @Value("${actors.session.max_concurrent_sessions_per_device:1}") 324 @Value("${actors.session.max_concurrent_sessions_per_device:1}")
315 @Getter 325 @Getter
316 private long maxConcurrentSessionsPerDevice; 326 private long maxConcurrentSessionsPerDevice;
@@ -69,7 +69,9 @@ import org.thingsboard.server.dao.edge.EdgeService; @@ -69,7 +69,9 @@ import org.thingsboard.server.dao.edge.EdgeService;
69 import org.thingsboard.server.dao.entityview.EntityViewService; 69 import org.thingsboard.server.dao.entityview.EntityViewService;
70 import org.thingsboard.server.dao.nosql.CassandraStatementTask; 70 import org.thingsboard.server.dao.nosql.CassandraStatementTask;
71 import org.thingsboard.server.dao.nosql.TbResultSetFuture; 71 import org.thingsboard.server.dao.nosql.TbResultSetFuture;
  72 +import org.thingsboard.server.dao.ota.OtaPackageService;
72 import org.thingsboard.server.dao.relation.RelationService; 73 import org.thingsboard.server.dao.relation.RelationService;
  74 +import org.thingsboard.server.dao.resource.ResourceService;
73 import org.thingsboard.server.dao.rule.RuleChainService; 75 import org.thingsboard.server.dao.rule.RuleChainService;
74 import org.thingsboard.server.dao.tenant.TenantService; 76 import org.thingsboard.server.dao.tenant.TenantService;
75 import org.thingsboard.server.dao.timeseries.TimeseriesService; 77 import org.thingsboard.server.dao.timeseries.TimeseriesService;
@@ -487,6 +489,16 @@ class DefaultTbContext implements TbContext { @@ -487,6 +489,16 @@ class DefaultTbContext implements TbContext {
487 } 489 }
488 490
489 @Override 491 @Override
  492 + public ResourceService getResourceService() {
  493 + return mainCtx.getResourceService();
  494 + }
  495 +
  496 + @Override
  497 + public OtaPackageService getOtaPackageService() {
  498 + return mainCtx.getOtaPackageService();
  499 + }
  500 +
  501 + @Override
490 public RuleEngineDeviceProfileCache getDeviceProfileCache() { 502 public RuleEngineDeviceProfileCache getDeviceProfileCache() {
491 return mainCtx.getDeviceProfileCache(); 503 return mainCtx.getDeviceProfileCache();
492 } 504 }
@@ -110,8 +110,11 @@ public class AlarmController extends BaseController { @@ -110,8 +110,11 @@ public class AlarmController extends BaseController {
110 checkParameter(ALARM_ID, strAlarmId); 110 checkParameter(ALARM_ID, strAlarmId);
111 try { 111 try {
112 AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); 112 AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
113 - checkAlarmId(alarmId, Operation.WRITE); 113 + Alarm alarm = checkAlarmId(alarmId, Operation.WRITE);
114 114
  115 + logEntityAction(alarm.getOriginator(), alarm,
  116 + getCurrentUser().getCustomerId(),
  117 + ActionType.ALARM_DELETE, null);
115 sendEntityNotificationMsg(getTenantId(), alarmId, EdgeEventActionType.DELETED); 118 sendEntityNotificationMsg(getTenantId(), alarmId, EdgeEventActionType.DELETED);
116 119
117 return alarmService.deleteAlarm(getTenantId(), alarmId); 120 return alarmService.deleteAlarm(getTenantId(), alarmId);
@@ -17,7 +17,6 @@ package org.thingsboard.server.controller; @@ -17,7 +17,6 @@ package org.thingsboard.server.controller;
17 17
18 import com.fasterxml.jackson.core.JsonProcessingException; 18 import com.fasterxml.jackson.core.JsonProcessingException;
19 import com.fasterxml.jackson.databind.ObjectMapper; 19 import com.fasterxml.jackson.databind.ObjectMapper;
20 -import com.fasterxml.jackson.databind.node.ArrayNode;  
21 import com.fasterxml.jackson.databind.node.ObjectNode; 20 import com.fasterxml.jackson.databind.node.ObjectNode;
22 import lombok.Getter; 21 import lombok.Getter;
23 import lombok.extern.slf4j.Slf4j; 22 import lombok.extern.slf4j.Slf4j;
@@ -31,7 +30,6 @@ import org.springframework.web.bind.annotation.ExceptionHandler; @@ -31,7 +30,6 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
31 import org.thingsboard.server.common.data.Customer; 30 import org.thingsboard.server.common.data.Customer;
32 import org.thingsboard.server.common.data.Dashboard; 31 import org.thingsboard.server.common.data.Dashboard;
33 import org.thingsboard.server.common.data.DashboardInfo; 32 import org.thingsboard.server.common.data.DashboardInfo;
34 -import org.thingsboard.server.common.data.DataConstants;  
35 import org.thingsboard.server.common.data.Device; 33 import org.thingsboard.server.common.data.Device;
36 import org.thingsboard.server.common.data.DeviceInfo; 34 import org.thingsboard.server.common.data.DeviceInfo;
37 import org.thingsboard.server.common.data.DeviceProfile; 35 import org.thingsboard.server.common.data.DeviceProfile;
@@ -79,10 +77,6 @@ import org.thingsboard.server.common.data.id.TenantProfileId; @@ -79,10 +77,6 @@ import org.thingsboard.server.common.data.id.TenantProfileId;
79 import org.thingsboard.server.common.data.id.UserId; 77 import org.thingsboard.server.common.data.id.UserId;
80 import org.thingsboard.server.common.data.id.WidgetTypeId; 78 import org.thingsboard.server.common.data.id.WidgetTypeId;
81 import org.thingsboard.server.common.data.id.WidgetsBundleId; 79 import org.thingsboard.server.common.data.id.WidgetsBundleId;
82 -import org.thingsboard.server.common.data.kv.AttributeKvEntry;  
83 -import org.thingsboard.server.common.data.kv.DataType;  
84 -import org.thingsboard.server.common.data.kv.KvEntry;  
85 -import org.thingsboard.server.common.data.kv.TsKvEntry;  
86 import org.thingsboard.server.common.data.page.PageLink; 80 import org.thingsboard.server.common.data.page.PageLink;
87 import org.thingsboard.server.common.data.page.SortOrder; 81 import org.thingsboard.server.common.data.page.SortOrder;
88 import org.thingsboard.server.common.data.page.TimePageLink; 82 import org.thingsboard.server.common.data.page.TimePageLink;
@@ -94,9 +88,6 @@ import org.thingsboard.server.common.data.rule.RuleChainType; @@ -94,9 +88,6 @@ import org.thingsboard.server.common.data.rule.RuleChainType;
94 import org.thingsboard.server.common.data.rule.RuleNode; 88 import org.thingsboard.server.common.data.rule.RuleNode;
95 import org.thingsboard.server.common.data.widget.WidgetTypeDetails; 89 import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
96 import org.thingsboard.server.common.data.widget.WidgetsBundle; 90 import org.thingsboard.server.common.data.widget.WidgetsBundle;
97 -import org.thingsboard.server.common.msg.TbMsg;  
98 -import org.thingsboard.server.common.msg.TbMsgDataType;  
99 -import org.thingsboard.server.common.msg.TbMsgMetaData;  
100 import org.thingsboard.server.dao.asset.AssetService; 91 import org.thingsboard.server.dao.asset.AssetService;
101 import org.thingsboard.server.dao.attributes.AttributesService; 92 import org.thingsboard.server.dao.attributes.AttributesService;
102 import org.thingsboard.server.dao.audit.AuditLogService; 93 import org.thingsboard.server.dao.audit.AuditLogService;
@@ -127,6 +118,7 @@ import org.thingsboard.server.gen.transport.TransportProtos; @@ -127,6 +118,7 @@ import org.thingsboard.server.gen.transport.TransportProtos;
127 import org.thingsboard.server.queue.discovery.PartitionService; 118 import org.thingsboard.server.queue.discovery.PartitionService;
128 import org.thingsboard.server.queue.provider.TbQueueProducerProvider; 119 import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
129 import org.thingsboard.server.queue.util.TbCoreComponent; 120 import org.thingsboard.server.queue.util.TbCoreComponent;
  121 +import org.thingsboard.server.service.action.RuleEngineEntityActionService;
130 import org.thingsboard.server.service.component.ComponentDiscoveryService; 122 import org.thingsboard.server.service.component.ComponentDiscoveryService;
131 import org.thingsboard.server.service.ota.OtaPackageStateService; 123 import org.thingsboard.server.service.ota.OtaPackageStateService;
132 import org.thingsboard.server.service.edge.EdgeNotificationService; 124 import org.thingsboard.server.service.edge.EdgeNotificationService;
@@ -147,11 +139,9 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; @@ -147,11 +139,9 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
147 import javax.mail.MessagingException; 139 import javax.mail.MessagingException;
148 import javax.servlet.http.HttpServletResponse; 140 import javax.servlet.http.HttpServletResponse;
149 import java.util.List; 141 import java.util.List;
150 -import java.util.Map;  
151 import java.util.Optional; 142 import java.util.Optional;
152 import java.util.Set; 143 import java.util.Set;
153 import java.util.UUID; 144 import java.util.UUID;
154 -import java.util.stream.Collectors;  
155 145
156 import static org.thingsboard.server.dao.service.Validator.validateId; 146 import static org.thingsboard.server.dao.service.Validator.validateId;
157 147
@@ -279,6 +269,9 @@ public abstract class BaseController { @@ -279,6 +269,9 @@ public abstract class BaseController {
279 @Autowired(required = false) 269 @Autowired(required = false)
280 protected EdgeGrpcService edgeGrpcService; 270 protected EdgeGrpcService edgeGrpcService;
281 271
  272 + @Autowired
  273 + protected RuleEngineEntityActionService ruleEngineEntityActionService;
  274 +
282 @Value("${server.log_controller_error_stack_trace}") 275 @Value("${server.log_controller_error_stack_trace}")
283 @Getter 276 @Getter
284 private boolean logControllerErrorStackTrace; 277 private boolean logControllerErrorStackTrace;
@@ -809,7 +802,7 @@ public abstract class BaseController { @@ -809,7 +802,7 @@ public abstract class BaseController {
809 customerId = user.getCustomerId(); 802 customerId = user.getCustomerId();
810 } 803 }
811 if (e == null) { 804 if (e == null) {
812 - pushEntityActionToRuleEngine(entityId, entity, user, customerId, actionType, additionalInfo); 805 + ruleEngineEntityActionService.pushEntityActionToRuleEngine(entityId, entity, user.getTenantId(), customerId, actionType, user, additionalInfo);
813 } 806 }
814 auditLogService.logEntityAction(user.getTenantId(), customerId, user.getId(), user.getName(), entityId, entity, actionType, e, additionalInfo); 807 auditLogService.logEntityAction(user.getTenantId(), customerId, user.getId(), user.getName(), entityId, entity, actionType, e, additionalInfo);
815 } 808 }
@@ -819,184 +812,6 @@ public abstract class BaseController { @@ -819,184 +812,6 @@ public abstract class BaseController {
819 return error != null ? (Exception.class.isInstance(error) ? (Exception) error : new Exception(error)) : null; 812 return error != null ? (Exception.class.isInstance(error) ? (Exception) error : new Exception(error)) : null;
820 } 813 }
821 814
822 - private <E extends HasName, I extends EntityId> void pushEntityActionToRuleEngine(I entityId, E entity, User user, CustomerId customerId,  
823 - ActionType actionType, Object... additionalInfo) {  
824 - String msgType = null;  
825 - switch (actionType) {  
826 - case ADDED:  
827 - msgType = DataConstants.ENTITY_CREATED;  
828 - break;  
829 - case DELETED:  
830 - msgType = DataConstants.ENTITY_DELETED;  
831 - break;  
832 - case UPDATED:  
833 - msgType = DataConstants.ENTITY_UPDATED;  
834 - break;  
835 - case ASSIGNED_TO_CUSTOMER:  
836 - msgType = DataConstants.ENTITY_ASSIGNED;  
837 - break;  
838 - case UNASSIGNED_FROM_CUSTOMER:  
839 - msgType = DataConstants.ENTITY_UNASSIGNED;  
840 - break;  
841 - case ATTRIBUTES_UPDATED:  
842 - msgType = DataConstants.ATTRIBUTES_UPDATED;  
843 - break;  
844 - case ATTRIBUTES_DELETED:  
845 - msgType = DataConstants.ATTRIBUTES_DELETED;  
846 - break;  
847 - case ALARM_ACK:  
848 - msgType = DataConstants.ALARM_ACK;  
849 - break;  
850 - case ALARM_CLEAR:  
851 - msgType = DataConstants.ALARM_CLEAR;  
852 - break;  
853 - case ASSIGNED_FROM_TENANT:  
854 - msgType = DataConstants.ENTITY_ASSIGNED_FROM_TENANT;  
855 - break;  
856 - case ASSIGNED_TO_TENANT:  
857 - msgType = DataConstants.ENTITY_ASSIGNED_TO_TENANT;  
858 - break;  
859 - case PROVISION_SUCCESS:  
860 - msgType = DataConstants.PROVISION_SUCCESS;  
861 - break;  
862 - case PROVISION_FAILURE:  
863 - msgType = DataConstants.PROVISION_FAILURE;  
864 - break;  
865 - case TIMESERIES_UPDATED:  
866 - msgType = DataConstants.TIMESERIES_UPDATED;  
867 - break;  
868 - case TIMESERIES_DELETED:  
869 - msgType = DataConstants.TIMESERIES_DELETED;  
870 - break;  
871 - case ASSIGNED_TO_EDGE:  
872 - msgType = DataConstants.ENTITY_ASSIGNED_TO_EDGE;  
873 - break;  
874 - case UNASSIGNED_FROM_EDGE:  
875 - msgType = DataConstants.ENTITY_UNASSIGNED_FROM_EDGE;  
876 - break;  
877 - }  
878 - if (!StringUtils.isEmpty(msgType)) {  
879 - try {  
880 - TbMsgMetaData metaData = new TbMsgMetaData();  
881 - metaData.putValue("userId", user.getId().toString());  
882 - metaData.putValue("userName", user.getName());  
883 - if (customerId != null && !customerId.isNullUid()) {  
884 - metaData.putValue("customerId", customerId.toString());  
885 - }  
886 - if (actionType == ActionType.ASSIGNED_TO_CUSTOMER) {  
887 - String strCustomerId = extractParameter(String.class, 1, additionalInfo);  
888 - String strCustomerName = extractParameter(String.class, 2, additionalInfo);  
889 - metaData.putValue("assignedCustomerId", strCustomerId);  
890 - metaData.putValue("assignedCustomerName", strCustomerName);  
891 - } else if (actionType == ActionType.UNASSIGNED_FROM_CUSTOMER) {  
892 - String strCustomerId = extractParameter(String.class, 1, additionalInfo);  
893 - String strCustomerName = extractParameter(String.class, 2, additionalInfo);  
894 - metaData.putValue("unassignedCustomerId", strCustomerId);  
895 - metaData.putValue("unassignedCustomerName", strCustomerName);  
896 - } else if (actionType == ActionType.ASSIGNED_FROM_TENANT) {  
897 - String strTenantId = extractParameter(String.class, 0, additionalInfo);  
898 - String strTenantName = extractParameter(String.class, 1, additionalInfo);  
899 - metaData.putValue("assignedFromTenantId", strTenantId);  
900 - metaData.putValue("assignedFromTenantName", strTenantName);  
901 - } else if (actionType == ActionType.ASSIGNED_TO_TENANT) {  
902 - String strTenantId = extractParameter(String.class, 0, additionalInfo);  
903 - String strTenantName = extractParameter(String.class, 1, additionalInfo);  
904 - metaData.putValue("assignedToTenantId", strTenantId);  
905 - metaData.putValue("assignedToTenantName", strTenantName);  
906 - } else if (actionType == ActionType.ASSIGNED_TO_EDGE) {  
907 - String strEdgeId = extractParameter(String.class, 1, additionalInfo);  
908 - String strEdgeName = extractParameter(String.class, 2, additionalInfo);  
909 - metaData.putValue("assignedEdgeId", strEdgeId);  
910 - metaData.putValue("assignedEdgeName", strEdgeName);  
911 - } else if (actionType == ActionType.UNASSIGNED_FROM_EDGE) {  
912 - String strEdgeId = extractParameter(String.class, 1, additionalInfo);  
913 - String strEdgeName = extractParameter(String.class, 2, additionalInfo);  
914 - metaData.putValue("unassignedEdgeId", strEdgeId);  
915 - metaData.putValue("unassignedEdgeName", strEdgeName);  
916 - }  
917 - ObjectNode entityNode;  
918 - if (entity != null) {  
919 - entityNode = json.valueToTree(entity);  
920 - if (entityId.getEntityType() == EntityType.DASHBOARD) {  
921 - entityNode.put("configuration", "");  
922 - }  
923 - } else {  
924 - entityNode = json.createObjectNode();  
925 - if (actionType == ActionType.ATTRIBUTES_UPDATED) {  
926 - String scope = extractParameter(String.class, 0, additionalInfo);  
927 - @SuppressWarnings("unchecked")  
928 - List<AttributeKvEntry> attributes = extractParameter(List.class, 1, additionalInfo);  
929 - metaData.putValue(DataConstants.SCOPE, scope);  
930 - if (attributes != null) {  
931 - for (AttributeKvEntry attr : attributes) {  
932 - addKvEntry(entityNode, attr);  
933 - }  
934 - }  
935 - } else if (actionType == ActionType.ATTRIBUTES_DELETED) {  
936 - String scope = extractParameter(String.class, 0, additionalInfo);  
937 - @SuppressWarnings("unchecked")  
938 - List<String> keys = extractParameter(List.class, 1, additionalInfo);  
939 - metaData.putValue(DataConstants.SCOPE, scope);  
940 - ArrayNode attrsArrayNode = entityNode.putArray("attributes");  
941 - if (keys != null) {  
942 - keys.forEach(attrsArrayNode::add);  
943 - }  
944 - } else if (actionType == ActionType.TIMESERIES_UPDATED) {  
945 - @SuppressWarnings("unchecked")  
946 - List<TsKvEntry> timeseries = extractParameter(List.class, 0, additionalInfo);  
947 - addTimeseries(entityNode, timeseries);  
948 - } else if (actionType == ActionType.TIMESERIES_DELETED) {  
949 - @SuppressWarnings("unchecked")  
950 - List<String> keys = extractParameter(List.class, 0, additionalInfo);  
951 - if (keys != null) {  
952 - ArrayNode timeseriesArrayNode = entityNode.putArray("timeseries");  
953 - keys.forEach(timeseriesArrayNode::add);  
954 - }  
955 - entityNode.put("startTs", extractParameter(Long.class, 1, additionalInfo));  
956 - entityNode.put("endTs", extractParameter(Long.class, 2, additionalInfo));  
957 - }  
958 - }  
959 - TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, json.writeValueAsString(entityNode));  
960 - TenantId tenantId = user.getTenantId();  
961 - if (tenantId.isNullUid()) {  
962 - if (entity instanceof HasTenantId) {  
963 - tenantId = ((HasTenantId) entity).getTenantId();  
964 - }  
965 - }  
966 - tbClusterService.pushMsgToRuleEngine(tenantId, entityId, tbMsg, null);  
967 - } catch (Exception e) {  
968 - log.warn("[{}] Failed to push entity action to rule engine: {}", entityId, actionType, e);  
969 - }  
970 - }  
971 - }  
972 -  
973 - private void addKvEntry(ObjectNode entityNode, KvEntry kvEntry) throws Exception {  
974 - if (kvEntry.getDataType() == DataType.BOOLEAN) {  
975 - kvEntry.getBooleanValue().ifPresent(value -> entityNode.put(kvEntry.getKey(), value));  
976 - } else if (kvEntry.getDataType() == DataType.DOUBLE) {  
977 - kvEntry.getDoubleValue().ifPresent(value -> entityNode.put(kvEntry.getKey(), value));  
978 - } else if (kvEntry.getDataType() == DataType.LONG) {  
979 - kvEntry.getLongValue().ifPresent(value -> entityNode.put(kvEntry.getKey(), value));  
980 - } else if (kvEntry.getDataType() == DataType.JSON) {  
981 - if (kvEntry.getJsonValue().isPresent()) {  
982 - entityNode.set(kvEntry.getKey(), json.readTree(kvEntry.getJsonValue().get()));  
983 - }  
984 - } else {  
985 - entityNode.put(kvEntry.getKey(), kvEntry.getValueAsString());  
986 - }  
987 - }  
988 -  
989 - private <T> T extractParameter(Class<T> clazz, int index, Object... additionalInfo) {  
990 - T result = null;  
991 - if (additionalInfo != null && additionalInfo.length > index) {  
992 - Object paramObject = additionalInfo[index];  
993 - if (clazz.isInstance(paramObject)) {  
994 - result = clazz.cast(paramObject);  
995 - }  
996 - }  
997 - return result;  
998 - }  
999 -  
1000 protected <E extends HasName> String entityToStr(E entity) { 815 protected <E extends HasName> String entityToStr(E entity) {
1001 try { 816 try {
1002 return json.writeValueAsString(json.valueToTree(entity)); 817 return json.writeValueAsString(json.valueToTree(entity));
@@ -1093,23 +908,6 @@ public abstract class BaseController { @@ -1093,23 +908,6 @@ public abstract class BaseController {
1093 return result; 908 return result;
1094 } 909 }
1095 910
1096 - private void addTimeseries(ObjectNode entityNode, List<TsKvEntry> timeseries) throws Exception {  
1097 - if (timeseries != null && !timeseries.isEmpty()) {  
1098 - ArrayNode result = entityNode.putArray("timeseries");  
1099 - Map<Long, List<TsKvEntry>> groupedTelemetry = timeseries.stream()  
1100 - .collect(Collectors.groupingBy(TsKvEntry::getTs));  
1101 - for (Map.Entry<Long, List<TsKvEntry>> entry : groupedTelemetry.entrySet()) {  
1102 - ObjectNode element = json.createObjectNode();  
1103 - element.put("ts", entry.getKey());  
1104 - ObjectNode values = element.putObject("values");  
1105 - for (TsKvEntry tsKvEntry : entry.getValue()) {  
1106 - addKvEntry(values, tsKvEntry);  
1107 - }  
1108 - result.add(element);  
1109 - }  
1110 - }  
1111 - }  
1112 -  
1113 protected void processDashboardIdFromAdditionalInfo(ObjectNode additionalInfo, String requiredFields) throws ThingsboardException { 911 protected void processDashboardIdFromAdditionalInfo(ObjectNode additionalInfo, String requiredFields) throws ThingsboardException {
1114 String dashboardId = additionalInfo.has(requiredFields) ? additionalInfo.get(requiredFields).asText() : null; 912 String dashboardId = additionalInfo.has(requiredFields) ? additionalInfo.get(requiredFields).asText() : null;
1115 if (dashboardId != null && !dashboardId.equals("null")) { 913 if (dashboardId != null && !dashboardId.equals("null")) {
@@ -64,6 +64,10 @@ public class OtaPackageController extends BaseController { @@ -64,6 +64,10 @@ public class OtaPackageController extends BaseController {
64 OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId)); 64 OtaPackageId otaPackageId = new OtaPackageId(toUUID(strOtaPackageId));
65 OtaPackage otaPackage = checkOtaPackageId(otaPackageId, Operation.READ); 65 OtaPackage otaPackage = checkOtaPackageId(otaPackageId, Operation.READ);
66 66
  67 + if (otaPackage.hasUrl()) {
  68 + return ResponseEntity.badRequest().build();
  69 + }
  70 +
67 ByteArrayResource resource = new ByteArrayResource(otaPackage.getData().array()); 71 ByteArrayResource resource = new ByteArrayResource(otaPackage.getData().array());
68 return ResponseEntity.ok() 72 return ResponseEntity.ok()
69 .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + otaPackage.getFileName()) 73 .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + otaPackage.getFileName())
@@ -182,11 +186,10 @@ public class OtaPackageController extends BaseController { @@ -182,11 +186,10 @@ public class OtaPackageController extends BaseController {
182 } 186 }
183 187
184 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") 188 @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
185 - @RequestMapping(value = "/otaPackages/{deviceProfileId}/{type}/{hasData}", method = RequestMethod.GET) 189 + @RequestMapping(value = "/otaPackages/{deviceProfileId}/{type}", method = RequestMethod.GET)
186 @ResponseBody 190 @ResponseBody
187 public PageData<OtaPackageInfo> getOtaPackages(@PathVariable("deviceProfileId") String strDeviceProfileId, 191 public PageData<OtaPackageInfo> getOtaPackages(@PathVariable("deviceProfileId") String strDeviceProfileId,
188 @PathVariable("type") String strType, 192 @PathVariable("type") String strType,
189 - @PathVariable("hasData") boolean hasData,  
190 @RequestParam int pageSize, 193 @RequestParam int pageSize,
191 @RequestParam int page, 194 @RequestParam int page,
192 @RequestParam(required = false) String textSearch, 195 @RequestParam(required = false) String textSearch,
@@ -197,7 +200,7 @@ public class OtaPackageController extends BaseController { @@ -197,7 +200,7 @@ public class OtaPackageController extends BaseController {
197 try { 200 try {
198 PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); 201 PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
199 return checkNotNull(otaPackageService.findTenantOtaPackagesByTenantIdAndDeviceProfileIdAndTypeAndHasData(getTenantId(), 202 return checkNotNull(otaPackageService.findTenantOtaPackagesByTenantIdAndDeviceProfileIdAndTypeAndHasData(getTenantId(),
200 - new DeviceProfileId(toUUID(strDeviceProfileId)), OtaPackageType.valueOf(strType), hasData, pageLink)); 203 + new DeviceProfileId(toUUID(strDeviceProfileId)), OtaPackageType.valueOf(strType), pageLink));
201 } catch (Exception e) { 204 } catch (Exception e) {
202 throw handleException(e); 205 throw handleException(e);
203 } 206 }
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.action;
  17 +
  18 +import com.fasterxml.jackson.databind.ObjectMapper;
  19 +import com.fasterxml.jackson.databind.node.ArrayNode;
  20 +import com.fasterxml.jackson.databind.node.ObjectNode;
  21 +import lombok.RequiredArgsConstructor;
  22 +import lombok.extern.slf4j.Slf4j;
  23 +import org.apache.commons.lang3.StringUtils;
  24 +import org.springframework.stereotype.Service;
  25 +import org.thingsboard.server.common.data.DataConstants;
  26 +import org.thingsboard.server.common.data.EntityType;
  27 +import org.thingsboard.server.common.data.HasName;
  28 +import org.thingsboard.server.common.data.HasTenantId;
  29 +import org.thingsboard.server.common.data.User;
  30 +import org.thingsboard.server.common.data.audit.ActionType;
  31 +import org.thingsboard.server.common.data.id.CustomerId;
  32 +import org.thingsboard.server.common.data.id.EntityId;
  33 +import org.thingsboard.server.common.data.id.TenantId;
  34 +import org.thingsboard.server.common.data.kv.AttributeKvEntry;
  35 +import org.thingsboard.server.common.data.kv.DataType;
  36 +import org.thingsboard.server.common.data.kv.KvEntry;
  37 +import org.thingsboard.server.common.data.kv.TsKvEntry;
  38 +import org.thingsboard.server.common.msg.TbMsg;
  39 +import org.thingsboard.server.common.msg.TbMsgDataType;
  40 +import org.thingsboard.server.common.msg.TbMsgMetaData;
  41 +import org.thingsboard.server.queue.util.TbCoreComponent;
  42 +import org.thingsboard.server.service.queue.TbClusterService;
  43 +
  44 +import java.util.List;
  45 +import java.util.Map;
  46 +import java.util.stream.Collectors;
  47 +
  48 +@TbCoreComponent
  49 +@Service
  50 +@RequiredArgsConstructor
  51 +@Slf4j
  52 +public class RuleEngineEntityActionService {
  53 + private final TbClusterService tbClusterService;
  54 +
  55 + private static final ObjectMapper json = new ObjectMapper();
  56 +
  57 + public void pushEntityActionToRuleEngine(EntityId entityId, HasName entity, TenantId tenantId, CustomerId customerId,
  58 + ActionType actionType, User user, Object... additionalInfo) {
  59 + String msgType = null;
  60 + switch (actionType) {
  61 + case ADDED:
  62 + msgType = DataConstants.ENTITY_CREATED;
  63 + break;
  64 + case DELETED:
  65 + msgType = DataConstants.ENTITY_DELETED;
  66 + break;
  67 + case UPDATED:
  68 + msgType = DataConstants.ENTITY_UPDATED;
  69 + break;
  70 + case ASSIGNED_TO_CUSTOMER:
  71 + msgType = DataConstants.ENTITY_ASSIGNED;
  72 + break;
  73 + case UNASSIGNED_FROM_CUSTOMER:
  74 + msgType = DataConstants.ENTITY_UNASSIGNED;
  75 + break;
  76 + case ATTRIBUTES_UPDATED:
  77 + msgType = DataConstants.ATTRIBUTES_UPDATED;
  78 + break;
  79 + case ATTRIBUTES_DELETED:
  80 + msgType = DataConstants.ATTRIBUTES_DELETED;
  81 + break;
  82 + case ALARM_ACK:
  83 + msgType = DataConstants.ALARM_ACK;
  84 + break;
  85 + case ALARM_CLEAR:
  86 + msgType = DataConstants.ALARM_CLEAR;
  87 + break;
  88 + case ALARM_DELETE:
  89 + msgType = DataConstants.ALARM_DELETE;
  90 + break;
  91 + case ASSIGNED_FROM_TENANT:
  92 + msgType = DataConstants.ENTITY_ASSIGNED_FROM_TENANT;
  93 + break;
  94 + case ASSIGNED_TO_TENANT:
  95 + msgType = DataConstants.ENTITY_ASSIGNED_TO_TENANT;
  96 + break;
  97 + case PROVISION_SUCCESS:
  98 + msgType = DataConstants.PROVISION_SUCCESS;
  99 + break;
  100 + case PROVISION_FAILURE:
  101 + msgType = DataConstants.PROVISION_FAILURE;
  102 + break;
  103 + case TIMESERIES_UPDATED:
  104 + msgType = DataConstants.TIMESERIES_UPDATED;
  105 + break;
  106 + case TIMESERIES_DELETED:
  107 + msgType = DataConstants.TIMESERIES_DELETED;
  108 + break;
  109 + case ASSIGNED_TO_EDGE:
  110 + msgType = DataConstants.ENTITY_ASSIGNED_TO_EDGE;
  111 + break;
  112 + case UNASSIGNED_FROM_EDGE:
  113 + msgType = DataConstants.ENTITY_UNASSIGNED_FROM_EDGE;
  114 + break;
  115 + }
  116 + if (!StringUtils.isEmpty(msgType)) {
  117 + try {
  118 + TbMsgMetaData metaData = new TbMsgMetaData();
  119 + if (user != null) {
  120 + metaData.putValue("userId", user.getId().toString());
  121 + metaData.putValue("userName", user.getName());
  122 + }
  123 + if (customerId != null && !customerId.isNullUid()) {
  124 + metaData.putValue("customerId", customerId.toString());
  125 + }
  126 + if (actionType == ActionType.ASSIGNED_TO_CUSTOMER) {
  127 + String strCustomerId = extractParameter(String.class, 1, additionalInfo);
  128 + String strCustomerName = extractParameter(String.class, 2, additionalInfo);
  129 + metaData.putValue("assignedCustomerId", strCustomerId);
  130 + metaData.putValue("assignedCustomerName", strCustomerName);
  131 + } else if (actionType == ActionType.UNASSIGNED_FROM_CUSTOMER) {
  132 + String strCustomerId = extractParameter(String.class, 1, additionalInfo);
  133 + String strCustomerName = extractParameter(String.class, 2, additionalInfo);
  134 + metaData.putValue("unassignedCustomerId", strCustomerId);
  135 + metaData.putValue("unassignedCustomerName", strCustomerName);
  136 + } else if (actionType == ActionType.ASSIGNED_FROM_TENANT) {
  137 + String strTenantId = extractParameter(String.class, 0, additionalInfo);
  138 + String strTenantName = extractParameter(String.class, 1, additionalInfo);
  139 + metaData.putValue("assignedFromTenantId", strTenantId);
  140 + metaData.putValue("assignedFromTenantName", strTenantName);
  141 + } else if (actionType == ActionType.ASSIGNED_TO_TENANT) {
  142 + String strTenantId = extractParameter(String.class, 0, additionalInfo);
  143 + String strTenantName = extractParameter(String.class, 1, additionalInfo);
  144 + metaData.putValue("assignedToTenantId", strTenantId);
  145 + metaData.putValue("assignedToTenantName", strTenantName);
  146 + } else if (actionType == ActionType.ASSIGNED_TO_EDGE) {
  147 + String strEdgeId = extractParameter(String.class, 1, additionalInfo);
  148 + String strEdgeName = extractParameter(String.class, 2, additionalInfo);
  149 + metaData.putValue("assignedEdgeId", strEdgeId);
  150 + metaData.putValue("assignedEdgeName", strEdgeName);
  151 + } else if (actionType == ActionType.UNASSIGNED_FROM_EDGE) {
  152 + String strEdgeId = extractParameter(String.class, 1, additionalInfo);
  153 + String strEdgeName = extractParameter(String.class, 2, additionalInfo);
  154 + metaData.putValue("unassignedEdgeId", strEdgeId);
  155 + metaData.putValue("unassignedEdgeName", strEdgeName);
  156 + }
  157 + ObjectNode entityNode;
  158 + if (entity != null) {
  159 + entityNode = json.valueToTree(entity);
  160 + if (entityId.getEntityType() == EntityType.DASHBOARD) {
  161 + entityNode.put("configuration", "");
  162 + }
  163 + } else {
  164 + entityNode = json.createObjectNode();
  165 + if (actionType == ActionType.ATTRIBUTES_UPDATED) {
  166 + String scope = extractParameter(String.class, 0, additionalInfo);
  167 + @SuppressWarnings("unchecked")
  168 + List<AttributeKvEntry> attributes = extractParameter(List.class, 1, additionalInfo);
  169 + metaData.putValue(DataConstants.SCOPE, scope);
  170 + if (attributes != null) {
  171 + for (AttributeKvEntry attr : attributes) {
  172 + addKvEntry(entityNode, attr);
  173 + }
  174 + }
  175 + } else if (actionType == ActionType.ATTRIBUTES_DELETED) {
  176 + String scope = extractParameter(String.class, 0, additionalInfo);
  177 + @SuppressWarnings("unchecked")
  178 + List<String> keys = extractParameter(List.class, 1, additionalInfo);
  179 + metaData.putValue(DataConstants.SCOPE, scope);
  180 + ArrayNode attrsArrayNode = entityNode.putArray("attributes");
  181 + if (keys != null) {
  182 + keys.forEach(attrsArrayNode::add);
  183 + }
  184 + } else if (actionType == ActionType.TIMESERIES_UPDATED) {
  185 + @SuppressWarnings("unchecked")
  186 + List<TsKvEntry> timeseries = extractParameter(List.class, 0, additionalInfo);
  187 + addTimeseries(entityNode, timeseries);
  188 + } else if (actionType == ActionType.TIMESERIES_DELETED) {
  189 + @SuppressWarnings("unchecked")
  190 + List<String> keys = extractParameter(List.class, 0, additionalInfo);
  191 + if (keys != null) {
  192 + ArrayNode timeseriesArrayNode = entityNode.putArray("timeseries");
  193 + keys.forEach(timeseriesArrayNode::add);
  194 + }
  195 + entityNode.put("startTs", extractParameter(Long.class, 1, additionalInfo));
  196 + entityNode.put("endTs", extractParameter(Long.class, 2, additionalInfo));
  197 + }
  198 + }
  199 + TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, json.writeValueAsString(entityNode));
  200 + if (tenantId.isNullUid()) {
  201 + if (entity instanceof HasTenantId) {
  202 + tenantId = ((HasTenantId) entity).getTenantId();
  203 + }
  204 + }
  205 + tbClusterService.pushMsgToRuleEngine(tenantId, entityId, tbMsg, null);
  206 + } catch (Exception e) {
  207 + log.warn("[{}] Failed to push entity action to rule engine: {}", entityId, actionType, e);
  208 + }
  209 + }
  210 + }
  211 +
  212 +
  213 + private <T> T extractParameter(Class<T> clazz, int index, Object... additionalInfo) {
  214 + T result = null;
  215 + if (additionalInfo != null && additionalInfo.length > index) {
  216 + Object paramObject = additionalInfo[index];
  217 + if (clazz.isInstance(paramObject)) {
  218 + result = clazz.cast(paramObject);
  219 + }
  220 + }
  221 + return result;
  222 + }
  223 +
  224 + private void addTimeseries(ObjectNode entityNode, List<TsKvEntry> timeseries) throws Exception {
  225 + if (timeseries != null && !timeseries.isEmpty()) {
  226 + ArrayNode result = entityNode.putArray("timeseries");
  227 + Map<Long, List<TsKvEntry>> groupedTelemetry = timeseries.stream()
  228 + .collect(Collectors.groupingBy(TsKvEntry::getTs));
  229 + for (Map.Entry<Long, List<TsKvEntry>> entry : groupedTelemetry.entrySet()) {
  230 + ObjectNode element = json.createObjectNode();
  231 + element.put("ts", entry.getKey());
  232 + ObjectNode values = element.putObject("values");
  233 + for (TsKvEntry tsKvEntry : entry.getValue()) {
  234 + addKvEntry(values, tsKvEntry);
  235 + }
  236 + result.add(element);
  237 + }
  238 + }
  239 + }
  240 +
  241 + private void addKvEntry(ObjectNode entityNode, KvEntry kvEntry) throws Exception {
  242 + if (kvEntry.getDataType() == DataType.BOOLEAN) {
  243 + kvEntry.getBooleanValue().ifPresent(value -> entityNode.put(kvEntry.getKey(), value));
  244 + } else if (kvEntry.getDataType() == DataType.DOUBLE) {
  245 + kvEntry.getDoubleValue().ifPresent(value -> entityNode.put(kvEntry.getKey(), value));
  246 + } else if (kvEntry.getDataType() == DataType.LONG) {
  247 + kvEntry.getLongValue().ifPresent(value -> entityNode.put(kvEntry.getKey(), value));
  248 + } else if (kvEntry.getDataType() == DataType.JSON) {
  249 + if (kvEntry.getJsonValue().isPresent()) {
  250 + entityNode.set(kvEntry.getKey(), json.readTree(kvEntry.getJsonValue().get()));
  251 + }
  252 + } else {
  253 + entityNode.put(kvEntry.getKey(), kvEntry.getValueAsString());
  254 + }
  255 + }
  256 +}
@@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.DataConstants; @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.DataConstants;
24 import org.thingsboard.server.common.data.Device; 24 import org.thingsboard.server.common.data.Device;
25 import org.thingsboard.server.common.data.DeviceProfile; 25 import org.thingsboard.server.common.data.DeviceProfile;
26 import org.thingsboard.server.common.data.OtaPackageInfo; 26 import org.thingsboard.server.common.data.OtaPackageInfo;
  27 +import org.thingsboard.server.common.data.StringUtils;
27 import org.thingsboard.server.common.data.id.DeviceId; 28 import org.thingsboard.server.common.data.id.DeviceId;
28 import org.thingsboard.server.common.data.id.OtaPackageId; 29 import org.thingsboard.server.common.data.id.OtaPackageId;
29 import org.thingsboard.server.common.data.id.TenantId; 30 import org.thingsboard.server.common.data.id.TenantId;
@@ -65,6 +66,7 @@ import static org.thingsboard.server.common.data.ota.OtaPackageKey.SIZE; @@ -65,6 +66,7 @@ import static org.thingsboard.server.common.data.ota.OtaPackageKey.SIZE;
65 import static org.thingsboard.server.common.data.ota.OtaPackageKey.STATE; 66 import static org.thingsboard.server.common.data.ota.OtaPackageKey.STATE;
66 import static org.thingsboard.server.common.data.ota.OtaPackageKey.TITLE; 67 import static org.thingsboard.server.common.data.ota.OtaPackageKey.TITLE;
67 import static org.thingsboard.server.common.data.ota.OtaPackageKey.TS; 68 import static org.thingsboard.server.common.data.ota.OtaPackageKey.TS;
  69 +import static org.thingsboard.server.common.data.ota.OtaPackageKey.URL;
68 import static org.thingsboard.server.common.data.ota.OtaPackageKey.VERSION; 70 import static org.thingsboard.server.common.data.ota.OtaPackageKey.VERSION;
69 import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; 71 import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE;
70 import static org.thingsboard.server.common.data.ota.OtaPackageType.SOFTWARE; 72 import static org.thingsboard.server.common.data.ota.OtaPackageType.SOFTWARE;
@@ -261,11 +263,12 @@ public class DefaultOtaPackageStateService implements OtaPackageStateService { @@ -261,11 +263,12 @@ public class DefaultOtaPackageStateService implements OtaPackageStateService {
261 } 263 }
262 264
263 265
264 - private void update(Device device, OtaPackageInfo firmware, long ts) { 266 + private void update(Device device, OtaPackageInfo otaPackage, long ts) {
265 TenantId tenantId = device.getTenantId(); 267 TenantId tenantId = device.getTenantId();
266 DeviceId deviceId = device.getId(); 268 DeviceId deviceId = device.getId();
  269 + OtaPackageType otaPackageType = otaPackage.getType();
267 270
268 - BasicTsKvEntry status = new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(getTelemetryKey(firmware.getType(), STATE), OtaPackageUpdateStatus.INITIATED.name())); 271 + BasicTsKvEntry status = new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(getTelemetryKey(otaPackageType, STATE), OtaPackageUpdateStatus.INITIATED.name()));
269 272
270 telemetryService.saveAndNotify(tenantId, deviceId, Collections.singletonList(status), new FutureCallback<>() { 273 telemetryService.saveAndNotify(tenantId, deviceId, Collections.singletonList(status), new FutureCallback<>() {
271 @Override 274 @Override
@@ -280,11 +283,37 @@ public class DefaultOtaPackageStateService implements OtaPackageStateService { @@ -280,11 +283,37 @@ public class DefaultOtaPackageStateService implements OtaPackageStateService {
280 }); 283 });
281 284
282 List<AttributeKvEntry> attributes = new ArrayList<>(); 285 List<AttributeKvEntry> attributes = new ArrayList<>();
283 - attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(firmware.getType(), TITLE), firmware.getTitle())));  
284 - attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(firmware.getType(), VERSION), firmware.getVersion())));  
285 - attributes.add(new BaseAttributeKvEntry(ts, new LongDataEntry(getAttributeKey(firmware.getType(), SIZE), firmware.getDataSize())));  
286 - attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(firmware.getType(), CHECKSUM_ALGORITHM), firmware.getChecksumAlgorithm().name())));  
287 - attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(firmware.getType(), CHECKSUM), firmware.getChecksum()))); 286 + attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(otaPackageType, TITLE), otaPackage.getTitle())));
  287 + attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(otaPackageType, VERSION), otaPackage.getVersion())));
  288 + if (otaPackage.hasUrl()) {
  289 + attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(otaPackageType, URL), otaPackage.getUrl())));
  290 + List<String> attrToRemove = new ArrayList<>();
  291 +
  292 + if (otaPackage.getDataSize() == null) {
  293 + attrToRemove.add(getAttributeKey(otaPackageType, SIZE));
  294 + } else {
  295 + attributes.add(new BaseAttributeKvEntry(ts, new LongDataEntry(getAttributeKey(otaPackageType, SIZE), otaPackage.getDataSize())));
  296 + }
  297 +
  298 + if (otaPackage.getChecksumAlgorithm() != null) {
  299 + attrToRemove.add(getAttributeKey(otaPackageType, CHECKSUM_ALGORITHM));
  300 + } else {
  301 + attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(otaPackageType, CHECKSUM_ALGORITHM), otaPackage.getChecksumAlgorithm().name())));
  302 + }
  303 +
  304 + if (StringUtils.isEmpty(otaPackage.getChecksum())) {
  305 + attrToRemove.add(getAttributeKey(otaPackageType, CHECKSUM));
  306 + } else {
  307 + attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(otaPackageType, CHECKSUM), otaPackage.getChecksum())));
  308 + }
  309 +
  310 + remove(device, otaPackageType, attrToRemove);
  311 + } else {
  312 + attributes.add(new BaseAttributeKvEntry(ts, new LongDataEntry(getAttributeKey(otaPackageType, SIZE), otaPackage.getDataSize())));
  313 + attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(otaPackageType, CHECKSUM_ALGORITHM), otaPackage.getChecksumAlgorithm().name())));
  314 + attributes.add(new BaseAttributeKvEntry(ts, new StringDataEntry(getAttributeKey(otaPackageType, CHECKSUM), otaPackage.getChecksum())));
  315 + remove(device, otaPackageType, Collections.singletonList(getAttributeKey(otaPackageType, URL)));
  316 + }
288 317
289 telemetryService.saveAndNotify(tenantId, deviceId, DataConstants.SHARED_SCOPE, attributes, new FutureCallback<>() { 318 telemetryService.saveAndNotify(tenantId, deviceId, DataConstants.SHARED_SCOPE, attributes, new FutureCallback<>() {
290 @Override 319 @Override
@@ -299,20 +328,24 @@ public class DefaultOtaPackageStateService implements OtaPackageStateService { @@ -299,20 +328,24 @@ public class DefaultOtaPackageStateService implements OtaPackageStateService {
299 }); 328 });
300 } 329 }
301 330
302 - private void remove(Device device, OtaPackageType firmwareType) {  
303 - telemetryService.deleteAndNotify(device.getTenantId(), device.getId(), DataConstants.SHARED_SCOPE, OtaPackageUtil.getAttributeKeys(firmwareType), 331 + private void remove(Device device, OtaPackageType otaPackageType) {
  332 + remove(device, otaPackageType, OtaPackageUtil.getAttributeKeys(otaPackageType));
  333 + }
  334 +
  335 + private void remove(Device device, OtaPackageType otaPackageType, List<String> attributesKeys) {
  336 + telemetryService.deleteAndNotify(device.getTenantId(), device.getId(), DataConstants.SHARED_SCOPE, attributesKeys,
304 new FutureCallback<>() { 337 new FutureCallback<>() {
305 @Override 338 @Override
306 public void onSuccess(@Nullable Void tmp) { 339 public void onSuccess(@Nullable Void tmp) {
307 - log.trace("[{}] Success remove target firmware attributes!", device.getId()); 340 + log.trace("[{}] Success remove target {} attributes!", device.getId(), otaPackageType);
308 Set<AttributeKey> keysToNotify = new HashSet<>(); 341 Set<AttributeKey> keysToNotify = new HashSet<>();
309 - OtaPackageUtil.ALL_FW_ATTRIBUTE_KEYS.forEach(key -> keysToNotify.add(new AttributeKey(DataConstants.SHARED_SCOPE, key))); 342 + attributesKeys.forEach(key -> keysToNotify.add(new AttributeKey(DataConstants.SHARED_SCOPE, key)));
310 tbClusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(device.getTenantId(), device.getId(), keysToNotify), null); 343 tbClusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(device.getTenantId(), device.getId(), keysToNotify), null);
311 } 344 }
312 345
313 @Override 346 @Override
314 public void onFailure(Throwable t) { 347 public void onFailure(Throwable t) {
315 - log.error("[{}] Failed to remove target firmware attributes!", device.getId(), t); 348 + log.error("[{}] Failed to remove target {} attributes!", device.getId(), otaPackageType, t);
316 } 349 }
317 }); 350 });
318 } 351 }
@@ -157,6 +157,11 @@ public class DefaultTbResourceService implements TbResourceService { @@ -157,6 +157,11 @@ public class DefaultTbResourceService implements TbResourceService {
157 resourceService.deleteResourcesByTenantId(tenantId); 157 resourceService.deleteResourcesByTenantId(tenantId);
158 } 158 }
159 159
  160 + @Override
  161 + public long sumDataSizeByTenantId(TenantId tenantId) {
  162 + return resourceService.sumDataSizeByTenantId(tenantId);
  163 + }
  164 +
160 private Comparator<? super LwM2mObject> getComparator(String sortProperty, String sortOrder) { 165 private Comparator<? super LwM2mObject> getComparator(String sortProperty, String sortOrder) {
161 Comparator<LwM2mObject> comparator; 166 Comparator<LwM2mObject> comparator;
162 if ("name".equals(sortProperty)) { 167 if ("name".equals(sortProperty)) {
@@ -55,4 +55,5 @@ public interface TbResourceService { @@ -55,4 +55,5 @@ public interface TbResourceService {
55 55
56 void deleteResourcesByTenantId(TenantId tenantId); 56 void deleteResourcesByTenantId(TenantId tenantId);
57 57
  58 + long sumDataSizeByTenantId(TenantId tenantId);
58 } 59 }
@@ -536,6 +536,9 @@ public class DefaultTransportApiService implements TransportApiService { @@ -536,6 +536,9 @@ public class DefaultTransportApiService implements TransportApiService {
536 536
537 if (otaPackageInfo == null) { 537 if (otaPackageInfo == null) {
538 builder.setResponseStatus(TransportProtos.ResponseStatus.NOT_FOUND); 538 builder.setResponseStatus(TransportProtos.ResponseStatus.NOT_FOUND);
  539 + } else if (otaPackageInfo.hasUrl()) {
  540 + builder.setResponseStatus(TransportProtos.ResponseStatus.FAILURE);
  541 + log.trace("[{}] Can`t send OtaPackage with URL data!", otaPackageInfo.getId());
539 } else { 542 } else {
540 builder.setResponseStatus(TransportProtos.ResponseStatus.SUCCESS); 543 builder.setResponseStatus(TransportProtos.ResponseStatus.SUCCESS);
541 builder.setOtaPackageIdMSB(otaPackageId.getId().getMostSignificantBits()); 544 builder.setOtaPackageIdMSB(otaPackageId.getId().getMostSignificantBits());
@@ -17,9 +17,9 @@ package org.thingsboard.server.service.ttl; @@ -17,9 +17,9 @@ package org.thingsboard.server.service.ttl;
17 17
18 import lombok.extern.slf4j.Slf4j; 18 import lombok.extern.slf4j.Slf4j;
19 import org.springframework.beans.factory.annotation.Value; 19 import org.springframework.beans.factory.annotation.Value;
20 -import org.thingsboard.server.dao.util.PsqlDao;  
21 20
22 import java.sql.Connection; 21 import java.sql.Connection;
  22 +import java.sql.DriverManager;
23 import java.sql.ResultSet; 23 import java.sql.ResultSet;
24 import java.sql.SQLException; 24 import java.sql.SQLException;
25 import java.sql.SQLWarning; 25 import java.sql.SQLWarning;
@@ -62,4 +62,8 @@ public abstract class AbstractCleanUpService { @@ -62,4 +62,8 @@ public abstract class AbstractCleanUpService {
62 62
63 protected abstract void doCleanUp(Connection connection) throws SQLException; 63 protected abstract void doCleanUp(Connection connection) throws SQLException;
64 64
  65 + protected Connection getConnection() throws SQLException {
  66 + return DriverManager.getConnection(dbUrl, dbUserName, dbPassword);
  67 + }
  68 +
65 } 69 }
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.ttl.alarms;
  17 +
  18 +import lombok.RequiredArgsConstructor;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.springframework.beans.factory.annotation.Value;
  21 +import org.springframework.scheduling.annotation.Scheduled;
  22 +import org.springframework.stereotype.Service;
  23 +import org.thingsboard.server.common.data.alarm.Alarm;
  24 +import org.thingsboard.server.common.data.audit.ActionType;
  25 +import org.thingsboard.server.common.data.id.AlarmId;
  26 +import org.thingsboard.server.common.data.id.TenantId;
  27 +import org.thingsboard.server.common.data.page.PageData;
  28 +import org.thingsboard.server.common.data.page.PageLink;
  29 +import org.thingsboard.server.common.data.page.SortOrder;
  30 +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
  31 +import org.thingsboard.server.common.msg.queue.ServiceType;
  32 +import org.thingsboard.server.dao.alarm.AlarmDao;
  33 +import org.thingsboard.server.dao.alarm.AlarmService;
  34 +import org.thingsboard.server.dao.relation.RelationService;
  35 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
  36 +import org.thingsboard.server.dao.tenant.TenantDao;
  37 +import org.thingsboard.server.dao.util.PsqlDao;
  38 +import org.thingsboard.server.queue.discovery.PartitionService;
  39 +import org.thingsboard.server.queue.util.TbCoreComponent;
  40 +import org.thingsboard.server.service.action.RuleEngineEntityActionService;
  41 +import org.thingsboard.server.service.ttl.AbstractCleanUpService;
  42 +
  43 +import java.sql.Connection;
  44 +import java.sql.SQLException;
  45 +import java.util.Date;
  46 +import java.util.Optional;
  47 +import java.util.UUID;
  48 +import java.util.concurrent.TimeUnit;
  49 +
  50 +@TbCoreComponent
  51 +@Service
  52 +@Slf4j
  53 +@RequiredArgsConstructor
  54 +public class AlarmsCleanUpService {
  55 + @Value("${sql.ttl.alarms.removal_batch_size}")
  56 + private Integer removalBatchSize;
  57 +
  58 + private final TenantDao tenantDao;
  59 + private final AlarmDao alarmDao;
  60 + private final AlarmService alarmService;
  61 + private final RelationService relationService;
  62 + private final RuleEngineEntityActionService ruleEngineEntityActionService;
  63 + private final PartitionService partitionService;
  64 + private final TbTenantProfileCache tenantProfileCache;
  65 +
  66 + @Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.alarms.checking_interval})}", fixedDelayString = "${sql.ttl.alarms.checking_interval}")
  67 + public void cleanUp() {
  68 + PageLink tenantsBatchRequest = new PageLink(10_000, 0);
  69 + PageLink removalBatchRequest = new PageLink(removalBatchSize, 0 );
  70 + PageData<TenantId> tenantsIds;
  71 + do {
  72 + tenantsIds = tenantDao.findTenantsIds(tenantsBatchRequest);
  73 + for (TenantId tenantId : tenantsIds.getData()) {
  74 + if (!partitionService.resolve(ServiceType.TB_CORE, tenantId, tenantId).isMyPartition()) {
  75 + continue;
  76 + }
  77 +
  78 + Optional<DefaultTenantProfileConfiguration> tenantProfileConfiguration = tenantProfileCache.get(tenantId).getProfileConfiguration();
  79 + if (tenantProfileConfiguration.isEmpty() || tenantProfileConfiguration.get().getAlarmsTtlDays() == 0) {
  80 + continue;
  81 + }
  82 +
  83 + long ttl = TimeUnit.DAYS.toMillis(tenantProfileConfiguration.get().getAlarmsTtlDays());
  84 + long expirationTime = System.currentTimeMillis() - ttl;
  85 +
  86 + long totalRemoved = 0;
  87 + while (true) {
  88 + PageData<AlarmId> toRemove = alarmDao.findAlarmsIdsByEndTsBeforeAndTenantId(expirationTime, tenantId, removalBatchRequest);
  89 + toRemove.getData().forEach(alarmId -> {
  90 + relationService.deleteEntityRelations(tenantId, alarmId);
  91 + Alarm alarm = alarmService.deleteAlarm(tenantId, alarmId).getAlarm();
  92 + ruleEngineEntityActionService.pushEntityActionToRuleEngine(alarm.getOriginator(), alarm, tenantId, null, ActionType.ALARM_DELETE, null);
  93 + });
  94 +
  95 + totalRemoved += toRemove.getTotalElements();
  96 + if (!toRemove.hasNext()) {
  97 + break;
  98 + }
  99 + }
  100 +
  101 + if (totalRemoved > 0) {
  102 + log.info("Removed {} outdated alarm(s) for tenant {} older than {}", totalRemoved, tenantId, new Date(expirationTime));
  103 + }
  104 + }
  105 +
  106 + tenantsBatchRequest = tenantsBatchRequest.nextPageLink();
  107 + } while (tenantsIds.hasNext());
  108 + }
  109 +
  110 +}
@@ -40,7 +40,7 @@ public class EdgeEventsCleanUpService extends AbstractCleanUpService { @@ -40,7 +40,7 @@ public class EdgeEventsCleanUpService extends AbstractCleanUpService {
40 @Scheduled(initialDelayString = "${sql.ttl.edge_events.execution_interval_ms}", fixedDelayString = "${sql.ttl.edge_events.execution_interval_ms}") 40 @Scheduled(initialDelayString = "${sql.ttl.edge_events.execution_interval_ms}", fixedDelayString = "${sql.ttl.edge_events.execution_interval_ms}")
41 public void cleanUp() { 41 public void cleanUp() {
42 if (ttlTaskExecutionEnabled) { 42 if (ttlTaskExecutionEnabled) {
43 - try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { 43 + try (Connection conn = getConnection()) {
44 doCleanUp(conn); 44 doCleanUp(conn);
45 } catch (SQLException e) { 45 } catch (SQLException e) {
46 log.error("SQLException occurred during TTL task execution ", e); 46 log.error("SQLException occurred during TTL task execution ", e);
@@ -43,7 +43,7 @@ public class EventsCleanUpService extends AbstractCleanUpService { @@ -43,7 +43,7 @@ public class EventsCleanUpService extends AbstractCleanUpService {
43 @Scheduled(initialDelayString = "${sql.ttl.events.execution_interval_ms}", fixedDelayString = "${sql.ttl.events.execution_interval_ms}") 43 @Scheduled(initialDelayString = "${sql.ttl.events.execution_interval_ms}", fixedDelayString = "${sql.ttl.events.execution_interval_ms}")
44 public void cleanUp() { 44 public void cleanUp() {
45 if (ttlTaskExecutionEnabled) { 45 if (ttlTaskExecutionEnabled) {
46 - try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { 46 + try (Connection conn = getConnection()) {
47 doCleanUp(conn); 47 doCleanUp(conn);
48 } catch (SQLException e) { 48 } catch (SQLException e) {
49 log.error("SQLException occurred during TTL task execution ", e); 49 log.error("SQLException occurred during TTL task execution ", e);
@@ -36,7 +36,7 @@ public abstract class AbstractTimeseriesCleanUpService extends AbstractCleanUpSe @@ -36,7 +36,7 @@ public abstract class AbstractTimeseriesCleanUpService extends AbstractCleanUpSe
36 @Scheduled(initialDelayString = "${sql.ttl.ts.execution_interval_ms}", fixedDelayString = "${sql.ttl.ts.execution_interval_ms}") 36 @Scheduled(initialDelayString = "${sql.ttl.ts.execution_interval_ms}", fixedDelayString = "${sql.ttl.ts.execution_interval_ms}")
37 public void cleanUp() { 37 public void cleanUp() {
38 if (ttlTaskExecutionEnabled) { 38 if (ttlTaskExecutionEnabled) {
39 - try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { 39 + try (Connection conn = getConnection()) {
40 doCleanUp(conn); 40 doCleanUp(conn);
41 } catch (SQLException e) { 41 } catch (SQLException e) {
42 log.error("SQLException occurred during TTL task execution ", e); 42 log.error("SQLException occurred during TTL task execution ", e);
@@ -273,6 +273,9 @@ sql: @@ -273,6 +273,9 @@ sql:
273 enabled: "${SQL_TTL_EDGE_EVENTS_ENABLED:true}" 273 enabled: "${SQL_TTL_EDGE_EVENTS_ENABLED:true}"
274 execution_interval_ms: "${SQL_TTL_EDGE_EVENTS_EXECUTION_INTERVAL:86400000}" # Number of milliseconds. The current value corresponds to one day 274 execution_interval_ms: "${SQL_TTL_EDGE_EVENTS_EXECUTION_INTERVAL:86400000}" # Number of milliseconds. The current value corresponds to one day
275 edge_events_ttl: "${SQL_TTL_EDGE_EVENTS_TTL:2628000}" # Number of seconds. The current value corresponds to one month 275 edge_events_ttl: "${SQL_TTL_EDGE_EVENTS_TTL:2628000}" # Number of seconds. The current value corresponds to one month
  276 + alarms:
  277 + checking_interval: "${SQL_ALARMS_TTL_CHECKING_INTERVAL:7200000}" # Number of milliseconds. The current value corresponds to two hours
  278 + removal_batch_size: "${SQL_ALARMS_TTL_REMOVAL_BATCH_SIZE:3000}" # To delete outdated alarms not all at once but in batches
276 279
277 # Actor system parameters 280 # Actor system parameters
278 actors: 281 actors:
@@ -50,7 +50,7 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes @@ -50,7 +50,7 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes
50 private static final String FILE_NAME = "filename.txt"; 50 private static final String FILE_NAME = "filename.txt";
51 private static final String VERSION = "v1.0"; 51 private static final String VERSION = "v1.0";
52 private static final String CONTENT_TYPE = "text/plain"; 52 private static final String CONTENT_TYPE = "text/plain";
53 - private static final String CHECKSUM_ALGORITHM = "sha256"; 53 + private static final String CHECKSUM_ALGORITHM = "SHA256";
54 private static final String CHECKSUM = "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a"; 54 private static final String CHECKSUM = "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a";
55 private static final ByteBuffer DATA = ByteBuffer.wrap(new byte[]{1}); 55 private static final ByteBuffer DATA = ByteBuffer.wrap(new byte[]{1});
56 56
@@ -257,7 +257,7 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes @@ -257,7 +257,7 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes
257 @Test 257 @Test
258 public void testFindTenantFirmwaresByHasData() throws Exception { 258 public void testFindTenantFirmwaresByHasData() throws Exception {
259 List<OtaPackageInfo> otaPackagesWithData = new ArrayList<>(); 259 List<OtaPackageInfo> otaPackagesWithData = new ArrayList<>();
260 - List<OtaPackageInfo> otaPackagesWithoutData = new ArrayList<>(); 260 + List<OtaPackageInfo> allOtaPackages = new ArrayList<>();
261 261
262 for (int i = 0; i < 165; i++) { 262 for (int i = 0; i < 165; i++) {
263 OtaPackageInfo firmwareInfo = new OtaPackageInfo(); 263 OtaPackageInfo firmwareInfo = new OtaPackageInfo();
@@ -272,44 +272,45 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes @@ -272,44 +272,45 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes
272 MockMultipartFile testData = new MockMultipartFile("file", FILE_NAME, CONTENT_TYPE, DATA.array()); 272 MockMultipartFile testData = new MockMultipartFile("file", FILE_NAME, CONTENT_TYPE, DATA.array());
273 273
274 OtaPackage savedFirmware = savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM); 274 OtaPackage savedFirmware = savaData("/api/otaPackage/" + savedFirmwareInfo.getId().getId().toString() + "?checksum={checksum}&checksumAlgorithm={checksumAlgorithm}", testData, CHECKSUM, CHECKSUM_ALGORITHM);
275 - otaPackagesWithData.add(new OtaPackageInfo(savedFirmware));  
276 - } else {  
277 - otaPackagesWithoutData.add(savedFirmwareInfo); 275 + savedFirmwareInfo = new OtaPackageInfo(savedFirmware);
  276 + otaPackagesWithData.add(savedFirmwareInfo);
278 } 277 }
  278 +
  279 + allOtaPackages.add(savedFirmwareInfo);
279 } 280 }
280 281
281 - List<OtaPackageInfo> loadedFirmwaresWithData = new ArrayList<>(); 282 + List<OtaPackageInfo> loadedOtaPackagesWithData = new ArrayList<>();
282 PageLink pageLink = new PageLink(24); 283 PageLink pageLink = new PageLink(24);
283 PageData<OtaPackageInfo> pageData; 284 PageData<OtaPackageInfo> pageData;
284 do { 285 do {
285 - pageData = doGetTypedWithPageLink("/api/otaPackages/" + deviceProfileId.toString() + "/FIRMWARE/true?", 286 + pageData = doGetTypedWithPageLink("/api/otaPackages/" + deviceProfileId.toString() + "/FIRMWARE?",
286 new TypeReference<>() { 287 new TypeReference<>() {
287 }, pageLink); 288 }, pageLink);
288 - loadedFirmwaresWithData.addAll(pageData.getData()); 289 + loadedOtaPackagesWithData.addAll(pageData.getData());
289 if (pageData.hasNext()) { 290 if (pageData.hasNext()) {
290 pageLink = pageLink.nextPageLink(); 291 pageLink = pageLink.nextPageLink();
291 } 292 }
292 } while (pageData.hasNext()); 293 } while (pageData.hasNext());
293 294
294 - List<OtaPackageInfo> loadedFirmwaresWithoutData = new ArrayList<>(); 295 + List<OtaPackageInfo> allLoadedOtaPackages = new ArrayList<>();
295 pageLink = new PageLink(24); 296 pageLink = new PageLink(24);
296 do { 297 do {
297 - pageData = doGetTypedWithPageLink("/api/otaPackages/" + deviceProfileId.toString() + "/FIRMWARE/false?", 298 + pageData = doGetTypedWithPageLink("/api/otaPackages?",
298 new TypeReference<>() { 299 new TypeReference<>() {
299 }, pageLink); 300 }, pageLink);
300 - loadedFirmwaresWithoutData.addAll(pageData.getData()); 301 + allLoadedOtaPackages.addAll(pageData.getData());
301 if (pageData.hasNext()) { 302 if (pageData.hasNext()) {
302 pageLink = pageLink.nextPageLink(); 303 pageLink = pageLink.nextPageLink();
303 } 304 }
304 } while (pageData.hasNext()); 305 } while (pageData.hasNext());
305 306
306 Collections.sort(otaPackagesWithData, idComparator); 307 Collections.sort(otaPackagesWithData, idComparator);
307 - Collections.sort(otaPackagesWithoutData, idComparator);  
308 - Collections.sort(loadedFirmwaresWithData, idComparator);  
309 - Collections.sort(loadedFirmwaresWithoutData, idComparator); 308 + Collections.sort(allOtaPackages, idComparator);
  309 + Collections.sort(loadedOtaPackagesWithData, idComparator);
  310 + Collections.sort(allLoadedOtaPackages, idComparator);
310 311
311 - Assert.assertEquals(otaPackagesWithData, loadedFirmwaresWithData);  
312 - Assert.assertEquals(otaPackagesWithoutData, loadedFirmwaresWithoutData); 312 + Assert.assertEquals(otaPackagesWithData, loadedOtaPackagesWithData);
  313 + Assert.assertEquals(allOtaPackages, allLoadedOtaPackages);
313 } 314 }
314 315
315 316
@@ -19,17 +19,24 @@ import com.datastax.oss.driver.api.core.uuid.Uuids; @@ -19,17 +19,24 @@ import com.datastax.oss.driver.api.core.uuid.Uuids;
19 import org.junit.After; 19 import org.junit.After;
20 import org.junit.Assert; 20 import org.junit.Assert;
21 import org.junit.Before; 21 import org.junit.Before;
  22 +import org.junit.Rule;
22 import org.junit.Test; 23 import org.junit.Test;
  24 +import org.junit.rules.ExpectedException;
23 import org.springframework.beans.factory.annotation.Autowired; 25 import org.springframework.beans.factory.annotation.Autowired;
  26 +import org.thingsboard.server.common.data.EntityInfo;
  27 +import org.thingsboard.server.common.data.OtaPackage;
24 import org.thingsboard.server.common.data.ResourceType; 28 import org.thingsboard.server.common.data.ResourceType;
25 import org.thingsboard.server.common.data.TbResource; 29 import org.thingsboard.server.common.data.TbResource;
26 import org.thingsboard.server.common.data.TbResourceInfo; 30 import org.thingsboard.server.common.data.TbResourceInfo;
27 import org.thingsboard.server.common.data.Tenant; 31 import org.thingsboard.server.common.data.Tenant;
  32 +import org.thingsboard.server.common.data.TenantProfile;
28 import org.thingsboard.server.common.data.User; 33 import org.thingsboard.server.common.data.User;
  34 +import org.thingsboard.server.common.data.exception.ThingsboardException;
29 import org.thingsboard.server.common.data.id.TenantId; 35 import org.thingsboard.server.common.data.id.TenantId;
30 import org.thingsboard.server.common.data.page.PageData; 36 import org.thingsboard.server.common.data.page.PageData;
31 import org.thingsboard.server.common.data.page.PageLink; 37 import org.thingsboard.server.common.data.page.PageLink;
32 import org.thingsboard.server.common.data.security.Authority; 38 import org.thingsboard.server.common.data.security.Authority;
  39 +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
33 import org.thingsboard.server.controller.AbstractControllerTest; 40 import org.thingsboard.server.controller.AbstractControllerTest;
34 import org.thingsboard.server.dao.exception.DataValidationException; 41 import org.thingsboard.server.dao.exception.DataValidationException;
35 import org.thingsboard.server.dao.service.DaoSqlTest; 42 import org.thingsboard.server.dao.service.DaoSqlTest;
@@ -109,6 +116,64 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { @@ -109,6 +116,64 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest {
109 .andExpect(status().isOk()); 116 .andExpect(status().isOk());
110 } 117 }
111 118
  119 + @Rule
  120 + public ExpectedException thrown = ExpectedException.none();
  121 +
  122 + @Test
  123 + public void testSaveResourceWithMaxSumDataSizeOutOfLimit() throws Exception {
  124 + loginSysAdmin();
  125 + long limit = 1;
  126 + EntityInfo defaultTenantProfileInfo = doGet("/api/tenantProfileInfo/default", EntityInfo.class);
  127 + TenantProfile defaultTenantProfile = doGet("/api/tenantProfile/" + defaultTenantProfileInfo.getId().getId().toString(), TenantProfile.class);
  128 + defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxResourcesInBytes(limit).build());
  129 + doPost("/api/tenantProfile", defaultTenantProfile, TenantProfile.class);
  130 +
  131 + loginTenantAdmin();
  132 +
  133 + Assert.assertEquals(0, resourceService.sumDataSizeByTenantId(tenantId));
  134 +
  135 + createResource("test", DEFAULT_FILE_NAME);
  136 +
  137 + Assert.assertEquals(1, resourceService.sumDataSizeByTenantId(tenantId));
  138 +
  139 + try {
  140 + thrown.expect(DataValidationException.class);
  141 + thrown.expectMessage(String.format("Failed to create the tb resource, files size limit is exhausted %d bytes!", limit));
  142 + createResource("test1", 1 + DEFAULT_FILE_NAME);
  143 + } finally {
  144 + defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxResourcesInBytes(0).build());
  145 + loginSysAdmin();
  146 + doPost("/api/tenantProfile", defaultTenantProfile, TenantProfile.class);
  147 + }
  148 + }
  149 +
  150 + @Test
  151 + public void sumDataSizeByTenantId() throws ThingsboardException {
  152 + Assert.assertEquals(0, resourceService.sumDataSizeByTenantId(tenantId));
  153 +
  154 + createResource("test", DEFAULT_FILE_NAME);
  155 + Assert.assertEquals(1, resourceService.sumDataSizeByTenantId(tenantId));
  156 +
  157 + int maxSumDataSize = 8;
  158 +
  159 + for (int i = 2; i <= maxSumDataSize; i++) {
  160 + createResource("test" + i, i + DEFAULT_FILE_NAME);
  161 + Assert.assertEquals(i, resourceService.sumDataSizeByTenantId(tenantId));
  162 + }
  163 +
  164 + Assert.assertEquals(maxSumDataSize, resourceService.sumDataSizeByTenantId(tenantId));
  165 + }
  166 +
  167 + private TbResource createResource(String title, String filename) throws ThingsboardException {
  168 + TbResource resource = new TbResource();
  169 + resource.setTenantId(tenantId);
  170 + resource.setTitle(title);
  171 + resource.setResourceType(ResourceType.JKS);
  172 + resource.setFileName(filename);
  173 + resource.setData("1");
  174 + return resourceService.saveResource(resource);
  175 + }
  176 +
112 @Test 177 @Test
113 public void testSaveTbResource() throws Exception { 178 public void testSaveTbResource() throws Exception {
114 TbResource resource = new TbResource(); 179 TbResource resource = new TbResource();
@@ -44,9 +44,11 @@ public interface OtaPackageService { @@ -44,9 +44,11 @@ public interface OtaPackageService {
44 44
45 PageData<OtaPackageInfo> findTenantOtaPackagesByTenantId(TenantId tenantId, PageLink pageLink); 45 PageData<OtaPackageInfo> findTenantOtaPackagesByTenantId(TenantId tenantId, PageLink pageLink);
46 46
47 - PageData<OtaPackageInfo> findTenantOtaPackagesByTenantIdAndDeviceProfileIdAndTypeAndHasData(TenantId tenantId, DeviceProfileId deviceProfileId, OtaPackageType otaPackageType, boolean hasData, PageLink pageLink); 47 + PageData<OtaPackageInfo> findTenantOtaPackagesByTenantIdAndDeviceProfileIdAndTypeAndHasData(TenantId tenantId, DeviceProfileId deviceProfileId, OtaPackageType otaPackageType, PageLink pageLink);
48 48
49 void deleteOtaPackage(TenantId tenantId, OtaPackageId otaPackageId); 49 void deleteOtaPackage(TenantId tenantId, OtaPackageId otaPackageId);
50 50
51 void deleteOtaPackagesByTenantId(TenantId tenantId); 51 void deleteOtaPackagesByTenantId(TenantId tenantId);
  52 +
  53 + long sumDataSizeByTenantId(TenantId tenantId);
52 } 54 }
@@ -49,5 +49,5 @@ public interface ResourceService { @@ -49,5 +49,5 @@ public interface ResourceService {
49 49
50 void deleteResourcesByTenantId(TenantId tenantId); 50 void deleteResourcesByTenantId(TenantId tenantId);
51 51
52 - 52 + long sumDataSizeByTenantId(TenantId tenantId);
53 } 53 }
@@ -66,6 +66,7 @@ public class DataConstants { @@ -66,6 +66,7 @@ public class DataConstants {
66 public static final String TIMESERIES_DELETED = "TIMESERIES_DELETED"; 66 public static final String TIMESERIES_DELETED = "TIMESERIES_DELETED";
67 public static final String ALARM_ACK = "ALARM_ACK"; 67 public static final String ALARM_ACK = "ALARM_ACK";
68 public static final String ALARM_CLEAR = "ALARM_CLEAR"; 68 public static final String ALARM_CLEAR = "ALARM_CLEAR";
  69 + public static final String ALARM_DELETE = "ALARM_DELETE";
69 public static final String ENTITY_ASSIGNED_FROM_TENANT = "ENTITY_ASSIGNED_FROM_TENANT"; 70 public static final String ENTITY_ASSIGNED_FROM_TENANT = "ENTITY_ASSIGNED_FROM_TENANT";
70 public static final String ENTITY_ASSIGNED_TO_TENANT = "ENTITY_ASSIGNED_TO_TENANT"; 71 public static final String ENTITY_ASSIGNED_TO_TENANT = "ENTITY_ASSIGNED_TO_TENANT";
71 public static final String PROVISION_SUCCESS = "PROVISION_SUCCESS"; 72 public static final String PROVISION_SUCCESS = "PROVISION_SUCCESS";
@@ -37,8 +37,8 @@ public class OtaPackage extends OtaPackageInfo { @@ -37,8 +37,8 @@ public class OtaPackage extends OtaPackageInfo {
37 super(id); 37 super(id);
38 } 38 }
39 39
40 - public OtaPackage(OtaPackage firmware) {  
41 - super(firmware);  
42 - this.data = firmware.getData(); 40 + public OtaPackage(OtaPackage otaPackage) {
  41 + super(otaPackage);
  42 + this.data = otaPackage.getData();
43 } 43 }
44 } 44 }
@@ -37,6 +37,7 @@ public class OtaPackageInfo extends SearchTextBasedWithAdditionalInfo<OtaPackage @@ -37,6 +37,7 @@ public class OtaPackageInfo extends SearchTextBasedWithAdditionalInfo<OtaPackage
37 private OtaPackageType type; 37 private OtaPackageType type;
38 private String title; 38 private String title;
39 private String version; 39 private String version;
  40 + private String url;
40 private boolean hasData; 41 private boolean hasData;
41 private String fileName; 42 private String fileName;
42 private String contentType; 43 private String contentType;
@@ -60,6 +61,7 @@ public class OtaPackageInfo extends SearchTextBasedWithAdditionalInfo<OtaPackage @@ -60,6 +61,7 @@ public class OtaPackageInfo extends SearchTextBasedWithAdditionalInfo<OtaPackage
60 this.type = otaPackageInfo.getType(); 61 this.type = otaPackageInfo.getType();
61 this.title = otaPackageInfo.getTitle(); 62 this.title = otaPackageInfo.getTitle();
62 this.version = otaPackageInfo.getVersion(); 63 this.version = otaPackageInfo.getVersion();
  64 + this.url = otaPackageInfo.getUrl();
63 this.hasData = otaPackageInfo.isHasData(); 65 this.hasData = otaPackageInfo.isHasData();
64 this.fileName = otaPackageInfo.getFileName(); 66 this.fileName = otaPackageInfo.getFileName();
65 this.contentType = otaPackageInfo.getContentType(); 67 this.contentType = otaPackageInfo.getContentType();
@@ -78,4 +80,9 @@ public class OtaPackageInfo extends SearchTextBasedWithAdditionalInfo<OtaPackage @@ -78,4 +80,9 @@ public class OtaPackageInfo extends SearchTextBasedWithAdditionalInfo<OtaPackage
78 public String getName() { 80 public String getName() {
79 return title; 81 return title;
80 } 82 }
  83 +
  84 + @JsonIgnore
  85 + public boolean hasUrl() {
  86 + return StringUtils.isNotEmpty(url);
  87 + }
81 } 88 }
@@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.validation.NoXss;
27 27
28 import java.io.ByteArrayInputStream; 28 import java.io.ByteArrayInputStream;
29 import java.io.IOException; 29 import java.io.IOException;
  30 +import java.util.Optional;
30 31
31 import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.mapper; 32 import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.mapper;
32 33
@@ -92,6 +93,13 @@ public class TenantProfile extends SearchTextBased<TenantProfileId> implements H @@ -92,6 +93,13 @@ public class TenantProfile extends SearchTextBased<TenantProfileId> implements H
92 } 93 }
93 } 94 }
94 95
  96 + @JsonIgnore
  97 + public Optional<DefaultTenantProfileConfiguration> getProfileConfiguration() {
  98 + return Optional.ofNullable(getProfileData().getConfiguration())
  99 + .filter(profileConfiguration -> profileConfiguration instanceof DefaultTenantProfileConfiguration)
  100 + .map(profileConfiguration -> (DefaultTenantProfileConfiguration) profileConfiguration);
  101 + }
  102 +
95 public TenantProfileData createDefaultTenantProfileData() { 103 public TenantProfileData createDefaultTenantProfileData() {
96 TenantProfileData tpd = new TenantProfileData(); 104 TenantProfileData tpd = new TenantProfileData();
97 tpd.setConfiguration(new DefaultTenantProfileConfiguration()); 105 tpd.setConfiguration(new DefaultTenantProfileConfiguration());
@@ -39,6 +39,7 @@ public enum ActionType { @@ -39,6 +39,7 @@ public enum ActionType {
39 RELATIONS_DELETED(false), 39 RELATIONS_DELETED(false),
40 ALARM_ACK(false), 40 ALARM_ACK(false),
41 ALARM_CLEAR(false), 41 ALARM_CLEAR(false),
  42 + ALARM_DELETE(false),
42 LOGIN(false), 43 LOGIN(false),
43 LOGOUT(false), 44 LOGOUT(false),
44 LOCKOUT(false), 45 LOCKOUT(false),
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.common.data.exception;
  17 +
  18 +public class ApiUsageLimitsExceededException extends RuntimeException {
  19 + public ApiUsageLimitsExceededException(String message) {
  20 + super(message);
  21 + }
  22 +
  23 + public ApiUsageLimitsExceededException() {
  24 + }
  25 +}
@@ -19,7 +19,7 @@ import lombok.Getter; @@ -19,7 +19,7 @@ import lombok.Getter;
19 19
20 public enum OtaPackageKey { 20 public enum OtaPackageKey {
21 21
22 - TITLE("title"), VERSION("version"), TS("ts"), STATE("state"), SIZE("size"), CHECKSUM("checksum"), CHECKSUM_ALGORITHM("checksum_algorithm"); 22 + TITLE("title"), VERSION("version"), TS("ts"), STATE("state"), SIZE("size"), CHECKSUM("checksum"), CHECKSUM_ALGORITHM("checksum_algorithm"), URL("url");
23 23
24 @Getter 24 @Getter
25 private final String value; 25 private final String value;
@@ -17,10 +17,11 @@ package org.thingsboard.server.common.data.page; @@ -17,10 +17,11 @@ package org.thingsboard.server.common.data.page;
17 17
18 import com.fasterxml.jackson.annotation.JsonCreator; 18 import com.fasterxml.jackson.annotation.JsonCreator;
19 import com.fasterxml.jackson.annotation.JsonProperty; 19 import com.fasterxml.jackson.annotation.JsonProperty;
20 -import org.thingsboard.server.common.data.BaseData;  
21 20
22 import java.util.Collections; 21 import java.util.Collections;
23 import java.util.List; 22 import java.util.List;
  23 +import java.util.function.Function;
  24 +import java.util.stream.Collectors;
24 25
25 public class PageData<T> { 26 public class PageData<T> {
26 27
@@ -61,4 +62,8 @@ public class PageData<T> { @@ -61,4 +62,8 @@ public class PageData<T> {
61 return hasNext; 62 return hasNext;
62 } 63 }
63 64
  65 + public <D> PageData<D> mapData(Function<T, D> mapper) {
  66 + return new PageData<>(getData().stream().map(mapper).collect(Collectors.toList()), getTotalPages(), getTotalElements(), hasNext());
  67 + }
  68 +
64 } 69 }
@@ -34,6 +34,8 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura @@ -34,6 +34,8 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
34 private long maxUsers; 34 private long maxUsers;
35 private long maxDashboards; 35 private long maxDashboards;
36 private long maxRuleChains; 36 private long maxRuleChains;
  37 + private long maxResourcesInBytes;
  38 + private long maxOtaPackagesInBytes;
37 39
38 private String transportTenantMsgRateLimit; 40 private String transportTenantMsgRateLimit;
39 private String transportTenantTelemetryMsgRateLimit; 41 private String transportTenantTelemetryMsgRateLimit;
@@ -53,6 +55,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura @@ -53,6 +55,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
53 private long maxCreatedAlarms; 55 private long maxCreatedAlarms;
54 56
55 private int defaultStorageTtlDays; 57 private int defaultStorageTtlDays;
  58 + private int alarmsTtlDays;
56 59
57 private double warnThreshold; 60 private double warnThreshold;
58 61
@@ -18,6 +18,7 @@ package org.thingsboard.server.dao; @@ -18,6 +18,7 @@ package org.thingsboard.server.dao;
18 import com.google.common.util.concurrent.ListenableFuture; 18 import com.google.common.util.concurrent.ListenableFuture;
19 import org.thingsboard.server.common.data.id.TenantId; 19 import org.thingsboard.server.common.data.id.TenantId;
20 20
  21 +import java.util.Collection;
21 import java.util.List; 22 import java.util.List;
22 import java.util.UUID; 23 import java.util.UUID;
23 24
@@ -33,4 +34,6 @@ public interface Dao<T> { @@ -33,4 +34,6 @@ public interface Dao<T> {
33 34
34 boolean removeById(TenantId tenantId, UUID id); 35 boolean removeById(TenantId tenantId, UUID id);
35 36
  37 + void removeAllByIds(Collection<UUID> ids);
  38 +
36 } 39 }
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao;
  17 +
  18 +import org.thingsboard.server.common.data.id.TenantId;
  19 +
  20 +public interface TenantEntityWithDataDao {
  21 +
  22 + Long sumDataSizeByTenantId(TenantId tenantId);
  23 +}
@@ -21,10 +21,12 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo; @@ -21,10 +21,12 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo;
21 import org.thingsboard.server.common.data.alarm.AlarmQuery; 21 import org.thingsboard.server.common.data.alarm.AlarmQuery;
22 import org.thingsboard.server.common.data.alarm.AlarmSeverity; 22 import org.thingsboard.server.common.data.alarm.AlarmSeverity;
23 import org.thingsboard.server.common.data.alarm.AlarmStatus; 23 import org.thingsboard.server.common.data.alarm.AlarmStatus;
  24 +import org.thingsboard.server.common.data.id.AlarmId;
24 import org.thingsboard.server.common.data.id.CustomerId; 25 import org.thingsboard.server.common.data.id.CustomerId;
25 import org.thingsboard.server.common.data.id.EntityId; 26 import org.thingsboard.server.common.data.id.EntityId;
26 import org.thingsboard.server.common.data.id.TenantId; 27 import org.thingsboard.server.common.data.id.TenantId;
27 import org.thingsboard.server.common.data.page.PageData; 28 import org.thingsboard.server.common.data.page.PageData;
  29 +import org.thingsboard.server.common.data.page.PageLink;
28 import org.thingsboard.server.common.data.query.AlarmData; 30 import org.thingsboard.server.common.data.query.AlarmData;
29 import org.thingsboard.server.common.data.query.AlarmDataQuery; 31 import org.thingsboard.server.common.data.query.AlarmDataQuery;
30 import org.thingsboard.server.dao.Dao; 32 import org.thingsboard.server.dao.Dao;
@@ -54,4 +56,7 @@ public interface AlarmDao extends Dao<Alarm> { @@ -54,4 +56,7 @@ public interface AlarmDao extends Dao<Alarm> {
54 AlarmDataQuery query, Collection<EntityId> orderedEntityIds); 56 AlarmDataQuery query, Collection<EntityId> orderedEntityIds);
55 57
56 Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, Set<AlarmStatus> status); 58 Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, Set<AlarmStatus> status);
  59 +
  60 + PageData<AlarmId> findAlarmsIdsByEndTsBeforeAndTenantId(Long time, TenantId tenantId, PageLink pageLink);
  61 +
57 } 62 }
@@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.alarm.AlarmQuery; @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.alarm.AlarmQuery;
34 import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; 34 import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
35 import org.thingsboard.server.common.data.alarm.AlarmSeverity; 35 import org.thingsboard.server.common.data.alarm.AlarmSeverity;
36 import org.thingsboard.server.common.data.alarm.AlarmStatus; 36 import org.thingsboard.server.common.data.alarm.AlarmStatus;
  37 +import org.thingsboard.server.common.data.exception.ApiUsageLimitsExceededException;
37 import org.thingsboard.server.common.data.id.AlarmId; 38 import org.thingsboard.server.common.data.id.AlarmId;
38 import org.thingsboard.server.common.data.id.CustomerId; 39 import org.thingsboard.server.common.data.id.CustomerId;
39 import org.thingsboard.server.common.data.id.EntityId; 40 import org.thingsboard.server.common.data.id.EntityId;
@@ -119,7 +120,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ @@ -119,7 +120,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
119 Alarm existing = alarmDao.findLatestByOriginatorAndType(alarm.getTenantId(), alarm.getOriginator(), alarm.getType()).get(); 120 Alarm existing = alarmDao.findLatestByOriginatorAndType(alarm.getTenantId(), alarm.getOriginator(), alarm.getType()).get();
120 if (existing == null || existing.getStatus().isCleared()) { 121 if (existing == null || existing.getStatus().isCleared()) {
121 if (!alarmCreationEnabled) { 122 if (!alarmCreationEnabled) {
122 - throw new IllegalStateException("Alarm creation is disabled"); 123 + throw new ApiUsageLimitsExceededException("Alarms creation is disabled");
123 } 124 }
124 return createAlarm(alarm); 125 return createAlarm(alarm);
125 } else { 126 } else {
@@ -487,6 +487,7 @@ public class ModelConstants { @@ -487,6 +487,7 @@ public class ModelConstants {
487 public static final String OTA_PACKAGE_TYPE_COLUMN = "type"; 487 public static final String OTA_PACKAGE_TYPE_COLUMN = "type";
488 public static final String OTA_PACKAGE_TILE_COLUMN = TITLE_PROPERTY; 488 public static final String OTA_PACKAGE_TILE_COLUMN = TITLE_PROPERTY;
489 public static final String OTA_PACKAGE_VERSION_COLUMN = "version"; 489 public static final String OTA_PACKAGE_VERSION_COLUMN = "version";
  490 + public static final String OTA_PACKAGE_URL_COLUMN = "url";
490 public static final String OTA_PACKAGE_FILE_NAME_COLUMN = "file_name"; 491 public static final String OTA_PACKAGE_FILE_NAME_COLUMN = "file_name";
491 public static final String OTA_PACKAGE_CONTENT_TYPE_COLUMN = "content_type"; 492 public static final String OTA_PACKAGE_CONTENT_TYPE_COLUMN = "content_type";
492 public static final String OTA_PACKAGE_CHECKSUM_ALGORITHM_COLUMN = "checksum_algorithm"; 493 public static final String OTA_PACKAGE_CHECKSUM_ALGORITHM_COLUMN = "checksum_algorithm";
@@ -51,6 +51,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_TABLE_ @@ -51,6 +51,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_TABLE_
51 import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_TENANT_ID_COLUMN; 51 import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_TENANT_ID_COLUMN;
52 import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_TILE_COLUMN; 52 import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_TILE_COLUMN;
53 import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_TYPE_COLUMN; 53 import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_TYPE_COLUMN;
  54 +import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_URL_COLUMN;
54 import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_VERSION_COLUMN; 55 import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_VERSION_COLUMN;
55 import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; 56 import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
56 57
@@ -77,6 +78,9 @@ public class OtaPackageEntity extends BaseSqlEntity<OtaPackage> implements Searc @@ -77,6 +78,9 @@ public class OtaPackageEntity extends BaseSqlEntity<OtaPackage> implements Searc
77 @Column(name = OTA_PACKAGE_VERSION_COLUMN) 78 @Column(name = OTA_PACKAGE_VERSION_COLUMN)
78 private String version; 79 private String version;
79 80
  81 + @Column(name = OTA_PACKAGE_URL_COLUMN)
  82 + private String url;
  83 +
80 @Column(name = OTA_PACKAGE_FILE_NAME_COLUMN) 84 @Column(name = OTA_PACKAGE_FILE_NAME_COLUMN)
81 private String fileName; 85 private String fileName;
82 86
@@ -118,6 +122,7 @@ public class OtaPackageEntity extends BaseSqlEntity<OtaPackage> implements Searc @@ -118,6 +122,7 @@ public class OtaPackageEntity extends BaseSqlEntity<OtaPackage> implements Searc
118 this.type = firmware.getType(); 122 this.type = firmware.getType();
119 this.title = firmware.getTitle(); 123 this.title = firmware.getTitle();
120 this.version = firmware.getVersion(); 124 this.version = firmware.getVersion();
  125 + this.url = firmware.getUrl();
121 this.fileName = firmware.getFileName(); 126 this.fileName = firmware.getFileName();
122 this.contentType = firmware.getContentType(); 127 this.contentType = firmware.getContentType();
123 this.checksumAlgorithm = firmware.getChecksumAlgorithm(); 128 this.checksumAlgorithm = firmware.getChecksumAlgorithm();
@@ -148,6 +153,7 @@ public class OtaPackageEntity extends BaseSqlEntity<OtaPackage> implements Searc @@ -148,6 +153,7 @@ public class OtaPackageEntity extends BaseSqlEntity<OtaPackage> implements Searc
148 firmware.setType(type); 153 firmware.setType(type);
149 firmware.setTitle(title); 154 firmware.setTitle(title);
150 firmware.setVersion(version); 155 firmware.setVersion(version);
  156 + firmware.setUrl(url);
151 firmware.setFileName(fileName); 157 firmware.setFileName(fileName);
152 firmware.setContentType(contentType); 158 firmware.setContentType(contentType);
153 firmware.setChecksumAlgorithm(checksumAlgorithm); 159 firmware.setChecksumAlgorithm(checksumAlgorithm);
@@ -22,6 +22,7 @@ import org.hibernate.annotations.Type; @@ -22,6 +22,7 @@ import org.hibernate.annotations.Type;
22 import org.hibernate.annotations.TypeDef; 22 import org.hibernate.annotations.TypeDef;
23 import org.thingsboard.common.util.JacksonUtil; 23 import org.thingsboard.common.util.JacksonUtil;
24 import org.thingsboard.server.common.data.OtaPackageInfo; 24 import org.thingsboard.server.common.data.OtaPackageInfo;
  25 +import org.thingsboard.server.common.data.StringUtils;
25 import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; 26 import org.thingsboard.server.common.data.ota.ChecksumAlgorithm;
26 import org.thingsboard.server.common.data.ota.OtaPackageType; 27 import org.thingsboard.server.common.data.ota.OtaPackageType;
27 import org.thingsboard.server.common.data.id.DeviceProfileId; 28 import org.thingsboard.server.common.data.id.DeviceProfileId;
@@ -50,6 +51,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_TABLE_ @@ -50,6 +51,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_TABLE_
50 import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_TENANT_ID_COLUMN; 51 import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_TENANT_ID_COLUMN;
51 import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_TILE_COLUMN; 52 import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_TILE_COLUMN;
52 import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_TYPE_COLUMN; 53 import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_TYPE_COLUMN;
  54 +import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_URL_COLUMN;
53 import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_VERSION_COLUMN; 55 import static org.thingsboard.server.dao.model.ModelConstants.OTA_PACKAGE_VERSION_COLUMN;
54 import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY; 56 import static org.thingsboard.server.dao.model.ModelConstants.SEARCH_TEXT_PROPERTY;
55 57
@@ -76,6 +78,9 @@ public class OtaPackageInfoEntity extends BaseSqlEntity<OtaPackageInfo> implemen @@ -76,6 +78,9 @@ public class OtaPackageInfoEntity extends BaseSqlEntity<OtaPackageInfo> implemen
76 @Column(name = OTA_PACKAGE_VERSION_COLUMN) 78 @Column(name = OTA_PACKAGE_VERSION_COLUMN)
77 private String version; 79 private String version;
78 80
  81 + @Column(name = OTA_PACKAGE_URL_COLUMN)
  82 + private String url;
  83 +
79 @Column(name = OTA_PACKAGE_FILE_NAME_COLUMN) 84 @Column(name = OTA_PACKAGE_FILE_NAME_COLUMN)
80 private String fileName; 85 private String fileName;
81 86
@@ -116,6 +121,7 @@ public class OtaPackageInfoEntity extends BaseSqlEntity<OtaPackageInfo> implemen @@ -116,6 +121,7 @@ public class OtaPackageInfoEntity extends BaseSqlEntity<OtaPackageInfo> implemen
116 } 121 }
117 this.title = firmware.getTitle(); 122 this.title = firmware.getTitle();
118 this.version = firmware.getVersion(); 123 this.version = firmware.getVersion();
  124 + this.url = firmware.getUrl();
119 this.fileName = firmware.getFileName(); 125 this.fileName = firmware.getFileName();
120 this.contentType = firmware.getContentType(); 126 this.contentType = firmware.getContentType();
121 this.checksumAlgorithm = firmware.getChecksumAlgorithm(); 127 this.checksumAlgorithm = firmware.getChecksumAlgorithm();
@@ -125,7 +131,7 @@ public class OtaPackageInfoEntity extends BaseSqlEntity<OtaPackageInfo> implemen @@ -125,7 +131,7 @@ public class OtaPackageInfoEntity extends BaseSqlEntity<OtaPackageInfo> implemen
125 } 131 }
126 132
127 public OtaPackageInfoEntity(UUID id, long createdTime, UUID tenantId, UUID deviceProfileId, OtaPackageType type, String title, String version, 133 public OtaPackageInfoEntity(UUID id, long createdTime, UUID tenantId, UUID deviceProfileId, OtaPackageType type, String title, String version,
128 - String fileName, String contentType, ChecksumAlgorithm checksumAlgorithm, String checksum, Long dataSize, 134 + String url, String fileName, String contentType, ChecksumAlgorithm checksumAlgorithm, String checksum, Long dataSize,
129 Object additionalInfo, boolean hasData) { 135 Object additionalInfo, boolean hasData) {
130 this.id = id; 136 this.id = id;
131 this.createdTime = createdTime; 137 this.createdTime = createdTime;
@@ -134,6 +140,7 @@ public class OtaPackageInfoEntity extends BaseSqlEntity<OtaPackageInfo> implemen @@ -134,6 +140,7 @@ public class OtaPackageInfoEntity extends BaseSqlEntity<OtaPackageInfo> implemen
134 this.type = type; 140 this.type = type;
135 this.title = title; 141 this.title = title;
136 this.version = version; 142 this.version = version;
  143 + this.url = url;
137 this.fileName = fileName; 144 this.fileName = fileName;
138 this.contentType = contentType; 145 this.contentType = contentType;
139 this.checksumAlgorithm = checksumAlgorithm; 146 this.checksumAlgorithm = checksumAlgorithm;
@@ -164,6 +171,7 @@ public class OtaPackageInfoEntity extends BaseSqlEntity<OtaPackageInfo> implemen @@ -164,6 +171,7 @@ public class OtaPackageInfoEntity extends BaseSqlEntity<OtaPackageInfo> implemen
164 firmware.setType(type); 171 firmware.setType(type);
165 firmware.setTitle(title); 172 firmware.setTitle(title);
166 firmware.setVersion(version); 173 firmware.setVersion(version);
  174 + firmware.setUrl(url);
167 firmware.setFileName(fileName); 175 firmware.setFileName(fileName);
168 firmware.setContentType(contentType); 176 firmware.setContentType(contentType);
169 firmware.setChecksumAlgorithm(checksumAlgorithm); 177 firmware.setChecksumAlgorithm(checksumAlgorithm);
@@ -20,28 +20,32 @@ import com.google.common.hash.Hashing; @@ -20,28 +20,32 @@ import com.google.common.hash.Hashing;
20 import com.google.common.util.concurrent.ListenableFuture; 20 import com.google.common.util.concurrent.ListenableFuture;
21 import lombok.RequiredArgsConstructor; 21 import lombok.RequiredArgsConstructor;
22 import lombok.extern.slf4j.Slf4j; 22 import lombok.extern.slf4j.Slf4j;
23 -import org.apache.commons.lang3.StringUtils;  
24 import org.hibernate.exception.ConstraintViolationException; 23 import org.hibernate.exception.ConstraintViolationException;
  24 +import org.springframework.beans.factory.annotation.Autowired;
25 import org.springframework.cache.Cache; 25 import org.springframework.cache.Cache;
26 import org.springframework.cache.CacheManager; 26 import org.springframework.cache.CacheManager;
27 import org.springframework.cache.annotation.Cacheable; 27 import org.springframework.cache.annotation.Cacheable;
  28 +import org.springframework.context.annotation.Lazy;
28 import org.springframework.stereotype.Service; 29 import org.springframework.stereotype.Service;
29 import org.thingsboard.server.cache.ota.OtaPackageDataCache; 30 import org.thingsboard.server.cache.ota.OtaPackageDataCache;
30 import org.thingsboard.server.common.data.DeviceProfile; 31 import org.thingsboard.server.common.data.DeviceProfile;
31 import org.thingsboard.server.common.data.OtaPackage; 32 import org.thingsboard.server.common.data.OtaPackage;
32 import org.thingsboard.server.common.data.OtaPackageInfo; 33 import org.thingsboard.server.common.data.OtaPackageInfo;
  34 +import org.thingsboard.server.common.data.StringUtils;
33 import org.thingsboard.server.common.data.Tenant; 35 import org.thingsboard.server.common.data.Tenant;
34 -import org.thingsboard.server.common.data.ota.ChecksumAlgorithm;  
35 -import org.thingsboard.server.common.data.ota.OtaPackageType;  
36 import org.thingsboard.server.common.data.id.DeviceProfileId; 36 import org.thingsboard.server.common.data.id.DeviceProfileId;
37 import org.thingsboard.server.common.data.id.OtaPackageId; 37 import org.thingsboard.server.common.data.id.OtaPackageId;
38 import org.thingsboard.server.common.data.id.TenantId; 38 import org.thingsboard.server.common.data.id.TenantId;
  39 +import org.thingsboard.server.common.data.ota.ChecksumAlgorithm;
  40 +import org.thingsboard.server.common.data.ota.OtaPackageType;
39 import org.thingsboard.server.common.data.page.PageData; 41 import org.thingsboard.server.common.data.page.PageData;
40 import org.thingsboard.server.common.data.page.PageLink; 42 import org.thingsboard.server.common.data.page.PageLink;
  43 +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
41 import org.thingsboard.server.dao.device.DeviceProfileDao; 44 import org.thingsboard.server.dao.device.DeviceProfileDao;
42 import org.thingsboard.server.dao.exception.DataValidationException; 45 import org.thingsboard.server.dao.exception.DataValidationException;
43 import org.thingsboard.server.dao.service.DataValidator; 46 import org.thingsboard.server.dao.service.DataValidator;
44 import org.thingsboard.server.dao.service.PaginatedRemover; 47 import org.thingsboard.server.dao.service.PaginatedRemover;
  48 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
45 import org.thingsboard.server.dao.tenant.TenantDao; 49 import org.thingsboard.server.dao.tenant.TenantDao;
46 50
47 import java.nio.ByteBuffer; 51 import java.nio.ByteBuffer;
@@ -50,6 +54,7 @@ import java.util.List; @@ -50,6 +54,7 @@ import java.util.List;
50 import java.util.Optional; 54 import java.util.Optional;
51 55
52 import static org.thingsboard.server.common.data.CacheConstants.OTA_PACKAGE_CACHE; 56 import static org.thingsboard.server.common.data.CacheConstants.OTA_PACKAGE_CACHE;
  57 +import static org.thingsboard.server.common.data.EntityType.OTA_PACKAGE;
53 import static org.thingsboard.server.dao.service.Validator.validateId; 58 import static org.thingsboard.server.dao.service.Validator.validateId;
54 import static org.thingsboard.server.dao.service.Validator.validatePageLink; 59 import static org.thingsboard.server.dao.service.Validator.validatePageLink;
55 60
@@ -67,6 +72,10 @@ public class BaseOtaPackageService implements OtaPackageService { @@ -67,6 +72,10 @@ public class BaseOtaPackageService implements OtaPackageService {
67 private final CacheManager cacheManager; 72 private final CacheManager cacheManager;
68 private final OtaPackageDataCache otaPackageDataCache; 73 private final OtaPackageDataCache otaPackageDataCache;
69 74
  75 + @Autowired
  76 + @Lazy
  77 + private TbTenantProfileCache tenantProfileCache;
  78 +
70 @Override 79 @Override
71 public OtaPackageInfo saveOtaPackageInfo(OtaPackageInfo otaPackageInfo) { 80 public OtaPackageInfo saveOtaPackageInfo(OtaPackageInfo otaPackageInfo) {
72 log.trace("Executing saveOtaPackageInfo [{}]", otaPackageInfo); 81 log.trace("Executing saveOtaPackageInfo [{}]", otaPackageInfo);
@@ -172,11 +181,11 @@ public class BaseOtaPackageService implements OtaPackageService { @@ -172,11 +181,11 @@ public class BaseOtaPackageService implements OtaPackageService {
172 } 181 }
173 182
174 @Override 183 @Override
175 - public PageData<OtaPackageInfo> findTenantOtaPackagesByTenantIdAndDeviceProfileIdAndTypeAndHasData(TenantId tenantId, DeviceProfileId deviceProfileId, OtaPackageType otaPackageType, boolean hasData, PageLink pageLink) {  
176 - log.trace("Executing findTenantOtaPackagesByTenantIdAndHasData, tenantId [{}], hasData [{}] pageLink [{}]", tenantId, hasData, pageLink); 184 + public PageData<OtaPackageInfo> findTenantOtaPackagesByTenantIdAndDeviceProfileIdAndTypeAndHasData(TenantId tenantId, DeviceProfileId deviceProfileId, OtaPackageType otaPackageType, PageLink pageLink) {
  185 + log.trace("Executing findTenantOtaPackagesByTenantIdAndHasData, tenantId [{}], pageLink [{}]", tenantId, pageLink);
177 validateId(tenantId, INCORRECT_TENANT_ID + tenantId); 186 validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
178 validatePageLink(pageLink); 187 validatePageLink(pageLink);
179 - return otaPackageInfoDao.findOtaPackageInfoByTenantIdAndDeviceProfileIdAndTypeAndHasData(tenantId, deviceProfileId, otaPackageType, hasData, pageLink); 188 + return otaPackageInfoDao.findOtaPackageInfoByTenantIdAndDeviceProfileIdAndTypeAndHasData(tenantId, deviceProfileId, otaPackageType, pageLink);
180 } 189 }
181 190
182 @Override 191 @Override
@@ -205,6 +214,11 @@ public class BaseOtaPackageService implements OtaPackageService { @@ -205,6 +214,11 @@ public class BaseOtaPackageService implements OtaPackageService {
205 } 214 }
206 215
207 @Override 216 @Override
  217 + public long sumDataSizeByTenantId(TenantId tenantId) {
  218 + return otaPackageDao.sumDataSizeByTenantId(tenantId);
  219 + }
  220 +
  221 + @Override
208 public void deleteOtaPackagesByTenantId(TenantId tenantId) { 222 public void deleteOtaPackagesByTenantId(TenantId tenantId) {
209 log.trace("Executing deleteOtaPackagesByTenantId, tenantId [{}]", tenantId); 223 log.trace("Executing deleteOtaPackagesByTenantId, tenantId [{}]", tenantId);
210 validateId(tenantId, INCORRECT_TENANT_ID + tenantId); 224 validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
@@ -228,30 +242,42 @@ public class BaseOtaPackageService implements OtaPackageService { @@ -228,30 +242,42 @@ public class BaseOtaPackageService implements OtaPackageService {
228 private DataValidator<OtaPackage> otaPackageValidator = new DataValidator<>() { 242 private DataValidator<OtaPackage> otaPackageValidator = new DataValidator<>() {
229 243
230 @Override 244 @Override
  245 + protected void validateCreate(TenantId tenantId, OtaPackage otaPackage) {
  246 + DefaultTenantProfileConfiguration profileConfiguration =
  247 + (DefaultTenantProfileConfiguration) tenantProfileCache.get(tenantId).getProfileData().getConfiguration();
  248 + long maxOtaPackagesInBytes = profileConfiguration.getMaxOtaPackagesInBytes();
  249 + validateMaxSumDataSizePerTenant(tenantId, otaPackageDao, maxOtaPackagesInBytes, otaPackage.getDataSize(), OTA_PACKAGE);
  250 + }
  251 +
  252 + @Override
231 protected void validateDataImpl(TenantId tenantId, OtaPackage otaPackage) { 253 protected void validateDataImpl(TenantId tenantId, OtaPackage otaPackage) {
232 validateImpl(otaPackage); 254 validateImpl(otaPackage);
233 255
234 - if (StringUtils.isEmpty(otaPackage.getFileName())) {  
235 - throw new DataValidationException("OtaPackage file name should be specified!");  
236 - } 256 + if (!otaPackage.hasUrl()) {
  257 + if (StringUtils.isEmpty(otaPackage.getFileName())) {
  258 + throw new DataValidationException("OtaPackage file name should be specified!");
  259 + }
237 260
238 - if (StringUtils.isEmpty(otaPackage.getContentType())) {  
239 - throw new DataValidationException("OtaPackage content type should be specified!");  
240 - } 261 + if (StringUtils.isEmpty(otaPackage.getContentType())) {
  262 + throw new DataValidationException("OtaPackage content type should be specified!");
  263 + }
241 264
242 - if (otaPackage.getChecksumAlgorithm() == null) {  
243 - throw new DataValidationException("OtaPackage checksum algorithm should be specified!");  
244 - }  
245 - if (StringUtils.isEmpty(otaPackage.getChecksum())) {  
246 - throw new DataValidationException("OtaPackage checksum should be specified!");  
247 - } 265 + if (otaPackage.getChecksumAlgorithm() == null) {
  266 + throw new DataValidationException("OtaPackage checksum algorithm should be specified!");
  267 + }
  268 + if (StringUtils.isEmpty(otaPackage.getChecksum())) {
  269 + throw new DataValidationException("OtaPackage checksum should be specified!");
  270 + }
248 271
249 - String currentChecksum; 272 + String currentChecksum;
250 273
251 - currentChecksum = generateChecksum(otaPackage.getChecksumAlgorithm(), otaPackage.getData()); 274 + currentChecksum = generateChecksum(otaPackage.getChecksumAlgorithm(), otaPackage.getData());
252 275
253 - if (!currentChecksum.equals(otaPackage.getChecksum())) {  
254 - throw new DataValidationException("Wrong otaPackage file!"); 276 + if (!currentChecksum.equals(otaPackage.getChecksum())) {
  277 + throw new DataValidationException("Wrong otaPackage file!");
  278 + }
  279 + } else {
  280 + //TODO: validate url
255 } 281 }
256 } 282 }
257 283
@@ -264,6 +290,13 @@ public class BaseOtaPackageService implements OtaPackageService { @@ -264,6 +290,13 @@ public class BaseOtaPackageService implements OtaPackageService {
264 if (otaPackageOld.getData() != null && !otaPackageOld.getData().equals(otaPackage.getData())) { 290 if (otaPackageOld.getData() != null && !otaPackageOld.getData().equals(otaPackage.getData())) {
265 throw new DataValidationException("Updating otaPackage data is prohibited!"); 291 throw new DataValidationException("Updating otaPackage data is prohibited!");
266 } 292 }
  293 +
  294 + if (otaPackageOld.getData() == null && otaPackage.getData() != null) {
  295 + DefaultTenantProfileConfiguration profileConfiguration =
  296 + (DefaultTenantProfileConfiguration) tenantProfileCache.get(tenantId).getProfileData().getConfiguration();
  297 + long maxOtaPackagesInBytes = profileConfiguration.getMaxOtaPackagesInBytes();
  298 + validateMaxSumDataSizePerTenant(tenantId, otaPackageDao, maxOtaPackagesInBytes, otaPackage.getDataSize(), OTA_PACKAGE);
  299 + }
267 } 300 }
268 }; 301 };
269 302
@@ -16,8 +16,11 @@ @@ -16,8 +16,11 @@
16 package org.thingsboard.server.dao.ota; 16 package org.thingsboard.server.dao.ota;
17 17
18 import org.thingsboard.server.common.data.OtaPackage; 18 import org.thingsboard.server.common.data.OtaPackage;
  19 +import org.thingsboard.server.common.data.id.TenantId;
19 import org.thingsboard.server.dao.Dao; 20 import org.thingsboard.server.dao.Dao;
  21 +import org.thingsboard.server.dao.TenantEntityDao;
  22 +import org.thingsboard.server.dao.TenantEntityWithDataDao;
20 23
21 -public interface OtaPackageDao extends Dao<OtaPackage> {  
22 - 24 +public interface OtaPackageDao extends Dao<OtaPackage>, TenantEntityWithDataDao {
  25 + Long sumDataSizeByTenantId(TenantId tenantId);
23 } 26 }
@@ -28,7 +28,7 @@ public interface OtaPackageInfoDao extends Dao<OtaPackageInfo> { @@ -28,7 +28,7 @@ public interface OtaPackageInfoDao extends Dao<OtaPackageInfo> {
28 28
29 PageData<OtaPackageInfo> findOtaPackageInfoByTenantId(TenantId tenantId, PageLink pageLink); 29 PageData<OtaPackageInfo> findOtaPackageInfoByTenantId(TenantId tenantId, PageLink pageLink);
30 30
31 - PageData<OtaPackageInfo> findOtaPackageInfoByTenantIdAndDeviceProfileIdAndTypeAndHasData(TenantId tenantId, DeviceProfileId deviceProfileId, OtaPackageType otaPackageType, boolean hasData, PageLink pageLink); 31 + PageData<OtaPackageInfo> findOtaPackageInfoByTenantIdAndDeviceProfileIdAndTypeAndHasData(TenantId tenantId, DeviceProfileId deviceProfileId, OtaPackageType otaPackageType, PageLink pageLink);
32 32
33 boolean isOtaPackageUsed(OtaPackageId otaPackageId, OtaPackageType otaPackageType, DeviceProfileId deviceProfileId); 33 boolean isOtaPackageUsed(OtaPackageId otaPackageId, OtaPackageType otaPackageType, DeviceProfileId deviceProfileId);
34 34
@@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture; @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture;
19 import lombok.extern.slf4j.Slf4j; 19 import lombok.extern.slf4j.Slf4j;
20 import org.apache.commons.lang3.StringUtils; 20 import org.apache.commons.lang3.StringUtils;
21 import org.hibernate.exception.ConstraintViolationException; 21 import org.hibernate.exception.ConstraintViolationException;
  22 +import org.springframework.context.annotation.Lazy;
22 import org.springframework.stereotype.Service; 23 import org.springframework.stereotype.Service;
23 import org.thingsboard.server.common.data.ResourceType; 24 import org.thingsboard.server.common.data.ResourceType;
24 import org.thingsboard.server.common.data.TbResource; 25 import org.thingsboard.server.common.data.TbResource;
@@ -28,16 +29,19 @@ import org.thingsboard.server.common.data.id.TbResourceId; @@ -28,16 +29,19 @@ import org.thingsboard.server.common.data.id.TbResourceId;
28 import org.thingsboard.server.common.data.id.TenantId; 29 import org.thingsboard.server.common.data.id.TenantId;
29 import org.thingsboard.server.common.data.page.PageData; 30 import org.thingsboard.server.common.data.page.PageData;
30 import org.thingsboard.server.common.data.page.PageLink; 31 import org.thingsboard.server.common.data.page.PageLink;
  32 +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
31 import org.thingsboard.server.dao.exception.DataValidationException; 33 import org.thingsboard.server.dao.exception.DataValidationException;
32 import org.thingsboard.server.dao.model.ModelConstants; 34 import org.thingsboard.server.dao.model.ModelConstants;
33 import org.thingsboard.server.dao.service.DataValidator; 35 import org.thingsboard.server.dao.service.DataValidator;
34 import org.thingsboard.server.dao.service.PaginatedRemover; 36 import org.thingsboard.server.dao.service.PaginatedRemover;
35 import org.thingsboard.server.dao.service.Validator; 37 import org.thingsboard.server.dao.service.Validator;
  38 +import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
36 import org.thingsboard.server.dao.tenant.TenantDao; 39 import org.thingsboard.server.dao.tenant.TenantDao;
37 40
38 import java.util.List; 41 import java.util.List;
39 import java.util.Optional; 42 import java.util.Optional;
40 43
  44 +import static org.thingsboard.server.common.data.EntityType.TB_RESOURCE;
41 import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID; 45 import static org.thingsboard.server.dao.device.DeviceServiceImpl.INCORRECT_TENANT_ID;
42 import static org.thingsboard.server.dao.service.Validator.validateId; 46 import static org.thingsboard.server.dao.service.Validator.validateId;
43 47
@@ -49,12 +53,13 @@ public class BaseResourceService implements ResourceService { @@ -49,12 +53,13 @@ public class BaseResourceService implements ResourceService {
49 private final TbResourceDao resourceDao; 53 private final TbResourceDao resourceDao;
50 private final TbResourceInfoDao resourceInfoDao; 54 private final TbResourceInfoDao resourceInfoDao;
51 private final TenantDao tenantDao; 55 private final TenantDao tenantDao;
  56 + private final TbTenantProfileCache tenantProfileCache;
52 57
53 -  
54 - public BaseResourceService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, TenantDao tenantDao) { 58 + public BaseResourceService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, TenantDao tenantDao, @Lazy TbTenantProfileCache tenantProfileCache) {
55 this.resourceDao = resourceDao; 59 this.resourceDao = resourceDao;
56 this.resourceInfoDao = resourceInfoDao; 60 this.resourceInfoDao = resourceInfoDao;
57 this.tenantDao = tenantDao; 61 this.tenantDao = tenantDao;
  62 + this.tenantProfileCache = tenantProfileCache;
58 } 63 }
59 64
60 @Override 65 @Override
@@ -143,9 +148,24 @@ public class BaseResourceService implements ResourceService { @@ -143,9 +148,24 @@ public class BaseResourceService implements ResourceService {
143 tenantResourcesRemover.removeEntities(tenantId, tenantId); 148 tenantResourcesRemover.removeEntities(tenantId, tenantId);
144 } 149 }
145 150
  151 + @Override
  152 + public long sumDataSizeByTenantId(TenantId tenantId) {
  153 + return resourceDao.sumDataSizeByTenantId(tenantId);
  154 + }
  155 +
146 private DataValidator<TbResource> resourceValidator = new DataValidator<>() { 156 private DataValidator<TbResource> resourceValidator = new DataValidator<>() {
147 157
148 @Override 158 @Override
  159 + protected void validateCreate(TenantId tenantId, TbResource resource) {
  160 + if (tenantId != null && !TenantId.SYS_TENANT_ID.equals(tenantId) ) {
  161 + DefaultTenantProfileConfiguration profileConfiguration =
  162 + (DefaultTenantProfileConfiguration) tenantProfileCache.get(tenantId).getProfileData().getConfiguration();
  163 + long maxSumResourcesDataInBytes = profileConfiguration.getMaxResourcesInBytes();
  164 + validateMaxSumDataSizePerTenant(tenantId, resourceDao, maxSumResourcesDataInBytes, resource.getData().length(), TB_RESOURCE);
  165 + }
  166 + }
  167 +
  168 + @Override
149 protected void validateDataImpl(TenantId tenantId, TbResource resource) { 169 protected void validateDataImpl(TenantId tenantId, TbResource resource) {
150 if (StringUtils.isEmpty(resource.getTitle())) { 170 if (StringUtils.isEmpty(resource.getTitle())) {
151 throw new DataValidationException("Resource title should be specified!"); 171 throw new DataValidationException("Resource title should be specified!");
@@ -21,10 +21,11 @@ import org.thingsboard.server.common.data.id.TenantId; @@ -21,10 +21,11 @@ import org.thingsboard.server.common.data.id.TenantId;
21 import org.thingsboard.server.common.data.page.PageData; 21 import org.thingsboard.server.common.data.page.PageData;
22 import org.thingsboard.server.common.data.page.PageLink; 22 import org.thingsboard.server.common.data.page.PageLink;
23 import org.thingsboard.server.dao.Dao; 23 import org.thingsboard.server.dao.Dao;
  24 +import org.thingsboard.server.dao.TenantEntityWithDataDao;
24 25
25 import java.util.List; 26 import java.util.List;
26 27
27 -public interface TbResourceDao extends Dao<TbResource> { 28 +public interface TbResourceDao extends Dao<TbResource>, TenantEntityWithDataDao {
28 29
29 TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceId); 30 TbResource getResource(TenantId tenantId, ResourceType resourceType, String resourceId);
30 31
@@ -23,8 +23,10 @@ import org.hibernate.validator.cfg.ConstraintMapping; @@ -23,8 +23,10 @@ import org.hibernate.validator.cfg.ConstraintMapping;
23 import org.thingsboard.server.common.data.BaseData; 23 import org.thingsboard.server.common.data.BaseData;
24 import org.thingsboard.server.common.data.EntityType; 24 import org.thingsboard.server.common.data.EntityType;
25 import org.thingsboard.server.common.data.id.TenantId; 25 import org.thingsboard.server.common.data.id.TenantId;
  26 +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
26 import org.thingsboard.server.common.data.validation.NoXss; 27 import org.thingsboard.server.common.data.validation.NoXss;
27 import org.thingsboard.server.dao.TenantEntityDao; 28 import org.thingsboard.server.dao.TenantEntityDao;
  29 +import org.thingsboard.server.dao.TenantEntityWithDataDao;
28 import org.thingsboard.server.dao.exception.DataValidationException; 30 import org.thingsboard.server.dao.exception.DataValidationException;
29 31
30 import javax.validation.ConstraintViolation; 32 import javax.validation.ConstraintViolation;
@@ -123,6 +125,19 @@ public abstract class DataValidator<D extends BaseData<?>> { @@ -123,6 +125,19 @@ public abstract class DataValidator<D extends BaseData<?>> {
123 } 125 }
124 } 126 }
125 127
  128 + protected void validateMaxSumDataSizePerTenant(TenantId tenantId,
  129 + TenantEntityWithDataDao dataDao,
  130 + long maxSumDataSize,
  131 + long currentDataSize,
  132 + EntityType entityType) {
  133 + if (maxSumDataSize > 0) {
  134 + if (dataDao.sumDataSizeByTenantId(tenantId) + currentDataSize > maxSumDataSize) {
  135 + throw new DataValidationException(String.format("Failed to create the %s, files size limit is exhausted %d bytes!",
  136 + entityType.name().toLowerCase().replaceAll("_", " "), maxSumDataSize));
  137 + }
  138 + }
  139 + }
  140 +
126 protected static void validateJsonStructure(JsonNode expectedNode, JsonNode actualNode) { 141 protected static void validateJsonStructure(JsonNode expectedNode, JsonNode actualNode) {
127 Set<String> expectedFields = new HashSet<>(); 142 Set<String> expectedFields = new HashSet<>();
128 Iterator<String> fieldsIterator = expectedNode.fieldNames(); 143 Iterator<String> fieldsIterator = expectedNode.fieldNames();
@@ -26,6 +26,7 @@ import org.thingsboard.server.dao.Dao; @@ -26,6 +26,7 @@ import org.thingsboard.server.dao.Dao;
26 import org.thingsboard.server.dao.DaoUtil; 26 import org.thingsboard.server.dao.DaoUtil;
27 import org.thingsboard.server.dao.model.BaseEntity; 27 import org.thingsboard.server.dao.model.BaseEntity;
28 28
  29 +import java.util.Collection;
29 import java.util.List; 30 import java.util.List;
30 import java.util.Optional; 31 import java.util.Optional;
31 import java.util.UUID; 32 import java.util.UUID;
@@ -87,6 +88,12 @@ public abstract class JpaAbstractDao<E extends BaseEntity<D>, D> @@ -87,6 +88,12 @@ public abstract class JpaAbstractDao<E extends BaseEntity<D>, D>
87 return !getCrudRepository().existsById(id); 88 return !getCrudRepository().existsById(id);
88 } 89 }
89 90
  91 + @Transactional
  92 + public void removeAllByIds(Collection<UUID> ids) {
  93 + CrudRepository<E, UUID> repository = getCrudRepository();
  94 + ids.forEach(repository::deleteById);
  95 + }
  96 +
90 @Override 97 @Override
91 public List<D> find(TenantId tenantId) { 98 public List<D> find(TenantId tenantId) {
92 List<E> entities = Lists.newArrayList(getCrudRepository().findAll()); 99 List<E> entities = Lists.newArrayList(getCrudRepository().findAll());
@@ -17,11 +17,13 @@ package org.thingsboard.server.dao.sql.alarm; @@ -17,11 +17,13 @@ package org.thingsboard.server.dao.sql.alarm;
17 17
18 import org.springframework.data.domain.Page; 18 import org.springframework.data.domain.Page;
19 import org.springframework.data.domain.Pageable; 19 import org.springframework.data.domain.Pageable;
  20 +import org.springframework.data.jpa.repository.Modifying;
20 import org.springframework.data.jpa.repository.Query; 21 import org.springframework.data.jpa.repository.Query;
21 import org.springframework.data.repository.CrudRepository; 22 import org.springframework.data.repository.CrudRepository;
22 import org.springframework.data.repository.query.Param; 23 import org.springframework.data.repository.query.Param;
23 import org.thingsboard.server.common.data.alarm.AlarmSeverity; 24 import org.thingsboard.server.common.data.alarm.AlarmSeverity;
24 import org.thingsboard.server.common.data.alarm.AlarmStatus; 25 import org.thingsboard.server.common.data.alarm.AlarmStatus;
  26 +import org.thingsboard.server.common.data.id.TenantId;
25 import org.thingsboard.server.dao.model.sql.AlarmEntity; 27 import org.thingsboard.server.dao.model.sql.AlarmEntity;
26 import org.thingsboard.server.dao.model.sql.AlarmInfoEntity; 28 import org.thingsboard.server.dao.model.sql.AlarmInfoEntity;
27 29
@@ -159,4 +161,8 @@ public interface AlarmRepository extends CrudRepository<AlarmEntity, UUID> { @@ -159,4 +161,8 @@ public interface AlarmRepository extends CrudRepository<AlarmEntity, UUID> {
159 @Param("affectedEntityId") UUID affectedEntityId, 161 @Param("affectedEntityId") UUID affectedEntityId,
160 @Param("affectedEntityType") String affectedEntityType, 162 @Param("affectedEntityType") String affectedEntityType,
161 @Param("alarmStatuses") Set<AlarmStatus> alarmStatuses); 163 @Param("alarmStatuses") Set<AlarmStatus> alarmStatuses);
  164 +
  165 + @Query("SELECT a.id FROM AlarmEntity a WHERE a.tenantId = :tenantId AND a.createdTime < :time AND a.endTs < :time")
  166 + Page<UUID> findAlarmsIdsByEndTsBeforeAndTenantId(@Param("time") Long time, @Param("tenantId") UUID tenantId, Pageable pageable);
  167 +
162 } 168 }
@@ -26,10 +26,12 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo; @@ -26,10 +26,12 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo;
26 import org.thingsboard.server.common.data.alarm.AlarmQuery; 26 import org.thingsboard.server.common.data.alarm.AlarmQuery;
27 import org.thingsboard.server.common.data.alarm.AlarmSeverity; 27 import org.thingsboard.server.common.data.alarm.AlarmSeverity;
28 import org.thingsboard.server.common.data.alarm.AlarmStatus; 28 import org.thingsboard.server.common.data.alarm.AlarmStatus;
  29 +import org.thingsboard.server.common.data.id.AlarmId;
29 import org.thingsboard.server.common.data.id.CustomerId; 30 import org.thingsboard.server.common.data.id.CustomerId;
30 import org.thingsboard.server.common.data.id.EntityId; 31 import org.thingsboard.server.common.data.id.EntityId;
31 import org.thingsboard.server.common.data.id.TenantId; 32 import org.thingsboard.server.common.data.id.TenantId;
32 import org.thingsboard.server.common.data.page.PageData; 33 import org.thingsboard.server.common.data.page.PageData;
  34 +import org.thingsboard.server.common.data.page.PageLink;
33 import org.thingsboard.server.common.data.query.AlarmData; 35 import org.thingsboard.server.common.data.query.AlarmData;
34 import org.thingsboard.server.common.data.query.AlarmDataQuery; 36 import org.thingsboard.server.common.data.query.AlarmDataQuery;
35 import org.thingsboard.server.dao.DaoUtil; 37 import org.thingsboard.server.dao.DaoUtil;
@@ -161,4 +163,10 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A @@ -161,4 +163,10 @@ public class JpaAlarmDao extends JpaAbstractDao<AlarmEntity, Alarm> implements A
161 public Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, Set<AlarmStatus> statuses) { 163 public Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, Set<AlarmStatus> statuses) {
162 return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), entityId.getEntityType().name(), statuses); 164 return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), entityId.getEntityType().name(), statuses);
163 } 165 }
  166 +
  167 + @Override
  168 + public PageData<AlarmId> findAlarmsIdsByEndTsBeforeAndTenantId(Long time, TenantId tenantId, PageLink pageLink) {
  169 + return DaoUtil.pageToPageData(alarmRepository.findAlarmsIdsByEndTsBeforeAndTenantId(time, tenantId.getId(), DaoUtil.toPageable(pageLink)))
  170 + .mapData(AlarmId::new);
  171 + }
164 } 172 }
@@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired;
20 import org.springframework.data.repository.CrudRepository; 20 import org.springframework.data.repository.CrudRepository;
21 import org.springframework.stereotype.Component; 21 import org.springframework.stereotype.Component;
22 import org.thingsboard.server.common.data.OtaPackage; 22 import org.thingsboard.server.common.data.OtaPackage;
  23 +import org.thingsboard.server.common.data.id.TenantId;
23 import org.thingsboard.server.dao.ota.OtaPackageDao; 24 import org.thingsboard.server.dao.ota.OtaPackageDao;
24 import org.thingsboard.server.dao.model.sql.OtaPackageEntity; 25 import org.thingsboard.server.dao.model.sql.OtaPackageEntity;
25 import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; 26 import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao;
@@ -43,4 +44,8 @@ public class JpaOtaPackageDao extends JpaAbstractSearchTextDao<OtaPackageEntity, @@ -43,4 +44,8 @@ public class JpaOtaPackageDao extends JpaAbstractSearchTextDao<OtaPackageEntity,
43 return otaPackageRepository; 44 return otaPackageRepository;
44 } 45 }
45 46
  47 + @Override
  48 + public Long sumDataSizeByTenantId(TenantId tenantId) {
  49 + return otaPackageRepository.sumDataSizeByTenantId(tenantId.getId());
  50 + }
46 } 51 }
@@ -76,13 +76,12 @@ public class JpaOtaPackageInfoDao extends JpaAbstractSearchTextDao<OtaPackageInf @@ -76,13 +76,12 @@ public class JpaOtaPackageInfoDao extends JpaAbstractSearchTextDao<OtaPackageInf
76 } 76 }
77 77
78 @Override 78 @Override
79 - public PageData<OtaPackageInfo> findOtaPackageInfoByTenantIdAndDeviceProfileIdAndTypeAndHasData(TenantId tenantId, DeviceProfileId deviceProfileId, OtaPackageType otaPackageType, boolean hasData, PageLink pageLink) { 79 + public PageData<OtaPackageInfo> findOtaPackageInfoByTenantIdAndDeviceProfileIdAndTypeAndHasData(TenantId tenantId, DeviceProfileId deviceProfileId, OtaPackageType otaPackageType, PageLink pageLink) {
80 return DaoUtil.toPageData(otaPackageInfoRepository 80 return DaoUtil.toPageData(otaPackageInfoRepository
81 .findAllByTenantIdAndTypeAndDeviceProfileIdAndHasData( 81 .findAllByTenantIdAndTypeAndDeviceProfileIdAndHasData(
82 tenantId.getId(), 82 tenantId.getId(),
83 deviceProfileId.getId(), 83 deviceProfileId.getId(),
84 otaPackageType, 84 otaPackageType,
85 - hasData,  
86 Objects.toString(pageLink.getTextSearch(), ""), 85 Objects.toString(pageLink.getTextSearch(), ""),
87 DaoUtil.toPageable(pageLink))); 86 DaoUtil.toPageable(pageLink)));
88 } 87 }
@@ -26,27 +26,26 @@ import org.thingsboard.server.dao.model.sql.OtaPackageInfoEntity; @@ -26,27 +26,26 @@ import org.thingsboard.server.dao.model.sql.OtaPackageInfoEntity;
26 import java.util.UUID; 26 import java.util.UUID;
27 27
28 public interface OtaPackageInfoRepository extends CrudRepository<OtaPackageInfoEntity, UUID> { 28 public interface OtaPackageInfoRepository extends CrudRepository<OtaPackageInfoEntity, UUID> {
29 - @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, f.data IS NOT NULL) FROM OtaPackageEntity f WHERE " + 29 + @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, CASE WHEN (f.data IS NOT NULL OR f.url IS NOT NULL) THEN true ELSE false END) FROM OtaPackageEntity f WHERE " +
30 "f.tenantId = :tenantId " + 30 "f.tenantId = :tenantId " +
31 "AND LOWER(f.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") 31 "AND LOWER(f.searchText) LIKE LOWER(CONCAT(:searchText, '%'))")
32 Page<OtaPackageInfoEntity> findAllByTenantId(@Param("tenantId") UUID tenantId, 32 Page<OtaPackageInfoEntity> findAllByTenantId(@Param("tenantId") UUID tenantId,
33 @Param("searchText") String searchText, 33 @Param("searchText") String searchText,
34 Pageable pageable); 34 Pageable pageable);
35 35
36 - @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, f.data IS NOT NULL) FROM OtaPackageEntity f WHERE " + 36 + @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, true) FROM OtaPackageEntity f WHERE " +
37 "f.tenantId = :tenantId " + 37 "f.tenantId = :tenantId " +
38 "AND f.deviceProfileId = :deviceProfileId " + 38 "AND f.deviceProfileId = :deviceProfileId " +
39 "AND f.type = :type " + 39 "AND f.type = :type " +
40 - "AND ((f.data IS NOT NULL AND :hasData = true) OR (f.data IS NULL AND :hasData = false ))" + 40 + "AND (f.data IS NOT NULL OR f.url IS NOT NULL) " +
41 "AND LOWER(f.searchText) LIKE LOWER(CONCAT(:searchText, '%'))") 41 "AND LOWER(f.searchText) LIKE LOWER(CONCAT(:searchText, '%'))")
42 Page<OtaPackageInfoEntity> findAllByTenantIdAndTypeAndDeviceProfileIdAndHasData(@Param("tenantId") UUID tenantId, 42 Page<OtaPackageInfoEntity> findAllByTenantIdAndTypeAndDeviceProfileIdAndHasData(@Param("tenantId") UUID tenantId,
43 @Param("deviceProfileId") UUID deviceProfileId, 43 @Param("deviceProfileId") UUID deviceProfileId,
44 @Param("type") OtaPackageType type, 44 @Param("type") OtaPackageType type,
45 - @Param("hasData") boolean hasData,  
46 @Param("searchText") String searchText, 45 @Param("searchText") String searchText,
47 Pageable pageable); 46 Pageable pageable);
48 47
49 - @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, f.data IS NOT NULL) FROM OtaPackageEntity f WHERE f.id = :id") 48 + @Query("SELECT new OtaPackageInfoEntity(f.id, f.createdTime, f.tenantId, f.deviceProfileId, f.type, f.title, f.version, f.url, f.fileName, f.contentType, f.checksumAlgorithm, f.checksum, f.dataSize, f.additionalInfo, CASE WHEN (f.data IS NOT NULL OR f.url IS NOT NULL) THEN true ELSE false END) FROM OtaPackageEntity f WHERE f.id = :id")
50 OtaPackageInfoEntity findOtaPackageInfoById(@Param("id") UUID id); 49 OtaPackageInfoEntity findOtaPackageInfoById(@Param("id") UUID id);
51 50
52 @Query(value = "SELECT exists(SELECT * " + 51 @Query(value = "SELECT exists(SELECT * " +
@@ -15,10 +15,15 @@ @@ -15,10 +15,15 @@
15 */ 15 */
16 package org.thingsboard.server.dao.sql.ota; 16 package org.thingsboard.server.dao.sql.ota;
17 17
  18 +import org.springframework.data.jpa.repository.Query;
18 import org.springframework.data.repository.CrudRepository; 19 import org.springframework.data.repository.CrudRepository;
  20 +import org.springframework.data.repository.query.Param;
19 import org.thingsboard.server.dao.model.sql.OtaPackageEntity; 21 import org.thingsboard.server.dao.model.sql.OtaPackageEntity;
  22 +import org.thingsboard.server.dao.model.sql.OtaPackageInfoEntity;
20 23
21 import java.util.UUID; 24 import java.util.UUID;
22 25
23 public interface OtaPackageRepository extends CrudRepository<OtaPackageEntity, UUID> { 26 public interface OtaPackageRepository extends CrudRepository<OtaPackageEntity, UUID> {
  27 + @Query(value = "SELECT COALESCE(SUM(ota.data_size), 0) FROM ota_package ota WHERE ota.tenant_id = :tenantId AND ota.data IS NOT NULL", nativeQuery = true)
  28 + Long sumDataSizeByTenantId(@Param("tenantId") UUID tenantId);
24 } 29 }
@@ -92,4 +92,8 @@ public class JpaTbResourceDao extends JpaAbstractSearchTextDao<TbResourceEntity, @@ -92,4 +92,8 @@ public class JpaTbResourceDao extends JpaAbstractSearchTextDao<TbResourceEntity,
92 resourceType.name(), objectIds)); 92 resourceType.name(), objectIds));
93 } 93 }
94 94
  95 + @Override
  96 + public Long sumDataSizeByTenantId(TenantId tenantId) {
  97 + return resourceRepository.sumDataSizeByTenantId(tenantId.getId());
  98 + }
95 } 99 }
@@ -77,4 +77,7 @@ public interface TbResourceRepository extends CrudRepository<TbResourceEntity, U @@ -77,4 +77,7 @@ public interface TbResourceRepository extends CrudRepository<TbResourceEntity, U
77 @Param("systemAdminId") UUID sysAdminId, 77 @Param("systemAdminId") UUID sysAdminId,
78 @Param("resourceType") String resourceType, 78 @Param("resourceType") String resourceType,
79 @Param("resourceIds") String[] objectIds); 79 @Param("resourceIds") String[] objectIds);
  80 +
  81 + @Query(value = "SELECT COALESCE(SUM(LENGTH(r.data)), 0) FROM resource r WHERE r.tenant_id = :tenantId", nativeQuery = true)
  82 + Long sumDataSizeByTenantId(@Param("tenantId") UUID tenantId);
80 } 83 }
@@ -74,4 +74,10 @@ public class JpaTenantDao extends JpaAbstractSearchTextDao<TenantEntity, Tenant> @@ -74,4 +74,10 @@ public class JpaTenantDao extends JpaAbstractSearchTextDao<TenantEntity, Tenant>
74 Objects.toString(pageLink.getTextSearch(), ""), 74 Objects.toString(pageLink.getTextSearch(), ""),
75 DaoUtil.toPageable(pageLink, TenantInfoEntity.tenantInfoColumnMap))); 75 DaoUtil.toPageable(pageLink, TenantInfoEntity.tenantInfoColumnMap)));
76 } 76 }
  77 +
  78 + @Override
  79 + public PageData<TenantId> findTenantsIds(PageLink pageLink) {
  80 + return DaoUtil.pageToPageData(tenantRepository.findTenantsIds(DaoUtil.toPageable(pageLink))).mapData(TenantId::new);
  81 + }
  82 +
77 } 83 }
@@ -50,4 +50,8 @@ public interface TenantRepository extends PagingAndSortingRepository<TenantEntit @@ -50,4 +50,8 @@ public interface TenantRepository extends PagingAndSortingRepository<TenantEntit
50 Page<TenantInfoEntity> findTenantInfoByRegionNextPage(@Param("region") String region, 50 Page<TenantInfoEntity> findTenantInfoByRegionNextPage(@Param("region") String region,
51 @Param("textSearch") String textSearch, 51 @Param("textSearch") String textSearch,
52 Pageable pageable); 52 Pageable pageable);
  53 +
  54 + @Query("SELECT t.id FROM TenantEntity t")
  55 + Page<UUID> findTenantsIds(Pageable pageable);
  56 +
53 } 57 }
@@ -46,5 +46,7 @@ public interface TenantDao extends Dao<Tenant> { @@ -46,5 +46,7 @@ public interface TenantDao extends Dao<Tenant> {
46 PageData<Tenant> findTenantsByRegion(TenantId tenantId, String region, PageLink pageLink); 46 PageData<Tenant> findTenantsByRegion(TenantId tenantId, String region, PageLink pageLink);
47 47
48 PageData<TenantInfo> findTenantInfosByRegion(TenantId tenantId, String region, PageLink pageLink); 48 PageData<TenantInfo> findTenantInfosByRegion(TenantId tenantId, String region, PageLink pageLink);
49 - 49 +
  50 + PageData<TenantId> findTenantsIds(PageLink pageLink);
  51 +
50 } 52 }
@@ -168,6 +168,7 @@ CREATE TABLE IF NOT EXISTS ota_package ( @@ -168,6 +168,7 @@ CREATE TABLE IF NOT EXISTS ota_package (
168 type varchar(32) NOT NULL, 168 type varchar(32) NOT NULL,
169 title varchar(255) NOT NULL, 169 title varchar(255) NOT NULL,
170 version varchar(255) NOT NULL, 170 version varchar(255) NOT NULL,
  171 + url varchar(255),
171 file_name varchar(255), 172 file_name varchar(255),
172 content_type varchar(255), 173 content_type varchar(255),
173 checksum_algorithm varchar(32), 174 checksum_algorithm varchar(32),
@@ -186,6 +186,7 @@ CREATE TABLE IF NOT EXISTS ota_package ( @@ -186,6 +186,7 @@ CREATE TABLE IF NOT EXISTS ota_package (
186 type varchar(32) NOT NULL, 186 type varchar(32) NOT NULL,
187 title varchar(255) NOT NULL, 187 title varchar(255) NOT NULL,
188 version varchar(255) NOT NULL, 188 version varchar(255) NOT NULL,
  189 + url varchar(255),
189 file_name varchar(255), 190 file_name varchar(255),
190 content_type varchar(255), 191 content_type varchar(255),
191 checksum_algorithm varchar(32), 192 checksum_algorithm varchar(32),
@@ -59,6 +59,7 @@ import org.thingsboard.server.dao.relation.RelationService; @@ -59,6 +59,7 @@ import org.thingsboard.server.dao.relation.RelationService;
59 import org.thingsboard.server.dao.resource.ResourceService; 59 import org.thingsboard.server.dao.resource.ResourceService;
60 import org.thingsboard.server.dao.rule.RuleChainService; 60 import org.thingsboard.server.dao.rule.RuleChainService;
61 import org.thingsboard.server.dao.settings.AdminSettingsService; 61 import org.thingsboard.server.dao.settings.AdminSettingsService;
  62 +import org.thingsboard.server.dao.tenant.DefaultTbTenantProfileCache;
62 import org.thingsboard.server.dao.tenant.TenantProfileService; 63 import org.thingsboard.server.dao.tenant.TenantProfileService;
63 import org.thingsboard.server.dao.tenant.TenantService; 64 import org.thingsboard.server.dao.tenant.TenantService;
64 import org.thingsboard.server.dao.timeseries.TimeseriesService; 65 import org.thingsboard.server.dao.timeseries.TimeseriesService;
@@ -158,10 +159,12 @@ public abstract class AbstractServiceTest { @@ -158,10 +159,12 @@ public abstract class AbstractServiceTest {
158 @Autowired 159 @Autowired
159 protected ResourceService resourceService; 160 protected ResourceService resourceService;
160 161
161 -  
162 @Autowired 162 @Autowired
163 protected OtaPackageService otaPackageService; 163 protected OtaPackageService otaPackageService;
164 164
  165 + @Autowired
  166 + protected DefaultTbTenantProfileCache tenantProfileCache;
  167 +
165 public class IdComparator<D extends HasId> implements Comparator<D> { 168 public class IdComparator<D extends HasId> implements Comparator<D> {
166 @Override 169 @Override
167 public int compare(D o1, D o2) { 170 public int compare(D o1, D o2) {
@@ -28,11 +28,13 @@ import org.thingsboard.server.common.data.DeviceProfile; @@ -28,11 +28,13 @@ import org.thingsboard.server.common.data.DeviceProfile;
28 import org.thingsboard.server.common.data.OtaPackage; 28 import org.thingsboard.server.common.data.OtaPackage;
29 import org.thingsboard.server.common.data.OtaPackageInfo; 29 import org.thingsboard.server.common.data.OtaPackageInfo;
30 import org.thingsboard.server.common.data.Tenant; 30 import org.thingsboard.server.common.data.Tenant;
31 -import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; 31 +import org.thingsboard.server.common.data.TenantProfile;
32 import org.thingsboard.server.common.data.id.DeviceProfileId; 32 import org.thingsboard.server.common.data.id.DeviceProfileId;
33 import org.thingsboard.server.common.data.id.TenantId; 33 import org.thingsboard.server.common.data.id.TenantId;
  34 +import org.thingsboard.server.common.data.ota.ChecksumAlgorithm;
34 import org.thingsboard.server.common.data.page.PageData; 35 import org.thingsboard.server.common.data.page.PageData;
35 import org.thingsboard.server.common.data.page.PageLink; 36 import org.thingsboard.server.common.data.page.PageLink;
  37 +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
36 import org.thingsboard.server.dao.exception.DataValidationException; 38 import org.thingsboard.server.dao.exception.DataValidationException;
37 39
38 import java.nio.ByteBuffer; 40 import java.nio.ByteBuffer;
@@ -50,7 +52,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { @@ -50,7 +52,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest {
50 private static final String CONTENT_TYPE = "text/plain"; 52 private static final String CONTENT_TYPE = "text/plain";
51 private static final ChecksumAlgorithm CHECKSUM_ALGORITHM = ChecksumAlgorithm.SHA256; 53 private static final ChecksumAlgorithm CHECKSUM_ALGORITHM = ChecksumAlgorithm.SHA256;
52 private static final String CHECKSUM = "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a"; 54 private static final String CHECKSUM = "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a";
53 - private static final ByteBuffer DATA = ByteBuffer.wrap(new byte[]{1}); 55 + private static final long DATA_SIZE = 1L;
  56 + private static final ByteBuffer DATA = ByteBuffer.wrap(new byte[]{(int) DATA_SIZE});
  57 + private static final String URL = "http://firmware.test.org";
54 58
55 private IdComparator<OtaPackageInfo> idComparator = new IdComparator<>(); 59 private IdComparator<OtaPackageInfo> idComparator = new IdComparator<>();
56 60
@@ -78,6 +82,41 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { @@ -78,6 +82,41 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest {
78 @After 82 @After
79 public void after() { 83 public void after() {
80 tenantService.deleteTenant(tenantId); 84 tenantService.deleteTenant(tenantId);
  85 + tenantProfileService.deleteTenantProfiles(tenantId);
  86 + }
  87 +
  88 + @Test
  89 + public void testSaveOtaPackageWithMaxSumDataSizeOutOfLimit() {
  90 + TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId);
  91 + defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxOtaPackagesInBytes(DATA_SIZE).build());
  92 + tenantProfileService.saveTenantProfile(tenantId, defaultTenantProfile);
  93 +
  94 + Assert.assertEquals(0, otaPackageService.sumDataSizeByTenantId(tenantId));
  95 +
  96 + createFirmware(tenantId, "1");
  97 + Assert.assertEquals(1, otaPackageService.sumDataSizeByTenantId(tenantId));
  98 +
  99 + thrown.expect(DataValidationException.class);
  100 + thrown.expectMessage(String.format("Failed to create the ota package, files size limit is exhausted %d bytes!", DATA_SIZE));
  101 + createFirmware(tenantId, "2");
  102 + }
  103 +
  104 + @Test
  105 + public void sumDataSizeByTenantId() {
  106 + Assert.assertEquals(0, otaPackageService.sumDataSizeByTenantId(tenantId));
  107 +
  108 + createFirmware(tenantId, "0.1");
  109 + Assert.assertEquals(1, otaPackageService.sumDataSizeByTenantId(tenantId));
  110 +
  111 + int maxSumDataSize = 8;
  112 + List<OtaPackage> packages = new ArrayList<>(maxSumDataSize);
  113 +
  114 + for (int i = 2; i <= maxSumDataSize; i++) {
  115 + packages.add(createFirmware(tenantId, "0." + i));
  116 + Assert.assertEquals(i, otaPackageService.sumDataSizeByTenantId(tenantId));
  117 + }
  118 +
  119 + Assert.assertEquals(maxSumDataSize, otaPackageService.sumDataSizeByTenantId(tenantId));
81 } 120 }
82 121
83 @Test 122 @Test
@@ -93,6 +132,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { @@ -93,6 +132,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest {
93 firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); 132 firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM);
94 firmware.setChecksum(CHECKSUM); 133 firmware.setChecksum(CHECKSUM);
95 firmware.setData(DATA); 134 firmware.setData(DATA);
  135 + firmware.setDataSize(DATA_SIZE);
96 OtaPackage savedFirmware = otaPackageService.saveOtaPackage(firmware); 136 OtaPackage savedFirmware = otaPackageService.saveOtaPackage(firmware);
97 137
98 Assert.assertNotNull(savedFirmware); 138 Assert.assertNotNull(savedFirmware);
@@ -114,6 +154,35 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { @@ -114,6 +154,35 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest {
114 } 154 }
115 155
116 @Test 156 @Test
  157 + public void testSaveFirmwareWithUrl() {
  158 + OtaPackageInfo firmware = new OtaPackageInfo();
  159 + firmware.setTenantId(tenantId);
  160 + firmware.setDeviceProfileId(deviceProfileId);
  161 + firmware.setType(FIRMWARE);
  162 + firmware.setTitle(TITLE);
  163 + firmware.setVersion(VERSION);
  164 + firmware.setUrl(URL);
  165 + firmware.setDataSize(0L);
  166 + OtaPackageInfo savedFirmware = otaPackageService.saveOtaPackageInfo(firmware);
  167 +
  168 + Assert.assertNotNull(savedFirmware);
  169 + Assert.assertNotNull(savedFirmware.getId());
  170 + Assert.assertTrue(savedFirmware.getCreatedTime() > 0);
  171 + Assert.assertEquals(firmware.getTenantId(), savedFirmware.getTenantId());
  172 + Assert.assertEquals(firmware.getTitle(), savedFirmware.getTitle());
  173 + Assert.assertEquals(firmware.getFileName(), savedFirmware.getFileName());
  174 + Assert.assertEquals(firmware.getContentType(), savedFirmware.getContentType());
  175 +
  176 + savedFirmware.setAdditionalInfo(JacksonUtil.newObjectNode());
  177 + otaPackageService.saveOtaPackageInfo(savedFirmware);
  178 +
  179 + OtaPackage foundFirmware = otaPackageService.findOtaPackageById(tenantId, savedFirmware.getId());
  180 + Assert.assertEquals(foundFirmware.getTitle(), savedFirmware.getTitle());
  181 +
  182 + otaPackageService.deleteOtaPackage(tenantId, savedFirmware.getId());
  183 + }
  184 +
  185 + @Test
117 public void testSaveFirmwareInfoAndUpdateWithData() { 186 public void testSaveFirmwareInfoAndUpdateWithData() {
118 OtaPackageInfo firmwareInfo = new OtaPackageInfo(); 187 OtaPackageInfo firmwareInfo = new OtaPackageInfo();
119 firmwareInfo.setTenantId(tenantId); 188 firmwareInfo.setTenantId(tenantId);
@@ -141,6 +210,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { @@ -141,6 +210,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest {
141 firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); 210 firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM);
142 firmware.setChecksum(CHECKSUM); 211 firmware.setChecksum(CHECKSUM);
143 firmware.setData(DATA); 212 firmware.setData(DATA);
  213 + firmware.setDataSize(DATA_SIZE);
144 214
145 otaPackageService.saveOtaPackage(firmware); 215 otaPackageService.saveOtaPackage(firmware);
146 216
@@ -345,50 +415,15 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { @@ -345,50 +415,15 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest {
345 415
346 @Test 416 @Test
347 public void testSaveFirmwareWithExistingTitleAndVersion() { 417 public void testSaveFirmwareWithExistingTitleAndVersion() {
348 - OtaPackage firmware = new OtaPackage();  
349 - firmware.setTenantId(tenantId);  
350 - firmware.setDeviceProfileId(deviceProfileId);  
351 - firmware.setType(FIRMWARE);  
352 - firmware.setTitle(TITLE);  
353 - firmware.setVersion(VERSION);  
354 - firmware.setFileName(FILE_NAME);  
355 - firmware.setContentType(CONTENT_TYPE);  
356 - firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM);  
357 - firmware.setChecksum(CHECKSUM);  
358 - firmware.setData(DATA);  
359 - otaPackageService.saveOtaPackage(firmware);  
360 -  
361 - OtaPackage newFirmware = new OtaPackage();  
362 - newFirmware.setTenantId(tenantId);  
363 - newFirmware.setDeviceProfileId(deviceProfileId);  
364 - newFirmware.setType(FIRMWARE);  
365 - newFirmware.setTitle(TITLE);  
366 - newFirmware.setVersion(VERSION);  
367 - newFirmware.setFileName(FILE_NAME);  
368 - newFirmware.setContentType(CONTENT_TYPE);  
369 - newFirmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM);  
370 - newFirmware.setChecksum(CHECKSUM);  
371 - newFirmware.setData(DATA);  
372 - 418 + createFirmware(tenantId, VERSION);
373 thrown.expect(DataValidationException.class); 419 thrown.expect(DataValidationException.class);
374 thrown.expectMessage("OtaPackage with such title and version already exists!"); 420 thrown.expectMessage("OtaPackage with such title and version already exists!");
375 - otaPackageService.saveOtaPackage(newFirmware); 421 + createFirmware(tenantId, VERSION);
376 } 422 }
377 423
378 @Test 424 @Test
379 public void testDeleteFirmwareWithReferenceByDevice() { 425 public void testDeleteFirmwareWithReferenceByDevice() {
380 - OtaPackage firmware = new OtaPackage();  
381 - firmware.setTenantId(tenantId);  
382 - firmware.setDeviceProfileId(deviceProfileId);  
383 - firmware.setType(FIRMWARE);  
384 - firmware.setTitle(TITLE);  
385 - firmware.setVersion(VERSION);  
386 - firmware.setFileName(FILE_NAME);  
387 - firmware.setContentType(CONTENT_TYPE);  
388 - firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM);  
389 - firmware.setChecksum(CHECKSUM);  
390 - firmware.setData(DATA);  
391 - OtaPackage savedFirmware = otaPackageService.saveOtaPackage(firmware); 426 + OtaPackage savedFirmware = createFirmware(tenantId, VERSION);
392 427
393 Device device = new Device(); 428 Device device = new Device();
394 device.setTenantId(tenantId); 429 device.setTenantId(tenantId);
@@ -409,18 +444,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { @@ -409,18 +444,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest {
409 444
410 @Test 445 @Test
411 public void testUpdateDeviceProfileId() { 446 public void testUpdateDeviceProfileId() {
412 - OtaPackage firmware = new OtaPackage();  
413 - firmware.setTenantId(tenantId);  
414 - firmware.setDeviceProfileId(deviceProfileId);  
415 - firmware.setType(FIRMWARE);  
416 - firmware.setTitle(TITLE);  
417 - firmware.setVersion(VERSION);  
418 - firmware.setFileName(FILE_NAME);  
419 - firmware.setContentType(CONTENT_TYPE);  
420 - firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM);  
421 - firmware.setChecksum(CHECKSUM);  
422 - firmware.setData(DATA);  
423 - OtaPackage savedFirmware = otaPackageService.saveOtaPackage(firmware); 447 + OtaPackage savedFirmware = createFirmware(tenantId, VERSION);
424 448
425 try { 449 try {
426 thrown.expect(DataValidationException.class); 450 thrown.expect(DataValidationException.class);
@@ -448,6 +472,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { @@ -448,6 +472,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest {
448 firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); 472 firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM);
449 firmware.setChecksum(CHECKSUM); 473 firmware.setChecksum(CHECKSUM);
450 firmware.setData(DATA); 474 firmware.setData(DATA);
  475 + firmware.setDataSize(DATA_SIZE);
451 OtaPackage savedFirmware = otaPackageService.saveOtaPackage(firmware); 476 OtaPackage savedFirmware = otaPackageService.saveOtaPackage(firmware);
452 477
453 savedDeviceProfile.setFirmwareId(savedFirmware.getId()); 478 savedDeviceProfile.setFirmwareId(savedFirmware.getId());
@@ -465,18 +490,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { @@ -465,18 +490,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest {
465 490
466 @Test 491 @Test
467 public void testFindFirmwareById() { 492 public void testFindFirmwareById() {
468 - OtaPackage firmware = new OtaPackage();  
469 - firmware.setTenantId(tenantId);  
470 - firmware.setDeviceProfileId(deviceProfileId);  
471 - firmware.setType(FIRMWARE);  
472 - firmware.setTitle(TITLE);  
473 - firmware.setVersion(VERSION);  
474 - firmware.setFileName(FILE_NAME);  
475 - firmware.setContentType(CONTENT_TYPE);  
476 - firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM);  
477 - firmware.setChecksum(CHECKSUM);  
478 - firmware.setData(DATA);  
479 - OtaPackage savedFirmware = otaPackageService.saveOtaPackage(firmware); 493 + OtaPackage savedFirmware = createFirmware(tenantId, VERSION);
480 494
481 OtaPackage foundFirmware = otaPackageService.findOtaPackageById(tenantId, savedFirmware.getId()); 495 OtaPackage foundFirmware = otaPackageService.findOtaPackageById(tenantId, savedFirmware.getId());
482 Assert.assertNotNull(foundFirmware); 496 Assert.assertNotNull(foundFirmware);
@@ -502,18 +516,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { @@ -502,18 +516,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest {
502 516
503 @Test 517 @Test
504 public void testDeleteFirmware() { 518 public void testDeleteFirmware() {
505 - OtaPackage firmware = new OtaPackage();  
506 - firmware.setTenantId(tenantId);  
507 - firmware.setDeviceProfileId(deviceProfileId);  
508 - firmware.setType(FIRMWARE);  
509 - firmware.setTitle(TITLE);  
510 - firmware.setVersion(VERSION);  
511 - firmware.setFileName(FILE_NAME);  
512 - firmware.setContentType(CONTENT_TYPE);  
513 - firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM);  
514 - firmware.setChecksum(CHECKSUM);  
515 - firmware.setData(DATA);  
516 - OtaPackage savedFirmware = otaPackageService.saveOtaPackage(firmware); 519 + OtaPackage savedFirmware = createFirmware(tenantId, VERSION);
517 520
518 OtaPackage foundFirmware = otaPackageService.findOtaPackageById(tenantId, savedFirmware.getId()); 521 OtaPackage foundFirmware = otaPackageService.findOtaPackageById(tenantId, savedFirmware.getId());
519 Assert.assertNotNull(foundFirmware); 522 Assert.assertNotNull(foundFirmware);
@@ -526,23 +529,25 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { @@ -526,23 +529,25 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest {
526 public void testFindTenantFirmwaresByTenantId() { 529 public void testFindTenantFirmwaresByTenantId() {
527 List<OtaPackageInfo> firmwares = new ArrayList<>(); 530 List<OtaPackageInfo> firmwares = new ArrayList<>();
528 for (int i = 0; i < 165; i++) { 531 for (int i = 0; i < 165; i++) {
529 - OtaPackage firmware = new OtaPackage();  
530 - firmware.setTenantId(tenantId);  
531 - firmware.setDeviceProfileId(deviceProfileId);  
532 - firmware.setType(FIRMWARE);  
533 - firmware.setTitle(TITLE);  
534 - firmware.setVersion(VERSION + i);  
535 - firmware.setFileName(FILE_NAME);  
536 - firmware.setContentType(CONTENT_TYPE);  
537 - firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM);  
538 - firmware.setChecksum(CHECKSUM);  
539 - firmware.setData(DATA);  
540 -  
541 - OtaPackageInfo info = new OtaPackageInfo(otaPackageService.saveOtaPackage(firmware)); 532 + OtaPackageInfo info = new OtaPackageInfo(createFirmware(tenantId, VERSION + i));
542 info.setHasData(true); 533 info.setHasData(true);
543 firmwares.add(info); 534 firmwares.add(info);
544 } 535 }
545 536
  537 + OtaPackageInfo firmwareWithUrl = new OtaPackageInfo();
  538 + firmwareWithUrl.setTenantId(tenantId);
  539 + firmwareWithUrl.setDeviceProfileId(deviceProfileId);
  540 + firmwareWithUrl.setType(FIRMWARE);
  541 + firmwareWithUrl.setTitle(TITLE);
  542 + firmwareWithUrl.setVersion(VERSION);
  543 + firmwareWithUrl.setUrl(URL);
  544 + firmwareWithUrl.setDataSize(0L);
  545 +
  546 + OtaPackageInfo savedFwWithUrl = otaPackageService.saveOtaPackageInfo(firmwareWithUrl);
  547 + savedFwWithUrl.setHasData(true);
  548 +
  549 + firmwares.add(savedFwWithUrl);
  550 +
546 List<OtaPackageInfo> loadedFirmwares = new ArrayList<>(); 551 List<OtaPackageInfo> loadedFirmwares = new ArrayList<>();
547 PageLink pageLink = new PageLink(16); 552 PageLink pageLink = new PageLink(16);
548 PageData<OtaPackageInfo> pageData; 553 PageData<OtaPackageInfo> pageData;
@@ -571,58 +576,38 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { @@ -571,58 +576,38 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest {
571 public void testFindTenantFirmwaresByTenantIdAndHasData() { 576 public void testFindTenantFirmwaresByTenantIdAndHasData() {
572 List<OtaPackageInfo> firmwares = new ArrayList<>(); 577 List<OtaPackageInfo> firmwares = new ArrayList<>();
573 for (int i = 0; i < 165; i++) { 578 for (int i = 0; i < 165; i++) {
574 - OtaPackageInfo firmwareInfo = new OtaPackageInfo();  
575 - firmwareInfo.setTenantId(tenantId);  
576 - firmwareInfo.setDeviceProfileId(deviceProfileId);  
577 - firmwareInfo.setType(FIRMWARE);  
578 - firmwareInfo.setTitle(TITLE);  
579 - firmwareInfo.setVersion(VERSION + i);  
580 - firmwareInfo.setFileName(FILE_NAME);  
581 - firmwareInfo.setContentType(CONTENT_TYPE);  
582 - firmwareInfo.setChecksumAlgorithm(CHECKSUM_ALGORITHM);  
583 - firmwareInfo.setChecksum(CHECKSUM);  
584 - firmwareInfo.setDataSize((long) DATA.array().length);  
585 - firmwares.add(otaPackageService.saveOtaPackageInfo(firmwareInfo)); 579 + firmwares.add(new OtaPackageInfo(otaPackageService.saveOtaPackage(createFirmware(tenantId, VERSION + i))));
586 } 580 }
587 581
  582 + OtaPackageInfo firmwareWithUrl = new OtaPackageInfo();
  583 + firmwareWithUrl.setTenantId(tenantId);
  584 + firmwareWithUrl.setDeviceProfileId(deviceProfileId);
  585 + firmwareWithUrl.setType(FIRMWARE);
  586 + firmwareWithUrl.setTitle(TITLE);
  587 + firmwareWithUrl.setVersion(VERSION);
  588 + firmwareWithUrl.setUrl(URL);
  589 + firmwareWithUrl.setDataSize(0L);
  590 +
  591 + OtaPackageInfo savedFwWithUrl = otaPackageService.saveOtaPackageInfo(firmwareWithUrl);
  592 + savedFwWithUrl.setHasData(true);
  593 +
  594 + firmwares.add(savedFwWithUrl);
  595 +
588 List<OtaPackageInfo> loadedFirmwares = new ArrayList<>(); 596 List<OtaPackageInfo> loadedFirmwares = new ArrayList<>();
589 PageLink pageLink = new PageLink(16); 597 PageLink pageLink = new PageLink(16);
590 PageData<OtaPackageInfo> pageData; 598 PageData<OtaPackageInfo> pageData;
591 do { 599 do {
592 - pageData = otaPackageService.findTenantOtaPackagesByTenantIdAndDeviceProfileIdAndTypeAndHasData(tenantId, deviceProfileId, FIRMWARE, false, pageLink); 600 + pageData = otaPackageService.findTenantOtaPackagesByTenantIdAndDeviceProfileIdAndTypeAndHasData(tenantId, deviceProfileId, FIRMWARE, pageLink);
593 loadedFirmwares.addAll(pageData.getData()); 601 loadedFirmwares.addAll(pageData.getData());
594 if (pageData.hasNext()) { 602 if (pageData.hasNext()) {
595 pageLink = pageLink.nextPageLink(); 603 pageLink = pageLink.nextPageLink();
596 } 604 }
597 } while (pageData.hasNext()); 605 } while (pageData.hasNext());
598 606
599 - Collections.sort(firmwares, idComparator);  
600 - Collections.sort(loadedFirmwares, idComparator);  
601 -  
602 - Assert.assertEquals(firmwares, loadedFirmwares);  
603 -  
604 - firmwares.forEach(f -> {  
605 - OtaPackage firmware = new OtaPackage(f.getId());  
606 - firmware.setCreatedTime(f.getCreatedTime());  
607 - firmware.setTenantId(f.getTenantId());  
608 - firmware.setDeviceProfileId(deviceProfileId);  
609 - firmware.setType(FIRMWARE);  
610 - firmware.setTitle(f.getTitle());  
611 - firmware.setVersion(f.getVersion());  
612 - firmware.setFileName(FILE_NAME);  
613 - firmware.setContentType(CONTENT_TYPE);  
614 - firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM);  
615 - firmware.setChecksum(CHECKSUM);  
616 - firmware.setData(DATA);  
617 - firmware.setDataSize((long) DATA.array().length);  
618 - otaPackageService.saveOtaPackage(firmware);  
619 - f.setHasData(true);  
620 - });  
621 -  
622 loadedFirmwares = new ArrayList<>(); 607 loadedFirmwares = new ArrayList<>();
623 pageLink = new PageLink(16); 608 pageLink = new PageLink(16);
624 do { 609 do {
625 - pageData = otaPackageService.findTenantOtaPackagesByTenantIdAndDeviceProfileIdAndTypeAndHasData(tenantId, deviceProfileId, FIRMWARE, true, pageLink); 610 + pageData = otaPackageService.findTenantOtaPackagesByTenantIdAndDeviceProfileIdAndTypeAndHasData(tenantId, deviceProfileId, FIRMWARE, pageLink);
626 loadedFirmwares.addAll(pageData.getData()); 611 loadedFirmwares.addAll(pageData.getData());
627 if (pageData.hasNext()) { 612 if (pageData.hasNext()) {
628 pageLink = pageLink.nextPageLink(); 613 pageLink = pageLink.nextPageLink();
@@ -642,4 +627,20 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { @@ -642,4 +627,20 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest {
642 Assert.assertTrue(pageData.getData().isEmpty()); 627 Assert.assertTrue(pageData.getData().isEmpty());
643 } 628 }
644 629
  630 + private OtaPackage createFirmware(TenantId tenantId, String version) {
  631 + OtaPackage firmware = new OtaPackage();
  632 + firmware.setTenantId(tenantId);
  633 + firmware.setDeviceProfileId(deviceProfileId);
  634 + firmware.setType(FIRMWARE);
  635 + firmware.setTitle(TITLE);
  636 + firmware.setVersion(version);
  637 + firmware.setFileName(FILE_NAME);
  638 + firmware.setContentType(CONTENT_TYPE);
  639 + firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM);
  640 + firmware.setChecksum(CHECKSUM);
  641 + firmware.setData(DATA);
  642 + firmware.setDataSize(DATA_SIZE);
  643 + return otaPackageService.saveOtaPackage(firmware);
  644 + }
  645 +
645 } 646 }
@@ -24,7 +24,7 @@ const topicProperties = config.get('kafka.topic_properties'); @@ -24,7 +24,7 @@ const topicProperties = config.get('kafka.topic_properties');
24 const kafkaClientId = config.get('kafka.client_id'); 24 const kafkaClientId = config.get('kafka.client_id');
25 const acks = Number(config.get('kafka.acks')); 25 const acks = Number(config.get('kafka.acks'));
26 const requestTimeout = Number(config.get('kafka.requestTimeout')); 26 const requestTimeout = Number(config.get('kafka.requestTimeout'));
27 -const compressionType = (config.get('kafka.requestTimeout') === "gzip") ? CompressionTypes.GZIP : CompressionTypes.None; 27 +const compressionType = (config.get('kafka.compression') === "gzip") ? CompressionTypes.GZIP : CompressionTypes.None;
28 28
29 let kafkaClient; 29 let kafkaClient;
30 let kafkaAdmin; 30 let kafkaAdmin;
@@ -19,18 +19,25 @@ import com.fasterxml.jackson.databind.JsonNode; @@ -19,18 +19,25 @@ import com.fasterxml.jackson.databind.JsonNode;
19 import com.fasterxml.jackson.databind.ObjectMapper; 19 import com.fasterxml.jackson.databind.ObjectMapper;
20 import com.fasterxml.jackson.databind.node.ObjectNode; 20 import com.fasterxml.jackson.databind.node.ObjectNode;
21 import org.springframework.core.ParameterizedTypeReference; 21 import org.springframework.core.ParameterizedTypeReference;
  22 +import org.springframework.core.io.ByteArrayResource;
  23 +import org.springframework.core.io.Resource;
22 import org.springframework.http.HttpEntity; 24 import org.springframework.http.HttpEntity;
  25 +import org.springframework.http.HttpHeaders;
23 import org.springframework.http.HttpMethod; 26 import org.springframework.http.HttpMethod;
24 import org.springframework.http.HttpRequest; 27 import org.springframework.http.HttpRequest;
25 import org.springframework.http.HttpStatus; 28 import org.springframework.http.HttpStatus;
  29 +import org.springframework.http.MediaType;
26 import org.springframework.http.ResponseEntity; 30 import org.springframework.http.ResponseEntity;
27 import org.springframework.http.client.ClientHttpRequestExecution; 31 import org.springframework.http.client.ClientHttpRequestExecution;
28 import org.springframework.http.client.ClientHttpRequestInterceptor; 32 import org.springframework.http.client.ClientHttpRequestInterceptor;
29 import org.springframework.http.client.ClientHttpResponse; 33 import org.springframework.http.client.ClientHttpResponse;
30 import org.springframework.http.client.support.HttpRequestWrapper; 34 import org.springframework.http.client.support.HttpRequestWrapper;
  35 +import org.springframework.util.LinkedMultiValueMap;
  36 +import org.springframework.util.MultiValueMap;
31 import org.springframework.util.StringUtils; 37 import org.springframework.util.StringUtils;
32 import org.springframework.web.client.HttpClientErrorException; 38 import org.springframework.web.client.HttpClientErrorException;
33 import org.springframework.web.client.RestTemplate; 39 import org.springframework.web.client.RestTemplate;
  40 +import org.springframework.web.multipart.MultipartFile;
34 import org.thingsboard.common.util.ThingsBoardExecutors; 41 import org.thingsboard.common.util.ThingsBoardExecutors;
35 import org.thingsboard.rest.client.utils.RestJsonConverter; 42 import org.thingsboard.rest.client.utils.RestJsonConverter;
36 import org.thingsboard.server.common.data.AdminSettings; 43 import org.thingsboard.server.common.data.AdminSettings;
@@ -48,6 +55,10 @@ import org.thingsboard.server.common.data.EntitySubtype; @@ -48,6 +55,10 @@ import org.thingsboard.server.common.data.EntitySubtype;
48 import org.thingsboard.server.common.data.EntityView; 55 import org.thingsboard.server.common.data.EntityView;
49 import org.thingsboard.server.common.data.EntityViewInfo; 56 import org.thingsboard.server.common.data.EntityViewInfo;
50 import org.thingsboard.server.common.data.Event; 57 import org.thingsboard.server.common.data.Event;
  58 +import org.thingsboard.server.common.data.OtaPackage;
  59 +import org.thingsboard.server.common.data.OtaPackageInfo;
  60 +import org.thingsboard.server.common.data.TbResource;
  61 +import org.thingsboard.server.common.data.TbResourceInfo;
51 import org.thingsboard.server.common.data.Tenant; 62 import org.thingsboard.server.common.data.Tenant;
52 import org.thingsboard.server.common.data.TenantInfo; 63 import org.thingsboard.server.common.data.TenantInfo;
53 import org.thingsboard.server.common.data.TenantProfile; 64 import org.thingsboard.server.common.data.TenantProfile;
@@ -78,8 +89,10 @@ import org.thingsboard.server.common.data.id.EdgeId; @@ -78,8 +89,10 @@ import org.thingsboard.server.common.data.id.EdgeId;
78 import org.thingsboard.server.common.data.id.EntityId; 89 import org.thingsboard.server.common.data.id.EntityId;
79 import org.thingsboard.server.common.data.id.EntityViewId; 90 import org.thingsboard.server.common.data.id.EntityViewId;
80 import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; 91 import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId;
  92 +import org.thingsboard.server.common.data.id.OtaPackageId;
81 import org.thingsboard.server.common.data.id.RuleChainId; 93 import org.thingsboard.server.common.data.id.RuleChainId;
82 import org.thingsboard.server.common.data.id.RuleNodeId; 94 import org.thingsboard.server.common.data.id.RuleNodeId;
  95 +import org.thingsboard.server.common.data.id.TbResourceId;
83 import org.thingsboard.server.common.data.id.TenantId; 96 import org.thingsboard.server.common.data.id.TenantId;
84 import org.thingsboard.server.common.data.id.TenantProfileId; 97 import org.thingsboard.server.common.data.id.TenantProfileId;
85 import org.thingsboard.server.common.data.id.UserId; 98 import org.thingsboard.server.common.data.id.UserId;
@@ -91,6 +104,8 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; @@ -91,6 +104,8 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
91 import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; 104 import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo;
92 import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; 105 import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
93 import org.thingsboard.server.common.data.oauth2.OAuth2ClientsParams; 106 import org.thingsboard.server.common.data.oauth2.OAuth2ClientsParams;
  107 +import org.thingsboard.server.common.data.ota.ChecksumAlgorithm;
  108 +import org.thingsboard.server.common.data.ota.OtaPackageType;
94 import org.thingsboard.server.common.data.page.PageData; 109 import org.thingsboard.server.common.data.page.PageData;
95 import org.thingsboard.server.common.data.page.PageLink; 110 import org.thingsboard.server.common.data.page.PageLink;
96 import org.thingsboard.server.common.data.page.SortOrder; 111 import org.thingsboard.server.common.data.page.SortOrder;
@@ -127,9 +142,9 @@ import java.util.HashMap; @@ -127,9 +142,9 @@ import java.util.HashMap;
127 import java.util.List; 142 import java.util.List;
128 import java.util.Map; 143 import java.util.Map;
129 import java.util.Optional; 144 import java.util.Optional;
  145 +import java.util.UUID;
130 import java.util.concurrent.ConcurrentHashMap; 146 import java.util.concurrent.ConcurrentHashMap;
131 import java.util.concurrent.ExecutorService; 147 import java.util.concurrent.ExecutorService;
132 -import java.util.concurrent.Executors;  
133 import java.util.concurrent.Future; 148 import java.util.concurrent.Future;
134 import java.util.stream.Collectors; 149 import java.util.stream.Collectors;
135 150
@@ -147,7 +162,6 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { @@ -147,7 +162,6 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
147 private final ObjectMapper objectMapper = new ObjectMapper(); 162 private final ObjectMapper objectMapper = new ObjectMapper();
148 private ExecutorService service = ThingsBoardExecutors.newWorkStealingPool(10, getClass()); 163 private ExecutorService service = ThingsBoardExecutors.newWorkStealingPool(10, getClass());
149 164
150 -  
151 protected static final String ACTIVATE_TOKEN_REGEX = "/api/noauth/activate?activateToken="; 165 protected static final String ACTIVATE_TOKEN_REGEX = "/api/noauth/activate?activateToken=";
152 166
153 public RestClient(String baseURL) { 167 public RestClient(String baseURL) {
@@ -1238,6 +1252,21 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { @@ -1238,6 +1252,21 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
1238 HttpEntity.EMPTY, Device.class, tenantId, deviceId).getBody(); 1252 HttpEntity.EMPTY, Device.class, tenantId, deviceId).getBody();
1239 } 1253 }
1240 1254
  1255 + public Long countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(OtaPackageType otaPackageType, DeviceProfileId deviceProfileId) {
  1256 + Map<String, String> params = new HashMap<>();
  1257 + params.put("otaPackageType", otaPackageType.name());
  1258 + params.put("deviceProfileId", deviceProfileId.getId().toString());
  1259 +
  1260 + return restTemplate.exchange(
  1261 + baseURL + "/api/devices/count/{otaPackageType}?deviceProfileId={deviceProfileId}",
  1262 + HttpMethod.GET,
  1263 + HttpEntity.EMPTY,
  1264 + new ParameterizedTypeReference<Long>() {
  1265 + },
  1266 + params
  1267 + ).getBody();
  1268 + }
  1269 +
1241 @Deprecated 1270 @Deprecated
1242 public Device createDevice(String name, String type) { 1271 public Device createDevice(String name, String type) {
1243 Device device = new Device(); 1272 Device device = new Device();
@@ -2830,6 +2859,176 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { @@ -2830,6 +2859,176 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
2830 restTemplate.postForEntity(baseURL + "/api/edge/sync/{edgeId}", null, EdgeId.class, params); 2859 restTemplate.postForEntity(baseURL + "/api/edge/sync/{edgeId}", null, EdgeId.class, params);
2831 } 2860 }
2832 2861
  2862 + public ResponseEntity<Resource> downloadResource(TbResourceId resourceId) {
  2863 + Map<String, String> params = new HashMap<>();
  2864 + params.put("resourceId", resourceId.getId().toString());
  2865 +
  2866 + return restTemplate.exchange(
  2867 + baseURL + "/api/resource/{resourceId}/download",
  2868 + HttpMethod.GET,
  2869 + HttpEntity.EMPTY,
  2870 + new ParameterizedTypeReference<>() {},
  2871 + params
  2872 + );
  2873 + }
  2874 +
  2875 + public TbResourceInfo getResourceInfoById(TbResourceId resourceId) {
  2876 + Map<String, String> params = new HashMap<>();
  2877 + params.put("resourceId", resourceId.getId().toString());
  2878 +
  2879 + return restTemplate.exchange(
  2880 + baseURL + "/api/resource/info/{resourceId}",
  2881 + HttpMethod.GET,
  2882 + HttpEntity.EMPTY,
  2883 + new ParameterizedTypeReference<TbResourceInfo>() {},
  2884 + params
  2885 + ).getBody();
  2886 + }
  2887 +
  2888 + public TbResource getResourceId(TbResourceId resourceId) {
  2889 + Map<String, String> params = new HashMap<>();
  2890 + params.put("resourceId", resourceId.getId().toString());
  2891 +
  2892 + return restTemplate.exchange(
  2893 + baseURL + "/api/resource/{resourceId}",
  2894 + HttpMethod.GET,
  2895 + HttpEntity.EMPTY,
  2896 + new ParameterizedTypeReference<TbResource>() {},
  2897 + params
  2898 + ).getBody();
  2899 + }
  2900 +
  2901 + public TbResource saveResource(TbResource resource) {
  2902 + return restTemplate.postForEntity(
  2903 + baseURL + "/api/resource",
  2904 + resource,
  2905 + TbResource.class
  2906 + ).getBody();
  2907 + }
  2908 +
  2909 + public PageData<TbResourceInfo> getResources(PageLink pageLink) {
  2910 + Map<String, String> params = new HashMap<>();
  2911 + addPageLinkToParam(params, pageLink);
  2912 + return restTemplate.exchange(
  2913 + baseURL + "/api/resource?" + getUrlParams(pageLink),
  2914 + HttpMethod.GET,
  2915 + HttpEntity.EMPTY,
  2916 + new ParameterizedTypeReference<PageData<TbResourceInfo>>() {},
  2917 + params
  2918 + ).getBody();
  2919 + }
  2920 +
  2921 + public void deleteResource(TbResourceId resourceId) {
  2922 + restTemplate.delete("/api/resource/{resourceId}", resourceId.getId().toString());
  2923 + }
  2924 +
  2925 + public ResponseEntity<Resource> downloadOtaPackage(OtaPackageId otaPackageId) {
  2926 + Map<String, String> params = new HashMap<>();
  2927 + params.put("otaPackageId", otaPackageId.getId().toString());
  2928 +
  2929 + return restTemplate.exchange(
  2930 + baseURL + "/api/otaPackage/{otaPackageId}/download",
  2931 + HttpMethod.GET,
  2932 + HttpEntity.EMPTY,
  2933 + new ParameterizedTypeReference<>() {},
  2934 + params
  2935 + );
  2936 + }
  2937 +
  2938 + public OtaPackageInfo getOtaPackageInfoById(OtaPackageId otaPackageId) {
  2939 + Map<String, String> params = new HashMap<>();
  2940 + params.put("otaPackageId", otaPackageId.getId().toString());
  2941 +
  2942 + return restTemplate.exchange(
  2943 + baseURL + "/api/otaPackage/info/{otaPackageId}",
  2944 + HttpMethod.GET,
  2945 + HttpEntity.EMPTY,
  2946 + new ParameterizedTypeReference<OtaPackageInfo>() {},
  2947 + params
  2948 + ).getBody();
  2949 + }
  2950 +
  2951 + public OtaPackage getOtaPackageById(OtaPackageId otaPackageId) {
  2952 + Map<String, String> params = new HashMap<>();
  2953 + params.put("otaPackageId", otaPackageId.getId().toString());
  2954 +
  2955 + return restTemplate.exchange(
  2956 + baseURL + "/api/otaPackage/{otaPackageId}",
  2957 + HttpMethod.GET,
  2958 + HttpEntity.EMPTY,
  2959 + new ParameterizedTypeReference<OtaPackage>() {},
  2960 + params
  2961 + ).getBody();
  2962 + }
  2963 +
  2964 + public OtaPackageInfo saveOtaPackageInfo(OtaPackageInfo otaPackageInfo) {
  2965 + return restTemplate.postForEntity(baseURL + "/api/otaPackage", otaPackageInfo, OtaPackageInfo.class).getBody();
  2966 + }
  2967 +
  2968 + public OtaPackage saveOtaPackageData(OtaPackageId otaPackageId, String checkSum, ChecksumAlgorithm checksumAlgorithm, MultipartFile file) throws Exception {
  2969 + HttpHeaders header = new HttpHeaders();
  2970 + header.setContentType(MediaType.MULTIPART_FORM_DATA);
  2971 +
  2972 + MultiValueMap<String, String> fileMap = new LinkedMultiValueMap<>();
  2973 + fileMap.add(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=file; filename=" + file.getName());
  2974 + HttpEntity<ByteArrayResource> fileEntity = new HttpEntity<>(new ByteArrayResource(file.getBytes()), fileMap);
  2975 +
  2976 + MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
  2977 + body.add("file", fileEntity);
  2978 + HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, header);
  2979 +
  2980 + Map<String, String> params = new HashMap<>();
  2981 + params.put("otaPackageId", otaPackageId.getId().toString());
  2982 + params.put("checksumAlgorithm", checksumAlgorithm.name());
  2983 + String url = "/api/otaPackage/{otaPackageId}?checksumAlgorithm={checksumAlgorithm}";
  2984 +
  2985 + if(checkSum != null) {
  2986 + url += "&checkSum={checkSum}";
  2987 + }
  2988 +
  2989 + return restTemplate.postForEntity(
  2990 + baseURL + url, requestEntity, OtaPackage.class, params
  2991 + ).getBody();
  2992 + }
  2993 +
  2994 + public PageData<OtaPackageInfo> getOtaPackages(PageLink pageLink) {
  2995 + Map<String, String> params = new HashMap<>();
  2996 + addPageLinkToParam(params, pageLink);
  2997 +
  2998 + return restTemplate.exchange(
  2999 + baseURL + "/api/otaPackages?" + getUrlParams(pageLink),
  3000 + HttpMethod.GET,
  3001 + HttpEntity.EMPTY,
  3002 + new ParameterizedTypeReference<PageData<OtaPackageInfo>>() {
  3003 + },
  3004 + params
  3005 + ).getBody();
  3006 + }
  3007 +
  3008 + public PageData<OtaPackageInfo> getOtaPackages(DeviceProfileId deviceProfileId,
  3009 + OtaPackageType otaPackageType,
  3010 + boolean hasData,
  3011 + PageLink pageLink) {
  3012 + Map<String, String> params = new HashMap<>();
  3013 + params.put("hasData", String.valueOf(hasData));
  3014 + params.put("deviceProfileId", deviceProfileId.getId().toString());
  3015 + params.put("type", otaPackageType.name());
  3016 + addPageLinkToParam(params, pageLink);
  3017 +
  3018 + return restTemplate.exchange(
  3019 + baseURL + "/api/otaPackages/{deviceProfileId}/{type}/{hasData}?" + getUrlParams(pageLink),
  3020 + HttpMethod.GET,
  3021 + HttpEntity.EMPTY,
  3022 + new ParameterizedTypeReference<PageData<OtaPackageInfo>>() {
  3023 + },
  3024 + params
  3025 + ).getBody();
  3026 + }
  3027 +
  3028 + public void deleteOtaPackage(OtaPackageId otaPackageId) {
  3029 + restTemplate.delete(baseURL + "/api/otaPackage/{otaPackageId}", otaPackageId.getId().toString());
  3030 + }
  3031 +
2833 @Deprecated 3032 @Deprecated
2834 public Optional<JsonNode> getAttributes(String accessToken, String clientKeys, String sharedKeys) { 3033 public Optional<JsonNode> getAttributes(String accessToken, String clientKeys, String sharedKeys) {
2835 Map<String, String> params = new HashMap<>(); 3034 Map<String, String> params = new HashMap<>();
@@ -47,7 +47,9 @@ import org.thingsboard.server.dao.edge.EdgeService; @@ -47,7 +47,9 @@ import org.thingsboard.server.dao.edge.EdgeService;
47 import org.thingsboard.server.dao.entityview.EntityViewService; 47 import org.thingsboard.server.dao.entityview.EntityViewService;
48 import org.thingsboard.server.dao.nosql.CassandraStatementTask; 48 import org.thingsboard.server.dao.nosql.CassandraStatementTask;
49 import org.thingsboard.server.dao.nosql.TbResultSetFuture; 49 import org.thingsboard.server.dao.nosql.TbResultSetFuture;
  50 +import org.thingsboard.server.dao.ota.OtaPackageService;
50 import org.thingsboard.server.dao.relation.RelationService; 51 import org.thingsboard.server.dao.relation.RelationService;
  52 +import org.thingsboard.server.dao.resource.ResourceService;
51 import org.thingsboard.server.dao.rule.RuleChainService; 53 import org.thingsboard.server.dao.rule.RuleChainService;
52 import org.thingsboard.server.dao.tenant.TenantService; 54 import org.thingsboard.server.dao.tenant.TenantService;
53 import org.thingsboard.server.dao.timeseries.TimeseriesService; 55 import org.thingsboard.server.dao.timeseries.TimeseriesService;
@@ -202,6 +204,10 @@ public interface TbContext { @@ -202,6 +204,10 @@ public interface TbContext {
202 204
203 EntityViewService getEntityViewService(); 205 EntityViewService getEntityViewService();
204 206
  207 + ResourceService getResourceService();
  208 +
  209 + OtaPackageService getOtaPackageService();
  210 +
205 RuleEngineDeviceProfileCache getDeviceProfileCache(); 211 RuleEngineDeviceProfileCache getDeviceProfileCache();
206 212
207 EdgeService getEdgeService(); 213 EdgeService getEdgeService();
@@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.alarm.Alarm; @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.alarm.Alarm;
29 import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; 29 import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey;
30 import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; 30 import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType;
31 import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; 31 import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm;
  32 +import org.thingsboard.server.common.data.exception.ApiUsageLimitsExceededException;
32 import org.thingsboard.server.common.data.id.DeviceId; 33 import org.thingsboard.server.common.data.id.DeviceId;
33 import org.thingsboard.server.common.data.id.DeviceProfileId; 34 import org.thingsboard.server.common.data.id.DeviceProfileId;
34 import org.thingsboard.server.common.data.id.EntityId; 35 import org.thingsboard.server.common.data.id.EntityId;
@@ -150,6 +151,8 @@ class DeviceState { @@ -150,6 +151,8 @@ class DeviceState {
150 stateChanged = processAlarmClearNotification(ctx, msg); 151 stateChanged = processAlarmClearNotification(ctx, msg);
151 } else if (msg.getType().equals(DataConstants.ALARM_ACK)) { 152 } else if (msg.getType().equals(DataConstants.ALARM_ACK)) {
152 processAlarmAckNotification(ctx, msg); 153 processAlarmAckNotification(ctx, msg);
  154 + } else if (msg.getType().equals(DataConstants.ALARM_DELETE)) {
  155 + processAlarmDeleteNotification(ctx, msg);
153 } else { 156 } else {
154 if (msg.getType().equals(DataConstants.ENTITY_ASSIGNED) || msg.getType().equals(DataConstants.ENTITY_UNASSIGNED)) { 157 if (msg.getType().equals(DataConstants.ENTITY_ASSIGNED) || msg.getType().equals(DataConstants.ENTITY_UNASSIGNED)) {
155 dynamicPredicateValueCtx.resetCustomer(); 158 dynamicPredicateValueCtx.resetCustomer();
@@ -193,6 +196,12 @@ class DeviceState { @@ -193,6 +196,12 @@ class DeviceState {
193 ctx.tellSuccess(msg); 196 ctx.tellSuccess(msg);
194 } 197 }
195 198
  199 + private void processAlarmDeleteNotification(TbContext ctx, TbMsg msg) {
  200 + Alarm alarm = JacksonUtil.fromString(msg.getData(), Alarm.class);
  201 + alarmStates.values().removeIf(alarmState -> alarmState.getCurrentAlarm().getId().equals(alarm.getId()));
  202 + ctx.tellSuccess(msg);
  203 + }
  204 +
196 private boolean processAttributesUpdateNotification(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException { 205 private boolean processAttributesUpdateNotification(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException {
197 String scope = msg.getMetaData().getValue(DataConstants.SCOPE); 206 String scope = msg.getMetaData().getValue(DataConstants.SCOPE);
198 if (StringUtils.isEmpty(scope)) { 207 if (StringUtils.isEmpty(scope)) {
@@ -253,7 +262,12 @@ class DeviceState { @@ -253,7 +262,12 @@ class DeviceState {
253 for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) { 262 for (DeviceProfileAlarm alarm : deviceProfile.getAlarmSettings()) {
254 AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(), 263 AlarmState alarmState = alarmStates.computeIfAbsent(alarm.getId(),
255 a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm), dynamicPredicateValueCtx)); 264 a -> new AlarmState(this.deviceProfile, deviceId, alarm, getOrInitPersistedAlarmState(alarm), dynamicPredicateValueCtx));
256 - stateChanged |= alarmState.process(ctx, msg, latestValues, update); 265 + try {
  266 + stateChanged |= alarmState.process(ctx, msg, latestValues, update);
  267 + } catch (ApiUsageLimitsExceededException e) {
  268 + alarmStates.remove(alarm.getId());
  269 + throw e;
  270 + }
257 } 271 }
258 } 272 }
259 } 273 }
@@ -152,6 +152,7 @@ export interface IStateController { @@ -152,6 +152,7 @@ export interface IStateController {
152 getStateIndex(): number; 152 getStateIndex(): number;
153 getStateIdAtIndex(index: number): string; 153 getStateIdAtIndex(index: number): string;
154 getEntityId(entityParamName: string): EntityId; 154 getEntityId(entityParamName: string): EntityId;
  155 + getCurrentStateName(): string;
155 } 156 }
156 157
157 export interface SubscriptionInfo { 158 export interface SubscriptionInfo {
@@ -45,6 +45,7 @@ import { ActionNotificationShow } from '@core/notification/notification.actions' @@ -45,6 +45,7 @@ import { ActionNotificationShow } from '@core/notification/notification.actions'
45 import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; 45 import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
46 import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component'; 46 import { AlertDialogComponent } from '@shared/components/dialog/alert-dialog.component';
47 import { OAuth2ClientInfo } from '@shared/models/oauth2.models'; 47 import { OAuth2ClientInfo } from '@shared/models/oauth2.models';
  48 +import { isMobileApp } from '@core/utils';
48 49
49 @Injectable({ 50 @Injectable({
50 providedIn: 'root' 51 providedIn: 'root'
@@ -194,11 +195,13 @@ export class AuthService { @@ -194,11 +195,13 @@ export class AuthService {
194 } 195 }
195 196
196 public gotoDefaultPlace(isAuthenticated: boolean) { 197 public gotoDefaultPlace(isAuthenticated: boolean) {
197 - const authState = getCurrentAuthState(this.store);  
198 - const url = this.defaultUrl(isAuthenticated, authState);  
199 - this.zone.run(() => {  
200 - this.router.navigateByUrl(url);  
201 - }); 198 + if (!isMobileApp()) {
  199 + const authState = getCurrentAuthState(this.store);
  200 + const url = this.defaultUrl(isAuthenticated, authState);
  201 + this.zone.run(() => {
  202 + this.router.navigateByUrl(url);
  203 + });
  204 + }
202 } 205 }
203 206
204 public loadOAuth2Clients(): Observable<Array<OAuth2ClientInfo>> { 207 public loadOAuth2Clients(): Observable<Array<OAuth2ClientInfo>> {
@@ -516,12 +519,15 @@ export class AuthService { @@ -516,12 +519,15 @@ export class AuthService {
516 return this.refreshTokenSubject !== null; 519 return this.refreshTokenSubject !== null;
517 } 520 }
518 521
519 - public setUserFromJwtToken(jwtToken, refreshToken, notify) { 522 + public setUserFromJwtToken(jwtToken, refreshToken, notify): Observable<boolean> {
  523 + const authenticatedSubject = new ReplaySubject<boolean>();
520 if (!jwtToken) { 524 if (!jwtToken) {
521 AuthService.clearTokenData(); 525 AuthService.clearTokenData();
522 if (notify) { 526 if (notify) {
523 this.notifyUnauthenticated(); 527 this.notifyUnauthenticated();
524 } 528 }
  529 + authenticatedSubject.next(false);
  530 + authenticatedSubject.complete();
525 } else { 531 } else {
526 this.updateAndValidateTokens(jwtToken, refreshToken, true); 532 this.updateAndValidateTokens(jwtToken, refreshToken, true);
527 if (notify) { 533 if (notify) {
@@ -530,16 +536,30 @@ export class AuthService { @@ -530,16 +536,30 @@ export class AuthService {
530 (authPayload) => { 536 (authPayload) => {
531 this.notifyUserLoaded(true); 537 this.notifyUserLoaded(true);
532 this.notifyAuthenticated(authPayload); 538 this.notifyAuthenticated(authPayload);
  539 + authenticatedSubject.next(true);
  540 + authenticatedSubject.complete();
533 }, 541 },
534 () => { 542 () => {
535 this.notifyUserLoaded(true); 543 this.notifyUserLoaded(true);
536 this.notifyUnauthenticated(); 544 this.notifyUnauthenticated();
  545 + authenticatedSubject.next(false);
  546 + authenticatedSubject.complete();
537 } 547 }
538 ); 548 );
539 } else { 549 } else {
540 - this.loadUser(false).subscribe(); 550 + this.loadUser(false).subscribe(
  551 + () => {
  552 + authenticatedSubject.next(true);
  553 + authenticatedSubject.complete();
  554 + },
  555 + () => {
  556 + authenticatedSubject.next(false);
  557 + authenticatedSubject.complete();
  558 + }
  559 + );
541 } 560 }
542 } 561 }
  562 + return authenticatedSubject;
543 } 563 }
544 564
545 private updateAndValidateTokens(jwtToken, refreshToken, notify: boolean) { 565 private updateAndValidateTokens(jwtToken, refreshToken, notify: boolean) {
@@ -29,6 +29,7 @@ import { DialogService } from '@core/services/dialog.service'; @@ -29,6 +29,7 @@ import { DialogService } from '@core/services/dialog.service';
29 import { TranslateService } from '@ngx-translate/core'; 29 import { TranslateService } from '@ngx-translate/core';
30 import { UtilsService } from '@core/services/utils.service'; 30 import { UtilsService } from '@core/services/utils.service';
31 import { isObject } from '@core/utils'; 31 import { isObject } from '@core/utils';
  32 +import { MobileService } from '@core/services/mobile.service';
32 33
33 @Injectable({ 34 @Injectable({
34 providedIn: 'root' 35 providedIn: 'root'
@@ -41,6 +42,7 @@ export class AuthGuard implements CanActivate, CanActivateChild { @@ -41,6 +42,7 @@ export class AuthGuard implements CanActivate, CanActivateChild {
41 private dialogService: DialogService, 42 private dialogService: DialogService,
42 private utils: UtilsService, 43 private utils: UtilsService,
43 private translate: TranslateService, 44 private translate: TranslateService,
  45 + private mobileService: MobileService,
44 private zone: NgZone) {} 46 private zone: NgZone) {}
45 47
46 getAuthState(): Observable<AuthState> { 48 getAuthState(): Observable<AuthState> {
@@ -108,6 +110,10 @@ export class AuthGuard implements CanActivate, CanActivateChild { @@ -108,6 +110,10 @@ export class AuthGuard implements CanActivate, CanActivateChild {
108 return of(false); 110 return of(false);
109 } 111 }
110 } 112 }
  113 + if (this.mobileService.isMobileApp() && !path.startsWith('dashboard.')) {
  114 + this.mobileService.handleMobileNavigation(path, params);
  115 + return of(false);
  116 + }
111 const defaultUrl = this.authService.defaultUrl(true, authState, path, params); 117 const defaultUrl = this.authService.defaultUrl(true, authState, path, params);
112 if (defaultUrl) { 118 if (defaultUrl) {
113 // this.authService.gotoDefaultPlace(true); 119 // this.authService.gotoDefaultPlace(true);
@@ -40,7 +40,7 @@ export class OtaPackageService { @@ -40,7 +40,7 @@ export class OtaPackageService {
40 40
41 public getOtaPackagesInfoByDeviceProfileId(pageLink: PageLink, deviceProfileId: string, type: OtaUpdateType, 41 public getOtaPackagesInfoByDeviceProfileId(pageLink: PageLink, deviceProfileId: string, type: OtaUpdateType,
42 hasData = true, config?: RequestConfig): Observable<PageData<OtaPackageInfo>> { 42 hasData = true, config?: RequestConfig): Observable<PageData<OtaPackageInfo>> {
43 - const url = `/api/otaPackages/${deviceProfileId}/${type}/${hasData}${pageLink.toQuery()}`; 43 + const url = `/api/otaPackages/${deviceProfileId}/${type}${pageLink.toQuery()}`;
44 return this.http.get<PageData<OtaPackageInfo>>(url, defaultHttpOptionsFromConfig(config)); 44 return this.http.get<PageData<OtaPackageInfo>>(url, defaultHttpOptionsFromConfig(config));
45 } 45 }
46 46
@@ -20,9 +20,14 @@ import { isDefined } from '@core/utils'; @@ -20,9 +20,14 @@ import { isDefined } from '@core/utils';
20 import { MobileActionResult, WidgetMobileActionResult, WidgetMobileActionType } from '@shared/models/widget.models'; 20 import { MobileActionResult, WidgetMobileActionResult, WidgetMobileActionType } from '@shared/models/widget.models';
21 import { from, of } from 'rxjs'; 21 import { from, of } from 'rxjs';
22 import { Observable } from 'rxjs/internal/Observable'; 22 import { Observable } from 'rxjs/internal/Observable';
23 -import { catchError } from 'rxjs/operators'; 23 +import { catchError, tap } from 'rxjs/operators';
  24 +import { OpenDashboardMessage, ReloadUserMessage, WindowMessage } from '@shared/models/window-message.model';
  25 +import { Params, Router } from '@angular/router';
  26 +import { AuthService } from '@core/auth/auth.service';
24 27
25 const dashboardStateNameHandler = 'tbMobileDashboardStateNameHandler'; 28 const dashboardStateNameHandler = 'tbMobileDashboardStateNameHandler';
  29 +const dashboardLoadedHandler = 'tbMobileDashboardLoadedHandler';
  30 +const navigationHandler = 'tbMobileNavigationHandler';
26 const mobileHandler = 'tbMobileHandler'; 31 const mobileHandler = 'tbMobileHandler';
27 32
28 // @dynamic 33 // @dynamic
@@ -34,10 +39,20 @@ export class MobileService { @@ -34,10 +39,20 @@ export class MobileService {
34 private readonly mobileApp; 39 private readonly mobileApp;
35 private readonly mobileChannel; 40 private readonly mobileChannel;
36 41
37 - constructor(@Inject(WINDOW) private window: Window) { 42 + private readonly onWindowMessageListener = this.onWindowMessage.bind(this);
  43 +
  44 + private reloadUserObservable: Observable<boolean>;
  45 + private lastDashboardId: string;
  46 +
  47 + constructor(@Inject(WINDOW) private window: Window,
  48 + private router: Router,
  49 + private authService: AuthService) {
38 const w = (this.window as any); 50 const w = (this.window as any);
39 this.mobileChannel = w.flutter_inappwebview; 51 this.mobileChannel = w.flutter_inappwebview;
40 this.mobileApp = isDefined(this.mobileChannel); 52 this.mobileApp = isDefined(this.mobileChannel);
  53 + if (this.mobileApp) {
  54 + window.addEventListener('message', this.onWindowMessageListener);
  55 + }
41 } 56 }
42 57
43 public isMobileApp(): boolean { 58 public isMobileApp(): boolean {
@@ -50,6 +65,12 @@ export class MobileService { @@ -50,6 +65,12 @@ export class MobileService {
50 } 65 }
51 } 66 }
52 67
  68 + public onDashboardLoaded() {
  69 + if (this.mobileApp) {
  70 + this.mobileChannel.callHandler(dashboardLoadedHandler);
  71 + }
  72 + }
  73 +
53 public handleWidgetMobileAction<T extends MobileActionResult>(type: WidgetMobileActionType, ...args: any[]): 74 public handleWidgetMobileAction<T extends MobileActionResult>(type: WidgetMobileActionType, ...args: any[]):
54 Observable<WidgetMobileActionResult<T>> { 75 Observable<WidgetMobileActionResult<T>> {
55 if (this.mobileApp) { 76 if (this.mobileApp) {
@@ -67,4 +88,82 @@ export class MobileService { @@ -67,4 +88,82 @@ export class MobileService {
67 } 88 }
68 } 89 }
69 90
  91 + public handleMobileNavigation(path?: string, params?: Params) {
  92 + if (this.mobileApp) {
  93 + this.mobileChannel.callHandler(navigationHandler, path, params);
  94 + }
  95 + }
  96 +
  97 + private onWindowMessage(event: MessageEvent) {
  98 + if (event.data) {
  99 + let message: WindowMessage;
  100 + try {
  101 + message = JSON.parse(event.data);
  102 + } catch (e) {}
  103 + if (message && message.type) {
  104 + switch (message.type) {
  105 + case 'openDashboardMessage':
  106 + const openDashboardMessage: OpenDashboardMessage = message.data;
  107 + this.openDashboard(openDashboardMessage);
  108 + break;
  109 + case 'reloadUserMessage':
  110 + const reloadUserMessage: ReloadUserMessage = message.data;
  111 + this.reloadUser(reloadUserMessage);
  112 + break;
  113 + }
  114 + }
  115 + }
  116 + }
  117 +
  118 + private openDashboard(openDashboardMessage: OpenDashboardMessage) {
  119 + if (openDashboardMessage && openDashboardMessage.dashboardId) {
  120 + if (this.reloadUserObservable) {
  121 + this.reloadUserObservable.subscribe(
  122 + (authenticated) => {
  123 + if (authenticated) {
  124 + this.doDashboardNavigation(openDashboardMessage);
  125 + }
  126 + }
  127 + );
  128 + } else {
  129 + this.doDashboardNavigation(openDashboardMessage);
  130 + }
  131 + }
  132 + }
  133 +
  134 + private doDashboardNavigation(openDashboardMessage: OpenDashboardMessage) {
  135 + let url = `/dashboard/${openDashboardMessage.dashboardId}`;
  136 + const params = [];
  137 + if (openDashboardMessage.state) {
  138 + params.push(`state=${openDashboardMessage.state}`);
  139 + }
  140 + if (openDashboardMessage.embedded) {
  141 + params.push(`embedded=true`);
  142 + }
  143 + if (openDashboardMessage.hideToolbar) {
  144 + params.push(`hideToolbar=true`);
  145 + }
  146 + if (this.lastDashboardId === openDashboardMessage.dashboardId) {
  147 + params.push(`reload=${new Date().getTime()}`);
  148 + }
  149 + if (params.length) {
  150 + url += `?${params.join('&')}`;
  151 + }
  152 + this.lastDashboardId = openDashboardMessage.dashboardId;
  153 + this.router.navigateByUrl(url, {replaceUrl: true});
  154 + }
  155 +
  156 + private reloadUser(reloadUserMessage: ReloadUserMessage) {
  157 + if (reloadUserMessage && reloadUserMessage.accessToken && reloadUserMessage.refreshToken) {
  158 + this.reloadUserObservable = this.authService.setUserFromJwtToken(reloadUserMessage.accessToken,
  159 + reloadUserMessage.refreshToken, true).pipe(
  160 + tap(
  161 + () => {
  162 + this.reloadUserObservable = null;
  163 + }
  164 + )
  165 + );
  166 + }
  167 + }
  168 +
70 } 169 }
@@ -441,3 +441,7 @@ export function generateSecret(length?: number): string { @@ -441,3 +441,7 @@ export function generateSecret(length?: number): string {
441 export function validateEntityId(entityId: EntityId | null): boolean { 441 export function validateEntityId(entityId: EntityId | null): boolean {
442 return isDefinedAndNotNull(entityId?.id) && entityId.id !== NULL_UUID && isDefinedAndNotNull(entityId?.entityType); 442 return isDefinedAndNotNull(entityId?.id) && entityId.id !== NULL_UUID && isDefinedAndNotNull(entityId?.entityType);
443 } 443 }
  444 +
  445 +export function isMobileApp(): boolean {
  446 + return isDefined((window as any).flutter_inappwebview);
  447 +}
@@ -87,7 +87,7 @@ @@ -87,7 +87,7 @@
87 (click)="updateDashboardImage($event)"> 87 (click)="updateDashboardImage($event)">
88 <mat-icon>wallpaper</mat-icon> 88 <mat-icon>wallpaper</mat-icon>
89 </button> 89 </button>
90 - <button [fxShow]="currentDashboardId && (isEdit || displayExport())" mat-icon-button 90 + <button [fxShow]="currentDashboardId && !isMobileApp && (isEdit || displayExport())" mat-icon-button
91 matTooltip="{{'dashboard.export' | translate}}" 91 matTooltip="{{'dashboard.export' | translate}}"
92 matTooltipPosition="below" 92 matTooltipPosition="below"
93 (click)="exportDashboard($event)"> 93 (click)="exportDashboard($event)">
@@ -323,6 +323,17 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -323,6 +323,17 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
323 this.runChangeDetection(); 323 this.runChangeDetection();
324 } 324 }
325 )); 325 ));
  326 + this.rxSubscriptions.push(this.route.queryParamMap.subscribe(
  327 + (paramMap) => {
  328 + if (paramMap.has('reload')) {
  329 + this.dashboardCtx.aliasController.updateAliases();
  330 + setTimeout(() => {
  331 + this.mobileService.handleDashboardStateName(this.dashboardCtx.stateController.getCurrentStateName());
  332 + this.mobileService.onDashboardLoaded();
  333 + });
  334 + }
  335 + }
  336 + ));
326 this.rxSubscriptions.push(this.breakpointObserver 337 this.rxSubscriptions.push(this.breakpointObserver
327 .observe(MediaBreakpoints['gt-sm']) 338 .observe(MediaBreakpoints['gt-sm'])
328 .subscribe((state: BreakpointState) => { 339 .subscribe((state: BreakpointState) => {
@@ -770,6 +781,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC @@ -770,6 +781,9 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
770 this.isRightLayoutOpened = openRightLayout ? true : false; 781 this.isRightLayoutOpened = openRightLayout ? true : false;
771 this.updateLayouts(layoutsData); 782 this.updateLayouts(layoutsData);
772 } 783 }
  784 + setTimeout(() => {
  785 + this.mobileService.onDashboardLoaded();
  786 + });
773 } 787 }
774 788
775 private updateLayouts(layoutsData?: DashboardLayoutsInfo) { 789 private updateLayouts(layoutsData?: DashboardLayoutsInfo) {
@@ -185,6 +185,10 @@ export class DefaultStateControllerComponent extends StateControllerComponent im @@ -185,6 +185,10 @@ export class DefaultStateControllerComponent extends StateControllerComponent im
185 return this.utils.customTranslation(state.name, id); 185 return this.utils.customTranslation(state.name, id);
186 } 186 }
187 187
  188 + public getCurrentStateName(): string {
  189 + return this.getStateName(this.stateObject[0].id, this.statesValue[this.stateObject[0].id]);
  190 + }
  191 +
188 public displayStateSelection(): boolean { 192 public displayStateSelection(): boolean {
189 return this.states && Object.keys(this.states).length > 1; 193 return this.states && Object.keys(this.states).length > 1;
190 } 194 }
@@ -235,6 +235,10 @@ export class EntityStateControllerComponent extends StateControllerComponent imp @@ -235,6 +235,10 @@ export class EntityStateControllerComponent extends StateControllerComponent imp
235 return result; 235 return result;
236 } 236 }
237 237
  238 + public getCurrentStateName(): string {
  239 + return this.getStateName(this.stateObject.length - 1);
  240 + }
  241 +
238 public selectedStateIndexChanged() { 242 public selectedStateIndexChanged() {
239 this.navigatePrevState(this.selectedStateIndex); 243 this.navigatePrevState(this.selectedStateIndex);
240 } 244 }
@@ -198,4 +198,6 @@ export abstract class StateControllerComponent implements IStateControllerCompon @@ -198,4 +198,6 @@ export abstract class StateControllerComponent implements IStateControllerCompon
198 198
199 public abstract updateState(id?: string, params?: StateParams, openRightLayout?: boolean): void; 199 public abstract updateState(id?: string, params?: StateParams, openRightLayout?: boolean): void;
200 200
  201 + public abstract getCurrentStateName(): string;
  202 +
201 } 203 }
@@ -89,6 +89,30 @@ @@ -89,6 +89,30 @@
89 </mat-error> 89 </mat-error>
90 </mat-form-field> 90 </mat-form-field>
91 <mat-form-field class="mat-block"> 91 <mat-form-field class="mat-block">
  92 + <mat-label translate>tenant-profile.maximum-resources-sum-data-size</mat-label>
  93 + <input matInput required min="0" step="1"
  94 + formControlName="maxResourcesInBytes"
  95 + type="number">
  96 + <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxResourcesInBytes').hasError('required')">
  97 + {{ 'tenant-profile.maximum-resources-sum-data-size-required' | translate}}
  98 + </mat-error>
  99 + <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxResourcesInBytes').hasError('min')">
  100 + {{ 'tenant-profile.maximum-resources-sum-data-size-range' | translate}}
  101 + </mat-error>
  102 + </mat-form-field>
  103 + <mat-form-field class="mat-block">
  104 + <mat-label translate>tenant-profile.maximum-ota-packages-sum-data-size</mat-label>
  105 + <input matInput required min="0" step="1"
  106 + formControlName="maxOtaPackagesInBytes"
  107 + type="number">
  108 + <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxOtaPackagesInBytes').hasError('required')">
  109 + {{ 'tenant-profile.maximum-ota-packages-sum-data-size-required' | translate}}
  110 + </mat-error>
  111 + <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('maxOtaPackagesInBytes').hasError('min')">
  112 + {{ 'tenant-profile.maximum-ota-packages-sum-data-size-range' | translate}}
  113 + </mat-error>
  114 + </mat-form-field>
  115 + <mat-form-field class="mat-block">
92 <mat-label translate>tenant-profile.max-transport-messages</mat-label> 116 <mat-label translate>tenant-profile.max-transport-messages</mat-label>
93 <input matInput required min="0" step="1" 117 <input matInput required min="0" step="1"
94 formControlName="maxTransportMessages" 118 formControlName="maxTransportMessages"
@@ -161,6 +185,18 @@ @@ -161,6 +185,18 @@
161 </mat-error> 185 </mat-error>
162 </mat-form-field> 186 </mat-form-field>
163 <mat-form-field class="mat-block"> 187 <mat-form-field class="mat-block">
  188 + <mat-label translate>tenant-profile.alarms-ttl-days</mat-label>
  189 + <input matInput required min="0" step="1"
  190 + formControlName="alarmsTtlDays"
  191 + type="number">
  192 + <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('alarmsTtlDays').hasError('required')">
  193 + {{ 'tenant-profile.alarms-ttl-days-required' | translate}}
  194 + </mat-error>
  195 + <mat-error *ngIf="defaultTenantProfileConfigurationFormGroup.get('alarmsTtlDays').hasError('min')">
  196 + {{ 'tenant-profile.alarms-ttl-days-days-range' | translate}}
  197 + </mat-error>
  198 + </mat-form-field>
  199 + <mat-form-field class="mat-block">
164 <mat-label translate>tenant-profile.max-rule-node-executions-per-message</mat-label> 200 <mat-label translate>tenant-profile.max-rule-node-executions-per-message</mat-label>
165 <input matInput required min="0" step="1" 201 <input matInput required min="0" step="1"
166 formControlName="maxRuleNodeExecutionsPerMessage" 202 formControlName="maxRuleNodeExecutionsPerMessage"
@@ -59,6 +59,8 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA @@ -59,6 +59,8 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
59 maxUsers: [null, [Validators.required, Validators.min(0)]], 59 maxUsers: [null, [Validators.required, Validators.min(0)]],
60 maxDashboards: [null, [Validators.required, Validators.min(0)]], 60 maxDashboards: [null, [Validators.required, Validators.min(0)]],
61 maxRuleChains: [null, [Validators.required, Validators.min(0)]], 61 maxRuleChains: [null, [Validators.required, Validators.min(0)]],
  62 + maxResourcesInBytes: [null, [Validators.required, Validators.min(0)]],
  63 + maxOtaPackagesInBytes: [null, [Validators.required, Validators.min(0)]],
62 transportTenantMsgRateLimit: [null, []], 64 transportTenantMsgRateLimit: [null, []],
63 transportTenantTelemetryMsgRateLimit: [null, []], 65 transportTenantTelemetryMsgRateLimit: [null, []],
64 transportTenantTelemetryDataPointsRateLimit: [null, []], 66 transportTenantTelemetryDataPointsRateLimit: [null, []],
@@ -74,7 +76,8 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA @@ -74,7 +76,8 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
74 maxEmails: [null, [Validators.required, Validators.min(0)]], 76 maxEmails: [null, [Validators.required, Validators.min(0)]],
75 maxSms: [null, [Validators.required, Validators.min(0)]], 77 maxSms: [null, [Validators.required, Validators.min(0)]],
76 maxCreatedAlarms: [null, [Validators.required, Validators.min(0)]], 78 maxCreatedAlarms: [null, [Validators.required, Validators.min(0)]],
77 - defaultStorageTtlDays: [null, [Validators.required, Validators.min(0)]] 79 + defaultStorageTtlDays: [null, [Validators.required, Validators.min(0)]],
  80 + alarmsTtlDays: [null, [Validators.required, Validators.min(0)]]
78 }); 81 });
79 this.defaultTenantProfileConfigurationFormGroup.valueChanges.subscribe(() => { 82 this.defaultTenantProfileConfigurationFormGroup.valueChanges.subscribe(() => {
80 this.updateModel(); 83 this.updateModel();
@@ -18,6 +18,7 @@ import { ContactBased } from '@shared/models/contact-based.model'; @@ -18,6 +18,7 @@ import { ContactBased } from '@shared/models/contact-based.model';
18 import { TenantId } from './id/tenant-id'; 18 import { TenantId } from './id/tenant-id';
19 import { TenantProfileId } from '@shared/models/id/tenant-profile-id'; 19 import { TenantProfileId } from '@shared/models/id/tenant-profile-id';
20 import { BaseData } from '@shared/models/base-data'; 20 import { BaseData } from '@shared/models/base-data';
  21 +import {Validators} from "@angular/forms";
21 22
22 export enum TenantProfileType { 23 export enum TenantProfileType {
23 DEFAULT = 'DEFAULT' 24 DEFAULT = 'DEFAULT'
@@ -30,6 +31,8 @@ export interface DefaultTenantProfileConfiguration { @@ -30,6 +31,8 @@ export interface DefaultTenantProfileConfiguration {
30 maxUsers: number; 31 maxUsers: number;
31 maxDashboards: number; 32 maxDashboards: number;
32 maxRuleChains: number; 33 maxRuleChains: number;
  34 + maxResourcesInBytes: number;
  35 + maxOtaPackagesInBytes: number;
33 36
34 transportTenantMsgRateLimit?: string; 37 transportTenantMsgRateLimit?: string;
35 transportTenantTelemetryMsgRateLimit?: string; 38 transportTenantTelemetryMsgRateLimit?: string;
@@ -68,6 +71,8 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan @@ -68,6 +71,8 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan
68 maxUsers: 0, 71 maxUsers: 0,
69 maxDashboards: 0, 72 maxDashboards: 0,
70 maxRuleChains: 0, 73 maxRuleChains: 0,
  74 + maxResourcesInBytes: 0,
  75 + maxOtaPackagesInBytes: 0,
71 maxTransportMessages: 0, 76 maxTransportMessages: 0,
72 maxTransportDataPoints: 0, 77 maxTransportDataPoints: 0,
73 maxREExecutions: 0, 78 maxREExecutions: 0,
@@ -14,9 +14,21 @@ @@ -14,9 +14,21 @@
14 /// limitations under the License. 14 /// limitations under the License.
15 /// 15 ///
16 16
17 -export type WindowMessageType = 'widgetException' | 'widgetEditModeInited' | 'widgetEditUpdated'; 17 +export type WindowMessageType = 'widgetException' | 'widgetEditModeInited' | 'widgetEditUpdated' | 'openDashboardMessage' | 'reloadUserMessage';
18 18
19 export interface WindowMessage { 19 export interface WindowMessage {
20 type: WindowMessageType; 20 type: WindowMessageType;
21 data?: any; 21 data?: any;
22 } 22 }
  23 +
  24 +export interface OpenDashboardMessage {
  25 + dashboardId: string;
  26 + state?: string;
  27 + hideToolbar?: boolean;
  28 + embedded?: boolean;
  29 +}
  30 +
  31 +export interface ReloadUserMessage {
  32 + accessToken: string;
  33 + refreshToken: string;
  34 +}
@@ -2504,6 +2504,12 @@ @@ -2504,6 +2504,12 @@
2504 "maximum-rule-chains": "Maximum number of rule chains (0 - unlimited)", 2504 "maximum-rule-chains": "Maximum number of rule chains (0 - unlimited)",
2505 "maximum-rule-chains-required": "Maximum number of rule chains is required.", 2505 "maximum-rule-chains-required": "Maximum number of rule chains is required.",
2506 "maximum-rule-chains-range": "Maximum number of rule chains can't be negative", 2506 "maximum-rule-chains-range": "Maximum number of rule chains can't be negative",
  2507 + "maximum-resources-sum-data-size": "Maximum sum of resource files size in bytes (0 - unlimited)",
  2508 + "maximum-resources-sum-data-size-required": "Maximum sum of resource files size is required.",
  2509 + "maximum-resources-sum-data-size-range": "Maximum sum of resource files size can`t be negative",
  2510 + "maximum-ota-packages-sum-data-size": "Maximum sum of ota package files size in bytes (0 - unlimited)",
  2511 + "maximum-ota-package-sum-data-size-required": "Maximum sum of ota package files size is required.",
  2512 + "maximum-ota-package-sum-data-size-range": "Maximum sum of ota package files size can`t be negative",
2507 "transport-tenant-msg-rate-limit": "Transport tenant messages rate limit.", 2513 "transport-tenant-msg-rate-limit": "Transport tenant messages rate limit.",
2508 "transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages rate limit.", 2514 "transport-tenant-telemetry-msg-rate-limit": "Transport tenant telemetry messages rate limit.",
2509 "transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points rate limit.", 2515 "transport-tenant-telemetry-data-points-rate-limit": "Transport tenant telemetry data points rate limit.",
@@ -2528,6 +2534,9 @@ @@ -2528,6 +2534,9 @@
2528 "default-storage-ttl-days": "Default storage TTL days (0 - unlimited)", 2534 "default-storage-ttl-days": "Default storage TTL days (0 - unlimited)",
2529 "default-storage-ttl-days-required": "Default storage TTL days is required.", 2535 "default-storage-ttl-days-required": "Default storage TTL days is required.",
2530 "default-storage-ttl-days-range": "Default storage TTL days can't be negative", 2536 "default-storage-ttl-days-range": "Default storage TTL days can't be negative",
  2537 + "alarms-ttl-days": "Alarms TTL days (0 - unlimited)",
  2538 + "alarms-ttl-days-required": "Alarms TTL days required",
  2539 + "alarms-ttl-days-days-range": "Alarms TTL days can't be negative",
2531 "max-rule-node-executions-per-message": "Maximum number of rule node executions per message (0 - unlimited)", 2540 "max-rule-node-executions-per-message": "Maximum number of rule node executions per message (0 - unlimited)",
2532 "max-rule-node-executions-per-message-required": "Maximum number of rule node executions per message is required.", 2541 "max-rule-node-executions-per-message-required": "Maximum number of rule node executions per message is required.",
2533 "max-rule-node-executions-per-message-range": "Maximum number of rule node executions per message can't be negative", 2542 "max-rule-node-executions-per-message-range": "Maximum number of rule node executions per message can't be negative",
@@ -174,6 +174,48 @@ $tb-dark-theme: get-tb-dark-theme( @@ -174,6 +174,48 @@ $tb-dark-theme: get-tb-dark-theme(
174 } 174 }
175 } 175 }
176 176
  177 +@mixin _mat-toolbar-inverse-color($palette) {
  178 + background: mat-color($palette, default-contrast);
  179 + color: $dark-primary-text;
  180 +}
  181 +
  182 +@mixin mat-fab-toolbar-inverse-theme($theme) {
  183 + $primary: map-get($theme, primary);
  184 + $accent: map-get($theme, accent);
  185 + $warn: map-get($theme, warn);
  186 + $background: map-get($theme, foreground);
  187 + $foreground: map-get($theme, background);
  188 +
  189 + mat-fab-toolbar {
  190 + .mat-fab-toolbar-background {
  191 + background: mat-color($background, app-bar);
  192 + color: mat-color($foreground, text);
  193 + }
  194 + &.mat-primary {
  195 + .mat-fab-toolbar-background {
  196 + @include _mat-toolbar-inverse-color($primary);
  197 + }
  198 + }
  199 + mat-toolbar {
  200 + &.mat-primary {
  201 + @include _mat-toolbar-inverse-color($primary);
  202 + button.mat-icon-button {
  203 + mat-icon {
  204 + color: mat-color($primary);
  205 + }
  206 + }
  207 + }
  208 + }
  209 + .mat-fab {
  210 + &.mat-primary {
  211 + background: mat-color($primary, default-contrast);
  212 + color: mat-color($primary);
  213 + }
  214 + }
  215 + }
  216 +
  217 +}
  218 +
177 @mixin tb-components-theme($theme) { 219 @mixin tb-components-theme($theme) {
178 $primary: map-get($theme, primary); 220 $primary: map-get($theme, primary);
179 221
@@ -184,6 +226,10 @@ $tb-dark-theme: get-tb-dark-theme( @@ -184,6 +226,10 @@ $tb-dark-theme: get-tb-dark-theme(
184 } 226 }
185 227
186 @include mat-fab-toolbar-theme($tb-theme); 228 @include mat-fab-toolbar-theme($tb-theme);
  229 +
  230 + div.tb-dashboard-page.mobile-app {
  231 + @include mat-fab-toolbar-inverse-theme($tb-theme);
  232 + }
187 } 233 }
188 234
189 .tb-default { 235 .tb-default {
@@ -195,3 +241,4 @@ $tb-dark-theme: get-tb-dark-theme( @@ -195,3 +241,4 @@ $tb-dark-theme: get-tb-dark-theme(
195 .tb-dark { 241 .tb-dark {
196 @include angular-material-theme($tb-dark-theme); 242 @include angular-material-theme($tb-dark-theme);
197 } 243 }
  244 +