Commit 9f9e613320f435ada5142ee048afb881fbad5540
Merge remote-tracking branch 'upstream/master' into feature/input-password/toggle
Showing
70 changed files
with
895 additions
and
294 deletions
@@ -290,6 +290,11 @@ | @@ -290,6 +290,11 @@ | ||
290 | <scope>test</scope> | 290 | <scope>test</scope> |
291 | </dependency> | 291 | </dependency> |
292 | <dependency> | 292 | <dependency> |
293 | + <groupId>org.awaitility</groupId> | ||
294 | + <artifactId>awaitility</artifactId> | ||
295 | + <scope>test</scope> | ||
296 | + </dependency> | ||
297 | + <dependency> | ||
293 | <groupId>org.mockito</groupId> | 298 | <groupId>org.mockito</groupId> |
294 | <artifactId>mockito-core</artifactId> | 299 | <artifactId>mockito-core</artifactId> |
295 | <scope>test</scope> | 300 | <scope>test</scope> |
@@ -381,11 +381,11 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { | @@ -381,11 +381,11 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { | ||
381 | } | 381 | } |
382 | 382 | ||
383 | private void reportSessionOpen() { | 383 | private void reportSessionOpen() { |
384 | - systemContext.getDeviceStateService().onDeviceConnect(deviceId); | 384 | + systemContext.getDeviceStateService().onDeviceConnect(tenantId, deviceId); |
385 | } | 385 | } |
386 | 386 | ||
387 | private void reportSessionClose() { | 387 | private void reportSessionClose() { |
388 | - systemContext.getDeviceStateService().onDeviceDisconnect(deviceId); | 388 | + systemContext.getDeviceStateService().onDeviceDisconnect(tenantId, deviceId); |
389 | } | 389 | } |
390 | 390 | ||
391 | private void handleGetAttributesRequest(TbActorCtx context, SessionInfoProto sessionInfo, GetAttributeRequestMsg request) { | 391 | private void handleGetAttributesRequest(TbActorCtx context, SessionInfoProto sessionInfo, GetAttributeRequestMsg request) { |
@@ -590,7 +590,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { | @@ -590,7 +590,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { | ||
590 | if (sessions.size() == 1) { | 590 | if (sessions.size() == 1) { |
591 | reportSessionOpen(); | 591 | reportSessionOpen(); |
592 | } | 592 | } |
593 | - systemContext.getDeviceStateService().onDeviceActivity(deviceId, System.currentTimeMillis()); | 593 | + systemContext.getDeviceStateService().onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); |
594 | dumpSessions(); | 594 | dumpSessions(); |
595 | } else if (msg.getEvent() == SessionEvent.CLOSED) { | 595 | } else if (msg.getEvent() == SessionEvent.CLOSED) { |
596 | log.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId); | 596 | log.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId); |
@@ -620,7 +620,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { | @@ -620,7 +620,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { | ||
620 | if (subscriptionInfo.getRpcSubscription()) { | 620 | if (subscriptionInfo.getRpcSubscription()) { |
621 | rpcSubscriptions.putIfAbsent(sessionId, sessionMD.getSessionInfo()); | 621 | rpcSubscriptions.putIfAbsent(sessionId, sessionMD.getSessionInfo()); |
622 | } | 622 | } |
623 | - systemContext.getDeviceStateService().onDeviceActivity(deviceId, subscriptionInfo.getLastActivityTime()); | 623 | + systemContext.getDeviceStateService().onDeviceActivity(tenantId, deviceId, subscriptionInfo.getLastActivityTime()); |
624 | dumpSessions(); | 624 | dumpSessions(); |
625 | } | 625 | } |
626 | 626 |
@@ -26,12 +26,12 @@ import org.springframework.web.bind.annotation.ResponseBody; | @@ -26,12 +26,12 @@ import org.springframework.web.bind.annotation.ResponseBody; | ||
26 | import org.springframework.web.bind.annotation.RestController; | 26 | import org.springframework.web.bind.annotation.RestController; |
27 | import org.thingsboard.rule.engine.api.MailService; | 27 | import org.thingsboard.rule.engine.api.MailService; |
28 | import org.thingsboard.rule.engine.api.SmsService; | 28 | import org.thingsboard.rule.engine.api.SmsService; |
29 | -import org.thingsboard.server.common.data.sms.config.TestSmsRequest; | ||
30 | import org.thingsboard.server.common.data.AdminSettings; | 29 | import org.thingsboard.server.common.data.AdminSettings; |
31 | import org.thingsboard.server.common.data.UpdateMessage; | 30 | import org.thingsboard.server.common.data.UpdateMessage; |
32 | import org.thingsboard.server.common.data.exception.ThingsboardException; | 31 | import org.thingsboard.server.common.data.exception.ThingsboardException; |
33 | import org.thingsboard.server.common.data.id.TenantId; | 32 | import org.thingsboard.server.common.data.id.TenantId; |
34 | import org.thingsboard.server.common.data.security.model.SecuritySettings; | 33 | import org.thingsboard.server.common.data.security.model.SecuritySettings; |
34 | +import org.thingsboard.server.common.data.sms.config.TestSmsRequest; | ||
35 | import org.thingsboard.server.dao.settings.AdminSettingsService; | 35 | import org.thingsboard.server.dao.settings.AdminSettingsService; |
36 | import org.thingsboard.server.queue.util.TbCoreComponent; | 36 | import org.thingsboard.server.queue.util.TbCoreComponent; |
37 | import org.thingsboard.server.service.security.permission.Operation; | 37 | import org.thingsboard.server.service.security.permission.Operation; |
@@ -67,7 +67,7 @@ public class AdminController extends BaseController { | @@ -67,7 +67,7 @@ public class AdminController extends BaseController { | ||
67 | accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); | 67 | accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); |
68 | AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key)); | 68 | AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key)); |
69 | if (adminSettings.getKey().equals("mail")) { | 69 | if (adminSettings.getKey().equals("mail")) { |
70 | - ((ObjectNode) adminSettings.getJsonValue()).put("password", ""); | 70 | + ((ObjectNode) adminSettings.getJsonValue()).remove("password"); |
71 | } | 71 | } |
72 | return adminSettings; | 72 | return adminSettings; |
73 | } catch (Exception e) { | 73 | } catch (Exception e) { |
@@ -84,7 +84,7 @@ public class AdminController extends BaseController { | @@ -84,7 +84,7 @@ public class AdminController extends BaseController { | ||
84 | adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings)); | 84 | adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings)); |
85 | if (adminSettings.getKey().equals("mail")) { | 85 | if (adminSettings.getKey().equals("mail")) { |
86 | mailService.updateMailConfiguration(); | 86 | mailService.updateMailConfiguration(); |
87 | - ((ObjectNode) adminSettings.getJsonValue()).put("password", ""); | 87 | + ((ObjectNode) adminSettings.getJsonValue()).remove("password"); |
88 | } else if (adminSettings.getKey().equals("sms")) { | 88 | } else if (adminSettings.getKey().equals("sms")) { |
89 | smsService.updateSmsConfiguration(); | 89 | smsService.updateSmsConfiguration(); |
90 | } | 90 | } |
@@ -126,6 +126,10 @@ public class AdminController extends BaseController { | @@ -126,6 +126,10 @@ public class AdminController extends BaseController { | ||
126 | accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); | 126 | accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); |
127 | adminSettings = checkNotNull(adminSettings); | 127 | adminSettings = checkNotNull(adminSettings); |
128 | if (adminSettings.getKey().equals("mail")) { | 128 | if (adminSettings.getKey().equals("mail")) { |
129 | + if(!adminSettings.getJsonValue().has("password")) { | ||
130 | + AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail")); | ||
131 | + ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText()); | ||
132 | + } | ||
129 | String email = getCurrentUser().getEmail(); | 133 | String email = getCurrentUser().getEmail(); |
130 | mailService.sendTestMail(adminSettings.getJsonValue(), email); | 134 | mailService.sendTestMail(adminSettings.getJsonValue(), email); |
131 | } | 135 | } |
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
@@ -215,6 +215,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { | @@ -215,6 +215,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { | ||
215 | node.put("password", ""); | 215 | node.put("password", ""); |
216 | node.put("tlsVersion", "TLSv1.2");//NOSONAR, key used to identify password field (not password value itself) | 216 | node.put("tlsVersion", "TLSv1.2");//NOSONAR, key used to identify password field (not password value itself) |
217 | node.put("enableProxy", false); | 217 | node.put("enableProxy", false); |
218 | + node.put("showChangePassword", false); | ||
218 | mailSettings.setJsonValue(node); | 219 | mailSettings.setJsonValue(node); |
219 | adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, mailSettings); | 220 | adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, mailSettings); |
220 | } | 221 | } |
@@ -17,6 +17,7 @@ package org.thingsboard.server.service.security.auth.oauth2; | @@ -17,6 +17,7 @@ package org.thingsboard.server.service.security.auth.oauth2; | ||
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.datatype.jsr310.JavaTimeModule; | ||
20 | import lombok.extern.slf4j.Slf4j; | 21 | import lombok.extern.slf4j.Slf4j; |
21 | import org.springframework.boot.web.client.RestTemplateBuilder; | 22 | import org.springframework.boot.web.client.RestTemplateBuilder; |
22 | import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; | 23 | import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; |
@@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.oauth2.OAuth2Registration; | @@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.oauth2.OAuth2Registration; | ||
29 | import org.thingsboard.server.dao.oauth2.OAuth2User; | 30 | import org.thingsboard.server.dao.oauth2.OAuth2User; |
30 | import org.thingsboard.server.service.security.model.SecurityUser; | 31 | import org.thingsboard.server.service.security.model.SecurityUser; |
31 | 32 | ||
33 | +import javax.annotation.PostConstruct; | ||
32 | import javax.servlet.http.HttpServletRequest; | 34 | import javax.servlet.http.HttpServletRequest; |
33 | 35 | ||
34 | @Service(value = "customOAuth2ClientMapper") | 36 | @Service(value = "customOAuth2ClientMapper") |
@@ -40,6 +42,15 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme | @@ -40,6 +42,15 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme | ||
40 | 42 | ||
41 | private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); | 43 | private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); |
42 | 44 | ||
45 | + @PostConstruct | ||
46 | + public void init() { | ||
47 | + // Register time module to parse Instant objects. | ||
48 | + // com.fasterxml.jackson.databind.exc.InvalidDefinitionException: | ||
49 | + // Java 8 date/time type `java.time.Instant` not supported by default: | ||
50 | + // add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling | ||
51 | + json.registerModule(new JavaTimeModule()); | ||
52 | + } | ||
53 | + | ||
43 | @Override | 54 | @Override |
44 | public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { | 55 | public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { |
45 | OAuth2MapperConfig config = registration.getMapperConfig(); | 56 | OAuth2MapperConfig config = registration.getMapperConfig(); |
@@ -15,6 +15,7 @@ | @@ -15,6 +15,7 @@ | ||
15 | */ | 15 | */ |
16 | package org.thingsboard.server.service.security.auth.oauth2; | 16 | package org.thingsboard.server.service.security.auth.oauth2; |
17 | 17 | ||
18 | +import lombok.extern.slf4j.Slf4j; | ||
18 | import org.springframework.beans.factory.annotation.Autowired; | 19 | import org.springframework.beans.factory.annotation.Autowired; |
19 | import org.springframework.security.core.Authentication; | 20 | import org.springframework.security.core.Authentication; |
20 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; | 21 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; |
@@ -42,6 +43,7 @@ import java.net.URLEncoder; | @@ -42,6 +43,7 @@ import java.net.URLEncoder; | ||
42 | import java.nio.charset.StandardCharsets; | 43 | import java.nio.charset.StandardCharsets; |
43 | import java.util.UUID; | 44 | import java.util.UUID; |
44 | 45 | ||
46 | +@Slf4j | ||
45 | @Component(value = "oauth2AuthenticationSuccessHandler") | 47 | @Component(value = "oauth2AuthenticationSuccessHandler") |
46 | public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { | 48 | public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { |
47 | 49 | ||
@@ -99,6 +101,8 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS | @@ -99,6 +101,8 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS | ||
99 | clearAuthenticationAttributes(request, response); | 101 | clearAuthenticationAttributes(request, response); |
100 | getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken()); | 102 | getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken()); |
101 | } catch (Exception e) { | 103 | } catch (Exception e) { |
104 | + log.debug("Error occurred during processing authentication success result. " + | ||
105 | + "request [{}], response [{}], authentication [{}]", request, response, authentication, e); | ||
102 | clearAuthenticationAttributes(request, response); | 106 | clearAuthenticationAttributes(request, response); |
103 | String errorPrefix; | 107 | String errorPrefix; |
104 | if (!StringUtils.isEmpty(callbackUrlScheme)) { | 108 | if (!StringUtils.isEmpty(callbackUrlScheme)) { |
@@ -137,7 +137,6 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | @@ -137,7 +137,6 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | ||
137 | private ExecutorService deviceStateExecutor; | 137 | private ExecutorService deviceStateExecutor; |
138 | private final ConcurrentMap<TopicPartitionInfo, Set<DeviceId>> partitionedDevices = new ConcurrentHashMap<>(); | 138 | private final ConcurrentMap<TopicPartitionInfo, Set<DeviceId>> partitionedDevices = new ConcurrentHashMap<>(); |
139 | final ConcurrentMap<DeviceId, DeviceStateData> deviceStates = new ConcurrentHashMap<>(); | 139 | final ConcurrentMap<DeviceId, DeviceStateData> deviceStates = new ConcurrentHashMap<>(); |
140 | - private final ConcurrentMap<DeviceId, Long> deviceLastSavedActivity = new ConcurrentHashMap<>(); | ||
141 | 140 | ||
142 | final Queue<Set<TopicPartitionInfo>> subscribeQueue = new ConcurrentLinkedQueue<>(); | 141 | final Queue<Set<TopicPartitionInfo>> subscribeQueue = new ConcurrentLinkedQueue<>(); |
143 | 142 | ||
@@ -192,7 +191,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | @@ -192,7 +191,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | ||
192 | } | 191 | } |
193 | 192 | ||
194 | @Override | 193 | @Override |
195 | - public void onDeviceConnect(DeviceId deviceId) { | 194 | + public void onDeviceConnect(TenantId tenantId, DeviceId deviceId) { |
196 | log.trace("on Device Connect [{}]", deviceId.getId()); | 195 | log.trace("on Device Connect [{}]", deviceId.getId()); |
197 | DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); | 196 | DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); |
198 | long ts = System.currentTimeMillis(); | 197 | long ts = System.currentTimeMillis(); |
@@ -200,23 +199,23 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | @@ -200,23 +199,23 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | ||
200 | save(deviceId, LAST_CONNECT_TIME, ts); | 199 | save(deviceId, LAST_CONNECT_TIME, ts); |
201 | pushRuleEngineMessage(stateData, CONNECT_EVENT); | 200 | pushRuleEngineMessage(stateData, CONNECT_EVENT); |
202 | checkAndUpdateState(deviceId, stateData); | 201 | checkAndUpdateState(deviceId, stateData); |
202 | + cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId); | ||
203 | } | 203 | } |
204 | 204 | ||
205 | @Override | 205 | @Override |
206 | - public void onDeviceActivity(DeviceId deviceId, long lastReportedActivity) { | 206 | + public void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long lastReportedActivity) { |
207 | log.trace("on Device Activity [{}], lastReportedActivity [{}]", deviceId.getId(), lastReportedActivity); | 207 | log.trace("on Device Activity [{}], lastReportedActivity [{}]", deviceId.getId(), lastReportedActivity); |
208 | - long lastSavedActivity = deviceLastSavedActivity.getOrDefault(deviceId, 0L); | ||
209 | - if (lastReportedActivity > 0 && lastReportedActivity > lastSavedActivity) { | ||
210 | - final DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); | 208 | + final DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); |
209 | + if (lastReportedActivity > 0 && lastReportedActivity > stateData.getState().getLastActivityTime()) { | ||
211 | updateActivityState(deviceId, stateData, lastReportedActivity); | 210 | updateActivityState(deviceId, stateData, lastReportedActivity); |
212 | } | 211 | } |
212 | + cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId); | ||
213 | } | 213 | } |
214 | 214 | ||
215 | void updateActivityState(DeviceId deviceId, DeviceStateData stateData, long lastReportedActivity) { | 215 | void updateActivityState(DeviceId deviceId, DeviceStateData stateData, long lastReportedActivity) { |
216 | log.trace("updateActivityState - fetched state {} for device {}, lastReportedActivity {}", stateData, deviceId, lastReportedActivity); | 216 | log.trace("updateActivityState - fetched state {} for device {}, lastReportedActivity {}", stateData, deviceId, lastReportedActivity); |
217 | if (stateData != null) { | 217 | if (stateData != null) { |
218 | save(deviceId, LAST_ACTIVITY_TIME, lastReportedActivity); | 218 | save(deviceId, LAST_ACTIVITY_TIME, lastReportedActivity); |
219 | - deviceLastSavedActivity.put(deviceId, lastReportedActivity); | ||
220 | DeviceState state = stateData.getState(); | 219 | DeviceState state = stateData.getState(); |
221 | state.setLastActivityTime(lastReportedActivity); | 220 | state.setLastActivityTime(lastReportedActivity); |
222 | if (!state.isActive()) { | 221 | if (!state.isActive()) { |
@@ -225,21 +224,23 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | @@ -225,21 +224,23 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | ||
225 | pushRuleEngineMessage(stateData, ACTIVITY_EVENT); | 224 | pushRuleEngineMessage(stateData, ACTIVITY_EVENT); |
226 | } | 225 | } |
227 | } else { | 226 | } else { |
228 | - log.warn("updateActivityState - fetched state IN NULL for device {}, lastReportedActivity {}", deviceId, lastReportedActivity); | 227 | + log.debug("updateActivityState - fetched state IN NULL for device {}, lastReportedActivity {}", deviceId, lastReportedActivity); |
228 | + cleanUpDeviceStateMap(deviceId); | ||
229 | } | 229 | } |
230 | } | 230 | } |
231 | 231 | ||
232 | @Override | 232 | @Override |
233 | - public void onDeviceDisconnect(DeviceId deviceId) { | 233 | + public void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId) { |
234 | DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); | 234 | DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); |
235 | long ts = System.currentTimeMillis(); | 235 | long ts = System.currentTimeMillis(); |
236 | stateData.getState().setLastDisconnectTime(ts); | 236 | stateData.getState().setLastDisconnectTime(ts); |
237 | save(deviceId, LAST_DISCONNECT_TIME, ts); | 237 | save(deviceId, LAST_DISCONNECT_TIME, ts); |
238 | pushRuleEngineMessage(stateData, DISCONNECT_EVENT); | 238 | pushRuleEngineMessage(stateData, DISCONNECT_EVENT); |
239 | + cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId); | ||
239 | } | 240 | } |
240 | 241 | ||
241 | @Override | 242 | @Override |
242 | - public void onDeviceInactivityTimeoutUpdate(DeviceId deviceId, long inactivityTimeout) { | 243 | + public void onDeviceInactivityTimeoutUpdate(TenantId tenantId, DeviceId deviceId, long inactivityTimeout) { |
243 | if (inactivityTimeout <= 0L) { | 244 | if (inactivityTimeout <= 0L) { |
244 | return; | 245 | return; |
245 | } | 246 | } |
@@ -247,6 +248,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | @@ -247,6 +248,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | ||
247 | DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); | 248 | DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); |
248 | stateData.getState().setInactivityTimeout(inactivityTimeout); | 249 | stateData.getState().setInactivityTimeout(inactivityTimeout); |
249 | checkAndUpdateState(deviceId, stateData); | 250 | checkAndUpdateState(deviceId, stateData); |
251 | + cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId); | ||
250 | } | 252 | } |
251 | 253 | ||
252 | @Override | 254 | @Override |
@@ -283,12 +285,10 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | @@ -283,12 +285,10 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | ||
283 | }, deviceStateExecutor); | 285 | }, deviceStateExecutor); |
284 | } else if (proto.getUpdated()) { | 286 | } else if (proto.getUpdated()) { |
285 | DeviceStateData stateData = getOrFetchDeviceStateData(device.getId()); | 287 | DeviceStateData stateData = getOrFetchDeviceStateData(device.getId()); |
286 | - if (stateData != null) { | ||
287 | - TbMsgMetaData md = new TbMsgMetaData(); | ||
288 | - md.putValue("deviceName", device.getName()); | ||
289 | - md.putValue("deviceType", device.getType()); | ||
290 | - stateData.setMetaData(md); | ||
291 | - } | 288 | + TbMsgMetaData md = new TbMsgMetaData(); |
289 | + md.putValue("deviceName", device.getName()); | ||
290 | + md.putValue("deviceType", device.getType()); | ||
291 | + stateData.setMetaData(md); | ||
292 | } | 292 | } |
293 | } else { | 293 | } else { |
294 | //Device was probably deleted while message was in queue; | 294 | //Device was probably deleted while message was in queue; |
@@ -356,10 +356,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | @@ -356,10 +356,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | ||
356 | // We no longer manage current partition of devices; | 356 | // We no longer manage current partition of devices; |
357 | removedPartitions.forEach(partition -> { | 357 | removedPartitions.forEach(partition -> { |
358 | Set<DeviceId> devices = partitionedDevices.remove(partition); | 358 | Set<DeviceId> devices = partitionedDevices.remove(partition); |
359 | - devices.forEach(deviceId -> { | ||
360 | - deviceStates.remove(deviceId); | ||
361 | - deviceLastSavedActivity.remove(deviceId); | ||
362 | - }); | 359 | + devices.forEach(this::cleanUpDeviceStateMap); |
363 | }); | 360 | }); |
364 | 361 | ||
365 | addedPartitions.forEach(tpi -> partitionedDevices.computeIfAbsent(tpi, key -> ConcurrentHashMap.newKeySet())); | 362 | addedPartitions.forEach(tpi -> partitionedDevices.computeIfAbsent(tpi, key -> ConcurrentHashMap.newKeySet())); |
@@ -463,11 +460,12 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | @@ -463,11 +460,12 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | ||
463 | 460 | ||
464 | void updateInactivityStateIfExpired() { | 461 | void updateInactivityStateIfExpired() { |
465 | final long ts = System.currentTimeMillis(); | 462 | final long ts = System.currentTimeMillis(); |
466 | - log.debug("Calculating state updates for {} devices", deviceStates.size()); | ||
467 | - Set<DeviceId> deviceIds = new HashSet<>(deviceStates.keySet()); | ||
468 | - for (DeviceId deviceId : deviceIds) { | ||
469 | - updateInactivityStateIfExpired(ts, deviceId); | ||
470 | - } | 463 | + partitionedDevices.forEach((tpi, deviceIds) -> { |
464 | + log.debug("Calculating state updates. tpi {} for {} devices", tpi.getFullTopicName(), deviceIds.size()); | ||
465 | + for (DeviceId deviceId : deviceIds) { | ||
466 | + updateInactivityStateIfExpired(ts, deviceId); | ||
467 | + } | ||
468 | + }); | ||
471 | } | 469 | } |
472 | 470 | ||
473 | void updateInactivityStateIfExpired(long ts, DeviceId deviceId) { | 471 | void updateInactivityStateIfExpired(long ts, DeviceId deviceId) { |
@@ -488,8 +486,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | @@ -488,8 +486,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | ||
488 | } | 486 | } |
489 | } else { | 487 | } else { |
490 | log.debug("[{}] Device that belongs to other server is detected and removed.", deviceId); | 488 | log.debug("[{}] Device that belongs to other server is detected and removed.", deviceId); |
491 | - deviceStates.remove(deviceId); | ||
492 | - deviceLastSavedActivity.remove(deviceId); | 489 | + cleanUpDeviceStateMap(deviceId); |
493 | } | 490 | } |
494 | } | 491 | } |
495 | 492 | ||
@@ -522,6 +519,15 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | @@ -522,6 +519,15 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | ||
522 | } | 519 | } |
523 | } | 520 | } |
524 | 521 | ||
522 | + private void cleanDeviceStateIfBelongsExternalPartition(TenantId tenantId, final DeviceId deviceId) { | ||
523 | + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId); | ||
524 | + if (!partitionedDevices.containsKey(tpi)) { | ||
525 | + cleanUpDeviceStateMap(deviceId); | ||
526 | + log.debug("[{}][{}] device belongs to external partition. Probably rebalancing is in progress. Topic: {}" | ||
527 | + , tenantId, deviceId, tpi.getFullTopicName()); | ||
528 | + } | ||
529 | + } | ||
530 | + | ||
525 | private void sendDeviceEvent(TenantId tenantId, DeviceId deviceId, boolean added, boolean updated, boolean deleted) { | 531 | private void sendDeviceEvent(TenantId tenantId, DeviceId deviceId, boolean added, boolean updated, boolean deleted) { |
526 | TransportProtos.DeviceStateServiceMsgProto.Builder builder = TransportProtos.DeviceStateServiceMsgProto.newBuilder(); | 532 | TransportProtos.DeviceStateServiceMsgProto.Builder builder = TransportProtos.DeviceStateServiceMsgProto.newBuilder(); |
527 | builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); | 533 | builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); |
@@ -536,13 +542,16 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | @@ -536,13 +542,16 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit | ||
536 | } | 542 | } |
537 | 543 | ||
538 | private void onDeviceDeleted(TenantId tenantId, DeviceId deviceId) { | 544 | private void onDeviceDeleted(TenantId tenantId, DeviceId deviceId) { |
539 | - deviceStates.remove(deviceId); | ||
540 | - deviceLastSavedActivity.remove(deviceId); | 545 | + cleanUpDeviceStateMap(deviceId); |
541 | TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId); | 546 | TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId); |
542 | Set<DeviceId> deviceIdSet = partitionedDevices.get(tpi); | 547 | Set<DeviceId> deviceIdSet = partitionedDevices.get(tpi); |
543 | deviceIdSet.remove(deviceId); | 548 | deviceIdSet.remove(deviceId); |
544 | } | 549 | } |
545 | 550 | ||
551 | + private void cleanUpDeviceStateMap(DeviceId deviceId) { | ||
552 | + deviceStates.remove(deviceId); | ||
553 | + } | ||
554 | + | ||
546 | private ListenableFuture<DeviceStateData> fetchDeviceState(Device device) { | 555 | private ListenableFuture<DeviceStateData> fetchDeviceState(Device device) { |
547 | ListenableFuture<DeviceStateData> future; | 556 | ListenableFuture<DeviceStateData> future; |
548 | if (persistToTelemetry) { | 557 | if (persistToTelemetry) { |
@@ -18,6 +18,7 @@ package org.thingsboard.server.service.state; | @@ -18,6 +18,7 @@ package org.thingsboard.server.service.state; | ||
18 | import org.springframework.context.ApplicationListener; | 18 | import org.springframework.context.ApplicationListener; |
19 | import org.thingsboard.server.common.data.Device; | 19 | import org.thingsboard.server.common.data.Device; |
20 | import org.thingsboard.server.common.data.id.DeviceId; | 20 | import org.thingsboard.server.common.data.id.DeviceId; |
21 | +import org.thingsboard.server.common.data.id.TenantId; | ||
21 | import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; | 22 | import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; |
22 | import org.thingsboard.server.gen.transport.TransportProtos; | 23 | import org.thingsboard.server.gen.transport.TransportProtos; |
23 | import org.thingsboard.server.common.msg.queue.TbCallback; | 24 | import org.thingsboard.server.common.msg.queue.TbCallback; |
@@ -33,13 +34,13 @@ public interface DeviceStateService extends ApplicationListener<PartitionChangeE | @@ -33,13 +34,13 @@ public interface DeviceStateService extends ApplicationListener<PartitionChangeE | ||
33 | 34 | ||
34 | void onDeviceDeleted(Device device); | 35 | void onDeviceDeleted(Device device); |
35 | 36 | ||
36 | - void onDeviceConnect(DeviceId deviceId); | 37 | + void onDeviceConnect(TenantId tenantId, DeviceId deviceId); |
37 | 38 | ||
38 | - void onDeviceActivity(DeviceId deviceId, long lastReportedActivityTime); | 39 | + void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long lastReportedActivityTime); |
39 | 40 | ||
40 | - void onDeviceDisconnect(DeviceId deviceId); | 41 | + void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId); |
41 | 42 | ||
42 | - void onDeviceInactivityTimeoutUpdate(DeviceId deviceId, long inactivityTimeout); | 43 | + void onDeviceInactivityTimeoutUpdate(TenantId tenantId, DeviceId deviceId, long inactivityTimeout); |
43 | 44 | ||
44 | void onQueueMsg(TransportProtos.DeviceStateServiceMsgProto proto, TbCallback bytes); | 45 | void onQueueMsg(TransportProtos.DeviceStateServiceMsgProto proto, TbCallback bytes); |
45 | 46 |
@@ -224,7 +224,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene | @@ -224,7 +224,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene | ||
224 | return subscriptionUpdate; | 224 | return subscriptionUpdate; |
225 | }); | 225 | }); |
226 | if (entityId.getEntityType() == EntityType.DEVICE) { | 226 | if (entityId.getEntityType() == EntityType.DEVICE) { |
227 | - updateDeviceInactivityTimeout(entityId, ts); | 227 | + updateDeviceInactivityTimeout(tenantId, entityId, ts); |
228 | } | 228 | } |
229 | callback.onSuccess(); | 229 | callback.onSuccess(); |
230 | } | 230 | } |
@@ -259,7 +259,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene | @@ -259,7 +259,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene | ||
259 | }); | 259 | }); |
260 | if (entityId.getEntityType() == EntityType.DEVICE) { | 260 | if (entityId.getEntityType() == EntityType.DEVICE) { |
261 | if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope)) { | 261 | if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope)) { |
262 | - updateDeviceInactivityTimeout(entityId, attributes); | 262 | + updateDeviceInactivityTimeout(tenantId, entityId, attributes); |
263 | } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { | 263 | } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { |
264 | clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onUpdate(tenantId, | 264 | clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onUpdate(tenantId, |
265 | new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes)) | 265 | new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes)) |
@@ -269,10 +269,10 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene | @@ -269,10 +269,10 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene | ||
269 | callback.onSuccess(); | 269 | callback.onSuccess(); |
270 | } | 270 | } |
271 | 271 | ||
272 | - private void updateDeviceInactivityTimeout(EntityId entityId, List<? extends KvEntry> kvEntries) { | 272 | + private void updateDeviceInactivityTimeout(TenantId tenantId, EntityId entityId, List<? extends KvEntry> kvEntries) { |
273 | for (KvEntry kvEntry : kvEntries) { | 273 | for (KvEntry kvEntry : kvEntries) { |
274 | if (kvEntry.getKey().equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT)) { | 274 | if (kvEntry.getKey().equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT)) { |
275 | - deviceStateService.onDeviceInactivityTimeoutUpdate(new DeviceId(entityId.getId()), kvEntry.getLongValue().orElse(0L)); | 275 | + deviceStateService.onDeviceInactivityTimeoutUpdate(tenantId, new DeviceId(entityId.getId()), kvEntry.getLongValue().orElse(0L)); |
276 | } | 276 | } |
277 | } | 277 | } |
278 | } | 278 | } |
@@ -588,6 +588,7 @@ transport: | @@ -588,6 +588,7 @@ transport: | ||
588 | bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}" | 588 | bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}" |
589 | bind_port: "${MQTT_BIND_PORT:1883}" | 589 | bind_port: "${MQTT_BIND_PORT:1883}" |
590 | timeout: "${MQTT_TIMEOUT:10000}" | 590 | timeout: "${MQTT_TIMEOUT:10000}" |
591 | + msg_queue_size_per_device_limit: "${MQTT_MSG_QUEUE_SIZE_PER_DEVICE_LIMIT:100}" # messages await in the queue before device connected state. This limit works on low level before TenantProfileLimits mechanism | ||
591 | netty: | 592 | netty: |
592 | leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}" | 593 | leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}" |
593 | boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}" | 594 | boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}" |
@@ -27,6 +27,7 @@ import org.springframework.mock.web.MockMultipartFile; | @@ -27,6 +27,7 @@ import org.springframework.mock.web.MockMultipartFile; | ||
27 | import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; | 27 | import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; |
28 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; | 28 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; |
29 | import org.thingsboard.common.util.JacksonUtil; | 29 | import org.thingsboard.common.util.JacksonUtil; |
30 | +import org.thingsboard.common.util.ThingsBoardThreadFactory; | ||
30 | import org.thingsboard.server.common.data.Device; | 31 | import org.thingsboard.server.common.data.Device; |
31 | import org.thingsboard.server.common.data.DeviceProfile; | 32 | import org.thingsboard.server.common.data.DeviceProfile; |
32 | import org.thingsboard.server.common.data.DeviceProfileProvisionType; | 33 | import org.thingsboard.server.common.data.DeviceProfileProvisionType; |
@@ -261,7 +262,7 @@ public class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest { | @@ -261,7 +262,7 @@ public class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest { | ||
261 | 262 | ||
262 | @Before | 263 | @Before |
263 | public void beforeTest() throws Exception { | 264 | public void beforeTest() throws Exception { |
264 | - executor = Executors.newScheduledThreadPool(10); | 265 | + executor = Executors.newScheduledThreadPool(10, ThingsBoardThreadFactory.forName("test-lwm2m-scheduled")); |
265 | loginTenantAdmin(); | 266 | loginTenantAdmin(); |
266 | 267 | ||
267 | String[] resources = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"}; | 268 | String[] resources = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"}; |
@@ -16,6 +16,8 @@ | @@ -16,6 +16,8 @@ | ||
16 | package org.thingsboard.server.transport.lwm2m; | 16 | package org.thingsboard.server.transport.lwm2m; |
17 | 17 | ||
18 | import com.fasterxml.jackson.core.type.TypeReference; | 18 | import com.fasterxml.jackson.core.type.TypeReference; |
19 | +import lombok.extern.slf4j.Slf4j; | ||
20 | +import org.junit.After; | ||
19 | import org.junit.Assert; | 21 | import org.junit.Assert; |
20 | import org.junit.Test; | 22 | import org.junit.Test; |
21 | import org.thingsboard.server.common.data.Device; | 23 | import org.thingsboard.server.common.data.Device; |
@@ -23,16 +25,18 @@ import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCr | @@ -23,16 +25,18 @@ import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCr | ||
23 | import org.thingsboard.server.common.data.kv.KvEntry; | 25 | import org.thingsboard.server.common.data.kv.KvEntry; |
24 | import org.thingsboard.server.common.data.kv.TsKvEntry; | 26 | import org.thingsboard.server.common.data.kv.TsKvEntry; |
25 | import org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus; | 27 | import org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus; |
26 | -import org.thingsboard.server.common.data.query.EntityKey; | ||
27 | -import org.thingsboard.server.common.data.query.EntityKeyType; | ||
28 | import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient; | 28 | import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient; |
29 | 29 | ||
30 | import java.util.Arrays; | 30 | import java.util.Arrays; |
31 | import java.util.Collections; | 31 | import java.util.Collections; |
32 | import java.util.Comparator; | 32 | import java.util.Comparator; |
33 | import java.util.List; | 33 | import java.util.List; |
34 | +import java.util.UUID; | ||
35 | +import java.util.concurrent.TimeUnit; | ||
34 | import java.util.stream.Collectors; | 36 | import java.util.stream.Collectors; |
35 | 37 | ||
38 | +import static org.awaitility.Awaitility.await; | ||
39 | +import static org.hamcrest.Matchers.is; | ||
36 | import static org.thingsboard.rest.client.utils.RestJsonConverter.toTimeseries; | 40 | import static org.thingsboard.rest.client.utils.RestJsonConverter.toTimeseries; |
37 | import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADED; | 41 | import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADED; |
38 | import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADING; | 42 | import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADING; |
@@ -43,8 +47,10 @@ import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDA | @@ -43,8 +47,10 @@ import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDA | ||
43 | import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATING; | 47 | import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATING; |
44 | import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.VERIFIED; | 48 | import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.VERIFIED; |
45 | 49 | ||
50 | +@Slf4j | ||
46 | public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { | 51 | public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { |
47 | 52 | ||
53 | + public static final int TIMEOUT = 30; | ||
48 | private final String OTA_TRANSPORT_CONFIGURATION = "{\n" + | 54 | private final String OTA_TRANSPORT_CONFIGURATION = "{\n" + |
49 | " \"observeAttr\": {\n" + | 55 | " \"observeAttr\": {\n" + |
50 | " \"keyName\": {\n" + | 56 | " \"keyName\": {\n" + |
@@ -122,6 +128,15 @@ public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { | @@ -122,6 +128,15 @@ public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { | ||
122 | " \"type\": \"LWM2M\"\n" + | 128 | " \"type\": \"LWM2M\"\n" + |
123 | "}"; | 129 | "}"; |
124 | 130 | ||
131 | + LwM2MTestClient client = null; | ||
132 | + | ||
133 | + @After | ||
134 | + public void tearDown() { | ||
135 | + if (client != null) { | ||
136 | + client.destroy(); | ||
137 | + } | ||
138 | + } | ||
139 | + | ||
125 | @Test | 140 | @Test |
126 | public void testConnectAndObserveTelemetry() throws Exception { | 141 | public void testConnectAndObserveTelemetry() throws Exception { |
127 | NoSecClientCredentials clientCredentials = new NoSecClientCredentials(); | 142 | NoSecClientCredentials clientCredentials = new NoSecClientCredentials(); |
@@ -196,37 +211,68 @@ public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { | @@ -196,37 +211,68 @@ public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { | ||
196 | } | 211 | } |
197 | } | 212 | } |
198 | 213 | ||
214 | + /** | ||
215 | + * This is the example how to use the AWAITILITY instead Thread.sleep() | ||
216 | + * Test will finish as fast as possible, but will await until TIMEOUT if a build machine is busy or slow | ||
217 | + * Check the detailed log output to learn how Awaitility polling the API and when exactly expected result appears | ||
218 | + * */ | ||
199 | @Test | 219 | @Test |
200 | public void testSoftwareUpdateByObject9() throws Exception { | 220 | public void testSoftwareUpdateByObject9() throws Exception { |
201 | - LwM2MTestClient client = null; | ||
202 | - try { | ||
203 | - createDeviceProfile(OTA_TRANSPORT_CONFIGURATION); | ||
204 | - NoSecClientCredentials clientCredentials = new NoSecClientCredentials(); | ||
205 | - clientCredentials.setEndpoint("OTA_" + ENDPOINT); | ||
206 | - Device device = createDevice(clientCredentials); | ||
207 | - | ||
208 | - device.setSoftwareId(createSoftware().getId()); | ||
209 | - device = doPost("/api/device", device, Device.class); | 221 | + //given |
222 | + final List<OtaPackageUpdateStatus> expectedStatuses = Collections.unmodifiableList(Arrays.asList( | ||
223 | + QUEUED, INITIATED, DOWNLOADING, DOWNLOADING, DOWNLOADING, DOWNLOADED, VERIFIED, UPDATED)); | ||
210 | 224 | ||
211 | - Thread.sleep(1000); | ||
212 | - | ||
213 | - client = new LwM2MTestClient(executor, "OTA_" + ENDPOINT); | ||
214 | - client.init(SECURITY, COAP_CONFIG); | ||
215 | - | ||
216 | - Thread.sleep(3000); | ||
217 | - | ||
218 | - List<TsKvEntry> ts = toTimeseries(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + device.getId().getId() + "/values/timeseries?orderBy=ASC&keys=sw_state&startTs=0&endTs=" + System.currentTimeMillis(), new TypeReference<>() { | ||
219 | - })); | ||
220 | - | ||
221 | - List<OtaPackageUpdateStatus> statuses = ts.stream().sorted(Comparator.comparingLong(TsKvEntry::getTs)).map(KvEntry::getValueAsString).map(OtaPackageUpdateStatus::valueOf).collect(Collectors.toList()); | 225 | + createDeviceProfile(OTA_TRANSPORT_CONFIGURATION); |
226 | + NoSecClientCredentials clientCredentials = new NoSecClientCredentials(); | ||
227 | + clientCredentials.setEndpoint("OTA_" + ENDPOINT); | ||
228 | + final Device device = createDevice(clientCredentials); | ||
229 | + device.setSoftwareId(createSoftware().getId()); | ||
230 | + | ||
231 | + log.warn("Saving by API " + device); | ||
232 | + final Device savedDevice = doPost("/api/device", device, Device.class); | ||
233 | + Assert.assertNotNull(savedDevice); | ||
234 | + log.warn("Device saved by API {}", savedDevice); | ||
235 | + | ||
236 | + log.warn("AWAIT atMost {} SECONDS on get device by API...", TIMEOUT); | ||
237 | + await() | ||
238 | + .atMost(TIMEOUT, TimeUnit.SECONDS) | ||
239 | + .until(() -> getDeviceFromAPI(device.getId().getId()), is(savedDevice)); | ||
240 | + log.warn("Got device by API."); | ||
241 | + | ||
242 | + //when | ||
243 | + log.warn("Init the client..."); | ||
244 | + client = new LwM2MTestClient(executor, "OTA_" + ENDPOINT); | ||
245 | + client.init(SECURITY, COAP_CONFIG); | ||
246 | + log.warn("Init done"); | ||
247 | + | ||
248 | + log.warn("AWAIT atMost {} SECONDS on timeseries List<TsKvEntry> by API with list size {}...", TIMEOUT, expectedStatuses.size()); | ||
249 | + await() | ||
250 | + .atMost(30, TimeUnit.SECONDS) | ||
251 | + .until(() -> getSwStateTelemetryFromAPI(device.getId().getId()) | ||
252 | + .size(), is(expectedStatuses.size())); | ||
253 | + log.warn("Got an expected await condition!"); | ||
254 | + | ||
255 | + //then | ||
256 | + log.warn("Fetching ts for the final asserts"); | ||
257 | + List<TsKvEntry> ts = getSwStateTelemetryFromAPI(device.getId().getId()); | ||
258 | + log.warn("Got an ts {}", ts); | ||
259 | + | ||
260 | + List<OtaPackageUpdateStatus> statuses = ts.stream().sorted(Comparator.comparingLong(TsKvEntry::getTs)).map(KvEntry::getValueAsString).map(OtaPackageUpdateStatus::valueOf).collect(Collectors.toList()); | ||
261 | + log.warn("Converted ts to statuses {}", statuses); | ||
262 | + | ||
263 | + Assert.assertEquals(expectedStatuses, statuses); | ||
264 | + } | ||
222 | 265 | ||
223 | - List<OtaPackageUpdateStatus> expectedStatuses = Arrays.asList(QUEUED, INITIATED, DOWNLOADING, DOWNLOADING, DOWNLOADING, DOWNLOADED, VERIFIED, UPDATED); | 266 | + private Device getDeviceFromAPI(UUID deviceId) throws Exception { |
267 | + final Device device = doGet("/api/device/" + deviceId, Device.class); | ||
268 | + log.warn("Fetched device by API for deviceId {}, device is {}", deviceId, device); | ||
269 | + return device; | ||
270 | + } | ||
224 | 271 | ||
225 | - Assert.assertEquals(expectedStatuses, statuses); | ||
226 | - } finally { | ||
227 | - if (client != null) { | ||
228 | - client.destroy(); | ||
229 | - } | ||
230 | - } | 272 | + private List<TsKvEntry> getSwStateTelemetryFromAPI(UUID deviceId) throws Exception { |
273 | + final List<TsKvEntry> tsKvEntries = toTimeseries(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?orderBy=ASC&keys=sw_state&startTs=0&endTs=" + System.currentTimeMillis(), new TypeReference<>() { | ||
274 | + })); | ||
275 | + log.warn("Fetched telemetry by API for deviceId {}, list size {}, tsKvEntries {}", deviceId, tsKvEntries.size(), tsKvEntries); | ||
276 | + return tsKvEntries; | ||
231 | } | 277 | } |
232 | } | 278 | } |
@@ -77,25 +77,34 @@ public class TbKafkaProducerTemplate<T extends TbQueueMsg> implements TbQueuePro | @@ -77,25 +77,34 @@ public class TbKafkaProducerTemplate<T extends TbQueueMsg> implements TbQueuePro | ||
77 | 77 | ||
78 | @Override | 78 | @Override |
79 | public void send(TopicPartitionInfo tpi, T msg, TbQueueCallback callback) { | 79 | public void send(TopicPartitionInfo tpi, T msg, TbQueueCallback callback) { |
80 | - createTopicIfNotExist(tpi); | ||
81 | - String key = msg.getKey().toString(); | ||
82 | - byte[] data = msg.getData(); | ||
83 | - ProducerRecord<String, byte[]> record; | ||
84 | - Iterable<Header> headers = msg.getHeaders().getData().entrySet().stream().map(e -> new RecordHeader(e.getKey(), e.getValue())).collect(Collectors.toList()); | ||
85 | - record = new ProducerRecord<>(tpi.getFullTopicName(), null, key, data, headers); | ||
86 | - producer.send(record, (metadata, exception) -> { | ||
87 | - if (exception == null) { | ||
88 | - if (callback != null) { | ||
89 | - callback.onSuccess(new KafkaTbQueueMsgMetadata(metadata)); | ||
90 | - } | ||
91 | - } else { | ||
92 | - if (callback != null) { | ||
93 | - callback.onFailure(exception); | 80 | + try { |
81 | + createTopicIfNotExist(tpi); | ||
82 | + String key = msg.getKey().toString(); | ||
83 | + byte[] data = msg.getData(); | ||
84 | + ProducerRecord<String, byte[]> record; | ||
85 | + Iterable<Header> headers = msg.getHeaders().getData().entrySet().stream().map(e -> new RecordHeader(e.getKey(), e.getValue())).collect(Collectors.toList()); | ||
86 | + record = new ProducerRecord<>(tpi.getFullTopicName(), null, key, data, headers); | ||
87 | + producer.send(record, (metadata, exception) -> { | ||
88 | + if (exception == null) { | ||
89 | + if (callback != null) { | ||
90 | + callback.onSuccess(new KafkaTbQueueMsgMetadata(metadata)); | ||
91 | + } | ||
94 | } else { | 92 | } else { |
95 | - log.warn("Producer template failure: {}", exception.getMessage(), exception); | 93 | + if (callback != null) { |
94 | + callback.onFailure(exception); | ||
95 | + } else { | ||
96 | + log.warn("Producer template failure: {}", exception.getMessage(), exception); | ||
97 | + } | ||
96 | } | 98 | } |
99 | + }); | ||
100 | + } catch (Exception e) { | ||
101 | + if (callback != null) { | ||
102 | + callback.onFailure(e); | ||
103 | + } else { | ||
104 | + log.warn("Producer template failure (send method wrapper): {}", e.getMessage(), e); | ||
97 | } | 105 | } |
98 | - }); | 106 | + throw e; |
107 | + } | ||
99 | } | 108 | } |
100 | 109 | ||
101 | private void createTopicIfNotExist(TopicPartitionInfo tpi) { | 110 | private void createTopicIfNotExist(TopicPartitionInfo tpi) { |
@@ -19,6 +19,9 @@ import lombok.RequiredArgsConstructor; | @@ -19,6 +19,9 @@ import lombok.RequiredArgsConstructor; | ||
19 | import lombok.extern.slf4j.Slf4j; | 19 | import lombok.extern.slf4j.Slf4j; |
20 | import org.eclipse.californium.elements.util.SslContextUtil; | 20 | import org.eclipse.californium.elements.util.SslContextUtil; |
21 | import org.eclipse.californium.scandium.config.DtlsConnectorConfig; | 21 | import org.eclipse.californium.scandium.config.DtlsConnectorConfig; |
22 | +import org.eclipse.leshan.core.model.ObjectLoader; | ||
23 | +import org.eclipse.leshan.core.model.ObjectModel; | ||
24 | +import org.eclipse.leshan.core.model.StaticModel; | ||
22 | import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager; | 25 | import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager; |
23 | import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServer; | 26 | import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServer; |
24 | import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServerBuilder; | 27 | import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServerBuilder; |
@@ -26,6 +29,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; | @@ -26,6 +29,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; | ||
26 | import org.springframework.stereotype.Component; | 29 | import org.springframework.stereotype.Component; |
27 | import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MBootstrapSecurityStore; | 30 | import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MBootstrapSecurityStore; |
28 | import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MInMemoryBootstrapConfigStore; | 31 | import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MInMemoryBootstrapConfigStore; |
32 | +import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MInMemoryBootstrapConfigurationAdapter; | ||
29 | import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2mDefaultBootstrapSessionManager; | 33 | import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2mDefaultBootstrapSessionManager; |
30 | import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig; | 34 | import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig; |
31 | import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; | 35 | import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; |
@@ -38,6 +42,7 @@ import java.security.KeyStoreException; | @@ -38,6 +42,7 @@ import java.security.KeyStoreException; | ||
38 | import java.security.PrivateKey; | 42 | import java.security.PrivateKey; |
39 | import java.security.PublicKey; | 43 | import java.security.PublicKey; |
40 | import java.security.cert.X509Certificate; | 44 | import java.security.cert.X509Certificate; |
45 | +import java.util.List; | ||
41 | 46 | ||
42 | import static org.thingsboard.server.transport.lwm2m.server.LwM2mNetworkConfig.getCoapConfig; | 47 | import static org.thingsboard.server.transport.lwm2m.server.LwM2mNetworkConfig.getCoapConfig; |
43 | 48 | ||
@@ -79,12 +84,14 @@ public class LwM2MTransportBootstrapService { | @@ -79,12 +84,14 @@ public class LwM2MTransportBootstrapService { | ||
79 | builder.setCoapConfig(getCoapConfig(bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(), serverConfig)); | 84 | builder.setCoapConfig(getCoapConfig(bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(), serverConfig)); |
80 | 85 | ||
81 | /* Define model provider (Create Models )*/ | 86 | /* Define model provider (Create Models )*/ |
87 | + List<ObjectModel> models = ObjectLoader.loadDefault(); | ||
88 | + builder.setModel(new StaticModel(models)); | ||
82 | 89 | ||
83 | /* Create credentials */ | 90 | /* Create credentials */ |
84 | this.setServerWithCredentials(builder); | 91 | this.setServerWithCredentials(builder); |
85 | 92 | ||
86 | -// /** Set securityStore with new ConfigStore */ | ||
87 | -// builder.setConfigStore(lwM2MInMemoryBootstrapConfigStore); | 93 | + /* Set securityStore with new ConfigStore */ |
94 | + builder.setConfigStore(new LwM2MInMemoryBootstrapConfigurationAdapter(lwM2MInMemoryBootstrapConfigStore)); | ||
88 | 95 | ||
89 | /* SecurityStore */ | 96 | /* SecurityStore */ |
90 | builder.setSecurityStore(lwM2MBootstrapSecurityStore); | 97 | builder.setSecurityStore(lwM2MBootstrapSecurityStore); |
@@ -74,15 +74,19 @@ public class LwM2MBootstrapConfig implements Serializable { | @@ -74,15 +74,19 @@ public class LwM2MBootstrapConfig implements Serializable { | ||
74 | configBs.servers.put(0, server0); | 74 | configBs.servers.put(0, server0); |
75 | /* Security Configuration (object 0) as defined in LWM2M 1.0.x TS. Bootstrap instance = 0 */ | 75 | /* Security Configuration (object 0) as defined in LWM2M 1.0.x TS. Bootstrap instance = 0 */ |
76 | this.bootstrapServer.setBootstrapServerIs(true); | 76 | this.bootstrapServer.setBootstrapServerIs(true); |
77 | - configBs.security.put(0, setServerSecurity(this.bootstrapServer.getHost(), this.bootstrapServer.getPort(), this.bootstrapServer.isBootstrapServerIs(), this.bootstrapServer.getSecurityMode(), this.bootstrapServer.getClientPublicKeyOrId(), this.bootstrapServer.getServerPublicKey(), this.bootstrapServer.getClientSecretKey(), this.bootstrapServer.getServerId())); | 77 | + configBs.security.put(0, setServerSecurity(this.lwm2mServer.getHost(), this.lwm2mServer.getPort(), this.lwm2mServer.getSecurityHost(), this.lwm2mServer.getSecurityPort(), this.bootstrapServer.isBootstrapServerIs(), this.bootstrapServer.getSecurityMode(), this.bootstrapServer.getClientPublicKeyOrId(), this.bootstrapServer.getServerPublicKey(), this.bootstrapServer.getClientSecretKey(), this.bootstrapServer.getServerId())); |
78 | /* Security Configuration (object 0) as defined in LWM2M 1.0.x TS. Server instance = 1 */ | 78 | /* Security Configuration (object 0) as defined in LWM2M 1.0.x TS. Server instance = 1 */ |
79 | - configBs.security.put(1, setServerSecurity(this.lwm2mServer.getHost(), this.lwm2mServer.getPort(), this.lwm2mServer.isBootstrapServerIs(), this.lwm2mServer.getSecurityMode(), this.lwm2mServer.getClientPublicKeyOrId(), this.lwm2mServer.getServerPublicKey(), this.lwm2mServer.getClientSecretKey(), this.lwm2mServer.getServerId())); | 79 | + configBs.security.put(1, setServerSecurity(this.lwm2mServer.getHost(), this.lwm2mServer.getPort(), this.lwm2mServer.getSecurityHost(), this.lwm2mServer.getSecurityPort(), this.lwm2mServer.isBootstrapServerIs(), this.lwm2mServer.getSecurityMode(), this.lwm2mServer.getClientPublicKeyOrId(), this.lwm2mServer.getServerPublicKey(), this.lwm2mServer.getClientSecretKey(), this.lwm2mServer.getServerId())); |
80 | return configBs; | 80 | return configBs; |
81 | } | 81 | } |
82 | 82 | ||
83 | - private BootstrapConfig.ServerSecurity setServerSecurity(String host, Integer port, boolean bootstrapServer, SecurityMode securityMode, String clientPublicKey, String serverPublicKey, String secretKey, int serverId) { | 83 | + private BootstrapConfig.ServerSecurity setServerSecurity(String host, Integer port, String securityHost, Integer securityPort, boolean bootstrapServer, SecurityMode securityMode, String clientPublicKey, String serverPublicKey, String secretKey, int serverId) { |
84 | BootstrapConfig.ServerSecurity serverSecurity = new BootstrapConfig.ServerSecurity(); | 84 | BootstrapConfig.ServerSecurity serverSecurity = new BootstrapConfig.ServerSecurity(); |
85 | - serverSecurity.uri = "coaps://" + host + ":" + Integer.toString(port); | 85 | + if (securityMode.equals(SecurityMode.NO_SEC)) { |
86 | + serverSecurity.uri = "coap://" + host + ":" + Integer.toString(port); | ||
87 | + } else { | ||
88 | + serverSecurity.uri = "coaps://" + securityHost + ":" + Integer.toString(securityPort); | ||
89 | + } | ||
86 | serverSecurity.bootstrapServer = bootstrapServer; | 90 | serverSecurity.bootstrapServer = bootstrapServer; |
87 | serverSecurity.securityMode = securityMode; | 91 | serverSecurity.securityMode = securityMode; |
88 | serverSecurity.publicKeyOrId = setPublicKeyOrId(clientPublicKey, securityMode); | 92 | serverSecurity.publicKeyOrId = setPublicKeyOrId(clientPublicKey, securityMode); |
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.transport.lwm2m.bootstrap.secure; | ||
17 | + | ||
18 | +import org.eclipse.leshan.server.bootstrap.BootstrapConfigStore; | ||
19 | +import org.eclipse.leshan.server.bootstrap.BootstrapConfigurationStoreAdapter; | ||
20 | + | ||
21 | +public class LwM2MInMemoryBootstrapConfigurationAdapter extends BootstrapConfigurationStoreAdapter { | ||
22 | + | ||
23 | + public LwM2MInMemoryBootstrapConfigurationAdapter(BootstrapConfigStore store) { | ||
24 | + super(store); | ||
25 | + } | ||
26 | + | ||
27 | +} |
@@ -31,24 +31,31 @@ public class LwM2MServerBootstrap { | @@ -31,24 +31,31 @@ public class LwM2MServerBootstrap { | ||
31 | 31 | ||
32 | String host = "0.0.0.0"; | 32 | String host = "0.0.0.0"; |
33 | Integer port = 0; | 33 | Integer port = 0; |
34 | + String securityHost = "0.0.0.0"; | ||
35 | + Integer securityPort = 0; | ||
34 | 36 | ||
35 | SecurityMode securityMode = SecurityMode.NO_SEC; | 37 | SecurityMode securityMode = SecurityMode.NO_SEC; |
36 | 38 | ||
37 | Integer serverId = 123; | 39 | Integer serverId = 123; |
38 | boolean bootstrapServerIs = false; | 40 | boolean bootstrapServerIs = false; |
39 | 41 | ||
40 | - public LwM2MServerBootstrap(){}; | 42 | + public LwM2MServerBootstrap() { |
43 | + } | ||
44 | + | ||
45 | + ; | ||
41 | 46 | ||
42 | public LwM2MServerBootstrap(LwM2MServerBootstrap bootstrapFromCredential, LwM2MServerBootstrap profileServerBootstrap) { | 47 | public LwM2MServerBootstrap(LwM2MServerBootstrap bootstrapFromCredential, LwM2MServerBootstrap profileServerBootstrap) { |
43 | - this.clientPublicKeyOrId = bootstrapFromCredential.getClientPublicKeyOrId(); | ||
44 | - this.clientSecretKey = bootstrapFromCredential.getClientSecretKey(); | ||
45 | - this.serverPublicKey = profileServerBootstrap.getServerPublicKey(); | ||
46 | - this.clientHoldOffTime = profileServerBootstrap.getClientHoldOffTime(); | ||
47 | - this.bootstrapServerAccountTimeout = profileServerBootstrap.getBootstrapServerAccountTimeout(); | ||
48 | - this.host = (profileServerBootstrap.getHost().equals("0.0.0.0")) ? "localhost" : profileServerBootstrap.getHost(); | ||
49 | - this.port = profileServerBootstrap.getPort(); | ||
50 | - this.securityMode = profileServerBootstrap.getSecurityMode(); | ||
51 | - this.serverId = profileServerBootstrap.getServerId(); | ||
52 | - this.bootstrapServerIs = profileServerBootstrap.bootstrapServerIs; | 48 | + this.clientPublicKeyOrId = bootstrapFromCredential.getClientPublicKeyOrId(); |
49 | + this.clientSecretKey = bootstrapFromCredential.getClientSecretKey(); | ||
50 | + this.serverPublicKey = profileServerBootstrap.getServerPublicKey(); | ||
51 | + this.clientHoldOffTime = profileServerBootstrap.getClientHoldOffTime(); | ||
52 | + this.bootstrapServerAccountTimeout = profileServerBootstrap.getBootstrapServerAccountTimeout(); | ||
53 | + this.host = (profileServerBootstrap.getHost().equals("0.0.0.0")) ? "localhost" : profileServerBootstrap.getHost(); | ||
54 | + this.port = profileServerBootstrap.getPort(); | ||
55 | + this.securityHost = (profileServerBootstrap.getSecurityHost().equals("0.0.0.0")) ? "localhost" : profileServerBootstrap.getSecurityHost(); | ||
56 | + this.securityPort = profileServerBootstrap.getSecurityPort(); | ||
57 | + this.securityMode = profileServerBootstrap.getSecurityMode(); | ||
58 | + this.serverId = profileServerBootstrap.getServerId(); | ||
59 | + this.bootstrapServerIs = profileServerBootstrap.bootstrapServerIs; | ||
53 | } | 60 | } |
54 | } | 61 | } |
@@ -93,7 +93,12 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { | @@ -93,7 +93,12 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { | ||
93 | log.debug("Fetched clients from store: {}", fetchedClients); | 93 | log.debug("Fetched clients from store: {}", fetchedClients); |
94 | fetchedClients.forEach(client -> { | 94 | fetchedClients.forEach(client -> { |
95 | lwM2mClientsByEndpoint.put(client.getEndpoint(), client); | 95 | lwM2mClientsByEndpoint.put(client.getEndpoint(), client); |
96 | - updateFetchedClient(nodeId, client); | 96 | + try { |
97 | + client.lock(); | ||
98 | + updateFetchedClient(nodeId, client); | ||
99 | + } finally { | ||
100 | + client.unlock(); | ||
101 | + } | ||
97 | }); | 102 | }); |
98 | } | 103 | } |
99 | 104 | ||
@@ -161,7 +166,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { | @@ -161,7 +166,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { | ||
161 | this.lwM2mClientsByRegistrationId.put(registration.getId(), client); | 166 | this.lwM2mClientsByRegistrationId.put(registration.getId(), client); |
162 | client.setState(LwM2MClientState.REGISTERED); | 167 | client.setState(LwM2MClientState.REGISTERED); |
163 | onUplink(client); | 168 | onUplink(client); |
164 | - if(!compareAndSetSleepFlag(client, false)){ | 169 | + if (!compareAndSetSleepFlag(client, false)) { |
165 | clientStore.put(client); | 170 | clientStore.put(client); |
166 | } | 171 | } |
167 | } finally { | 172 | } finally { |
@@ -311,7 +316,11 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { | @@ -311,7 +316,11 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { | ||
311 | public void update(LwM2mClient client) { | 316 | public void update(LwM2mClient client) { |
312 | client.lock(); | 317 | client.lock(); |
313 | try { | 318 | try { |
314 | - clientStore.put(client); | 319 | + if (client.getState().equals(LwM2MClientState.REGISTERED)) { |
320 | + clientStore.put(client); | ||
321 | + } else { | ||
322 | + log.error("[{}] Client is in invalid state: {}!", client.getEndpoint(), client.getState()); | ||
323 | + } | ||
315 | } finally { | 324 | } finally { |
316 | client.unlock(); | 325 | client.unlock(); |
317 | } | 326 | } |
@@ -106,6 +106,7 @@ public abstract class LwM2MClientOtaInfo<Strategy, State, Result> { | @@ -106,6 +106,7 @@ public abstract class LwM2MClientOtaInfo<Strategy, State, Result> { | ||
106 | 106 | ||
107 | public abstract OtaPackageType getType(); | 107 | public abstract OtaPackageType getType(); |
108 | 108 | ||
109 | + @JsonIgnore | ||
109 | public String getTargetPackageId() { | 110 | public String getTargetPackageId() { |
110 | return getPackageId(targetName, targetVersion); | 111 | return getPackageId(targetName, targetVersion); |
111 | } | 112 | } |
@@ -15,11 +15,13 @@ | @@ -15,11 +15,13 @@ | ||
15 | */ | 15 | */ |
16 | package org.thingsboard.server.transport.lwm2m.server.store; | 16 | package org.thingsboard.server.transport.lwm2m.server.store; |
17 | 17 | ||
18 | +import lombok.extern.slf4j.Slf4j; | ||
18 | import org.nustaq.serialization.FSTConfiguration; | 19 | import org.nustaq.serialization.FSTConfiguration; |
19 | import org.springframework.data.redis.connection.RedisClusterConnection; | 20 | import org.springframework.data.redis.connection.RedisClusterConnection; |
20 | import org.springframework.data.redis.connection.RedisConnectionFactory; | 21 | import org.springframework.data.redis.connection.RedisConnectionFactory; |
21 | import org.springframework.data.redis.core.Cursor; | 22 | import org.springframework.data.redis.core.Cursor; |
22 | import org.springframework.data.redis.core.ScanOptions; | 23 | import org.springframework.data.redis.core.ScanOptions; |
24 | +import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState; | ||
23 | import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; | 25 | import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; |
24 | 26 | ||
25 | import java.util.ArrayList; | 27 | import java.util.ArrayList; |
@@ -27,6 +29,7 @@ import java.util.HashSet; | @@ -27,6 +29,7 @@ import java.util.HashSet; | ||
27 | import java.util.List; | 29 | import java.util.List; |
28 | import java.util.Set; | 30 | import java.util.Set; |
29 | 31 | ||
32 | +@Slf4j | ||
30 | public class TbRedisLwM2MClientStore implements TbLwM2MClientStore { | 33 | public class TbRedisLwM2MClientStore implements TbLwM2MClientStore { |
31 | 34 | ||
32 | private static final String CLIENT_EP = "CLIENT#EP#"; | 35 | private static final String CLIENT_EP = "CLIENT#EP#"; |
@@ -76,9 +79,13 @@ public class TbRedisLwM2MClientStore implements TbLwM2MClientStore { | @@ -76,9 +79,13 @@ public class TbRedisLwM2MClientStore implements TbLwM2MClientStore { | ||
76 | 79 | ||
77 | @Override | 80 | @Override |
78 | public void put(LwM2mClient client) { | 81 | public void put(LwM2mClient client) { |
79 | - byte[] clientSerialized = serializer.asByteArray(client); | ||
80 | - try (var connection = connectionFactory.getConnection()) { | ||
81 | - connection.getSet(getKey(client.getEndpoint()), clientSerialized); | 82 | + if (client.getState().equals(LwM2MClientState.UNREGISTERED)) { |
83 | + log.error("[{}] Client is in invalid state: {}!", client.getEndpoint(), client.getState(), new Exception()); | ||
84 | + } else { | ||
85 | + byte[] clientSerialized = serializer.asByteArray(client); | ||
86 | + try (var connection = connectionFactory.getConnection()) { | ||
87 | + connection.getSet(getKey(client.getEndpoint()), clientSerialized); | ||
88 | + } | ||
82 | } | 89 | } |
83 | } | 90 | } |
84 | 91 |
@@ -89,6 +89,11 @@ | @@ -89,6 +89,11 @@ | ||
89 | <scope>test</scope> | 89 | <scope>test</scope> |
90 | </dependency> | 90 | </dependency> |
91 | <dependency> | 91 | <dependency> |
92 | + <groupId>org.awaitility</groupId> | ||
93 | + <artifactId>awaitility</artifactId> | ||
94 | + <scope>test</scope> | ||
95 | + </dependency> | ||
96 | + <dependency> | ||
92 | <groupId>org.mockito</groupId> | 97 | <groupId>org.mockito</groupId> |
93 | <artifactId>mockito-core</artifactId> | 98 | <artifactId>mockito-core</artifactId> |
94 | <scope>test</scope> | 99 | <scope>test</scope> |
@@ -23,10 +23,15 @@ import org.springframework.beans.factory.annotation.Autowired; | @@ -23,10 +23,15 @@ import org.springframework.beans.factory.annotation.Autowired; | ||
23 | import org.springframework.beans.factory.annotation.Value; | 23 | import org.springframework.beans.factory.annotation.Value; |
24 | import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; | 24 | import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; |
25 | import org.springframework.stereotype.Component; | 25 | import org.springframework.stereotype.Component; |
26 | +import org.thingsboard.common.util.ThingsBoardExecutors; | ||
26 | import org.thingsboard.server.common.transport.TransportContext; | 27 | import org.thingsboard.server.common.transport.TransportContext; |
27 | import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor; | 28 | import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor; |
28 | import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor; | 29 | import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor; |
29 | 30 | ||
31 | +import javax.annotation.PostConstruct; | ||
32 | +import javax.annotation.PreDestroy; | ||
33 | +import java.util.concurrent.ExecutorService; | ||
34 | + | ||
30 | /** | 35 | /** |
31 | * Created by ashvayka on 04.10.18. | 36 | * Created by ashvayka on 04.10.18. |
32 | */ | 37 | */ |
@@ -59,4 +64,8 @@ public class MqttTransportContext extends TransportContext { | @@ -59,4 +64,8 @@ public class MqttTransportContext extends TransportContext { | ||
59 | @Setter | 64 | @Setter |
60 | private SslHandler sslHandler; | 65 | private SslHandler sslHandler; |
61 | 66 | ||
67 | + @Getter | ||
68 | + @Value("${transport.mqtt.msg_queue_size_per_device_limit:100}") | ||
69 | + private int messageQueueSizePerDeviceLimit; | ||
70 | + | ||
62 | } | 71 | } |
@@ -123,9 +123,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | @@ -123,9 +123,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | ||
123 | private final SslHandler sslHandler; | 123 | private final SslHandler sslHandler; |
124 | private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap; | 124 | private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap; |
125 | 125 | ||
126 | - private final DeviceSessionCtx deviceSessionCtx; | ||
127 | - private volatile InetSocketAddress address; | ||
128 | - private volatile GatewaySessionHandler gatewaySessionHandler; | 126 | + final DeviceSessionCtx deviceSessionCtx; |
127 | + volatile InetSocketAddress address; | ||
128 | + volatile GatewaySessionHandler gatewaySessionHandler; | ||
129 | 129 | ||
130 | private final ConcurrentHashMap<String, String> otaPackSessions; | 130 | private final ConcurrentHashMap<String, String> otaPackSessions; |
131 | private final ConcurrentHashMap<String, Integer> chunkSizes; | 131 | private final ConcurrentHashMap<String, Integer> chunkSizes; |
@@ -164,8 +164,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | @@ -164,8 +164,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | ||
164 | } | 164 | } |
165 | } | 165 | } |
166 | 166 | ||
167 | - private void processMqttMsg(ChannelHandlerContext ctx, MqttMessage msg) { | ||
168 | - address = (InetSocketAddress) ctx.channel().remoteAddress(); | 167 | + void processMqttMsg(ChannelHandlerContext ctx, MqttMessage msg) { |
168 | + address = getAddress(ctx); | ||
169 | if (msg.fixedHeader() == null) { | 169 | if (msg.fixedHeader() == null) { |
170 | log.info("[{}:{}] Invalid message received", address.getHostName(), address.getPort()); | 170 | log.info("[{}:{}] Invalid message received", address.getHostName(), address.getPort()); |
171 | processDisconnect(ctx); | 171 | processDisconnect(ctx); |
@@ -177,10 +177,14 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | @@ -177,10 +177,14 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | ||
177 | } else if (deviceSessionCtx.isProvisionOnly()) { | 177 | } else if (deviceSessionCtx.isProvisionOnly()) { |
178 | processProvisionSessionMsg(ctx, msg); | 178 | processProvisionSessionMsg(ctx, msg); |
179 | } else { | 179 | } else { |
180 | - processRegularSessionMsg(ctx, msg); | 180 | + enqueueRegularSessionMsg(ctx, msg); |
181 | } | 181 | } |
182 | } | 182 | } |
183 | 183 | ||
184 | + InetSocketAddress getAddress(ChannelHandlerContext ctx) { | ||
185 | + return (InetSocketAddress) ctx.channel().remoteAddress(); | ||
186 | + } | ||
187 | + | ||
184 | private void processProvisionSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { | 188 | private void processProvisionSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { |
185 | switch (msg.fixedHeader().messageType()) { | 189 | switch (msg.fixedHeader().messageType()) { |
186 | case PUBLISH: | 190 | case PUBLISH: |
@@ -223,7 +227,42 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | @@ -223,7 +227,42 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | ||
223 | } | 227 | } |
224 | } | 228 | } |
225 | 229 | ||
226 | - private void processRegularSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { | 230 | + void enqueueRegularSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { |
231 | + final int queueSize = deviceSessionCtx.getMsgQueueSize().incrementAndGet(); | ||
232 | + if (queueSize > context.getMessageQueueSizePerDeviceLimit()) { | ||
233 | + log.warn("Closing current session because msq queue size for device {} exceed limit {} with msgQueueSize counter {} and actual queue size {}", | ||
234 | + deviceSessionCtx.getDeviceId(), context.getMessageQueueSizePerDeviceLimit(), queueSize, deviceSessionCtx.getMsgQueue().size()); | ||
235 | + ctx.close(); | ||
236 | + return; | ||
237 | + } | ||
238 | + | ||
239 | + deviceSessionCtx.getMsgQueue().add(msg); | ||
240 | + processMsgQueue(ctx); //Under the normal conditions the msg queue will contain 0 messages. Many messages will be processed on device connect event in separate thread pool | ||
241 | + } | ||
242 | + | ||
243 | + void processMsgQueue(ChannelHandlerContext ctx) { | ||
244 | + if (!deviceSessionCtx.isConnected()) { | ||
245 | + log.trace("[{}][{}] Postpone processing msg due to device is not connected. Msg queue size is {}", sessionId, deviceSessionCtx.getDeviceId(), deviceSessionCtx.getMsgQueue().size()); | ||
246 | + return; | ||
247 | + } | ||
248 | + while (!deviceSessionCtx.getMsgQueue().isEmpty()) { | ||
249 | + if (deviceSessionCtx.getMsgQueueProcessorLock().tryLock()) { | ||
250 | + try { | ||
251 | + MqttMessage msg; | ||
252 | + while ((msg = deviceSessionCtx.getMsgQueue().poll()) != null) { | ||
253 | + deviceSessionCtx.getMsgQueueSize().decrementAndGet(); | ||
254 | + processRegularSessionMsg(ctx, msg); | ||
255 | + } | ||
256 | + } finally { | ||
257 | + deviceSessionCtx.getMsgQueueProcessorLock().unlock(); | ||
258 | + } | ||
259 | + } else { | ||
260 | + return; | ||
261 | + } | ||
262 | + } | ||
263 | + } | ||
264 | + | ||
265 | + void processRegularSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { | ||
227 | switch (msg.fixedHeader().messageType()) { | 266 | switch (msg.fixedHeader().messageType()) { |
228 | case PUBLISH: | 267 | case PUBLISH: |
229 | processPublish(ctx, (MqttPublishMessage) msg); | 268 | processPublish(ctx, (MqttPublishMessage) msg); |
@@ -304,6 +343,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | @@ -304,6 +343,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | ||
304 | } | 343 | } |
305 | } catch (RuntimeException | AdaptorException e) { | 344 | } catch (RuntimeException | AdaptorException e) { |
306 | log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); | 345 | log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); |
346 | + ctx.close(); | ||
307 | } | 347 | } |
308 | } | 348 | } |
309 | 349 | ||
@@ -588,7 +628,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | @@ -588,7 +628,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | ||
588 | return new MqttMessage(mqttFixedHeader, mqttMessageIdVariableHeader); | 628 | return new MqttMessage(mqttFixedHeader, mqttMessageIdVariableHeader); |
589 | } | 629 | } |
590 | 630 | ||
591 | - private void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { | 631 | + void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { |
592 | log.info("[{}] Processing connect msg for client: {}!", sessionId, msg.payload().clientIdentifier()); | 632 | log.info("[{}] Processing connect msg for client: {}!", sessionId, msg.payload().clientIdentifier()); |
593 | String userName = msg.payload().userName(); | 633 | String userName = msg.payload().userName(); |
594 | String clientId = msg.payload().clientIdentifier(); | 634 | String clientId = msg.payload().clientIdentifier(); |
@@ -674,7 +714,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | @@ -674,7 +714,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | ||
674 | return null; | 714 | return null; |
675 | } | 715 | } |
676 | 716 | ||
677 | - private void processDisconnect(ChannelHandlerContext ctx) { | 717 | + void processDisconnect(ChannelHandlerContext ctx) { |
678 | ctx.close(); | 718 | ctx.close(); |
679 | log.info("[{}] Client disconnected!", sessionId); | 719 | log.info("[{}] Client disconnected!", sessionId); |
680 | doDisconnect(); | 720 | doDisconnect(); |
@@ -761,6 +801,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | @@ -761,6 +801,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | ||
761 | } | 801 | } |
762 | deviceSessionCtx.setDisconnected(); | 802 | deviceSessionCtx.setDisconnected(); |
763 | } | 803 | } |
804 | + | ||
805 | + if (!deviceSessionCtx.getMsgQueue().isEmpty()) { | ||
806 | + log.warn("doDisconnect for device {} but unprocessed messages {} left in the msg queue", deviceSessionCtx.getDeviceId(), deviceSessionCtx.getMsgQueue().size()); | ||
807 | + deviceSessionCtx.getMsgQueue().clear(); | ||
808 | + } | ||
764 | } | 809 | } |
765 | 810 | ||
766 | 811 | ||
@@ -778,7 +823,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | @@ -778,7 +823,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement | ||
778 | SessionMetaData sessionMetaData = transportService.registerAsyncSession(deviceSessionCtx.getSessionInfo(), MqttTransportHandler.this); | 823 | SessionMetaData sessionMetaData = transportService.registerAsyncSession(deviceSessionCtx.getSessionInfo(), MqttTransportHandler.this); |
779 | checkGatewaySession(sessionMetaData); | 824 | checkGatewaySession(sessionMetaData); |
780 | ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED, connectMessage)); | 825 | ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED, connectMessage)); |
826 | + deviceSessionCtx.setConnected(true); | ||
781 | log.info("[{}] Client connected!", sessionId); | 827 | log.info("[{}] Client connected!", sessionId); |
828 | + transportService.getCallbackExecutor().execute(() -> processMsgQueue(ctx)); //this callback will execute in Producer worker thread and hard or blocking work have to be submitted to the separate thread. | ||
782 | } | 829 | } |
783 | 830 | ||
784 | @Override | 831 | @Override |
@@ -18,6 +18,7 @@ package org.thingsboard.server.transport.mqtt.session; | @@ -18,6 +18,7 @@ package org.thingsboard.server.transport.mqtt.session; | ||
18 | import com.google.protobuf.Descriptors; | 18 | import com.google.protobuf.Descriptors; |
19 | import com.google.protobuf.DynamicMessage; | 19 | import com.google.protobuf.DynamicMessage; |
20 | import io.netty.channel.ChannelHandlerContext; | 20 | import io.netty.channel.ChannelHandlerContext; |
21 | +import io.netty.handler.codec.mqtt.MqttMessage; | ||
21 | import lombok.Getter; | 22 | import lombok.Getter; |
22 | import lombok.Setter; | 23 | import lombok.Setter; |
23 | import lombok.extern.slf4j.Slf4j; | 24 | import lombok.extern.slf4j.Slf4j; |
@@ -35,8 +36,11 @@ import org.thingsboard.server.transport.mqtt.util.MqttTopicFilter; | @@ -35,8 +36,11 @@ import org.thingsboard.server.transport.mqtt.util.MqttTopicFilter; | ||
35 | import org.thingsboard.server.transport.mqtt.util.MqttTopicFilterFactory; | 36 | import org.thingsboard.server.transport.mqtt.util.MqttTopicFilterFactory; |
36 | 37 | ||
37 | import java.util.UUID; | 38 | import java.util.UUID; |
39 | +import java.util.concurrent.ConcurrentLinkedQueue; | ||
38 | import java.util.concurrent.ConcurrentMap; | 40 | import java.util.concurrent.ConcurrentMap; |
39 | import java.util.concurrent.atomic.AtomicInteger; | 41 | import java.util.concurrent.atomic.AtomicInteger; |
42 | +import java.util.concurrent.locks.Lock; | ||
43 | +import java.util.concurrent.locks.ReentrantLock; | ||
40 | 44 | ||
41 | /** | 45 | /** |
42 | * @author Andrew Shvayka | 46 | * @author Andrew Shvayka |
@@ -45,14 +49,24 @@ import java.util.concurrent.atomic.AtomicInteger; | @@ -45,14 +49,24 @@ import java.util.concurrent.atomic.AtomicInteger; | ||
45 | public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { | 49 | public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { |
46 | 50 | ||
47 | @Getter | 51 | @Getter |
52 | + @Setter | ||
48 | private ChannelHandlerContext channel; | 53 | private ChannelHandlerContext channel; |
49 | 54 | ||
50 | @Getter | 55 | @Getter |
51 | - private MqttTransportContext context; | 56 | + private final MqttTransportContext context; |
52 | 57 | ||
53 | private final AtomicInteger msgIdSeq = new AtomicInteger(0); | 58 | private final AtomicInteger msgIdSeq = new AtomicInteger(0); |
54 | 59 | ||
55 | @Getter | 60 | @Getter |
61 | + private final ConcurrentLinkedQueue<MqttMessage> msgQueue = new ConcurrentLinkedQueue<>(); | ||
62 | + | ||
63 | + @Getter | ||
64 | + private final Lock msgQueueProcessorLock = new ReentrantLock(); | ||
65 | + | ||
66 | + @Getter | ||
67 | + private final AtomicInteger msgQueueSize = new AtomicInteger(0); | ||
68 | + | ||
69 | + @Getter | ||
56 | @Setter | 70 | @Setter |
57 | private boolean provisionOnly = false; | 71 | private boolean provisionOnly = false; |
58 | 72 | ||
@@ -73,10 +87,6 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { | @@ -73,10 +87,6 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { | ||
73 | this.context = context; | 87 | this.context = context; |
74 | } | 88 | } |
75 | 89 | ||
76 | - public void setChannel(ChannelHandlerContext channel) { | ||
77 | - this.channel = channel; | ||
78 | - } | ||
79 | - | ||
80 | public int nextMsgId() { | 90 | public int nextMsgId() { |
81 | return msgIdSeq.incrementAndGet(); | 91 | return msgIdSeq.incrementAndGet(); |
82 | } | 92 | } |
@@ -60,6 +60,7 @@ public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext imple | @@ -60,6 +60,7 @@ public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext imple | ||
60 | .setDeviceProfileIdLSB(deviceInfo.getDeviceProfileId().getId().getLeastSignificantBits()) | 60 | .setDeviceProfileIdLSB(deviceInfo.getDeviceProfileId().getId().getLeastSignificantBits()) |
61 | .build()); | 61 | .build()); |
62 | setDeviceInfo(deviceInfo); | 62 | setDeviceInfo(deviceInfo); |
63 | + setConnected(true); | ||
63 | setDeviceProfile(deviceProfile); | 64 | setDeviceProfile(deviceProfile); |
64 | this.transportService = transportService; | 65 | this.transportService = transportService; |
65 | } | 66 | } |
@@ -34,6 +34,7 @@ import io.netty.handler.codec.mqtt.MqttMessage; | @@ -34,6 +34,7 @@ import io.netty.handler.codec.mqtt.MqttMessage; | ||
34 | import io.netty.handler.codec.mqtt.MqttPublishMessage; | 34 | import io.netty.handler.codec.mqtt.MqttPublishMessage; |
35 | import lombok.extern.slf4j.Slf4j; | 35 | import lombok.extern.slf4j.Slf4j; |
36 | import org.springframework.util.CollectionUtils; | 36 | import org.springframework.util.CollectionUtils; |
37 | +import org.springframework.util.ConcurrentReferenceHashMap; | ||
37 | import org.springframework.util.StringUtils; | 38 | import org.springframework.util.StringUtils; |
38 | import org.thingsboard.server.common.data.id.DeviceId; | 39 | import org.thingsboard.server.common.data.id.DeviceId; |
39 | import org.thingsboard.server.common.transport.TransportService; | 40 | import org.thingsboard.server.common.transport.TransportService; |
@@ -66,6 +67,8 @@ import java.util.concurrent.ConcurrentMap; | @@ -66,6 +67,8 @@ import java.util.concurrent.ConcurrentMap; | ||
66 | import java.util.concurrent.locks.Lock; | 67 | import java.util.concurrent.locks.Lock; |
67 | import java.util.concurrent.locks.ReentrantLock; | 68 | import java.util.concurrent.locks.ReentrantLock; |
68 | 69 | ||
70 | +import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; | ||
71 | + | ||
69 | /** | 72 | /** |
70 | * Created by ashvayka on 19.01.17. | 73 | * Created by ashvayka on 19.01.17. |
71 | */ | 74 | */ |
@@ -82,7 +85,7 @@ public class GatewaySessionHandler { | @@ -82,7 +85,7 @@ public class GatewaySessionHandler { | ||
82 | private final UUID sessionId; | 85 | private final UUID sessionId; |
83 | private final ConcurrentMap<String, Lock> deviceCreationLockMap; | 86 | private final ConcurrentMap<String, Lock> deviceCreationLockMap; |
84 | private final ConcurrentMap<String, GatewayDeviceSessionCtx> devices; | 87 | private final ConcurrentMap<String, GatewayDeviceSessionCtx> devices; |
85 | - private final ConcurrentMap<String, SettableFuture<GatewayDeviceSessionCtx>> deviceFutures; | 88 | + private final ConcurrentMap<String, ListenableFuture<GatewayDeviceSessionCtx>> deviceFutures; |
86 | private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap; | 89 | private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap; |
87 | private final ChannelHandlerContext channel; | 90 | private final ChannelHandlerContext channel; |
88 | private final DeviceSessionCtx deviceSessionCtx; | 91 | private final DeviceSessionCtx deviceSessionCtx; |
@@ -95,11 +98,15 @@ public class GatewaySessionHandler { | @@ -95,11 +98,15 @@ public class GatewaySessionHandler { | ||
95 | this.sessionId = sessionId; | 98 | this.sessionId = sessionId; |
96 | this.devices = new ConcurrentHashMap<>(); | 99 | this.devices = new ConcurrentHashMap<>(); |
97 | this.deviceFutures = new ConcurrentHashMap<>(); | 100 | this.deviceFutures = new ConcurrentHashMap<>(); |
98 | - this.deviceCreationLockMap = new ConcurrentHashMap<>(); | 101 | + this.deviceCreationLockMap = createWeakMap(); |
99 | this.mqttQoSMap = deviceSessionCtx.getMqttQoSMap(); | 102 | this.mqttQoSMap = deviceSessionCtx.getMqttQoSMap(); |
100 | this.channel = deviceSessionCtx.getChannel(); | 103 | this.channel = deviceSessionCtx.getChannel(); |
101 | } | 104 | } |
102 | 105 | ||
106 | + ConcurrentReferenceHashMap<String, Lock> createWeakMap() { | ||
107 | + return new ConcurrentReferenceHashMap<>(16, ReferenceType.WEAK); | ||
108 | + } | ||
109 | + | ||
103 | public void onDeviceConnect(MqttPublishMessage mqttMsg) throws AdaptorException { | 110 | public void onDeviceConnect(MqttPublishMessage mqttMsg) throws AdaptorException { |
104 | if (isJsonPayloadType()) { | 111 | if (isJsonPayloadType()) { |
105 | onDeviceConnectJson(mqttMsg); | 112 | onDeviceConnectJson(mqttMsg); |
@@ -228,21 +235,22 @@ public class GatewaySessionHandler { | @@ -228,21 +235,22 @@ public class GatewaySessionHandler { | ||
228 | if (result == null) { | 235 | if (result == null) { |
229 | return getDeviceCreationFuture(deviceName, deviceType); | 236 | return getDeviceCreationFuture(deviceName, deviceType); |
230 | } else { | 237 | } else { |
231 | - return toCompletedFuture(result); | 238 | + return Futures.immediateFuture(result); |
232 | } | 239 | } |
233 | } finally { | 240 | } finally { |
234 | deviceCreationLock.unlock(); | 241 | deviceCreationLock.unlock(); |
235 | } | 242 | } |
236 | } else { | 243 | } else { |
237 | - return toCompletedFuture(result); | 244 | + return Futures.immediateFuture(result); |
238 | } | 245 | } |
239 | } | 246 | } |
240 | 247 | ||
241 | private ListenableFuture<GatewayDeviceSessionCtx> getDeviceCreationFuture(String deviceName, String deviceType) { | 248 | private ListenableFuture<GatewayDeviceSessionCtx> getDeviceCreationFuture(String deviceName, String deviceType) { |
242 | - SettableFuture<GatewayDeviceSessionCtx> future = deviceFutures.get(deviceName); | ||
243 | - if (future == null) { | ||
244 | - final SettableFuture<GatewayDeviceSessionCtx> futureToSet = SettableFuture.create(); | ||
245 | - deviceFutures.put(deviceName, futureToSet); | 249 | + final SettableFuture<GatewayDeviceSessionCtx> futureToSet = SettableFuture.create(); |
250 | + ListenableFuture<GatewayDeviceSessionCtx> future = deviceFutures.putIfAbsent(deviceName, futureToSet); | ||
251 | + if (future != null) { | ||
252 | + return future; | ||
253 | + } | ||
246 | try { | 254 | try { |
247 | transportService.process(GetOrCreateDeviceFromGatewayRequestMsg.newBuilder() | 255 | transportService.process(GetOrCreateDeviceFromGatewayRequestMsg.newBuilder() |
248 | .setDeviceName(deviceName) | 256 | .setDeviceName(deviceName) |
@@ -282,15 +290,6 @@ public class GatewaySessionHandler { | @@ -282,15 +290,6 @@ public class GatewaySessionHandler { | ||
282 | deviceFutures.remove(deviceName); | 290 | deviceFutures.remove(deviceName); |
283 | throw e; | 291 | throw e; |
284 | } | 292 | } |
285 | - } else { | ||
286 | - return future; | ||
287 | - } | ||
288 | - } | ||
289 | - | ||
290 | - private ListenableFuture<GatewayDeviceSessionCtx> toCompletedFuture(GatewayDeviceSessionCtx result) { | ||
291 | - SettableFuture<GatewayDeviceSessionCtx> future = SettableFuture.create(); | ||
292 | - future.set(result); | ||
293 | - return future; | ||
294 | } | 293 | } |
295 | 294 | ||
296 | private int getMsgId(MqttPublishMessage mqttMsg) { | 295 | private int getMsgId(MqttPublishMessage mqttMsg) { |
@@ -353,6 +352,7 @@ public class GatewaySessionHandler { | @@ -353,6 +352,7 @@ public class GatewaySessionHandler { | ||
353 | processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); | 352 | processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); |
354 | } catch (Throwable e) { | 353 | } catch (Throwable e) { |
355 | log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); | 354 | log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); |
355 | + channel.close(); | ||
356 | } | 356 | } |
357 | } | 357 | } |
358 | 358 | ||
@@ -384,6 +384,7 @@ public class GatewaySessionHandler { | @@ -384,6 +384,7 @@ public class GatewaySessionHandler { | ||
384 | processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); | 384 | processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); |
385 | } catch (Throwable e) { | 385 | } catch (Throwable e) { |
386 | log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, msg, e); | 386 | log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, msg, e); |
387 | + channel.close(); | ||
387 | } | 388 | } |
388 | } | 389 | } |
389 | 390 |
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.transport.mqtt; | ||
17 | + | ||
18 | +import io.netty.buffer.ByteBuf; | ||
19 | +import io.netty.buffer.EmptyByteBuf; | ||
20 | +import io.netty.buffer.PooledByteBufAllocator; | ||
21 | +import io.netty.channel.ChannelHandlerContext; | ||
22 | +import io.netty.handler.codec.mqtt.MqttConnectMessage; | ||
23 | +import io.netty.handler.codec.mqtt.MqttConnectPayload; | ||
24 | +import io.netty.handler.codec.mqtt.MqttConnectVariableHeader; | ||
25 | +import io.netty.handler.codec.mqtt.MqttFixedHeader; | ||
26 | +import io.netty.handler.codec.mqtt.MqttMessage; | ||
27 | +import io.netty.handler.codec.mqtt.MqttMessageType; | ||
28 | +import io.netty.handler.codec.mqtt.MqttPublishMessage; | ||
29 | +import io.netty.handler.codec.mqtt.MqttPublishVariableHeader; | ||
30 | +import io.netty.handler.codec.mqtt.MqttQoS; | ||
31 | +import io.netty.handler.ssl.SslHandler; | ||
32 | +import lombok.extern.slf4j.Slf4j; | ||
33 | +import org.junit.After; | ||
34 | +import org.junit.Before; | ||
35 | +import org.junit.Test; | ||
36 | +import org.junit.runner.RunWith; | ||
37 | +import org.mockito.Mock; | ||
38 | +import org.mockito.junit.MockitoJUnitRunner; | ||
39 | +import org.thingsboard.common.util.ThingsBoardThreadFactory; | ||
40 | + | ||
41 | +import java.net.InetSocketAddress; | ||
42 | +import java.nio.charset.StandardCharsets; | ||
43 | +import java.util.List; | ||
44 | +import java.util.concurrent.CountDownLatch; | ||
45 | +import java.util.concurrent.ExecutorService; | ||
46 | +import java.util.concurrent.Executors; | ||
47 | +import java.util.concurrent.TimeUnit; | ||
48 | +import java.util.concurrent.atomic.AtomicInteger; | ||
49 | +import java.util.stream.Collectors; | ||
50 | +import java.util.stream.Stream; | ||
51 | + | ||
52 | +import static org.hamcrest.MatcherAssert.assertThat; | ||
53 | +import static org.hamcrest.Matchers.contains; | ||
54 | +import static org.hamcrest.Matchers.empty; | ||
55 | +import static org.hamcrest.Matchers.greaterThan; | ||
56 | +import static org.hamcrest.Matchers.is; | ||
57 | +import static org.junit.Assert.fail; | ||
58 | +import static org.mockito.ArgumentMatchers.any; | ||
59 | +import static org.mockito.BDDMockito.willDoNothing; | ||
60 | +import static org.mockito.BDDMockito.willReturn; | ||
61 | +import static org.mockito.Mockito.never; | ||
62 | +import static org.mockito.Mockito.spy; | ||
63 | +import static org.mockito.Mockito.times; | ||
64 | +import static org.mockito.Mockito.verify; | ||
65 | + | ||
66 | +@Slf4j | ||
67 | +@RunWith(MockitoJUnitRunner.class) | ||
68 | +public class MqttTransportHandlerTest { | ||
69 | + | ||
70 | + public static final int MSG_QUEUE_LIMIT = 10; | ||
71 | + public static final InetSocketAddress IP_ADDR = new InetSocketAddress("127.0.0.1", 9876); | ||
72 | + public static final int TIMEOUT = 30; | ||
73 | + | ||
74 | + @Mock | ||
75 | + MqttTransportContext context; | ||
76 | + @Mock | ||
77 | + SslHandler sslHandler; | ||
78 | + @Mock | ||
79 | + ChannelHandlerContext ctx; | ||
80 | + | ||
81 | + AtomicInteger packedId = new AtomicInteger(); | ||
82 | + ExecutorService executor; | ||
83 | + MqttTransportHandler handler; | ||
84 | + | ||
85 | + @Before | ||
86 | + public void setUp() throws Exception { | ||
87 | + willReturn(MSG_QUEUE_LIMIT).given(context).getMessageQueueSizePerDeviceLimit(); | ||
88 | + | ||
89 | + handler = spy(new MqttTransportHandler(context, sslHandler)); | ||
90 | + willReturn(IP_ADDR).given(handler).getAddress(any()); | ||
91 | + } | ||
92 | + | ||
93 | + @After | ||
94 | + public void tearDown() { | ||
95 | + if (executor != null) { | ||
96 | + executor.shutdownNow(); | ||
97 | + } | ||
98 | + } | ||
99 | + | ||
100 | + MqttConnectMessage getMqttConnectMessage() { | ||
101 | + MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.CONNECT, true, MqttQoS.AT_LEAST_ONCE, false, 123); | ||
102 | + MqttConnectVariableHeader variableHeader = new MqttConnectVariableHeader("device", packedId.incrementAndGet(), true, true, true, 1, true, false, 60); | ||
103 | + MqttConnectPayload payload = new MqttConnectPayload("clientId", "topic", "message".getBytes(StandardCharsets.UTF_8), "username", "password".getBytes(StandardCharsets.UTF_8)); | ||
104 | + return new MqttConnectMessage(mqttFixedHeader, variableHeader, payload); | ||
105 | + } | ||
106 | + | ||
107 | + MqttPublishMessage getMqttPublishMessage() { | ||
108 | + MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, true, MqttQoS.AT_LEAST_ONCE, false, 123); | ||
109 | + MqttPublishVariableHeader variableHeader = new MqttPublishVariableHeader("v1/gateway/telemetry", packedId.incrementAndGet()); | ||
110 | + ByteBuf payload = new EmptyByteBuf(new PooledByteBufAllocator()); | ||
111 | + return new MqttPublishMessage(mqttFixedHeader, variableHeader, payload); | ||
112 | + } | ||
113 | + | ||
114 | + @Test | ||
115 | + public void givenMessageWithoutFixedHeader_whenProcessMqttMsg_thenProcessDisconnect() { | ||
116 | + MqttFixedHeader mqttFixedHeader = null; | ||
117 | + MqttMessage msg = new MqttMessage(mqttFixedHeader); | ||
118 | + willDoNothing().given(handler).processDisconnect(ctx); | ||
119 | + | ||
120 | + handler.processMqttMsg(ctx, msg); | ||
121 | + | ||
122 | + assertThat(handler.address, is(IP_ADDR)); | ||
123 | + verify(handler, times(1)).processDisconnect(ctx); | ||
124 | + } | ||
125 | + | ||
126 | + @Test | ||
127 | + public void givenMqttConnectMessage_whenProcessMqttMsg_thenProcessConnect() { | ||
128 | + MqttConnectMessage msg = getMqttConnectMessage(); | ||
129 | + willDoNothing().given(handler).processConnect(ctx, msg); | ||
130 | + | ||
131 | + handler.processMqttMsg(ctx, msg); | ||
132 | + | ||
133 | + assertThat(handler.address, is(IP_ADDR)); | ||
134 | + assertThat(handler.deviceSessionCtx.getChannel(), is(ctx)); | ||
135 | + verify(handler, never()).processDisconnect(any()); | ||
136 | + verify(handler, times(1)).processConnect(ctx, msg); | ||
137 | + } | ||
138 | + | ||
139 | + @Test | ||
140 | + public void givenQueueLimit_whenEnqueueRegularSessionMsgOverLimit_thenOK() { | ||
141 | + List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(MSG_QUEUE_LIMIT).collect(Collectors.toList()); | ||
142 | + messages.forEach(msg -> handler.enqueueRegularSessionMsg(ctx, msg)); | ||
143 | + assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(MSG_QUEUE_LIMIT)); | ||
144 | + assertThat(handler.deviceSessionCtx.getMsgQueue(), contains(messages.toArray())); | ||
145 | + } | ||
146 | + | ||
147 | + @Test | ||
148 | + public void givenQueueLimit_whenEnqueueRegularSessionMsgOverLimit_thenCtxClose() { | ||
149 | + final int limit = MSG_QUEUE_LIMIT + 1; | ||
150 | + willDoNothing().given(handler).processMsgQueue(ctx); | ||
151 | + List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(limit).collect(Collectors.toList()); | ||
152 | + | ||
153 | + messages.forEach((msg) -> handler.enqueueRegularSessionMsg(ctx, msg)); | ||
154 | + | ||
155 | + assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(limit)); | ||
156 | + verify(handler, times(limit)).enqueueRegularSessionMsg(any(), any()); | ||
157 | + verify(handler, times(MSG_QUEUE_LIMIT)).processMsgQueue(any()); | ||
158 | + verify(ctx, times(1)).close(); | ||
159 | + } | ||
160 | + | ||
161 | + @Test | ||
162 | + public void givenMqttConnectMessageAndPublishImmediately_whenProcessMqttMsg_thenEnqueueRegularSessionMsg() { | ||
163 | + givenMqttConnectMessage_whenProcessMqttMsg_thenProcessConnect(); | ||
164 | + | ||
165 | + List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(MSG_QUEUE_LIMIT).collect(Collectors.toList()); | ||
166 | + | ||
167 | + messages.forEach((msg) -> handler.processMqttMsg(ctx, msg)); | ||
168 | + | ||
169 | + assertThat(handler.address, is(IP_ADDR)); | ||
170 | + assertThat(handler.deviceSessionCtx.getChannel(), is(ctx)); | ||
171 | + assertThat(handler.deviceSessionCtx.isConnected(), is(false)); | ||
172 | + assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(MSG_QUEUE_LIMIT)); | ||
173 | + assertThat(handler.deviceSessionCtx.getMsgQueue(), contains(messages.toArray())); | ||
174 | + verify(handler, never()).processDisconnect(any()); | ||
175 | + verify(handler, times(1)).processConnect(any(), any()); | ||
176 | + verify(handler, times(MSG_QUEUE_LIMIT)).enqueueRegularSessionMsg(any(), any()); | ||
177 | + verify(handler, never()).processRegularSessionMsg(any(), any()); | ||
178 | + messages.forEach((msg) -> verify(handler, times(1)).enqueueRegularSessionMsg(ctx, msg)); | ||
179 | + } | ||
180 | + | ||
181 | + @Test | ||
182 | + public void givenMessageQueue_whenProcessMqttMsgConcurrently_thenEnqueueRegularSessionMsg() throws InterruptedException { | ||
183 | + //given | ||
184 | + assertThat(handler.deviceSessionCtx.isConnected(), is(false)); | ||
185 | + assertThat(MSG_QUEUE_LIMIT, greaterThan(2)); | ||
186 | + List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(MSG_QUEUE_LIMIT).collect(Collectors.toList()); | ||
187 | + messages.forEach((msg) -> handler.enqueueRegularSessionMsg(ctx, msg)); | ||
188 | + willDoNothing().given(handler).processRegularSessionMsg(any(), any()); | ||
189 | + executor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(getClass().getName())); | ||
190 | + | ||
191 | + CountDownLatch readyLatch = new CountDownLatch(MSG_QUEUE_LIMIT); | ||
192 | + CountDownLatch startLatch = new CountDownLatch(1); | ||
193 | + CountDownLatch finishLatch = new CountDownLatch(MSG_QUEUE_LIMIT); | ||
194 | + | ||
195 | + Stream.iterate(0, i -> i + 1).limit(MSG_QUEUE_LIMIT).forEach(x -> | ||
196 | + executor.submit(() -> { | ||
197 | + try { | ||
198 | + readyLatch.countDown(); | ||
199 | + assertThat(startLatch.await(TIMEOUT, TimeUnit.SECONDS), is(true)); | ||
200 | + handler.processMsgQueue(ctx); | ||
201 | + finishLatch.countDown(); | ||
202 | + } catch (Exception e) { | ||
203 | + log.error("Failed to run processMsgQueue", e); | ||
204 | + fail("Failed to run processMsgQueue"); | ||
205 | + } | ||
206 | + })); | ||
207 | + | ||
208 | + //when | ||
209 | + assertThat(readyLatch.await(TIMEOUT, TimeUnit.SECONDS), is(true)); | ||
210 | + handler.deviceSessionCtx.setConnected(true); | ||
211 | + startLatch.countDown(); | ||
212 | + assertThat(finishLatch.await(TIMEOUT, TimeUnit.SECONDS), is(true)); | ||
213 | + | ||
214 | + //then | ||
215 | + assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(0)); | ||
216 | + assertThat(handler.deviceSessionCtx.getMsgQueue(), empty()); | ||
217 | + verify(handler, times(MSG_QUEUE_LIMIT)).processRegularSessionMsg(any(), any()); | ||
218 | + messages.forEach((msg) -> verify(handler, times(1)).processRegularSessionMsg(ctx, msg)); | ||
219 | + } | ||
220 | + | ||
221 | +} |
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.transport.mqtt.session; | ||
17 | + | ||
18 | +import org.junit.Test; | ||
19 | + | ||
20 | +import java.util.WeakHashMap; | ||
21 | +import java.util.concurrent.ConcurrentMap; | ||
22 | +import java.util.concurrent.TimeUnit; | ||
23 | +import java.util.concurrent.locks.Lock; | ||
24 | +import java.util.concurrent.locks.ReentrantLock; | ||
25 | + | ||
26 | +import static org.awaitility.Awaitility.await; | ||
27 | +import static org.junit.Assert.assertTrue; | ||
28 | +import static org.mockito.BDDMockito.willCallRealMethod; | ||
29 | +import static org.mockito.Mockito.mock; | ||
30 | + | ||
31 | +public class GatewaySessionHandlerTest { | ||
32 | + | ||
33 | + @Test | ||
34 | + public void givenWeakHashMap_WhenGC_thenMapIsEmpty() { | ||
35 | + WeakHashMap<String, Lock> map = new WeakHashMap<>(); | ||
36 | + | ||
37 | + String deviceName = new String("device"); //constants are static and doesn't affected by GC, so use new instead | ||
38 | + map.put(deviceName, new ReentrantLock()); | ||
39 | + assertTrue(map.containsKey(deviceName)); | ||
40 | + | ||
41 | + deviceName = null; | ||
42 | + System.gc(); | ||
43 | + | ||
44 | + await().atMost(10, TimeUnit.SECONDS).until(() -> !map.containsKey("device")); | ||
45 | + } | ||
46 | + | ||
47 | + @Test | ||
48 | + public void givenConcurrentReferenceHashMap_WhenGC_thenMapIsEmpty() { | ||
49 | + GatewaySessionHandler gsh = mock(GatewaySessionHandler.class); | ||
50 | + willCallRealMethod().given(gsh).createWeakMap(); | ||
51 | + | ||
52 | + ConcurrentMap<String, Lock> map = gsh.createWeakMap(); | ||
53 | + map.put("device", new ReentrantLock()); | ||
54 | + assertTrue(map.containsKey("device")); | ||
55 | + | ||
56 | + System.gc(); | ||
57 | + | ||
58 | + await().atMost(10, TimeUnit.SECONDS).until(() -> !map.containsKey("device")); | ||
59 | + } | ||
60 | + | ||
61 | +} |
@@ -188,6 +188,7 @@ public class SnmpTransportContext extends TransportContext { | @@ -188,6 +188,7 @@ public class SnmpTransportContext extends TransportContext { | ||
188 | 188 | ||
189 | deviceSessionContext.setSessionInfo(sessionInfo); | 189 | deviceSessionContext.setSessionInfo(sessionInfo); |
190 | deviceSessionContext.setDeviceInfo(msg.getDeviceInfo()); | 190 | deviceSessionContext.setDeviceInfo(msg.getDeviceInfo()); |
191 | + deviceSessionContext.setConnected(true); | ||
191 | } else { | 192 | } else { |
192 | log.warn("[{}] Failed to process device auth", deviceSessionContext.getDeviceId()); | 193 | log.warn("[{}] Failed to process device auth", deviceSessionContext.getDeviceId()); |
193 | } | 194 | } |
@@ -56,6 +56,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MC | @@ -56,6 +56,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MC | ||
56 | import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; | 56 | import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; |
57 | import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; | 57 | import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; |
58 | 58 | ||
59 | +import java.util.concurrent.ExecutorService; | ||
60 | + | ||
59 | /** | 61 | /** |
60 | * Created by ashvayka on 04.10.18. | 62 | * Created by ashvayka on 04.10.18. |
61 | */ | 63 | */ |
@@ -131,4 +133,6 @@ public interface TransportService { | @@ -131,4 +133,6 @@ public interface TransportService { | ||
131 | void log(SessionInfoProto sessionInfo, String msg); | 133 | void log(SessionInfoProto sessionInfo, String msg); |
132 | 134 | ||
133 | void notifyAboutUplink(SessionInfoProto sessionInfo, TransportProtos.UplinkNotificationMsg build, TransportServiceCallback<Void> empty); | 135 | void notifyAboutUplink(SessionInfoProto sessionInfo, TransportProtos.UplinkNotificationMsg build, TransportServiceCallback<Void> empty); |
136 | + | ||
137 | + ExecutorService getCallbackExecutor(); | ||
134 | } | 138 | } |
@@ -1141,4 +1141,9 @@ public class DefaultTransportService implements TransportService { | @@ -1141,4 +1141,9 @@ public class DefaultTransportService implements TransportService { | ||
1141 | callback.onError(e); | 1141 | callback.onError(e); |
1142 | } | 1142 | } |
1143 | } | 1143 | } |
1144 | + | ||
1145 | + @Override | ||
1146 | + public ExecutorService getCallbackExecutor() { | ||
1147 | + return transportCallbackExecutor; | ||
1148 | + } | ||
1144 | } | 1149 | } |
@@ -46,6 +46,7 @@ public abstract class DeviceAwareSessionContext implements SessionContext { | @@ -46,6 +46,7 @@ public abstract class DeviceAwareSessionContext implements SessionContext { | ||
46 | @Setter | 46 | @Setter |
47 | private volatile TransportProtos.SessionInfoProto sessionInfo; | 47 | private volatile TransportProtos.SessionInfoProto sessionInfo; |
48 | 48 | ||
49 | + @Setter | ||
49 | private volatile boolean connected; | 50 | private volatile boolean connected; |
50 | 51 | ||
51 | public DeviceId getDeviceId() { | 52 | public DeviceId getDeviceId() { |
@@ -54,7 +55,6 @@ public abstract class DeviceAwareSessionContext implements SessionContext { | @@ -54,7 +55,6 @@ public abstract class DeviceAwareSessionContext implements SessionContext { | ||
54 | 55 | ||
55 | public void setDeviceInfo(TransportDeviceInfo deviceInfo) { | 56 | public void setDeviceInfo(TransportDeviceInfo deviceInfo) { |
56 | this.deviceInfo = deviceInfo; | 57 | this.deviceInfo = deviceInfo; |
57 | - this.connected = true; | ||
58 | this.deviceId = deviceInfo.getDeviceId(); | 58 | this.deviceId = deviceInfo.getDeviceId(); |
59 | } | 59 | } |
60 | 60 |
@@ -52,7 +52,7 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { | @@ -52,7 +52,7 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { | ||
52 | public AdminSettings saveAdminSettings(TenantId tenantId, AdminSettings adminSettings) { | 52 | public AdminSettings saveAdminSettings(TenantId tenantId, AdminSettings adminSettings) { |
53 | log.trace("Executing saveAdminSettings [{}]", adminSettings); | 53 | log.trace("Executing saveAdminSettings [{}]", adminSettings); |
54 | adminSettingsValidator.validate(adminSettings, data -> tenantId); | 54 | adminSettingsValidator.validate(adminSettings, data -> tenantId); |
55 | - if (adminSettings.getKey().equals("mail") && "".equals(adminSettings.getJsonValue().get("password").asText())) { | 55 | + if(adminSettings.getKey().equals("mail") && !adminSettings.getJsonValue().has("password")) { |
56 | AdminSettings mailSettings = findAdminSettingsByKey(tenantId, "mail"); | 56 | AdminSettings mailSettings = findAdminSettingsByKey(tenantId, "mail"); |
57 | if (mailSettings != null) { | 57 | if (mailSettings != null) { |
58 | ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText()); | 58 | ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText()); |
@@ -61,7 +61,7 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { | @@ -61,7 +61,7 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { | ||
61 | 61 | ||
62 | return adminSettingsDao.save(tenantId, adminSettings); | 62 | return adminSettingsDao.save(tenantId, adminSettings); |
63 | } | 63 | } |
64 | - | 64 | + |
65 | private DataValidator<AdminSettings> adminSettingsValidator = | 65 | private DataValidator<AdminSettings> adminSettingsValidator = |
66 | new DataValidator<AdminSettings>() { | 66 | new DataValidator<AdminSettings>() { |
67 | 67 |
@@ -100,7 +100,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq | @@ -100,7 +100,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq | ||
100 | } | 100 | } |
101 | 101 | ||
102 | @Override | 102 | @Override |
103 | - public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl) { | 103 | + public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key) { |
104 | return Futures.immediateFuture(null); | 104 | return Futures.immediateFuture(null); |
105 | } | 105 | } |
106 | 106 |
@@ -124,7 +124,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements | @@ -124,7 +124,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements | ||
124 | } | 124 | } |
125 | 125 | ||
126 | @Override | 126 | @Override |
127 | - public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl) { | 127 | + public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key) { |
128 | return Futures.immediateFuture(0); | 128 | return Futures.immediateFuture(0); |
129 | } | 129 | } |
130 | 130 |
@@ -170,7 +170,7 @@ public class BaseTimeseriesService implements TimeseriesService { | @@ -170,7 +170,7 @@ public class BaseTimeseriesService implements TimeseriesService { | ||
170 | if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) { | 170 | if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) { |
171 | throw new IncorrectParameterException("Telemetry data can't be stored for entity view. Read only"); | 171 | throw new IncorrectParameterException("Telemetry data can't be stored for entity view. Read only"); |
172 | } | 172 | } |
173 | - futures.add(timeseriesDao.savePartition(tenantId, entityId, tsKvEntry.getTs(), tsKvEntry.getKey(), ttl)); | 173 | + futures.add(timeseriesDao.savePartition(tenantId, entityId, tsKvEntry.getTs(), tsKvEntry.getKey())); |
174 | futures.add(Futures.transform(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry), v -> 0, MoreExecutors.directExecutor())); | 174 | futures.add(Futures.transform(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry), v -> 0, MoreExecutors.directExecutor())); |
175 | futures.add(timeseriesDao.save(tenantId, entityId, tsKvEntry, ttl)); | 175 | futures.add(timeseriesDao.save(tenantId, entityId, tsKvEntry, ttl)); |
176 | } | 176 | } |
@@ -181,11 +181,14 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD | @@ -181,11 +181,14 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD | ||
181 | } | 181 | } |
182 | 182 | ||
183 | @Override | 183 | @Override |
184 | - public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl) { | 184 | + public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key) { |
185 | if (isFixedPartitioning()) { | 185 | if (isFixedPartitioning()) { |
186 | return Futures.immediateFuture(null); | 186 | return Futures.immediateFuture(null); |
187 | } | 187 | } |
188 | - ttl = computeTtl(ttl); | 188 | + // DO NOT apply custom TTL to partition, otherwise, short TTL will remove partition too early |
189 | + // partitions must remain in the DB forever or be removed only by systemTtl | ||
190 | + // removal of empty partition is too expensive (we need to scan all data keys for these partitions with ALLOW FILTERING) | ||
191 | + long ttl = computeTtl(0); | ||
189 | long partition = toPartitionTs(tsKvEntryTs); | 192 | long partition = toPartitionTs(tsKvEntryTs); |
190 | if (cassandraTsPartitionsCache == null) { | 193 | if (cassandraTsPartitionsCache == null) { |
191 | return doSavePartition(tenantId, entityId, key, ttl, partition); | 194 | return doSavePartition(tenantId, entityId, key, ttl, partition); |
@@ -33,7 +33,7 @@ public interface TimeseriesDao { | @@ -33,7 +33,7 @@ public interface TimeseriesDao { | ||
33 | 33 | ||
34 | ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl); | 34 | ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl); |
35 | 35 | ||
36 | - ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl); | 36 | + ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key); |
37 | 37 | ||
38 | ListenableFuture<Void> remove(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query); | 38 | ListenableFuture<Void> remove(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query); |
39 | 39 |
@@ -100,10 +100,10 @@ public class CassandraPartitionsCacheTest { | @@ -100,10 +100,10 @@ public class CassandraPartitionsCacheTest { | ||
100 | long tsKvEntryTs = System.currentTimeMillis(); | 100 | long tsKvEntryTs = System.currentTimeMillis(); |
101 | 101 | ||
102 | for (int i = 0; i < 50000; i++) { | 102 | for (int i = 0; i < 50000; i++) { |
103 | - cassandraBaseTimeseriesDao.savePartition(tenantId, tenantId, tsKvEntryTs, "test" + i, 0); | 103 | + cassandraBaseTimeseriesDao.savePartition(tenantId, tenantId, tsKvEntryTs, "test" + i); |
104 | } | 104 | } |
105 | for (int i = 0; i < 60000; i++) { | 105 | for (int i = 0; i < 60000; i++) { |
106 | - cassandraBaseTimeseriesDao.savePartition(tenantId, tenantId, tsKvEntryTs, "test" + i, 0); | 106 | + cassandraBaseTimeseriesDao.savePartition(tenantId, tenantId, tsKvEntryTs, "test" + i); |
107 | } | 107 | } |
108 | verify(cassandraBaseTimeseriesDao, times(60000)).executeAsyncWrite(any(TenantId.class), any(Statement.class)); | 108 | verify(cassandraBaseTimeseriesDao, times(60000)).executeAsyncWrite(any(TenantId.class), any(Statement.class)); |
109 | } | 109 | } |
@@ -42,13 +42,14 @@ | @@ -42,13 +42,14 @@ | ||
42 | <spring-boot.version>2.3.12.RELEASE</spring-boot.version> | 42 | <spring-boot.version>2.3.12.RELEASE</spring-boot.version> |
43 | <spring.version>5.2.16.RELEASE</spring.version> | 43 | <spring.version>5.2.16.RELEASE</spring.version> |
44 | <spring-redis.version>5.2.11.RELEASE</spring-redis.version> | 44 | <spring-redis.version>5.2.11.RELEASE</spring-redis.version> |
45 | - <spring-security.version>5.4.1</spring-security.version> | 45 | + <spring-security.version>5.4.4</spring-security.version> |
46 | <spring-data-redis.version>2.4.3</spring-data-redis.version> | 46 | <spring-data-redis.version>2.4.3</spring-data-redis.version> |
47 | <jedis.version>3.3.0</jedis.version> | 47 | <jedis.version>3.3.0</jedis.version> |
48 | <jjwt.version>0.7.0</jjwt.version> | 48 | <jjwt.version>0.7.0</jjwt.version> |
49 | <json-path.version>2.2.0</json-path.version> | 49 | <json-path.version>2.2.0</json-path.version> |
50 | <junit.version>4.12</junit.version> | 50 | <junit.version>4.12</junit.version> |
51 | <jupiter.version>5.7.1</jupiter.version> | 51 | <jupiter.version>5.7.1</jupiter.version> |
52 | + <awaitility.version>4.1.0</awaitility.version> | ||
52 | <hamcrest.version>2.2</hamcrest.version> | 53 | <hamcrest.version>2.2</hamcrest.version> |
53 | <slf4j.version>1.7.7</slf4j.version> | 54 | <slf4j.version>1.7.7</slf4j.version> |
54 | <logback.version>1.2.3</logback.version> | 55 | <logback.version>1.2.3</logback.version> |
@@ -1382,6 +1383,12 @@ | @@ -1382,6 +1383,12 @@ | ||
1382 | <groupId>io.grpc</groupId> | 1383 | <groupId>io.grpc</groupId> |
1383 | <artifactId>grpc-netty</artifactId> | 1384 | <artifactId>grpc-netty</artifactId> |
1384 | <version>${grpc.version}</version> | 1385 | <version>${grpc.version}</version> |
1386 | + <exclusions> | ||
1387 | + <exclusion> | ||
1388 | + <groupId>io.netty</groupId> | ||
1389 | + <artifactId>*</artifactId> | ||
1390 | + </exclusion> | ||
1391 | + </exclusions> | ||
1385 | </dependency> | 1392 | </dependency> |
1386 | <dependency> | 1393 | <dependency> |
1387 | <groupId>io.grpc</groupId> | 1394 | <groupId>io.grpc</groupId> |
@@ -1438,6 +1445,12 @@ | @@ -1438,6 +1445,12 @@ | ||
1438 | <scope>test</scope> | 1445 | <scope>test</scope> |
1439 | </dependency> | 1446 | </dependency> |
1440 | <dependency> | 1447 | <dependency> |
1448 | + <groupId>org.awaitility</groupId> | ||
1449 | + <artifactId>awaitility</artifactId> | ||
1450 | + <version>${awaitility.version}</version> | ||
1451 | + <scope>test</scope> | ||
1452 | + </dependency> | ||
1453 | + <dependency> | ||
1441 | <groupId>org.hamcrest</groupId> | 1454 | <groupId>org.hamcrest</groupId> |
1442 | <artifactId>hamcrest</artifactId> | 1455 | <artifactId>hamcrest</artifactId> |
1443 | <version>${hamcrest.version}</version> | 1456 | <version>${hamcrest.version}</version> |
@@ -1580,6 +1593,12 @@ | @@ -1580,6 +1593,12 @@ | ||
1580 | <groupId>com.microsoft.azure</groupId> | 1593 | <groupId>com.microsoft.azure</groupId> |
1581 | <artifactId>azure-servicebus</artifactId> | 1594 | <artifactId>azure-servicebus</artifactId> |
1582 | <version>${azure-servicebus.version}</version> | 1595 | <version>${azure-servicebus.version}</version> |
1596 | + <exclusions> | ||
1597 | + <exclusion> | ||
1598 | + <groupId>io.netty</groupId> | ||
1599 | + <artifactId>*</artifactId> | ||
1600 | + </exclusion> | ||
1601 | + </exclusions> | ||
1583 | </dependency> | 1602 | </dependency> |
1584 | <dependency> | 1603 | <dependency> |
1585 | <groupId>org.passay</groupId> | 1604 | <groupId>org.passay</groupId> |
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java
@@ -188,7 +188,7 @@ class AlarmState { | @@ -188,7 +188,7 @@ class AlarmState { | ||
188 | setAlarmConditionMetadata(ruleState, metaData); | 188 | setAlarmConditionMetadata(ruleState, metaData); |
189 | TbMsg newMsg = ctx.newMsg(lastMsgQueueName != null ? lastMsgQueueName : ServiceQueue.MAIN, "ALARM", | 189 | TbMsg newMsg = ctx.newMsg(lastMsgQueueName != null ? lastMsgQueueName : ServiceQueue.MAIN, "ALARM", |
190 | originator, msg != null ? msg.getCustomerId() : null, metaData, data); | 190 | originator, msg != null ? msg.getCustomerId() : null, metaData, data); |
191 | - ctx.tellNext(newMsg, relationType); | 191 | + ctx.enqueueForTellNext(newMsg, relationType); |
192 | } | 192 | } |
193 | 193 | ||
194 | protected void setAlarmConditionMetadata(AlarmRuleState ruleState, TbMsgMetaData metaData) { | 194 | protected void setAlarmConditionMetadata(AlarmRuleState ruleState, TbMsgMetaData metaData) { |
@@ -120,7 +120,7 @@ public class TbSendRPCRequestNode implements TbNode { | @@ -120,7 +120,7 @@ public class TbSendRPCRequestNode implements TbNode { | ||
120 | ctx.enqueueForTellNext(next, TbRelationTypes.SUCCESS); | 120 | ctx.enqueueForTellNext(next, TbRelationTypes.SUCCESS); |
121 | } else { | 121 | } else { |
122 | TbMsg next = ctx.newMsg(msg.getQueueName(), msg.getType(), msg.getOriginator(), msg.getCustomerId(), msg.getMetaData(), wrap("error", ruleEngineDeviceRpcResponse.getError().get().name())); | 122 | TbMsg next = ctx.newMsg(msg.getQueueName(), msg.getType(), msg.getOriginator(), msg.getCustomerId(), msg.getMetaData(), wrap("error", ruleEngineDeviceRpcResponse.getError().get().name())); |
123 | - ctx.tellFailure(next, new RuntimeException(ruleEngineDeviceRpcResponse.getError().get().name())); | 123 | + ctx.enqueueForTellFailure(next, ruleEngineDeviceRpcResponse.getError().get().name()); |
124 | } | 124 | } |
125 | }); | 125 | }); |
126 | ctx.ack(msg); | 126 | ctx.ack(msg); |
@@ -196,7 +196,7 @@ public class TbDeviceProfileNodeTest { | @@ -196,7 +196,7 @@ public class TbDeviceProfileNodeTest { | ||
196 | TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); | 196 | TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); |
197 | node.onMsg(ctx, msg); | 197 | node.onMsg(ctx, msg); |
198 | verify(ctx).tellSuccess(msg); | 198 | verify(ctx).tellSuccess(msg); |
199 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | 199 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); |
200 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); | 200 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
201 | 201 | ||
202 | TbMsg theMsg2 = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "2"); | 202 | TbMsg theMsg2 = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "2"); |
@@ -207,7 +207,7 @@ public class TbDeviceProfileNodeTest { | @@ -207,7 +207,7 @@ public class TbDeviceProfileNodeTest { | ||
207 | TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); | 207 | TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); |
208 | node.onMsg(ctx, msg2); | 208 | node.onMsg(ctx, msg2); |
209 | verify(ctx).tellSuccess(msg2); | 209 | verify(ctx).tellSuccess(msg2); |
210 | - verify(ctx).tellNext(theMsg2, "Alarm Updated"); | 210 | + verify(ctx).enqueueForTellNext(theMsg2, "Alarm Updated"); |
211 | 211 | ||
212 | } | 212 | } |
213 | 213 | ||
@@ -289,7 +289,7 @@ public class TbDeviceProfileNodeTest { | @@ -289,7 +289,7 @@ public class TbDeviceProfileNodeTest { | ||
289 | 289 | ||
290 | node.onMsg(ctx, msg); | 290 | node.onMsg(ctx, msg); |
291 | verify(ctx).tellSuccess(msg); | 291 | verify(ctx).tellSuccess(msg); |
292 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | 292 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); |
293 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); | 293 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
294 | } | 294 | } |
295 | 295 | ||
@@ -376,7 +376,7 @@ public class TbDeviceProfileNodeTest { | @@ -376,7 +376,7 @@ public class TbDeviceProfileNodeTest { | ||
376 | 376 | ||
377 | node.onMsg(ctx, msg); | 377 | node.onMsg(ctx, msg); |
378 | verify(ctx).tellSuccess(msg); | 378 | verify(ctx).tellSuccess(msg); |
379 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | 379 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); |
380 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); | 380 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
381 | } | 381 | } |
382 | 382 | ||
@@ -445,7 +445,7 @@ public class TbDeviceProfileNodeTest { | @@ -445,7 +445,7 @@ public class TbDeviceProfileNodeTest { | ||
445 | 445 | ||
446 | node.onMsg(ctx, msg); | 446 | node.onMsg(ctx, msg); |
447 | verify(ctx).tellSuccess(msg); | 447 | verify(ctx).tellSuccess(msg); |
448 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | 448 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); |
449 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); | 449 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
450 | } | 450 | } |
451 | 451 | ||
@@ -554,7 +554,7 @@ public class TbDeviceProfileNodeTest { | @@ -554,7 +554,7 @@ public class TbDeviceProfileNodeTest { | ||
554 | 554 | ||
555 | node.onMsg(ctx, msg2); | 555 | node.onMsg(ctx, msg2); |
556 | verify(ctx).tellSuccess(msg2); | 556 | verify(ctx).tellSuccess(msg2); |
557 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | 557 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); |
558 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); | 558 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
559 | } | 559 | } |
560 | 560 | ||
@@ -678,7 +678,7 @@ public class TbDeviceProfileNodeTest { | @@ -678,7 +678,7 @@ public class TbDeviceProfileNodeTest { | ||
678 | 678 | ||
679 | node.onMsg(ctx, msg2); | 679 | node.onMsg(ctx, msg2); |
680 | verify(ctx).tellSuccess(msg2); | 680 | verify(ctx).tellSuccess(msg2); |
681 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | 681 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); |
682 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); | 682 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
683 | } | 683 | } |
684 | 684 | ||
@@ -781,7 +781,7 @@ public class TbDeviceProfileNodeTest { | @@ -781,7 +781,7 @@ public class TbDeviceProfileNodeTest { | ||
781 | 781 | ||
782 | node.onMsg(ctx, msg2); | 782 | node.onMsg(ctx, msg2); |
783 | verify(ctx).tellSuccess(msg2); | 783 | verify(ctx).tellSuccess(msg2); |
784 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | 784 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); |
785 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); | 785 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
786 | } | 786 | } |
787 | 787 | ||
@@ -897,7 +897,7 @@ public class TbDeviceProfileNodeTest { | @@ -897,7 +897,7 @@ public class TbDeviceProfileNodeTest { | ||
897 | 897 | ||
898 | node.onMsg(ctx, msg2); | 898 | node.onMsg(ctx, msg2); |
899 | verify(ctx).tellSuccess(msg2); | 899 | verify(ctx).tellSuccess(msg2); |
900 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | 900 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); |
901 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); | 901 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
902 | } | 902 | } |
903 | 903 | ||
@@ -999,7 +999,7 @@ public class TbDeviceProfileNodeTest { | @@ -999,7 +999,7 @@ public class TbDeviceProfileNodeTest { | ||
999 | 999 | ||
1000 | node.onMsg(ctx, msg2); | 1000 | node.onMsg(ctx, msg2); |
1001 | verify(ctx).tellSuccess(msg2); | 1001 | verify(ctx).tellSuccess(msg2); |
1002 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | 1002 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); |
1003 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); | 1003 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
1004 | } | 1004 | } |
1005 | 1005 | ||
@@ -1082,7 +1082,7 @@ public class TbDeviceProfileNodeTest { | @@ -1082,7 +1082,7 @@ public class TbDeviceProfileNodeTest { | ||
1082 | 1082 | ||
1083 | node.onMsg(ctx, msg); | 1083 | node.onMsg(ctx, msg); |
1084 | verify(ctx).tellSuccess(msg); | 1084 | verify(ctx).tellSuccess(msg); |
1085 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | 1085 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); |
1086 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); | 1086 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
1087 | } | 1087 | } |
1088 | 1088 | ||
@@ -1163,7 +1163,7 @@ public class TbDeviceProfileNodeTest { | @@ -1163,7 +1163,7 @@ public class TbDeviceProfileNodeTest { | ||
1163 | 1163 | ||
1164 | node.onMsg(ctx, msg); | 1164 | node.onMsg(ctx, msg); |
1165 | verify(ctx).tellSuccess(msg); | 1165 | verify(ctx).tellSuccess(msg); |
1166 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | 1166 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); |
1167 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); | 1167 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
1168 | } | 1168 | } |
1169 | 1169 | ||
@@ -1237,7 +1237,7 @@ public class TbDeviceProfileNodeTest { | @@ -1237,7 +1237,7 @@ public class TbDeviceProfileNodeTest { | ||
1237 | 1237 | ||
1238 | node.onMsg(ctx, msg); | 1238 | node.onMsg(ctx, msg); |
1239 | verify(ctx).tellSuccess(msg); | 1239 | verify(ctx).tellSuccess(msg); |
1240 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | 1240 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); |
1241 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); | 1241 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
1242 | } | 1242 | } |
1243 | 1243 | ||
@@ -1321,7 +1321,7 @@ public class TbDeviceProfileNodeTest { | @@ -1321,7 +1321,7 @@ public class TbDeviceProfileNodeTest { | ||
1321 | 1321 | ||
1322 | node.onMsg(ctx, msg); | 1322 | node.onMsg(ctx, msg); |
1323 | verify(ctx).tellSuccess(msg); | 1323 | verify(ctx).tellSuccess(msg); |
1324 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | 1324 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); |
1325 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); | 1325 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
1326 | 1326 | ||
1327 | } | 1327 | } |
@@ -1407,7 +1407,7 @@ public class TbDeviceProfileNodeTest { | @@ -1407,7 +1407,7 @@ public class TbDeviceProfileNodeTest { | ||
1407 | 1407 | ||
1408 | node.onMsg(ctx, msg); | 1408 | node.onMsg(ctx, msg); |
1409 | verify(ctx).tellSuccess(msg); | 1409 | verify(ctx).tellSuccess(msg); |
1410 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | 1410 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); |
1411 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); | 1411 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
1412 | } | 1412 | } |
1413 | 1413 |
@@ -89,6 +89,7 @@ transport: | @@ -89,6 +89,7 @@ transport: | ||
89 | bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}" | 89 | bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}" |
90 | bind_port: "${MQTT_BIND_PORT:1883}" | 90 | bind_port: "${MQTT_BIND_PORT:1883}" |
91 | timeout: "${MQTT_TIMEOUT:10000}" | 91 | timeout: "${MQTT_TIMEOUT:10000}" |
92 | + msg_queue_size_per_device_limit: "${MQTT_MSG_QUEUE_SIZE_PER_DEVICE_LIMIT:100}" # messages await in the queue before device connected state. This limit works on low level before TenantProfileLimits mechanism | ||
92 | netty: | 93 | netty: |
93 | leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}" | 94 | leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}" |
94 | boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}" | 95 | boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}" |
@@ -84,6 +84,8 @@ export interface WidgetActionsApi { | @@ -84,6 +84,8 @@ export interface WidgetActionsApi { | ||
84 | entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) => void; | 84 | entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) => void; |
85 | elementClick: ($event: Event) => void; | 85 | elementClick: ($event: Event) => void; |
86 | getActiveEntityInfo: () => SubscriptionEntityInfo; | 86 | getActiveEntityInfo: () => SubscriptionEntityInfo; |
87 | + openDashboardStateInSeparateDialog: (targetDashboardStateId: string, params?: StateParams, dialogTitle?: string, | ||
88 | + hideDashboardToolbar?: boolean, dialogWidth?: number, dialogHeight?: number) => void; | ||
87 | } | 89 | } |
88 | 90 | ||
89 | export interface AliasInfo { | 91 | export interface AliasInfo { |
@@ -50,11 +50,17 @@ export class AttributeService { | @@ -50,11 +50,17 @@ export class AttributeService { | ||
50 | } | 50 | } |
51 | 51 | ||
52 | public deleteEntityTimeseries(entityId: EntityId, timeseries: Array<AttributeData>, deleteAllDataForKeys = false, | 52 | public deleteEntityTimeseries(entityId: EntityId, timeseries: Array<AttributeData>, deleteAllDataForKeys = false, |
53 | - config?: RequestConfig): Observable<any> { | 53 | + startTs?: number, endTs?: number, config?: RequestConfig): Observable<any> { |
54 | const keys = timeseries.map(attribute => encodeURI(attribute.key)).join(','); | 54 | const keys = timeseries.map(attribute => encodeURI(attribute.key)).join(','); |
55 | - return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete` + | ||
56 | - `?keys=${keys}&deleteAllDataForKeys=${deleteAllDataForKeys}`, | ||
57 | - defaultHttpOptionsFromConfig(config)); | 55 | + let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete` + |
56 | + `?keys=${keys}&deleteAllDataForKeys=${deleteAllDataForKeys}`; | ||
57 | + if (isDefinedAndNotNull(startTs)) { | ||
58 | + url += `&startTs=${startTs}`; | ||
59 | + } | ||
60 | + if (isDefinedAndNotNull(endTs)) { | ||
61 | + url += `&endTs=${endTs}`; | ||
62 | + } | ||
63 | + return this.http.delete(url, defaultHttpOptionsFromConfig(config)); | ||
58 | } | 64 | } |
59 | 65 | ||
60 | public saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array<AttributeData>, | 66 | public saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array<AttributeData>, |
@@ -97,7 +103,7 @@ export class AttributeService { | @@ -97,7 +103,7 @@ export class AttributeService { | ||
97 | }); | 103 | }); |
98 | let deleteEntityTimeseriesObservable: Observable<any>; | 104 | let deleteEntityTimeseriesObservable: Observable<any>; |
99 | if (deleteTimeseries.length) { | 105 | if (deleteTimeseries.length) { |
100 | - deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true, config); | 106 | + deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true, null, null, config); |
101 | } else { | 107 | } else { |
102 | deleteEntityTimeseriesObservable = of(null); | 108 | deleteEntityTimeseriesObservable = of(null); |
103 | } | 109 | } |
@@ -18,7 +18,7 @@ import { Store } from '@ngrx/store'; | @@ -18,7 +18,7 @@ import { Store } from '@ngrx/store'; | ||
18 | import { AppState } from '@core/core.state'; | 18 | import { AppState } from '@core/core.state'; |
19 | import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; | 19 | import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; |
20 | import { ContactBased } from '@shared/models/contact-based.model'; | 20 | import { ContactBased } from '@shared/models/contact-based.model'; |
21 | -import { AfterViewInit, Directive } from '@angular/core'; | 21 | +import { AfterViewInit, ChangeDetectorRef, Directive } from '@angular/core'; |
22 | import { POSTAL_CODE_PATTERNS } from '@home/models/contact.models'; | 22 | import { POSTAL_CODE_PATTERNS } from '@home/models/contact.models'; |
23 | import { HasId } from '@shared/models/base-data'; | 23 | import { HasId } from '@shared/models/base-data'; |
24 | import { EntityComponent } from './entity.component'; | 24 | import { EntityComponent } from './entity.component'; |
@@ -30,8 +30,9 @@ export abstract class ContactBasedComponent<T extends ContactBased<HasId>> exten | @@ -30,8 +30,9 @@ export abstract class ContactBasedComponent<T extends ContactBased<HasId>> exten | ||
30 | protected constructor(protected store: Store<AppState>, | 30 | protected constructor(protected store: Store<AppState>, |
31 | protected fb: FormBuilder, | 31 | protected fb: FormBuilder, |
32 | protected entityValue: T, | 32 | protected entityValue: T, |
33 | - protected entitiesTableConfigValue: EntityTableConfig<T>) { | ||
34 | - super(store, fb, entityValue, entitiesTableConfigValue); | 33 | + protected entitiesTableConfigValue: EntityTableConfig<T>, |
34 | + protected cd: ChangeDetectorRef) { | ||
35 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | ||
35 | } | 36 | } |
36 | 37 | ||
37 | buildForm(entity: T): FormGroup { | 38 | buildForm(entity: T): FormGroup { |
@@ -17,7 +17,7 @@ | @@ -17,7 +17,7 @@ | ||
17 | import { BaseData, HasId } from '@shared/models/base-data'; | 17 | import { BaseData, HasId } from '@shared/models/base-data'; |
18 | import { FormBuilder, FormGroup } from '@angular/forms'; | 18 | import { FormBuilder, FormGroup } from '@angular/forms'; |
19 | import { PageComponent } from '@shared/components/page.component'; | 19 | import { PageComponent } from '@shared/components/page.component'; |
20 | -import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'; | 20 | +import { ChangeDetectorRef, Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'; |
21 | import { Store } from '@ngrx/store'; | 21 | import { Store } from '@ngrx/store'; |
22 | import { AppState } from '@core/core.state'; | 22 | import { AppState } from '@core/core.state'; |
23 | import { EntityAction } from '@home/models/entity/entity-component.models'; | 23 | import { EntityAction } from '@home/models/entity/entity-component.models'; |
@@ -50,6 +50,7 @@ export abstract class EntityComponent<T extends BaseData<HasId>, | @@ -50,6 +50,7 @@ export abstract class EntityComponent<T extends BaseData<HasId>, | ||
50 | @Input() | 50 | @Input() |
51 | set isEdit(isEdit: boolean) { | 51 | set isEdit(isEdit: boolean) { |
52 | this.isEditValue = isEdit; | 52 | this.isEditValue = isEdit; |
53 | + this.cd.markForCheck(); | ||
53 | this.updateFormState(); | 54 | this.updateFormState(); |
54 | } | 55 | } |
55 | 56 | ||
@@ -80,7 +81,8 @@ export abstract class EntityComponent<T extends BaseData<HasId>, | @@ -80,7 +81,8 @@ export abstract class EntityComponent<T extends BaseData<HasId>, | ||
80 | protected constructor(protected store: Store<AppState>, | 81 | protected constructor(protected store: Store<AppState>, |
81 | protected fb: FormBuilder, | 82 | protected fb: FormBuilder, |
82 | protected entityValue: T, | 83 | protected entityValue: T, |
83 | - protected entitiesTableConfigValue: C) { | 84 | + protected entitiesTableConfigValue: C, |
85 | + protected cd: ChangeDetectorRef) { | ||
84 | super(store); | 86 | super(store); |
85 | this.entityForm = this.buildForm(this.entityValue); | 87 | this.entityForm = this.buildForm(this.entityValue); |
86 | } | 88 | } |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, Inject, Input, Optional } from '@angular/core'; | 17 | +import { ChangeDetectorRef, Component, Inject, Input, Optional } from '@angular/core'; |
18 | import { Store } from '@ngrx/store'; | 18 | import { Store } from '@ngrx/store'; |
19 | import { AppState } from '@core/core.state'; | 19 | import { AppState } from '@core/core.state'; |
20 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | 20 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
@@ -77,8 +77,9 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> { | @@ -77,8 +77,9 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> { | ||
77 | protected translate: TranslateService, | 77 | protected translate: TranslateService, |
78 | @Optional() @Inject('entity') protected entityValue: DeviceProfile, | 78 | @Optional() @Inject('entity') protected entityValue: DeviceProfile, |
79 | @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<DeviceProfile>, | 79 | @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<DeviceProfile>, |
80 | - protected fb: FormBuilder) { | ||
81 | - super(store, fb, entityValue, entitiesTableConfigValue); | 80 | + protected fb: FormBuilder, |
81 | + protected cd: ChangeDetectorRef) { | ||
82 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | ||
82 | } | 83 | } |
83 | 84 | ||
84 | hideDelete() { | 85 | hideDelete() { |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, Inject, Input, Optional } from '@angular/core'; | 17 | +import { ChangeDetectorRef, Component, Inject, Input, Optional } from '@angular/core'; |
18 | import { Store } from '@ngrx/store'; | 18 | import { Store } from '@ngrx/store'; |
19 | import { AppState } from '@core/core.state'; | 19 | import { AppState } from '@core/core.state'; |
20 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | 20 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
@@ -43,8 +43,9 @@ export class TenantProfileComponent extends EntityComponent<TenantProfile> { | @@ -43,8 +43,9 @@ export class TenantProfileComponent extends EntityComponent<TenantProfile> { | ||
43 | protected translate: TranslateService, | 43 | protected translate: TranslateService, |
44 | @Optional() @Inject('entity') protected entityValue: TenantProfile, | 44 | @Optional() @Inject('entity') protected entityValue: TenantProfile, |
45 | @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<TenantProfile>, | 45 | @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<TenantProfile>, |
46 | - protected fb: FormBuilder) { | ||
47 | - super(store, fb, entityValue, entitiesTableConfigValue); | 46 | + protected fb: FormBuilder, |
47 | + protected cd: ChangeDetectorRef) { | ||
48 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | ||
48 | } | 49 | } |
49 | 50 | ||
50 | hideDelete() { | 51 | hideDelete() { |
@@ -284,7 +284,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -284,7 +284,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
284 | getActionDescriptors: this.getActionDescriptors.bind(this), | 284 | getActionDescriptors: this.getActionDescriptors.bind(this), |
285 | handleWidgetAction: this.handleWidgetAction.bind(this), | 285 | handleWidgetAction: this.handleWidgetAction.bind(this), |
286 | elementClick: this.elementClick.bind(this), | 286 | elementClick: this.elementClick.bind(this), |
287 | - getActiveEntityInfo: this.getActiveEntityInfo.bind(this) | 287 | + getActiveEntityInfo: this.getActiveEntityInfo.bind(this), |
288 | + openDashboardStateInSeparateDialog: this.openDashboardStateInSeparateDialog.bind(this) | ||
288 | }; | 289 | }; |
289 | 290 | ||
290 | this.widgetContext.customHeaderActions = []; | 291 | this.widgetContext.customHeaderActions = []; |
@@ -1025,7 +1026,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -1025,7 +1026,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
1025 | this.updateEntityParams(params, targetEntityParamName, targetEntityId, entityName, entityLabel); | 1026 | this.updateEntityParams(params, targetEntityParamName, targetEntityId, entityName, entityLabel); |
1026 | if (type === WidgetActionType.openDashboardState) { | 1027 | if (type === WidgetActionType.openDashboardState) { |
1027 | if (descriptor.openInSeparateDialog) { | 1028 | if (descriptor.openInSeparateDialog) { |
1028 | - this.openDashboardStateInDialog(descriptor, entityId, entityName, additionalParams, entityLabel); | 1029 | + this.openDashboardStateInSeparateDialog(descriptor.targetDashboardStateId, params, descriptor.dialogTitle, |
1030 | + descriptor.dialogHideDashboardToolbar, descriptor.dialogWidth, descriptor.dialogHeight); | ||
1029 | } else { | 1031 | } else { |
1030 | this.widgetContext.stateController.openState(targetDashboardStateId, params, descriptor.openRightLayout); | 1032 | this.widgetContext.stateController.openState(targetDashboardStateId, params, descriptor.openRightLayout); |
1031 | } | 1033 | } |
@@ -1276,22 +1278,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -1276,22 +1278,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
1276 | } | 1278 | } |
1277 | } | 1279 | } |
1278 | 1280 | ||
1279 | - private openDashboardStateInDialog(descriptor: WidgetActionDescriptor, | ||
1280 | - entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) { | 1281 | + private openDashboardStateInSeparateDialog(targetDashboardStateId: string, params?: StateParams, dialogTitle?: string, |
1282 | + hideDashboardToolbar = true, dialogWidth?: number, dialogHeight?: number) { | ||
1281 | const dashboard = deepClone(this.widgetContext.stateController.dashboardCtrl.dashboardCtx.getDashboard()); | 1283 | const dashboard = deepClone(this.widgetContext.stateController.dashboardCtrl.dashboardCtx.getDashboard()); |
1282 | const stateObject: StateObject = {}; | 1284 | const stateObject: StateObject = {}; |
1283 | - stateObject.params = {}; | ||
1284 | - const targetEntityParamName = descriptor.stateEntityParamName; | ||
1285 | - const targetDashboardStateId = descriptor.targetDashboardStateId; | ||
1286 | - let targetEntityId: EntityId; | ||
1287 | - if (descriptor.setEntityId) { | ||
1288 | - targetEntityId = entityId; | ||
1289 | - } | ||
1290 | - this.updateEntityParams(stateObject.params, targetEntityParamName, targetEntityId, entityName, entityLabel); | 1285 | + stateObject.params = params; |
1291 | if (targetDashboardStateId) { | 1286 | if (targetDashboardStateId) { |
1292 | stateObject.id = targetDashboardStateId; | 1287 | stateObject.id = targetDashboardStateId; |
1293 | } | 1288 | } |
1294 | - let title = descriptor.dialogTitle; | 1289 | + let title = dialogTitle; |
1295 | if (!title) { | 1290 | if (!title) { |
1296 | if (targetDashboardStateId && dashboard.configuration.states) { | 1291 | if (targetDashboardStateId && dashboard.configuration.states) { |
1297 | const dashboardState = dashboard.configuration.states[targetDashboardStateId]; | 1292 | const dashboardState = dashboard.configuration.states[targetDashboardStateId]; |
@@ -1304,7 +1299,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -1304,7 +1299,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
1304 | title = dashboard.title; | 1299 | title = dashboard.title; |
1305 | } | 1300 | } |
1306 | title = this.utils.customTranslation(title, title); | 1301 | title = this.utils.customTranslation(title, title); |
1307 | - const params = stateObject.params; | ||
1308 | const paramsEntityName = params && params.entityName ? params.entityName : ''; | 1302 | const paramsEntityName = params && params.entityName ? params.entityName : ''; |
1309 | const paramsEntityLabel = params && params.entityLabel ? params.entityLabel : ''; | 1303 | const paramsEntityLabel = params && params.entityLabel ? params.entityLabel : ''; |
1310 | title = insertVariable(title, 'entityName', paramsEntityName); | 1304 | title = insertVariable(title, 'entityName', paramsEntityName); |
@@ -1324,28 +1318,27 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | @@ -1324,28 +1318,27 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI | ||
1324 | dashboard, | 1318 | dashboard, |
1325 | state: objToBase64([ stateObject ]), | 1319 | state: objToBase64([ stateObject ]), |
1326 | title, | 1320 | title, |
1327 | - hideToolbar: descriptor.dialogHideDashboardToolbar, | ||
1328 | - width: descriptor.dialogWidth, | ||
1329 | - height: descriptor.dialogHeight | 1321 | + hideToolbar: hideDashboardToolbar, |
1322 | + width: dialogWidth, | ||
1323 | + height: dialogHeight | ||
1330 | } | 1324 | } |
1331 | }); | 1325 | }); |
1332 | } | 1326 | } |
1333 | 1327 | ||
1334 | private elementClick($event: Event) { | 1328 | private elementClick($event: Event) { |
1335 | - const e = ($event.target || $event.srcElement) as Element; | ||
1336 | - if (e.id) { | ||
1337 | - const descriptors = this.getActionDescriptors('elementClick'); | ||
1338 | - if (descriptors.length) { | ||
1339 | - descriptors.forEach((descriptor) => { | ||
1340 | - if (descriptor.name === e.id) { | ||
1341 | - $event.stopPropagation(); | ||
1342 | - const entityInfo = this.getActiveEntityInfo(); | ||
1343 | - const entityId = entityInfo ? entityInfo.entityId : null; | ||
1344 | - const entityName = entityInfo ? entityInfo.entityName : null; | ||
1345 | - const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null; | ||
1346 | - this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); | ||
1347 | - } | ||
1348 | - }); | 1329 | + const elementClicked = ($event.target || $event.srcElement) as Element; |
1330 | + const descriptors = this.getActionDescriptors('elementClick'); | ||
1331 | + if (descriptors.length) { | ||
1332 | + const idsList = descriptors.map(descriptor => `#${descriptor.name}`).join(','); | ||
1333 | + const targetElement = $(elementClicked).closest(idsList, this.widgetContext.$container[0]); | ||
1334 | + if (targetElement.length && targetElement[0].id) { | ||
1335 | + $event.stopPropagation(); | ||
1336 | + const descriptor = descriptors.find(descriptorInfo => descriptorInfo.name === targetElement[0].id); | ||
1337 | + const entityInfo = this.getActiveEntityInfo(); | ||
1338 | + const entityId = entityInfo ? entityInfo.entityId : null; | ||
1339 | + const entityName = entityInfo ? entityInfo.entityName : null; | ||
1340 | + const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null; | ||
1341 | + this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); | ||
1349 | } | 1342 | } |
1350 | } | 1343 | } |
1351 | } | 1344 | } |
@@ -39,7 +39,7 @@ | @@ -39,7 +39,7 @@ | ||
39 | </mat-form-field> | 39 | </mat-form-field> |
40 | <mat-form-field class="mat-block"> | 40 | <mat-form-field class="mat-block"> |
41 | <mat-label translate>admin.smtp-protocol</mat-label> | 41 | <mat-label translate>admin.smtp-protocol</mat-label> |
42 | - <mat-select matInput formControlName="smtpProtocol"> | 42 | + <mat-select formControlName="smtpProtocol"> |
43 | <mat-option *ngFor="let protocol of smtpProtocols" [value]="protocol"> | 43 | <mat-option *ngFor="let protocol of smtpProtocols" [value]="protocol"> |
44 | {{protocol.toUpperCase()}} | 44 | {{protocol.toUpperCase()}} |
45 | </mat-option> | 45 | </mat-option> |
@@ -127,7 +127,10 @@ | @@ -127,7 +127,10 @@ | ||
127 | <input matInput formControlName="username" placeholder="{{ 'common.enter-username' | translate }}" | 127 | <input matInput formControlName="username" placeholder="{{ 'common.enter-username' | translate }}" |
128 | autocomplete="new-username"/> | 128 | autocomplete="new-username"/> |
129 | </mat-form-field> | 129 | </mat-form-field> |
130 | - <mat-form-field class="mat-block"> | 130 | + <mat-checkbox *ngIf="showChangePassword" formControlName="changePassword" style="padding-bottom: 16px;"> |
131 | + {{ 'admin.change-password' | translate }} | ||
132 | + </mat-checkbox> | ||
133 | + <mat-form-field class="mat-block" *ngIf="mailSettings.get('changePassword').value || !showChangePassword"> | ||
131 | <mat-label translate>common.password</mat-label> | 134 | <mat-label translate>common.password</mat-label> |
132 | <input matInput formControlName="password" type="password" | 135 | <input matInput formControlName="password" type="password" |
133 | placeholder="{{ 'common.enter-password' | translate }}" autocomplete="new-password"/> | 136 | placeholder="{{ 'common.enter-password' | translate }}" autocomplete="new-password"/> |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, OnInit } from '@angular/core'; | 17 | +import { Component, OnDestroy, OnInit } from '@angular/core'; |
18 | import { Store } from '@ngrx/store'; | 18 | import { Store } from '@ngrx/store'; |
19 | import { AppState } from '@core/core.state'; | 19 | import { AppState } from '@core/core.state'; |
20 | import { PageComponent } from '@shared/components/page.component'; | 20 | import { PageComponent } from '@shared/components/page.component'; |
@@ -25,21 +25,26 @@ import { AdminService } from '@core/http/admin.service'; | @@ -25,21 +25,26 @@ import { AdminService } from '@core/http/admin.service'; | ||
25 | import { ActionNotificationShow } from '@core/notification/notification.actions'; | 25 | import { ActionNotificationShow } from '@core/notification/notification.actions'; |
26 | import { TranslateService } from '@ngx-translate/core'; | 26 | import { TranslateService } from '@ngx-translate/core'; |
27 | import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; | 27 | import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; |
28 | -import { isString } from '@core/utils'; | 28 | +import { isDefinedAndNotNull, isString } from '@core/utils'; |
29 | +import { Subject } from 'rxjs'; | ||
30 | +import { takeUntil } from 'rxjs/operators'; | ||
29 | 31 | ||
30 | @Component({ | 32 | @Component({ |
31 | selector: 'tb-mail-server', | 33 | selector: 'tb-mail-server', |
32 | templateUrl: './mail-server.component.html', | 34 | templateUrl: './mail-server.component.html', |
33 | styleUrls: ['./mail-server.component.scss', './settings-card.scss'] | 35 | styleUrls: ['./mail-server.component.scss', './settings-card.scss'] |
34 | }) | 36 | }) |
35 | -export class MailServerComponent extends PageComponent implements OnInit, HasConfirmForm { | 37 | +export class MailServerComponent extends PageComponent implements OnInit, OnDestroy, HasConfirmForm { |
36 | 38 | ||
37 | mailSettings: FormGroup; | 39 | mailSettings: FormGroup; |
38 | adminSettings: AdminSettings<MailServerSettings>; | 40 | adminSettings: AdminSettings<MailServerSettings>; |
39 | smtpProtocols = ['smtp', 'smtps']; | 41 | smtpProtocols = ['smtp', 'smtps']; |
42 | + showChangePassword = false; | ||
40 | 43 | ||
41 | tlsVersions = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3']; | 44 | tlsVersions = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3']; |
42 | 45 | ||
46 | + private destroy$ = new Subject(); | ||
47 | + | ||
43 | constructor(protected store: Store<AppState>, | 48 | constructor(protected store: Store<AppState>, |
44 | private router: Router, | 49 | private router: Router, |
45 | private adminService: AdminService, | 50 | private adminService: AdminService, |
@@ -56,12 +61,22 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon | @@ -56,12 +61,22 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon | ||
56 | if (this.adminSettings.jsonValue && isString(this.adminSettings.jsonValue.enableTls)) { | 61 | if (this.adminSettings.jsonValue && isString(this.adminSettings.jsonValue.enableTls)) { |
57 | this.adminSettings.jsonValue.enableTls = (this.adminSettings.jsonValue.enableTls as any) === 'true'; | 62 | this.adminSettings.jsonValue.enableTls = (this.adminSettings.jsonValue.enableTls as any) === 'true'; |
58 | } | 63 | } |
64 | + this.showChangePassword = | ||
65 | + isDefinedAndNotNull(this.adminSettings.jsonValue.showChangePassword) ? this.adminSettings.jsonValue.showChangePassword : true ; | ||
66 | + delete this.adminSettings.jsonValue.showChangePassword; | ||
59 | this.mailSettings.reset(this.adminSettings.jsonValue); | 67 | this.mailSettings.reset(this.adminSettings.jsonValue); |
68 | + this.enableMailPassword(!this.showChangePassword); | ||
60 | this.enableProxyChanged(); | 69 | this.enableProxyChanged(); |
61 | } | 70 | } |
62 | ); | 71 | ); |
63 | } | 72 | } |
64 | 73 | ||
74 | + ngOnDestroy() { | ||
75 | + this.destroy$.next(); | ||
76 | + this.destroy$.complete(); | ||
77 | + super.ngOnDestroy(); | ||
78 | + } | ||
79 | + | ||
65 | buildMailServerSettingsForm() { | 80 | buildMailServerSettingsForm() { |
66 | this.mailSettings = this.fb.group({ | 81 | this.mailSettings = this.fb.group({ |
67 | mailFrom: ['', [Validators.required]], | 82 | mailFrom: ['', [Validators.required]], |
@@ -81,14 +96,23 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon | @@ -81,14 +96,23 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon | ||
81 | proxyUser: [''], | 96 | proxyUser: [''], |
82 | proxyPassword: [''], | 97 | proxyPassword: [''], |
83 | username: [''], | 98 | username: [''], |
99 | + changePassword: [false], | ||
84 | password: [''] | 100 | password: [''] |
85 | }); | 101 | }); |
86 | this.registerDisableOnLoadFormControl(this.mailSettings.get('smtpProtocol')); | 102 | this.registerDisableOnLoadFormControl(this.mailSettings.get('smtpProtocol')); |
87 | this.registerDisableOnLoadFormControl(this.mailSettings.get('enableTls')); | 103 | this.registerDisableOnLoadFormControl(this.mailSettings.get('enableTls')); |
88 | this.registerDisableOnLoadFormControl(this.mailSettings.get('enableProxy')); | 104 | this.registerDisableOnLoadFormControl(this.mailSettings.get('enableProxy')); |
89 | - this.mailSettings.get('enableProxy').valueChanges.subscribe(() => { | 105 | + this.registerDisableOnLoadFormControl(this.mailSettings.get('changePassword')); |
106 | + this.mailSettings.get('enableProxy').valueChanges.pipe( | ||
107 | + takeUntil(this.destroy$) | ||
108 | + ).subscribe(() => { | ||
90 | this.enableProxyChanged(); | 109 | this.enableProxyChanged(); |
91 | }); | 110 | }); |
111 | + this.mailSettings.get('changePassword').valueChanges.pipe( | ||
112 | + takeUntil(this.destroy$) | ||
113 | + ).subscribe((value) => { | ||
114 | + this.enableMailPassword(value); | ||
115 | + }); | ||
92 | } | 116 | } |
93 | 117 | ||
94 | enableProxyChanged(): void { | 118 | enableProxyChanged(): void { |
@@ -102,8 +126,16 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon | @@ -102,8 +126,16 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon | ||
102 | } | 126 | } |
103 | } | 127 | } |
104 | 128 | ||
129 | + enableMailPassword(enable: boolean) { | ||
130 | + if (enable) { | ||
131 | + this.mailSettings.get('password').enable({emitEvent: false}); | ||
132 | + } else { | ||
133 | + this.mailSettings.get('password').disable({emitEvent: false}); | ||
134 | + } | ||
135 | + } | ||
136 | + | ||
105 | sendTestMail(): void { | 137 | sendTestMail(): void { |
106 | - this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value}; | 138 | + this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettingsFormValue}; |
107 | this.adminService.sendTestMail(this.adminSettings).subscribe( | 139 | this.adminService.sendTestMail(this.adminSettings).subscribe( |
108 | () => { | 140 | () => { |
109 | this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('admin.test-mail-sent'), | 141 | this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('admin.test-mail-sent'), |
@@ -113,13 +145,11 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon | @@ -113,13 +145,11 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon | ||
113 | } | 145 | } |
114 | 146 | ||
115 | save(): void { | 147 | save(): void { |
116 | - this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value}; | 148 | + this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettingsFormValue}; |
117 | this.adminService.saveAdminSettings(this.adminSettings).subscribe( | 149 | this.adminService.saveAdminSettings(this.adminSettings).subscribe( |
118 | (adminSettings) => { | 150 | (adminSettings) => { |
119 | - if (!adminSettings.jsonValue.password) { | ||
120 | - adminSettings.jsonValue.password = this.mailSettings.value.password; | ||
121 | - } | ||
122 | this.adminSettings = adminSettings; | 151 | this.adminSettings = adminSettings; |
152 | + this.showChangePassword = true; | ||
123 | this.mailSettings.reset(this.adminSettings.jsonValue); | 153 | this.mailSettings.reset(this.adminSettings.jsonValue); |
124 | } | 154 | } |
125 | ); | 155 | ); |
@@ -129,4 +159,9 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon | @@ -129,4 +159,9 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon | ||
129 | return this.mailSettings; | 159 | return this.mailSettings; |
130 | } | 160 | } |
131 | 161 | ||
162 | + private get mailSettingsFormValue(): MailServerSettings { | ||
163 | + const formValue = this.mailSettings.value; | ||
164 | + delete formValue.changePassword; | ||
165 | + return formValue; | ||
166 | + } | ||
132 | } | 167 | } |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; | 17 | +import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from '@angular/core'; |
18 | import { Subject } from 'rxjs'; | 18 | import { Subject } from 'rxjs'; |
19 | import { Store } from '@ngrx/store'; | 19 | import { Store } from '@ngrx/store'; |
20 | import { AppState } from '@core/core.state'; | 20 | import { AppState } from '@core/core.state'; |
@@ -30,7 +30,7 @@ import { | @@ -30,7 +30,7 @@ import { | ||
30 | ResourceTypeTranslationMap | 30 | ResourceTypeTranslationMap |
31 | } from '@shared/models/resource.models'; | 31 | } from '@shared/models/resource.models'; |
32 | import { pairwise, startWith, takeUntil } from 'rxjs/operators'; | 32 | import { pairwise, startWith, takeUntil } from 'rxjs/operators'; |
33 | -import { ActionNotificationShow } from "@core/notification/notification.actions"; | 33 | +import { ActionNotificationShow } from '@core/notification/notification.actions'; |
34 | 34 | ||
35 | @Component({ | 35 | @Component({ |
36 | selector: 'tb-resources-library', | 36 | selector: 'tb-resources-library', |
@@ -48,8 +48,9 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme | @@ -48,8 +48,9 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme | ||
48 | protected translate: TranslateService, | 48 | protected translate: TranslateService, |
49 | @Inject('entity') protected entityValue: Resource, | 49 | @Inject('entity') protected entityValue: Resource, |
50 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Resource>, | 50 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Resource>, |
51 | - public fb: FormBuilder) { | ||
52 | - super(store, fb, entityValue, entitiesTableConfigValue); | 51 | + public fb: FormBuilder, |
52 | + protected cd: ChangeDetectorRef) { | ||
53 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | ||
53 | } | 54 | } |
54 | 55 | ||
55 | ngOnInit() { | 56 | ngOnInit() { |
@@ -102,7 +103,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme | @@ -102,7 +103,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme | ||
102 | if (this.isAdd) { | 103 | if (this.isAdd) { |
103 | form.addControl('data', this.fb.control(null, Validators.required)); | 104 | form.addControl('data', this.fb.control(null, Validators.required)); |
104 | } | 105 | } |
105 | - return form | 106 | + return form; |
106 | } | 107 | } |
107 | 108 | ||
108 | updateForm(entity: Resource) { | 109 | updateForm(entity: Resource) { |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, Inject } from '@angular/core'; | 17 | +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; |
18 | import { Store } from '@ngrx/store'; | 18 | import { Store } from '@ngrx/store'; |
19 | import { AppState } from '@core/core.state'; | 19 | import { AppState } from '@core/core.state'; |
20 | import { EntityComponent } from '../../components/entity/entity.component'; | 20 | import { EntityComponent } from '../../components/entity/entity.component'; |
@@ -41,8 +41,9 @@ export class AssetComponent extends EntityComponent<AssetInfo> { | @@ -41,8 +41,9 @@ export class AssetComponent extends EntityComponent<AssetInfo> { | ||
41 | protected translate: TranslateService, | 41 | protected translate: TranslateService, |
42 | @Inject('entity') protected entityValue: AssetInfo, | 42 | @Inject('entity') protected entityValue: AssetInfo, |
43 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<AssetInfo>, | 43 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<AssetInfo>, |
44 | - public fb: FormBuilder) { | ||
45 | - super(store, fb, entityValue, entitiesTableConfigValue); | 44 | + public fb: FormBuilder, |
45 | + protected cd: ChangeDetectorRef) { | ||
46 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | ||
46 | } | 47 | } |
47 | 48 | ||
48 | ngOnInit() { | 49 | ngOnInit() { |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, Inject } from '@angular/core'; | 17 | +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; |
18 | import { Store } from '@ngrx/store'; | 18 | import { Store } from '@ngrx/store'; |
19 | import { AppState } from '@core/core.state'; | 19 | import { AppState } from '@core/core.state'; |
20 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | 20 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
@@ -42,8 +42,9 @@ export class CustomerComponent extends ContactBasedComponent<Customer> { | @@ -42,8 +42,9 @@ export class CustomerComponent extends ContactBasedComponent<Customer> { | ||
42 | protected translate: TranslateService, | 42 | protected translate: TranslateService, |
43 | @Inject('entity') protected entityValue: Customer, | 43 | @Inject('entity') protected entityValue: Customer, |
44 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Customer>, | 44 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Customer>, |
45 | - protected fb: FormBuilder) { | ||
46 | - super(store, fb, entityValue, entitiesTableConfigValue); | 45 | + protected fb: FormBuilder, |
46 | + protected cd: ChangeDetectorRef) { | ||
47 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | ||
47 | } | 48 | } |
48 | 49 | ||
49 | hideDelete() { | 50 | hideDelete() { |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, Inject } from '@angular/core'; | 17 | +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; |
18 | import { Store } from '@ngrx/store'; | 18 | import { Store } from '@ngrx/store'; |
19 | import { AppState } from '@core/core.state'; | 19 | import { AppState } from '@core/core.state'; |
20 | import { EntityComponent } from '../../components/entity/entity.component'; | 20 | import { EntityComponent } from '../../components/entity/entity.component'; |
@@ -49,8 +49,9 @@ export class DashboardFormComponent extends EntityComponent<Dashboard> { | @@ -49,8 +49,9 @@ export class DashboardFormComponent extends EntityComponent<Dashboard> { | ||
49 | private dashboardService: DashboardService, | 49 | private dashboardService: DashboardService, |
50 | @Inject('entity') protected entityValue: Dashboard, | 50 | @Inject('entity') protected entityValue: Dashboard, |
51 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Dashboard>, | 51 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Dashboard>, |
52 | - public fb: FormBuilder) { | ||
53 | - super(store, fb, entityValue, entitiesTableConfigValue); | 52 | + public fb: FormBuilder, |
53 | + protected cd: ChangeDetectorRef) { | ||
54 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | ||
54 | } | 55 | } |
55 | 56 | ||
56 | ngOnInit() { | 57 | ngOnInit() { |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, Inject } from '@angular/core'; | 17 | +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; |
18 | import { Store } from '@ngrx/store'; | 18 | import { Store } from '@ngrx/store'; |
19 | import { AppState } from '@core/core.state'; | 19 | import { AppState } from '@core/core.state'; |
20 | import { EntityComponent } from '../../components/entity/entity.component'; | 20 | import { EntityComponent } from '../../components/entity/entity.component'; |
@@ -56,8 +56,9 @@ export class DeviceComponent extends EntityComponent<DeviceInfo> { | @@ -56,8 +56,9 @@ export class DeviceComponent extends EntityComponent<DeviceInfo> { | ||
56 | protected translate: TranslateService, | 56 | protected translate: TranslateService, |
57 | @Inject('entity') protected entityValue: DeviceInfo, | 57 | @Inject('entity') protected entityValue: DeviceInfo, |
58 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<DeviceInfo>, | 58 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<DeviceInfo>, |
59 | - public fb: FormBuilder) { | ||
60 | - super(store, fb, entityValue, entitiesTableConfigValue); | 59 | + public fb: FormBuilder, |
60 | + protected cd: ChangeDetectorRef) { | ||
61 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | ||
61 | } | 62 | } |
62 | 63 | ||
63 | ngOnInit() { | 64 | ngOnInit() { |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, Inject } from '@angular/core'; | 17 | +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; |
18 | import { Store } from '@ngrx/store'; | 18 | import { Store } from '@ngrx/store'; |
19 | import { AppState } from '@core/core.state'; | 19 | import { AppState } from '@core/core.state'; |
20 | import { EntityComponent } from '@home/components/entity/entity.component'; | 20 | import { EntityComponent } from '@home/components/entity/entity.component'; |
@@ -42,8 +42,9 @@ export class EdgeComponent extends EntityComponent<EdgeInfo> { | @@ -42,8 +42,9 @@ export class EdgeComponent extends EntityComponent<EdgeInfo> { | ||
42 | protected translate: TranslateService, | 42 | protected translate: TranslateService, |
43 | @Inject('entity') protected entityValue: EdgeInfo, | 43 | @Inject('entity') protected entityValue: EdgeInfo, |
44 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<EdgeInfo>, | 44 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<EdgeInfo>, |
45 | - public fb: FormBuilder) { | ||
46 | - super(store, fb, entityValue, entitiesTableConfigValue); | 45 | + public fb: FormBuilder, |
46 | + protected cd: ChangeDetectorRef) { | ||
47 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | ||
47 | } | 48 | } |
48 | 49 | ||
49 | ngOnInit() { | 50 | ngOnInit() { |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, Inject } from '@angular/core'; | 17 | +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; |
18 | import { Store } from '@ngrx/store'; | 18 | import { Store } from '@ngrx/store'; |
19 | import { AppState } from '@core/core.state'; | 19 | import { AppState } from '@core/core.state'; |
20 | import { EntityComponent } from '../../components/entity/entity.component'; | 20 | import { EntityComponent } from '../../components/entity/entity.component'; |
@@ -53,8 +53,9 @@ export class EntityViewComponent extends EntityComponent<EntityViewInfo> { | @@ -53,8 +53,9 @@ export class EntityViewComponent extends EntityComponent<EntityViewInfo> { | ||
53 | protected translate: TranslateService, | 53 | protected translate: TranslateService, |
54 | @Inject('entity') protected entityValue: EntityViewInfo, | 54 | @Inject('entity') protected entityValue: EntityViewInfo, |
55 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<EntityViewInfo>, | 55 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<EntityViewInfo>, |
56 | - public fb: FormBuilder) { | ||
57 | - super(store, fb, entityValue, entitiesTableConfigValue); | 56 | + public fb: FormBuilder, |
57 | + protected cd: ChangeDetectorRef) { | ||
58 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | ||
58 | } | 59 | } |
59 | 60 | ||
60 | ngOnInit() { | 61 | ngOnInit() { |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; | 17 | +import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from '@angular/core'; |
18 | import { combineLatest, Subject } from 'rxjs'; | 18 | import { combineLatest, Subject } from 'rxjs'; |
19 | import { Store } from '@ngrx/store'; | 19 | import { Store } from '@ngrx/store'; |
20 | import { AppState } from '@core/core.state'; | 20 | import { AppState } from '@core/core.state'; |
@@ -50,8 +50,9 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O | @@ -50,8 +50,9 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O | ||
50 | protected translate: TranslateService, | 50 | protected translate: TranslateService, |
51 | @Inject('entity') protected entityValue: OtaPackage, | 51 | @Inject('entity') protected entityValue: OtaPackage, |
52 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<OtaPackage>, | 52 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<OtaPackage>, |
53 | - public fb: FormBuilder) { | ||
54 | - super(store, fb, entityValue, entitiesTableConfigValue); | 53 | + public fb: FormBuilder, |
54 | + protected cd: ChangeDetectorRef) { | ||
55 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | ||
55 | } | 56 | } |
56 | 57 | ||
57 | ngOnInit() { | 58 | ngOnInit() { |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, Inject } from '@angular/core'; | 17 | +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; |
18 | import { Store } from '@ngrx/store'; | 18 | import { Store } from '@ngrx/store'; |
19 | import { AppState } from '@core/core.state'; | 19 | import { AppState } from '@core/core.state'; |
20 | import { EntityComponent } from '../../components/entity/entity.component'; | 20 | import { EntityComponent } from '../../components/entity/entity.component'; |
@@ -37,8 +37,9 @@ export class RuleChainComponent extends EntityComponent<RuleChain> { | @@ -37,8 +37,9 @@ export class RuleChainComponent extends EntityComponent<RuleChain> { | ||
37 | protected translate: TranslateService, | 37 | protected translate: TranslateService, |
38 | @Inject('entity') protected entityValue: RuleChain, | 38 | @Inject('entity') protected entityValue: RuleChain, |
39 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<RuleChain>, | 39 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<RuleChain>, |
40 | - public fb: FormBuilder) { | ||
41 | - super(store, fb, entityValue, entitiesTableConfigValue); | 40 | + public fb: FormBuilder, |
41 | + protected cd: ChangeDetectorRef) { | ||
42 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | ||
42 | } | 43 | } |
43 | 44 | ||
44 | ngOnInit() { | 45 | ngOnInit() { |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, Inject } from '@angular/core'; | 17 | +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; |
18 | import { Store } from '@ngrx/store'; | 18 | import { Store } from '@ngrx/store'; |
19 | import { AppState } from '@core/core.state'; | 19 | import { AppState } from '@core/core.state'; |
20 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | 20 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
@@ -36,8 +36,9 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> { | @@ -36,8 +36,9 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> { | ||
36 | protected translate: TranslateService, | 36 | protected translate: TranslateService, |
37 | @Inject('entity') protected entityValue: TenantInfo, | 37 | @Inject('entity') protected entityValue: TenantInfo, |
38 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<TenantInfo>, | 38 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<TenantInfo>, |
39 | - protected fb: FormBuilder) { | ||
40 | - super(store, fb, entityValue, entitiesTableConfigValue); | 39 | + protected fb: FormBuilder, |
40 | + protected cd: ChangeDetectorRef) { | ||
41 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | ||
41 | } | 42 | } |
42 | 43 | ||
43 | hideDelete() { | 44 | hideDelete() { |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, Inject, Optional } from '@angular/core'; | 17 | +import { ChangeDetectorRef, Component, Inject, Optional } from '@angular/core'; |
18 | import { select, Store } from '@ngrx/store'; | 18 | import { select, Store } from '@ngrx/store'; |
19 | import { AppState } from '@core/core.state'; | 19 | import { AppState } from '@core/core.state'; |
20 | import { EntityComponent } from '../../components/entity/entity.component'; | 20 | import { EntityComponent } from '../../components/entity/entity.component'; |
@@ -43,8 +43,9 @@ export class UserComponent extends EntityComponent<User> { | @@ -43,8 +43,9 @@ export class UserComponent extends EntityComponent<User> { | ||
43 | constructor(protected store: Store<AppState>, | 43 | constructor(protected store: Store<AppState>, |
44 | @Optional() @Inject('entity') protected entityValue: User, | 44 | @Optional() @Inject('entity') protected entityValue: User, |
45 | @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<User>, | 45 | @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<User>, |
46 | - public fb: FormBuilder) { | ||
47 | - super(store, fb, entityValue, entitiesTableConfigValue); | 46 | + public fb: FormBuilder, |
47 | + protected cd: ChangeDetectorRef) { | ||
48 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | ||
48 | } | 49 | } |
49 | 50 | ||
50 | hideDelete() { | 51 | hideDelete() { |
@@ -14,7 +14,7 @@ | @@ -14,7 +14,7 @@ | ||
14 | /// limitations under the License. | 14 | /// limitations under the License. |
15 | /// | 15 | /// |
16 | 16 | ||
17 | -import { Component, Inject } from '@angular/core'; | 17 | +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; |
18 | import { Store } from '@ngrx/store'; | 18 | import { Store } from '@ngrx/store'; |
19 | import { AppState } from '@core/core.state'; | 19 | import { AppState } from '@core/core.state'; |
20 | import { EntityComponent } from '../../components/entity/entity.component'; | 20 | import { EntityComponent } from '../../components/entity/entity.component'; |
@@ -32,8 +32,9 @@ export class WidgetsBundleComponent extends EntityComponent<WidgetsBundle> { | @@ -32,8 +32,9 @@ export class WidgetsBundleComponent extends EntityComponent<WidgetsBundle> { | ||
32 | constructor(protected store: Store<AppState>, | 32 | constructor(protected store: Store<AppState>, |
33 | @Inject('entity') protected entityValue: WidgetsBundle, | 33 | @Inject('entity') protected entityValue: WidgetsBundle, |
34 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<WidgetsBundle>, | 34 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<WidgetsBundle>, |
35 | - public fb: FormBuilder) { | ||
36 | - super(store, fb, entityValue, entitiesTableConfigValue); | 35 | + public fb: FormBuilder, |
36 | + protected cd: ChangeDetectorRef) { | ||
37 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | ||
37 | } | 38 | } |
38 | 39 | ||
39 | hideDelete() { | 40 | hideDelete() { |
@@ -27,6 +27,7 @@ export interface AdminSettings<T> { | @@ -27,6 +27,7 @@ export interface AdminSettings<T> { | ||
27 | export declare type SmtpProtocol = 'smtp' | 'smtps'; | 27 | export declare type SmtpProtocol = 'smtp' | 'smtps'; |
28 | 28 | ||
29 | export interface MailServerSettings { | 29 | export interface MailServerSettings { |
30 | + showChangePassword: boolean; | ||
30 | mailFrom: string; | 31 | mailFrom: string; |
31 | smtpProtocol: SmtpProtocol; | 32 | smtpProtocol: SmtpProtocol; |
32 | smtpHost: string; | 33 | smtpHost: string; |
@@ -34,7 +35,8 @@ export interface MailServerSettings { | @@ -34,7 +35,8 @@ export interface MailServerSettings { | ||
34 | timeout: number; | 35 | timeout: number; |
35 | enableTls: boolean; | 36 | enableTls: boolean; |
36 | username: string; | 37 | username: string; |
37 | - password: string; | 38 | + changePassword?: boolean; |
39 | + password?: string; | ||
38 | enableProxy: boolean; | 40 | enableProxy: boolean; |
39 | proxyHost: string; | 41 | proxyHost: string; |
40 | proxyPort: number; | 42 | proxyPort: number; |
@@ -803,7 +803,7 @@ | @@ -803,7 +803,7 @@ | ||
803 | "rulechain-templates": "Regelkettenvorlagen", | 803 | "rulechain-templates": "Regelkettenvorlagen", |
804 | "rulechains": "Rand Regelketten", | 804 | "rulechains": "Rand Regelketten", |
805 | "search": "Kanten durchsuchen", | 805 | "search": "Kanten durchsuchen", |
806 | - "selected-edges": "{Anzahl, Plural, 1 {1 Kante} andere {# Kanten}} ausgewählt", | 806 | + "selected-edges": "{count, plural, 1 {1 Rand} other {# Rand} } ausgewählt", |
807 | "any-edge": "Beliebige Kante", | 807 | "any-edge": "Beliebige Kante", |
808 | "no-edge-types-matching": "Es wurden keine Kantentypen gefunden, die mit '{{entitySubtype}}' übereinstimmen.", | 808 | "no-edge-types-matching": "Es wurden keine Kantentypen gefunden, die mit '{{entitySubtype}}' übereinstimmen.", |
809 | "edge-type-list-empty": "Keine Kantentypen ausgewählt.", | 809 | "edge-type-list-empty": "Keine Kantentypen ausgewählt.", |
@@ -1452,7 +1452,7 @@ | @@ -1452,7 +1452,7 @@ | ||
1452 | "unset-auto-assign-to-edge-text": "Nach der Bestätigung wird die Kantenregelkette bei der Erstellung nicht mehr automatisch den Kanten zugewiesen.", | 1452 | "unset-auto-assign-to-edge-text": "Nach der Bestätigung wird die Kantenregelkette bei der Erstellung nicht mehr automatisch den Kanten zugewiesen.", |
1453 | "edge-template-root": "Vorlagenstamm", | 1453 | "edge-template-root": "Vorlagenstamm", |
1454 | "search": "Suchen Sie nach Regelketten", | 1454 | "search": "Suchen Sie nach Regelketten", |
1455 | - "selected-rulechains": "{count, plural, 1 {1 Regelkette} andere {# Regelketten}} ausgewählt", | 1455 | + "selected-rulechains": "{count, plural, 1 {1 Regelkette} other {# Regelketten} } ausgewählt", |
1456 | "open-rulechain": "Regelkette öffnen", | 1456 | "open-rulechain": "Regelkette öffnen", |
1457 | "assign-to-edge": "Rand zuweisen", | 1457 | "assign-to-edge": "Rand zuweisen", |
1458 | "edge-rulechain": "Kantenregelkette" | 1458 | "edge-rulechain": "Kantenregelkette" |
@@ -104,6 +104,7 @@ | @@ -104,6 +104,7 @@ | ||
104 | "proxy-port-range": "Proxy port should be in a range from 1 to 65535.", | 104 | "proxy-port-range": "Proxy port should be in a range from 1 to 65535.", |
105 | "proxy-user": "Proxy user", | 105 | "proxy-user": "Proxy user", |
106 | "proxy-password": "Proxy password", | 106 | "proxy-password": "Proxy password", |
107 | + "change-password": "Change password", | ||
107 | "send-test-mail": "Send test mail", | 108 | "send-test-mail": "Send test mail", |
108 | "sms-provider": "SMS provider", | 109 | "sms-provider": "SMS provider", |
109 | "sms-provider-settings": "SMS provider settings", | 110 | "sms-provider-settings": "SMS provider settings", |
@@ -413,8 +413,8 @@ | @@ -413,8 +413,8 @@ | ||
413 | "unassign-asset-from-edge": "Anular activo de bodre", | 413 | "unassign-asset-from-edge": "Anular activo de bodre", |
414 | "unassign-asset-from-edge-title": "¿Está seguro de que desea desasignar el activo '{{assetName}}'?", | 414 | "unassign-asset-from-edge-title": "¿Está seguro de que desea desasignar el activo '{{assetName}}'?", |
415 | "unassign-asset-from-edge-text": "Después de la confirmación, el activo no será asignado y el borde no podrá acceder a él", | 415 | "unassign-asset-from-edge-text": "Después de la confirmación, el activo no será asignado y el borde no podrá acceder a él", |
416 | - "unassign-assets-from-edge-action-title": "Anular asignación {count, plural, 1 {1 activo} other {# activos}} desde el borde", | ||
417 | - "unassign-assets-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 activo} other {# activos}}?", | 416 | + "unassign-assets-from-edge-action-title": "Anular asignación {count, plural, 1 {1 activo} other {# activos} } desde el borde", |
417 | + "unassign-assets-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 activo} other {# activos} }?", | ||
418 | "unassign-assets-from-edge-text": "Después de la confirmación, todos los activos seleccionados quedarán sin asignar y el borde no podrá acceder a ellos." | 418 | "unassign-assets-from-edge-text": "Después de la confirmación, todos los activos seleccionados quedarán sin asignar y el borde no podrá acceder a ellos." |
419 | }, | 419 | }, |
420 | "attribute": { | 420 | "attribute": { |
@@ -950,7 +950,7 @@ | @@ -950,7 +950,7 @@ | ||
950 | "assign-device-to-edge-text": "Seleccione los dispositivos para asignar al borde", | 950 | "assign-device-to-edge-text": "Seleccione los dispositivos para asignar al borde", |
951 | "unassign-device-from-edge-title": "¿Está seguro de que desea desasignar el dispositivo '{{deviceName}}'?", | 951 | "unassign-device-from-edge-title": "¿Está seguro de que desea desasignar el dispositivo '{{deviceName}}'?", |
952 | "unassign-device-from-edge-text": "Después de la confirmación, el dispositivo no será asignado y el borde no podrá acceder a él", | 952 | "unassign-device-from-edge-text": "Después de la confirmación, el dispositivo no será asignado y el borde no podrá acceder a él", |
953 | - "unassign-devices-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 dispositivo} other {# dispositivos}}?", | 953 | + "unassign-devices-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 dispositivo} other {# dispositivos} }?", |
954 | "unassign-devices-from-edge-text": "Después de la confirmación, todos los dispositivos seleccionados quedarán sin asignar y el borde no podrá acceder a ellos." | 954 | "unassign-devices-from-edge-text": "Después de la confirmación, todos los dispositivos seleccionados quedarán sin asignar y el borde no podrá acceder a ellos." |
955 | }, | 955 | }, |
956 | "device-profile": { | 956 | "device-profile": { |
@@ -1123,7 +1123,7 @@ | @@ -1123,7 +1123,7 @@ | ||
1123 | "delete": "Eliminar borde", | 1123 | "delete": "Eliminar borde", |
1124 | "delete-edge-title": "¿Está seguro de que desea eliminar el borde '{{edgeName}}'?", | 1124 | "delete-edge-title": "¿Está seguro de que desea eliminar el borde '{{edgeName}}'?", |
1125 | "delete-edge-text": "Tenga cuidado, después de la confirmación, el borde y todos los datos relacionados serán irrecuperables", | 1125 | "delete-edge-text": "Tenga cuidado, después de la confirmación, el borde y todos los datos relacionados serán irrecuperables", |
1126 | - "delete-edges-title": "¿Está seguro de que desea edge {count, plural, 1 {1 borde} other {# bordes}}?", | 1126 | + "delete-edges-title": "¿Está seguro de que desea edge {count, plural, 1 {1 borde} other {# bordes} }?", |
1127 | "delete-edges-text": "Tenga cuidado, después de la confirmación se eliminarán todos los bordes seleccionados y todos los datos relacionados se volverán irrecuperables", | 1127 | "delete-edges-text": "Tenga cuidado, después de la confirmación se eliminarán todos los bordes seleccionados y todos los datos relacionados se volverán irrecuperables", |
1128 | "name": "Nombre", | 1128 | "name": "Nombre", |
1129 | "name-starts-with": "Edge name starts with", | 1129 | "name-starts-with": "Edge name starts with", |
@@ -1156,7 +1156,7 @@ | @@ -1156,7 +1156,7 @@ | ||
1156 | "unassign-from-customer": "Anular asignación del cliente", | 1156 | "unassign-from-customer": "Anular asignación del cliente", |
1157 | "unassign-edge-title": "¿Está seguro de que desea desasignar el borde '{{edgeName}}'?", | 1157 | "unassign-edge-title": "¿Está seguro de que desea desasignar el borde '{{edgeName}}'?", |
1158 | "unassign-edge-text": "Después de la confirmación, el borde quedará sin asignar y el cliente no podrá acceder a él", | 1158 | "unassign-edge-text": "Después de la confirmación, el borde quedará sin asignar y el cliente no podrá acceder a él", |
1159 | - "unassign-edges-title": "¿Está seguro de que desea anular la asignación de {count, plural, 1 {1 borde} other {# bordes}}?", | 1159 | + "unassign-edges-title": "¿Está seguro de que desea anular la asignación de {count, plural, 1 {1 borde} other {# bordes} }?", |
1160 | "unassign-edges-text": "Después de la confirmación de todos los bordes seleccionados, se anulará la asignación y el cliente no podrá acceder a ellos.", | 1160 | "unassign-edges-text": "Después de la confirmación de todos los bordes seleccionados, se anulará la asignación y el cliente no podrá acceder a ellos.", |
1161 | "make-public": "Hacer público el borde", | 1161 | "make-public": "Hacer público el borde", |
1162 | "make-public-edge-title": "¿Estás seguro de que quieres hacer público el edge '{{edgeName}}'?", | 1162 | "make-public-edge-title": "¿Estás seguro de que quieres hacer público el edge '{{edgeName}}'?", |
@@ -1189,14 +1189,14 @@ | @@ -1189,14 +1189,14 @@ | ||
1189 | "rulechain-templates": "Plantillas, de cadena de reglas", | 1189 | "rulechain-templates": "Plantillas, de cadena de reglas", |
1190 | "rulechains": "Cadenas de regla de borde", | 1190 | "rulechains": "Cadenas de regla de borde", |
1191 | "search": "Bordes de búsqueda", | 1191 | "search": "Bordes de búsqueda", |
1192 | - "selected-edges": "{count, plural, 1 {1 borde} other {# bordes}} seleccionados", | 1192 | + "selected-edges": "{count, plural, 1 {1 borde} other {# bordes} } seleccionadas", |
1193 | "any-edge": "Cualquier bordee", | 1193 | "any-edge": "Cualquier bordee", |
1194 | "no-edge-types-matching": "No se encontraron tipos de aristas que coincidan con '{{entitySubtype}}'.", | 1194 | "no-edge-types-matching": "No se encontraron tipos de aristas que coincidan con '{{entitySubtype}}'.", |
1195 | "edge-type-list-empty": "No se seleccionó ningún tipo de borde.", | 1195 | "edge-type-list-empty": "No se seleccionó ningún tipo de borde.", |
1196 | "edge-types": "Tipos de bordes", | 1196 | "edge-types": "Tipos de bordes", |
1197 | "enter-edge-type": "Ingrese el tipo de borde", | 1197 | "enter-edge-type": "Ingrese el tipo de borde", |
1198 | "deployed": "Desplegada", | 1198 | "deployed": "Desplegada", |
1199 | - "pending": "Pending", | 1199 | + "pending": "Pendiente", |
1200 | "downlinks": "Enlaces descendentes", | 1200 | "downlinks": "Enlaces descendentes", |
1201 | "no-downlinks-prompt": "No se encontraron enlaces descendentes", | 1201 | "no-downlinks-prompt": "No se encontraron enlaces descendentes", |
1202 | "sync-process-started-successfully": "¡El proceso de sincronización se inició correctamente!", | 1202 | "sync-process-started-successfully": "¡El proceso de sincronización se inició correctamente!", |
@@ -1356,7 +1356,7 @@ | @@ -1356,7 +1356,7 @@ | ||
1356 | "type-api-usage-state": "Estado de uso de la API", | 1356 | "type-api-usage-state": "Estado de uso de la API", |
1357 | "type-edge": "Borde", | 1357 | "type-edge": "Borde", |
1358 | "type-edges": "Bordes", | 1358 | "type-edges": "Bordes", |
1359 | - "list-of-edges": "{cuenta, plural, 1 {Un borde} other {Lista de # bordes}}", | 1359 | + "list-of-edges": "{count, plural, 1 {Un borde} other {Lista de # bordes} }", |
1360 | "edge-name-starts-with": "Bordes cuyos nombres comienzan con '{{prefijo}}'" | 1360 | "edge-name-starts-with": "Bordes cuyos nombres comienzan con '{{prefijo}}'" |
1361 | }, | 1361 | }, |
1362 | "entity-field": { | 1362 | "entity-field": { |
@@ -1481,9 +1481,9 @@ | @@ -1481,9 +1481,9 @@ | ||
1481 | "assign-entity-view-to-edge-text": "Seleccione las vistas de entidad para asignar al borde", | 1481 | "assign-entity-view-to-edge-text": "Seleccione las vistas de entidad para asignar al borde", |
1482 | "unassign-entity-view-from-edge-title": "¿Está seguro de que desea anular la asignación de la vista de entidad '{{entityViewName}}'?", | 1482 | "unassign-entity-view-from-edge-title": "¿Está seguro de que desea anular la asignación de la vista de entidad '{{entityViewName}}'?", |
1483 | "unassign-entity-view-from-edge-text": "Después de la confirmación, la vista de entidad quedará sin asignar y el borde no podrá acceder a ella", | 1483 | "unassign-entity-view-from-edge-text": "Después de la confirmación, la vista de entidad quedará sin asignar y el borde no podrá acceder a ella", |
1484 | - "unassign-entity-views-from-edge-action-title": "Anular asignación {recuento, plural, 1 {1 vista de entidad} otras {# vistas de entidad}} del borde", | 1484 | + "unassign-entity-views-from-edge-action-title": "Anular asignación {count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } del borde", |
1485 | "unassign-entity-view-from-edge": "Anular asignación de vista de entidad", | 1485 | "unassign-entity-view-from-edge": "Anular asignación de vista de entidad", |
1486 | - "unassign-entity-views-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 vista de entidad} other {# vistas de entidad}}?", | 1486 | + "unassign-entity-views-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 vista de entidad} other {# vistas de entidad} }?", |
1487 | "unassign-entity-views-from-edge-text": "Después de la confirmación, todas las vistas de entidad seleccionadas no serán asignadas y el borde no podrá acceder a ellas" | 1487 | "unassign-entity-views-from-edge-text": "Después de la confirmación, todas las vistas de entidad seleccionadas no serán asignadas y el borde no podrá acceder a ellas" |
1488 | }, | 1488 | }, |
1489 | "event": { | 1489 | "event": { |
@@ -2074,9 +2074,9 @@ | @@ -2074,9 +2074,9 @@ | ||
2074 | "delete-rulechains": "Eliminar cadenas de reglas", | 2074 | "delete-rulechains": "Eliminar cadenas de reglas", |
2075 | "unassign-rulechain": "Anular asignación de cadena de reglas", | 2075 | "unassign-rulechain": "Anular asignación de cadena de reglas", |
2076 | "unassign-rulechains": "Anular asignación de cadenas de reglas", | 2076 | "unassign-rulechains": "Anular asignación de cadenas de reglas", |
2077 | - "unassign-rulechain-title": "¿Está seguro de que desea desasignar la cadena de reglas '{{ruleChainTitle}}'?", | 2077 | + "unassign-rulechain-title": "¿Está seguro de que desea desasignar la cadena de reglas '{{ruleChainName}}'?", |
2078 | "unassign-rulechain-from-edge-text": "Después de la confirmación, la cadena de reglas quedará sin asignar y el borde no podrá acceder a ella", | 2078 | "unassign-rulechain-from-edge-text": "Después de la confirmación, la cadena de reglas quedará sin asignar y el borde no podrá acceder a ella", |
2079 | - "unassign-rulechains-from-edge-action-title": "Anular asignación {count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas}} des bordes", | 2079 | + "unassign-rulechains-from-edge-action-title": "Anular asignación {count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas} } des bordes", |
2080 | "unassign-rulechains-from-edge-text": "Después de la confirmación, todas las cadenas de reglas seleccionadas quedarán sin asignar y el borde no podrá acceder a ellas", | 2080 | "unassign-rulechains-from-edge-text": "Después de la confirmación, todas las cadenas de reglas seleccionadas quedarán sin asignar y el borde no podrá acceder a ellas", |
2081 | "assign-rulechain-to-edge-title": "Asignar cadena (s) de reglas a borde", | 2081 | "assign-rulechain-to-edge-title": "Asignar cadena (s) de reglas a borde", |
2082 | "assign-rulechain-to-edge-text": "Seleccione las cadenas de reglas para asignar al borde", | 2082 | "assign-rulechain-to-edge-text": "Seleccione las cadenas de reglas para asignar al borde", |
@@ -736,7 +736,7 @@ | @@ -736,7 +736,7 @@ | ||
736 | "assign-device-to-edge-text":"Veuillez sélectionner la bordure pour attribuer le ou les dispositifs", | 736 | "assign-device-to-edge-text":"Veuillez sélectionner la bordure pour attribuer le ou les dispositifs", |
737 | "unassign-device-from-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{deviceName}} '?", | 737 | "unassign-device-from-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{deviceName}} '?", |
738 | "unassign-device-from-edge-text": "Après la confirmation, dispositif sera non attribué et ne sera pas accessible a la bordure.", | 738 | "unassign-device-from-edge-text": "Après la confirmation, dispositif sera non attribué et ne sera pas accessible a la bordure.", |
739 | - "unassign-devices-from-edge-title": "Voulez-vous vraiment annuler l'affectation de {count, plural, 1 {1 device} other {# devices}}?", | 739 | + "unassign-devices-from-edge-title": "Voulez-vous vraiment annuler l'affectation de {count, plural, 1 {1 device} other {# devices} }?", |
740 | "unassign-devices-from-edge-text": "Après la confirmation, tous les dispositifs sélectionnés ne seront pas attribues et ne seront pas accessibles par la bordure." | 740 | "unassign-devices-from-edge-text": "Après la confirmation, tous les dispositifs sélectionnés ne seront pas attribues et ne seront pas accessibles par la bordure." |
741 | }, | 741 | }, |
742 | "dialog": { | 742 | "dialog": { |
@@ -755,7 +755,7 @@ | @@ -755,7 +755,7 @@ | ||
755 | "delete": "Supprimer la bordure", | 755 | "delete": "Supprimer la bordure", |
756 | "delete-edge-title": "Êtes-vous sûr de vouloir supprimer la bordure '{{edgeName}}'?", | 756 | "delete-edge-title": "Êtes-vous sûr de vouloir supprimer la bordure '{{edgeName}}'?", |
757 | "delete-edge-text": "Faites attention, après la confirmation, la bordure et toutes les données associées deviendront irrécupérables", | 757 | "delete-edge-text": "Faites attention, après la confirmation, la bordure et toutes les données associées deviendront irrécupérables", |
758 | - "delete-edges-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 bordure} other {# bordure}}?", | 758 | + "delete-edges-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 bordure} other {# bordure} }?", |
759 | "delete-edges-text": "Faites attention, après la confirmation, tous les bordures sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", | 759 | "delete-edges-text": "Faites attention, après la confirmation, tous les bordures sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", |
760 | "name": "Nom", | 760 | "name": "Nom", |
761 | "name-starts-with": "Le nom du bord commence par", | 761 | "name-starts-with": "Le nom du bord commence par", |
@@ -788,7 +788,7 @@ | @@ -788,7 +788,7 @@ | ||
788 | "unassign-from-customer": "Retirer du client", | 788 | "unassign-from-customer": "Retirer du client", |
789 | "unassign-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{edgeName}}", | 789 | "unassign-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{edgeName}}", |
790 | "unassign-edge-text": "Après la confirmation, le dispositif ne sera pas attribué et ne sera pas accessible au client", | 790 | "unassign-edge-text": "Après la confirmation, le dispositif ne sera pas attribué et ne sera pas accessible au client", |
791 | - "unassign-edges-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 bordure} other {# bordures}}?", | 791 | + "unassign-edges-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 bordure} other {# bordures} }?", |
792 | "unassign-edges-text": "Après la confirmation, tous les bordures sélectionnés ne seront plus attribués et ne seront pas accessibles par le client.", | 792 | "unassign-edges-text": "Après la confirmation, tous les bordures sélectionnés ne seront plus attribués et ne seront pas accessibles par le client.", |
793 | "make-public": "Make edge public", | 793 | "make-public": "Make edge public", |
794 | "make-public-edge-title": "Are you sure you want to make the edge '{{edgeName}}' public?", | 794 | "make-public-edge-title": "Are you sure you want to make the edge '{{edgeName}}' public?", |
@@ -821,7 +821,7 @@ | @@ -821,7 +821,7 @@ | ||
821 | "rulechain-templates": "Modèles de chaîne de règles", | 821 | "rulechain-templates": "Modèles de chaîne de règles", |
822 | "rulechains": "Chaînes de règles de la bordure", | 822 | "rulechains": "Chaînes de règles de la bordure", |
823 | "search": "Rechercher les bords", | 823 | "search": "Rechercher les bords", |
824 | - "selected-edges": "{count, plural, 1 {1 edge} other {# bords}} sélectionné", | 824 | + "selected-edges": "{count, plural, 1 {1 bordure} other {# bords} } sélectionné", |
825 | "any-edge": "Tout bord", | 825 | "any-edge": "Tout bord", |
826 | "no-edge-types-matching": "Aucun type d'arête correspondant à \"{{entitySubtype}}\" n'a été trouvé.", | 826 | "no-edge-types-matching": "Aucun type d'arête correspondant à \"{{entitySubtype}}\" n'a été trouvé.", |
827 | "edge-type-list-empty": "Aucun type d'arête sélectionné.", | 827 | "edge-type-list-empty": "Aucun type d'arête sélectionné.", |
@@ -1479,9 +1479,9 @@ | @@ -1479,9 +1479,9 @@ | ||
1479 | "delete-rulechains": "Supprimer une chaînes de règles", | 1479 | "delete-rulechains": "Supprimer une chaînes de règles", |
1480 | "unassign-rulechain": "Retirer chaîne de règles", | 1480 | "unassign-rulechain": "Retirer chaîne de règles", |
1481 | "unassign-rulechains": "Retirer chaînes de règles", | 1481 | "unassign-rulechains": "Retirer chaînes de règles", |
1482 | - "unassign-rulechain-title": "AÊtes-vous sûr de vouloir retirer l'attribution de chaînes de règles '{{ruleChainTitle}}'?", | 1482 | + "unassign-rulechain-title": "AÊtes-vous sûr de vouloir retirer l'attribution de chaînes de règles '{{ruleChainName}}'?", |
1483 | "unassign-rulechain-from-edge-text": "Après la confirmation, l'actif sera non attribué et ne sera pas accessible a la bordure.", | 1483 | "unassign-rulechain-from-edge-text": "Après la confirmation, l'actif sera non attribué et ne sera pas accessible a la bordure.", |
1484 | - "unassign-rulechains-from-edge-action-title": "Retirer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles}} de la bordure", | 1484 | + "unassign-rulechains-from-edge-action-title": "Retirer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles} } de la bordure", |
1485 | "unassign-rulechains-from-edge-text": "Après la confirmation, tous les chaînes de règles sélectionnés ne seront pas attribués et ne seront pas accessibles a la bordure.", | 1485 | "unassign-rulechains-from-edge-text": "Après la confirmation, tous les chaînes de règles sélectionnés ne seront pas attribués et ne seront pas accessibles a la bordure.", |
1486 | "assign-rulechain-to-edge-title": "Attribuer les chaînes de règles a la bordure", | 1486 | "assign-rulechain-to-edge-title": "Attribuer les chaînes de règles a la bordure", |
1487 | "assign-rulechain-to-edge-text": "Veuillez sélectionner la bordure pour attribuer le ou les chaînes de règles", | 1487 | "assign-rulechain-to-edge-text": "Veuillez sélectionner la bordure pour attribuer le ou les chaînes de règles", |
@@ -1497,11 +1497,11 @@ | @@ -1497,11 +1497,11 @@ | ||
1497 | "unset-auto-assign-to-edge-text": "Après la confirmation, la chaîne de règles d'arêtes ne sera plus automatiquement affectée aux arêtes lors de la création.", | 1497 | "unset-auto-assign-to-edge-text": "Après la confirmation, la chaîne de règles d'arêtes ne sera plus automatiquement affectée aux arêtes lors de la création.", |
1498 | "edge-template-root": "Racine du modèle", | 1498 | "edge-template-root": "Racine du modèle", |
1499 | "search": "Rechercher des chaînes de règles", | 1499 | "search": "Rechercher des chaînes de règles", |
1500 | - "selected-rulechains": "{count, plural, 1 {1 rule chain} other {# rule chains}} sélectionné", | 1500 | + "selected-rulechains": "{count, plural, 1 {1 rule chain} other {# rule chains} } sélectionné", |
1501 | "open-rulechain": "Chaîne de règles ouverte", | 1501 | "open-rulechain": "Chaîne de règles ouverte", |
1502 | - "assign-to-edge": "Attribuer à Edge", | ||
1503 | - "edge-rulechain": "Chaîne de règles Edge", | ||
1504 | - "unassign-rulechains-from-edge-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 rulechain} other {# rulechains}}?" | 1502 | + "assign-to-edge": "Attribuer à Bordure", |
1503 | + "edge-rulechain": "Chaîne de règles Bordure", | ||
1504 | + "unassign-rulechains-from-edge-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 rulechain} other {# rulechains} }?" | ||
1505 | }, | 1505 | }, |
1506 | "rulenode": { | 1506 | "rulenode": { |
1507 | "add": "Ajouter un noeud de règle", | 1507 | "add": "Ajouter un noeud de règle", |