Commit 6823b8969f478ef6628ef7841add7b57aee147a4
1 parent
a1662d66
Refactoring of Telemetry Websockets
Showing
17 changed files
with
1209 additions
and
362 deletions
... | ... | @@ -19,7 +19,7 @@ import java.util.Map; |
19 | 19 | |
20 | 20 | import org.thingsboard.server.exception.ThingsboardErrorCode; |
21 | 21 | import org.thingsboard.server.exception.ThingsboardException; |
22 | -import org.thingsboard.server.controller.plugin.PluginWebSocketHandler; | |
22 | +import org.thingsboard.server.controller.plugin.TbWebSocketHandler; | |
23 | 23 | import org.thingsboard.server.service.security.model.SecurityUser; |
24 | 24 | import org.springframework.context.annotation.Bean; |
25 | 25 | import org.springframework.context.annotation.Configuration; |
... | ... | @@ -54,7 +54,7 @@ public class WebSocketConfiguration implements WebSocketConfigurer { |
54 | 54 | |
55 | 55 | @Override |
56 | 56 | public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { |
57 | - registry.addHandler(pluginWsHandler(), WS_PLUGIN_MAPPING).setAllowedOrigins("*") | |
57 | + registry.addHandler(wsHandler(), WS_PLUGIN_MAPPING).setAllowedOrigins("*") | |
58 | 58 | .addInterceptors(new HttpSessionHandshakeInterceptor(), new HandshakeInterceptor() { |
59 | 59 | |
60 | 60 | @Override |
... | ... | @@ -82,8 +82,8 @@ public class WebSocketConfiguration implements WebSocketConfigurer { |
82 | 82 | } |
83 | 83 | |
84 | 84 | @Bean |
85 | - public WebSocketHandler pluginWsHandler() { | |
86 | - return new PluginWebSocketHandler(); | |
85 | + public WebSocketHandler wsHandler() { | |
86 | + return new TbWebSocketHandler(); | |
87 | 87 | } |
88 | 88 | |
89 | 89 | protected SecurityUser getCurrentUser() throws ThingsboardException { | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2018 The Thingsboard Authors | |
3 | + * <p> | |
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 | + * <p> | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * <p> | |
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 | + */ | |
1 | 16 | package org.thingsboard.server.controller; |
2 | 17 | |
18 | +import com.fasterxml.jackson.databind.JsonNode; | |
3 | 19 | import com.google.common.base.Function; |
4 | 20 | import com.google.common.util.concurrent.FutureCallback; |
5 | 21 | import com.google.common.util.concurrent.Futures; |
6 | 22 | import com.google.common.util.concurrent.ListenableFuture; |
23 | +import com.google.gson.JsonElement; | |
24 | +import com.google.gson.JsonParser; | |
7 | 25 | import lombok.extern.slf4j.Slf4j; |
8 | 26 | import org.springframework.beans.factory.annotation.Autowired; |
9 | 27 | import org.springframework.http.HttpStatus; |
... | ... | @@ -11,47 +29,54 @@ import org.springframework.http.ResponseEntity; |
11 | 29 | import org.springframework.security.access.prepost.PreAuthorize; |
12 | 30 | import org.springframework.util.StringUtils; |
13 | 31 | import org.springframework.web.bind.annotation.PathVariable; |
32 | +import org.springframework.web.bind.annotation.RequestBody; | |
14 | 33 | import org.springframework.web.bind.annotation.RequestMapping; |
15 | 34 | import org.springframework.web.bind.annotation.RequestMethod; |
16 | 35 | import org.springframework.web.bind.annotation.RequestParam; |
17 | -import org.springframework.web.bind.annotation.ResponseStatus; | |
36 | +import org.springframework.web.bind.annotation.ResponseBody; | |
18 | 37 | import org.springframework.web.bind.annotation.RestController; |
19 | 38 | import org.springframework.web.context.request.async.DeferredResult; |
20 | -import org.thingsboard.server.actors.plugin.ValidationResult; | |
21 | -import org.thingsboard.server.common.data.Customer; | |
22 | 39 | import org.thingsboard.server.common.data.DataConstants; |
23 | -import org.thingsboard.server.common.data.Device; | |
24 | -import org.thingsboard.server.common.data.Tenant; | |
25 | -import org.thingsboard.server.common.data.asset.Asset; | |
40 | +import org.thingsboard.server.common.data.EntityType; | |
26 | 41 | import org.thingsboard.server.common.data.audit.ActionType; |
27 | -import org.thingsboard.server.common.data.id.AssetId; | |
28 | -import org.thingsboard.server.common.data.id.CustomerId; | |
29 | -import org.thingsboard.server.common.data.id.DeviceId; | |
30 | 42 | import org.thingsboard.server.common.data.id.EntityId; |
31 | 43 | import org.thingsboard.server.common.data.id.EntityIdFactory; |
32 | -import org.thingsboard.server.common.data.id.RuleChainId; | |
33 | -import org.thingsboard.server.common.data.id.TenantId; | |
34 | 44 | import org.thingsboard.server.common.data.id.UUIDBased; |
45 | +import org.thingsboard.server.common.data.kv.Aggregation; | |
35 | 46 | import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
47 | +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; | |
48 | +import org.thingsboard.server.common.data.kv.BaseTsKvQuery; | |
49 | +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; | |
50 | +import org.thingsboard.server.common.data.kv.BooleanDataEntry; | |
51 | +import org.thingsboard.server.common.data.kv.DoubleDataEntry; | |
36 | 52 | import org.thingsboard.server.common.data.kv.KvEntry; |
53 | +import org.thingsboard.server.common.data.kv.LongDataEntry; | |
54 | +import org.thingsboard.server.common.data.kv.StringDataEntry; | |
37 | 55 | import org.thingsboard.server.common.data.kv.TsKvEntry; |
38 | -import org.thingsboard.server.common.data.rule.RuleChain; | |
56 | +import org.thingsboard.server.common.data.kv.TsKvQuery; | |
57 | +import org.thingsboard.server.common.msg.core.TelemetryUploadRequest; | |
58 | +import org.thingsboard.server.common.transport.adaptor.JsonConverter; | |
39 | 59 | import org.thingsboard.server.dao.attributes.AttributesService; |
40 | 60 | import org.thingsboard.server.dao.timeseries.TimeseriesService; |
41 | 61 | import org.thingsboard.server.exception.ThingsboardException; |
42 | -import org.thingsboard.server.extensions.api.exception.ToErrorResponseEntity; | |
62 | +import org.thingsboard.server.extensions.api.exception.InvalidParametersException; | |
63 | +import org.thingsboard.server.extensions.api.exception.UncheckedApiException; | |
43 | 64 | import org.thingsboard.server.extensions.api.plugins.PluginConstants; |
44 | 65 | import org.thingsboard.server.extensions.core.plugin.telemetry.AttributeData; |
66 | +import org.thingsboard.server.extensions.core.plugin.telemetry.TsData; | |
67 | +import org.thingsboard.server.service.security.AccessValidator; | |
45 | 68 | import org.thingsboard.server.service.security.model.SecurityUser; |
69 | +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; | |
46 | 70 | |
47 | 71 | import javax.annotation.Nullable; |
48 | 72 | import javax.annotation.PreDestroy; |
49 | 73 | import java.util.ArrayList; |
50 | 74 | import java.util.Arrays; |
75 | +import java.util.LinkedHashMap; | |
51 | 76 | import java.util.List; |
77 | +import java.util.Map; | |
52 | 78 | import java.util.concurrent.ExecutorService; |
53 | 79 | import java.util.concurrent.Executors; |
54 | -import java.util.function.BiConsumer; | |
55 | 80 | import java.util.stream.Collectors; |
56 | 81 | |
57 | 82 | /** |
... | ... | @@ -62,9 +87,8 @@ import java.util.stream.Collectors; |
62 | 87 | @Slf4j |
63 | 88 | public class TelemetryController extends BaseController { |
64 | 89 | |
65 | - public static final String CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "Customer user is not allowed to perform this operation!"; | |
66 | - public static final String SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "System administrator is not allowed to perform this operation!"; | |
67 | - public static final String DEVICE_WITH_REQUESTED_ID_NOT_FOUND = "Device with requested id wasn't found!"; | |
90 | + @Autowired | |
91 | + private TelemetrySubscriptionService subscriptionService; | |
68 | 92 | |
69 | 93 | @Autowired |
70 | 94 | private AttributesService attributesService; |
... | ... | @@ -72,6 +96,9 @@ public class TelemetryController extends BaseController { |
72 | 96 | @Autowired |
73 | 97 | private TimeseriesService tsService; |
74 | 98 | |
99 | + @Autowired | |
100 | + private AccessValidator accessValidator; | |
101 | + | |
75 | 102 | private ExecutorService executor; |
76 | 103 | |
77 | 104 | public void initExecutor() { |
... | ... | @@ -87,117 +114,277 @@ public class TelemetryController extends BaseController { |
87 | 114 | |
88 | 115 | @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
89 | 116 | @RequestMapping(value = "/{entityType}/{entityId}/keys/ATTRIBUTES", method = RequestMethod.GET) |
90 | - @ResponseStatus(value = HttpStatus.OK) | |
117 | + @ResponseBody | |
91 | 118 | public DeferredResult<ResponseEntity> getAttributeKeys( |
92 | 119 | @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr) throws ThingsboardException { |
93 | - return validateEntityAndCallback(entityType, entityIdStr, | |
94 | - this::getAttributeKeysCallback, | |
95 | - (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR)); | |
120 | + return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr, this::getAttributeKeysCallback); | |
96 | 121 | } |
97 | 122 | |
98 | 123 | @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
99 | 124 | @RequestMapping(value = "/{entityType}/{entityId}/keys/ATTRIBUTES/{scope}", method = RequestMethod.GET) |
100 | - @ResponseStatus(value = HttpStatus.OK) | |
125 | + @ResponseBody | |
101 | 126 | public DeferredResult<ResponseEntity> getAttributeKeysByScope( |
102 | 127 | @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr |
103 | 128 | , @PathVariable("scope") String scope) throws ThingsboardException { |
104 | - return validateEntityAndCallback(entityType, entityIdStr, | |
105 | - (result, entityId) -> getAttributeKeysCallback(result, entityId, scope), | |
106 | - (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR)); | |
129 | + return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr, | |
130 | + (result, entityId) -> getAttributeKeysCallback(result, entityId, scope)); | |
107 | 131 | } |
108 | 132 | |
109 | 133 | @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
110 | 134 | @RequestMapping(value = "/{entityType}/{entityId}/values/ATTRIBUTES", method = RequestMethod.GET) |
111 | - @ResponseStatus(value = HttpStatus.OK) | |
135 | + @ResponseBody | |
112 | 136 | public DeferredResult<ResponseEntity> getAttributes( |
113 | 137 | @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, |
114 | 138 | @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { |
115 | 139 | SecurityUser user = getCurrentUser(); |
116 | - return validateEntityAndCallback(entityType, entityIdStr, | |
117 | - (result, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr), | |
118 | - (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR)); | |
140 | + return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr, | |
141 | + (result, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr)); | |
119 | 142 | } |
120 | 143 | |
121 | 144 | @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
122 | 145 | @RequestMapping(value = "/{entityType}/{entityId}/values/ATTRIBUTES/{scope}", method = RequestMethod.GET) |
123 | - @ResponseStatus(value = HttpStatus.OK) | |
146 | + @ResponseBody | |
124 | 147 | public DeferredResult<ResponseEntity> getAttributesByScope( |
125 | 148 | @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, |
126 | 149 | @PathVariable("scope") String scope, |
127 | 150 | @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { |
128 | 151 | SecurityUser user = getCurrentUser(); |
129 | - return validateEntityAndCallback(entityType, entityIdStr, | |
130 | - (result, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr), | |
131 | - (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR)); | |
152 | + return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr, | |
153 | + (result, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr)); | |
132 | 154 | } |
133 | 155 | |
134 | 156 | @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
135 | 157 | @RequestMapping(value = "/{entityType}/{entityId}/keys/TIMESERIES", method = RequestMethod.GET) |
136 | - @ResponseStatus(value = HttpStatus.OK) | |
158 | + @ResponseBody | |
137 | 159 | public DeferredResult<ResponseEntity> getTimeseriesKeys( |
138 | 160 | @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr) throws ThingsboardException { |
139 | - return validateEntityAndCallback(entityType, entityIdStr, | |
161 | + return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr, | |
140 | 162 | (result, entityId) -> { |
141 | 163 | Futures.addCallback(tsService.findAllLatest(entityId), getTsKeysToResponseCallback(result)); |
142 | - }, | |
143 | - (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR)); | |
164 | + }); | |
144 | 165 | } |
145 | 166 | |
146 | 167 | @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
147 | 168 | @RequestMapping(value = "/{entityType}/{entityId}/values/TIMESERIES", method = RequestMethod.GET) |
148 | - @ResponseStatus(value = HttpStatus.OK) | |
169 | + @ResponseBody | |
149 | 170 | public DeferredResult<ResponseEntity> getLatestTimeseries( |
150 | 171 | @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, |
151 | - @PathVariable("scope") String scope, | |
152 | 172 | @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { |
153 | 173 | SecurityUser user = getCurrentUser(); |
154 | 174 | |
155 | - return validateEntityAndCallback(entityType, entityIdStr, | |
156 | - (result, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr), | |
157 | - (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR)); | |
175 | + return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr, | |
176 | + (result, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keysStr)); | |
158 | 177 | } |
159 | 178 | |
160 | 179 | |
161 | 180 | @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") |
162 | 181 | @RequestMapping(value = "/{entityType}/{entityId}/values/TIMESERIES", method = RequestMethod.GET) |
163 | - @ResponseStatus(value = HttpStatus.OK) | |
164 | - public DeferredResult<ResponseEntity> getLatestTimeseries( | |
182 | + @ResponseBody | |
183 | + public DeferredResult<ResponseEntity> getTimeseries( | |
165 | 184 | @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, |
166 | - @PathVariable("scope") String scope, | |
167 | - @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException { | |
168 | - SecurityUser user = getCurrentUser(); | |
185 | + @RequestParam(name = "keys") String keys, | |
186 | + @RequestParam(name = "startTs") Long startTs, | |
187 | + @RequestParam(name = "endTs") Long endTs, | |
188 | + @RequestParam(name = "interval", defaultValue = "0") Long interval, | |
189 | + @RequestParam(name = "limit", defaultValue = "100") Integer limit, | |
190 | + @RequestParam(name = "agg", defaultValue = "NONE") String aggStr | |
191 | + ) throws ThingsboardException { | |
192 | + return accessValidator.validateEntityAndCallback(getCurrentUser(), entityType, entityIdStr, | |
193 | + (result, entityId) -> { | |
194 | + // If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted | |
195 | + Aggregation agg = interval == 0L ? Aggregation.valueOf(Aggregation.NONE.name()) : Aggregation.valueOf(aggStr); | |
196 | + List<TsKvQuery> queries = toKeysList(keys).stream().map(key -> new BaseTsKvQuery(key, startTs, endTs, interval, limit, agg)) | |
197 | + .collect(Collectors.toList()); | |
198 | + | |
199 | + Futures.addCallback(tsService.findAll(entityId, queries), getTsKvListCallback(result)); | |
200 | + }); | |
201 | + } | |
202 | + | |
203 | + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") | |
204 | + @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.POST) | |
205 | + @ResponseBody | |
206 | + public DeferredResult<ResponseEntity> saveDeviceAttributes(@PathVariable("deviceId") String deviceIdStr, @PathVariable("scope") String scope, | |
207 | + @RequestBody JsonNode request) throws ThingsboardException { | |
208 | + EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); | |
209 | + return saveAttributes(entityId, scope, request); | |
210 | + } | |
211 | + | |
212 | + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") | |
213 | + @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.POST) | |
214 | + @ResponseBody | |
215 | + public DeferredResult<ResponseEntity> saveEntityAttributesV1(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, | |
216 | + @PathVariable("scope") String scope, | |
217 | + @RequestBody JsonNode request) throws ThingsboardException { | |
218 | + EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); | |
219 | + return saveAttributes(entityId, scope, request); | |
220 | + } | |
169 | 221 | |
170 | - return validateEntityAndCallback(entityType, entityIdStr, | |
171 | - (result, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr), | |
172 | - (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR)); | |
222 | + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") | |
223 | + @RequestMapping(value = "/{entityType}/{entityId}/ATTRIBUTES/{scope}", method = RequestMethod.POST) | |
224 | + @ResponseBody | |
225 | + public DeferredResult<ResponseEntity> saveEntityAttributesV2(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, | |
226 | + @PathVariable("scope") String scope, | |
227 | + @RequestBody JsonNode request) throws ThingsboardException { | |
228 | + EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); | |
229 | + return saveAttributes(entityId, scope, request); | |
230 | + } | |
231 | + | |
232 | + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") | |
233 | + @RequestMapping(value = "/{entityType}/{entityId}/TIMESERIES/{scope}", method = RequestMethod.POST) | |
234 | + @ResponseBody | |
235 | + public DeferredResult<ResponseEntity> saveEntityTelemetry(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, | |
236 | + @PathVariable("scope") String scope, | |
237 | + @RequestBody String requestBody) throws ThingsboardException { | |
238 | + EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); | |
239 | + return saveTelemetry(entityId, requestBody, 0L); | |
240 | + } | |
241 | + | |
242 | + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") | |
243 | + @RequestMapping(value = "/{entityType}/{entityId}/TIMESERIES/{scope}/{ttl}", method = RequestMethod.POST) | |
244 | + @ResponseBody | |
245 | + public DeferredResult<ResponseEntity> saveEntityTelemetryWithTTL(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, | |
246 | + @PathVariable("scope") String scope, @PathVariable("ttl") Long ttl, | |
247 | + @RequestBody String requestBody) throws ThingsboardException { | |
248 | + EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); | |
249 | + return saveTelemetry(entityId, requestBody, ttl); | |
173 | 250 | } |
174 | 251 | |
175 | - private DeferredResult<ResponseEntity> validateEntityAndCallback(String entityType, String entityIdStr, | |
176 | - BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess, BiConsumer<DeferredResult<ResponseEntity>, Throwable> onFailure) throws ThingsboardException { | |
177 | - final DeferredResult<ResponseEntity> response = new DeferredResult<>(); | |
252 | + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") | |
253 | + @RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE) | |
254 | + @ResponseBody | |
255 | + public DeferredResult<ResponseEntity> deleteEntityAttributes(@PathVariable("deviceId") String deviceIdStr, | |
256 | + @PathVariable("scope") String scope, | |
257 | + @RequestParam(name = "keys") String keysStr) throws ThingsboardException { | |
258 | + EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr); | |
259 | + return deleteAttributes(entityId, scope, keysStr); | |
260 | + } | |
261 | + | |
262 | + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") | |
263 | + @RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE) | |
264 | + @ResponseBody | |
265 | + public DeferredResult<ResponseEntity> deleteEntityAttributes(@PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr, | |
266 | + @PathVariable("scope") String scope, | |
267 | + @RequestParam(name = "keys") String keysStr) throws ThingsboardException { | |
178 | 268 | EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr); |
269 | + return deleteAttributes(entityId, scope, keysStr); | |
270 | + } | |
271 | + | |
272 | + private DeferredResult<ResponseEntity> deleteAttributes(EntityId entityIdStr, String scope, String keysStr) throws ThingsboardException { | |
273 | + List<String> keys = toKeysList(keysStr); | |
274 | + if (keys.isEmpty()) { | |
275 | + return getImmediateDeferredResult("Empty keys: " + keysStr, HttpStatus.BAD_REQUEST); | |
276 | + } | |
277 | + SecurityUser user = getCurrentUser(); | |
278 | + if (DataConstants.SERVER_SCOPE.equals(scope) || | |
279 | + DataConstants.SHARED_SCOPE.equals(scope) || | |
280 | + DataConstants.CLIENT_SCOPE.equals(scope)) { | |
281 | + return accessValidator.validateEntityAndCallback(getCurrentUser(), entityIdStr, (result, entityId) -> { | |
282 | + ListenableFuture<List<Void>> future = attributesService.removeAll(entityId, scope, keys); | |
283 | + Futures.addCallback(future, new FutureCallback<List<Void>>() { | |
284 | + @Override | |
285 | + public void onSuccess(@Nullable List<Void> tmp) { | |
286 | + logAttributesDeleted(user, entityId, scope, keys, null); | |
287 | + result.setResult(new ResponseEntity<>(HttpStatus.OK)); | |
288 | + } | |
179 | 289 | |
180 | - validate(getCurrentUser(), entityId, new ValidationCallback(response, | |
181 | - new FutureCallback<DeferredResult<ResponseEntity>>() { | |
182 | 290 | @Override |
183 | - public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) { | |
184 | - onSuccess.accept(response, entityId); | |
291 | + public void onFailure(Throwable t) { | |
292 | + logAttributesDeleted(user, entityId, scope, keys, t); | |
293 | + result.setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); | |
294 | + } | |
295 | + }, executor); | |
296 | + }); | |
297 | + } else { | |
298 | + return getImmediateDeferredResult("Invalid attribute scope: " + scope, HttpStatus.BAD_REQUEST); | |
299 | + } | |
300 | + } | |
301 | + | |
302 | + private DeferredResult<ResponseEntity> saveAttributes(EntityId entityIdSrc, String scope, JsonNode json) throws ThingsboardException { | |
303 | + if (!DataConstants.SERVER_SCOPE.equals(scope) && !DataConstants.SHARED_SCOPE.equals(scope)) { | |
304 | + return getImmediateDeferredResult("Invalid scope: " + scope, HttpStatus.BAD_REQUEST); | |
305 | + } | |
306 | + if (json.isObject()) { | |
307 | + List<AttributeKvEntry> attributes = extractRequestAttributes(json); | |
308 | + if (attributes.isEmpty()) { | |
309 | + return getImmediateDeferredResult("No attributes data found in request body!", HttpStatus.BAD_REQUEST); | |
310 | + } | |
311 | + SecurityUser user = getCurrentUser(); | |
312 | + return accessValidator.validateEntityAndCallback(getCurrentUser(), entityIdSrc, (result, entityId) -> { | |
313 | + ListenableFuture<List<Void>> future = attributesService.save(entityId, scope, attributes); | |
314 | + Futures.addCallback(future, new FutureCallback<List<Void>>() { | |
315 | + @Override | |
316 | + public void onSuccess(@Nullable List<Void> tmp) { | |
317 | + logAttributesUpdated(user, entityId, scope, attributes, null); | |
318 | + result.setResult(new ResponseEntity(HttpStatus.OK)); | |
319 | + subscriptionService.onAttributesUpdateFromServer(entityId, scope, attributes); | |
185 | 320 | } |
186 | 321 | |
187 | 322 | @Override |
188 | 323 | public void onFailure(Throwable t) { |
189 | - onFailure.accept(response, t); | |
324 | + logAttributesUpdated(user, entityId, scope, attributes, t); | |
325 | + AccessValidator.handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR); | |
190 | 326 | } |
191 | - })); | |
327 | + }); | |
328 | + result.setResult(new ResponseEntity(HttpStatus.OK)); | |
329 | + }); | |
330 | + } else { | |
331 | + return getImmediateDeferredResult("Request is not a JSON object", HttpStatus.BAD_REQUEST); | |
332 | + } | |
333 | + } | |
334 | + | |
335 | + private DeferredResult<ResponseEntity> saveTelemetry(EntityId entityIdSrc, String requestBody, long ttl) throws ThingsboardException { | |
336 | + TelemetryUploadRequest telemetryRequest; | |
337 | + JsonElement telemetryJson; | |
338 | + try { | |
339 | + telemetryJson = new JsonParser().parse(requestBody); | |
340 | + } catch (Exception e) { | |
341 | + return getImmediateDeferredResult("Unable to parse timeseries payload: Invalid JSON body!", HttpStatus.BAD_REQUEST); | |
342 | + } | |
343 | + try { | |
344 | + telemetryRequest = JsonConverter.convertToTelemetry(telemetryJson); | |
345 | + } catch (Exception e) { | |
346 | + return getImmediateDeferredResult("Unable to parse timeseries payload. Invalid JSON body: " + e.getMessage(), HttpStatus.BAD_REQUEST); | |
347 | + } | |
348 | + List<TsKvEntry> entries = new ArrayList<>(); | |
349 | + for (Map.Entry<Long, List<KvEntry>> entry : telemetryRequest.getData().entrySet()) { | |
350 | + for (KvEntry kv : entry.getValue()) { | |
351 | + entries.add(new BasicTsKvEntry(entry.getKey(), kv)); | |
352 | + } | |
353 | + } | |
354 | + if (entries.isEmpty()) { | |
355 | + return getImmediateDeferredResult("No timeseries data found in request body!", HttpStatus.BAD_REQUEST); | |
356 | + } | |
357 | + SecurityUser user = getCurrentUser(); | |
358 | + return accessValidator.validateEntityAndCallback(getCurrentUser(), entityIdSrc, (result, entityId) -> { | |
359 | + ListenableFuture<List<Void>> future = tsService.save(entityId, entries, ttl); | |
360 | + Futures.addCallback(future, new FutureCallback<List<Void>>() { | |
361 | + @Override | |
362 | + public void onSuccess(@Nullable List<Void> tmp) { | |
363 | + result.setResult(new ResponseEntity(HttpStatus.OK)); | |
364 | + subscriptionService.onTimeseriesUpdateFromServer(entityId, entries); | |
365 | + } | |
192 | 366 | |
193 | - return response; | |
367 | + @Override | |
368 | + public void onFailure(Throwable t) { | |
369 | + AccessValidator.handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR); | |
370 | + } | |
371 | + }); | |
372 | + result.setResult(new ResponseEntity(HttpStatus.OK)); | |
373 | + }); | |
194 | 374 | } |
195 | 375 | |
196 | - private void getAttributeValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, String scope, String keys) { | |
197 | - List<String> keyList = null; | |
198 | - if (!StringUtils.isEmpty(keys)) { | |
199 | - keyList = Arrays.asList(keys.split(",")); | |
376 | + private void getLatestTimeseriesValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, String keys) { | |
377 | + ListenableFuture<List<TsKvEntry>> future; | |
378 | + if (StringUtils.isEmpty(keys)) { | |
379 | + future = tsService.findAllLatest(entityId); | |
380 | + } else { | |
381 | + future = tsService.findLatest(entityId, toKeysList(keys)); | |
200 | 382 | } |
383 | + Futures.addCallback(future, getTsKvListCallback(result)); | |
384 | + } | |
385 | + | |
386 | + private void getAttributeValuesCallback(@Nullable DeferredResult<ResponseEntity> result, SecurityUser user, EntityId entityId, String scope, String keys) { | |
387 | + List<String> keyList = toKeysList(keys); | |
201 | 388 | FutureCallback<List<AttributeKvEntry>> callback = getAttributeValuesToResponseCallback(result, user, scope, entityId, keyList); |
202 | 389 | if (!StringUtils.isEmpty(scope)) { |
203 | 390 | if (keyList != null && !keyList.isEmpty()) { |
... | ... | @@ -247,7 +434,7 @@ public class TelemetryController extends BaseController { |
247 | 434 | @Override |
248 | 435 | public void onFailure(Throwable e) { |
249 | 436 | log.error("Failed to fetch attributes", e); |
250 | - handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR); | |
437 | + AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR); | |
251 | 438 | } |
252 | 439 | }; |
253 | 440 | } |
... | ... | @@ -264,12 +451,13 @@ public class TelemetryController extends BaseController { |
264 | 451 | @Override |
265 | 452 | public void onFailure(Throwable e) { |
266 | 453 | log.error("Failed to fetch attributes", e); |
267 | - handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR); | |
454 | + AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR); | |
268 | 455 | } |
269 | 456 | }; |
270 | 457 | } |
271 | 458 | |
272 | - private FutureCallback<List<AttributeKvEntry>> getAttributeValuesToResponseCallback(final DeferredResult<ResponseEntity> response, final SecurityUser user, final String scope, | |
459 | + private FutureCallback<List<AttributeKvEntry>> getAttributeValuesToResponseCallback(final DeferredResult<ResponseEntity> response, | |
460 | + final SecurityUser user, final String scope, | |
273 | 461 | final EntityId entityId, final List<String> keyList) { |
274 | 462 | return new FutureCallback<List<AttributeKvEntry>>() { |
275 | 463 | @Override |
... | ... | @@ -284,12 +472,32 @@ public class TelemetryController extends BaseController { |
284 | 472 | public void onFailure(Throwable e) { |
285 | 473 | log.error("Failed to fetch attributes", e); |
286 | 474 | logAttributesRead(user, entityId, scope, keyList, e); |
287 | - handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR); | |
475 | + AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR); | |
288 | 476 | } |
289 | 477 | }; |
290 | 478 | } |
291 | 479 | |
292 | - private void logAttributesRead(SecurityUser user, EntityId entityId, String scope, List<String> keys, Throwable e) { | |
480 | + private FutureCallback<List<TsKvEntry>> getTsKvListCallback(final DeferredResult<ResponseEntity> response) { | |
481 | + return new FutureCallback<List<TsKvEntry>>() { | |
482 | + @Override | |
483 | + public void onSuccess(List<TsKvEntry> data) { | |
484 | + Map<String, List<TsData>> result = new LinkedHashMap<>(); | |
485 | + for (TsKvEntry entry : data) { | |
486 | + result.computeIfAbsent(entry.getKey(), k -> new ArrayList<>()) | |
487 | + .add(new TsData(entry.getTs(), entry.getValueAsString())); | |
488 | + } | |
489 | + response.setResult(new ResponseEntity<>(result, HttpStatus.OK)); | |
490 | + } | |
491 | + | |
492 | + @Override | |
493 | + public void onFailure(Throwable e) { | |
494 | + log.error("Failed to fetch historical data", e); | |
495 | + AccessValidator.handleError(e, response, HttpStatus.INTERNAL_SERVER_ERROR); | |
496 | + } | |
497 | + }; | |
498 | + } | |
499 | + | |
500 | + private void logAttributesDeleted(SecurityUser user, EntityId entityId, String scope, List<String> keys, Throwable e) { | |
293 | 501 | auditLogService.logEntityAction( |
294 | 502 | user.getTenantId(), |
295 | 503 | user.getCustomerId(), |
... | ... | @@ -297,163 +505,39 @@ public class TelemetryController extends BaseController { |
297 | 505 | user.getName(), |
298 | 506 | (UUIDBased & EntityId) entityId, |
299 | 507 | null, |
300 | - ActionType.ATTRIBUTES_READ, | |
508 | + ActionType.ATTRIBUTES_DELETED, | |
301 | 509 | toException(e), |
302 | 510 | scope, |
303 | 511 | keys); |
304 | 512 | } |
305 | 513 | |
306 | - private void handleError(Throwable e, final DeferredResult<ResponseEntity> response, HttpStatus defaultErrorStatus) { | |
307 | - ResponseEntity responseEntity; | |
308 | - if (e != null && e instanceof ToErrorResponseEntity) { | |
309 | - responseEntity = ((ToErrorResponseEntity) e).toErrorResponseEntity(); | |
310 | - } else if (e != null && e instanceof IllegalArgumentException) { | |
311 | - responseEntity = new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); | |
312 | - } else { | |
313 | - responseEntity = new ResponseEntity<>(defaultErrorStatus); | |
314 | - } | |
315 | - response.setResult(responseEntity); | |
316 | - } | |
317 | - | |
318 | - private void validate(SecurityUser currentUser, EntityId entityId, ValidationCallback callback) { | |
319 | - switch (entityId.getEntityType()) { | |
320 | - case DEVICE: | |
321 | - validateDevice(currentUser, entityId, callback); | |
322 | - return; | |
323 | - case ASSET: | |
324 | - validateAsset(currentUser, entityId, callback); | |
325 | - return; | |
326 | - case RULE_CHAIN: | |
327 | - validateRuleChain(currentUser, entityId, callback); | |
328 | - return; | |
329 | - case CUSTOMER: | |
330 | - validateCustomer(currentUser, entityId, callback); | |
331 | - return; | |
332 | - case TENANT: | |
333 | - validateTenant(currentUser, entityId, callback); | |
334 | - return; | |
335 | - default: | |
336 | - //TODO: add support of other entities | |
337 | - throw new IllegalStateException("Not Implemented!"); | |
338 | - } | |
339 | - } | |
340 | - | |
341 | - private void validateDevice(final SecurityUser currentUser, EntityId entityId, ValidationCallback callback) { | |
342 | - if (currentUser.isSystemAdmin()) { | |
343 | - callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); | |
344 | - } else { | |
345 | - ListenableFuture<Device> deviceFuture = deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId())); | |
346 | - Futures.addCallback(deviceFuture, getCallback(callback, device -> { | |
347 | - if (device == null) { | |
348 | - return ValidationResult.entityNotFound(DEVICE_WITH_REQUESTED_ID_NOT_FOUND); | |
349 | - } else { | |
350 | - if (!device.getTenantId().equals(currentUser.getTenantId())) { | |
351 | - return ValidationResult.accessDenied("Device doesn't belong to the current Tenant!"); | |
352 | - } else if (currentUser.isCustomerUser() && !device.getCustomerId().equals(currentUser.getCustomerId())) { | |
353 | - return ValidationResult.accessDenied("Device doesn't belong to the current Customer!"); | |
354 | - } else { | |
355 | - return ValidationResult.ok(); | |
356 | - } | |
357 | - } | |
358 | - })); | |
359 | - } | |
360 | - } | |
361 | - | |
362 | - private void validateAsset(final SecurityUser currentUser, EntityId entityId, ValidationCallback callback) { | |
363 | - if (currentUser.isSystemAdmin()) { | |
364 | - callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); | |
365 | - } else { | |
366 | - ListenableFuture<Asset> assetFuture = assetService.findAssetByIdAsync(new AssetId(entityId.getId())); | |
367 | - Futures.addCallback(assetFuture, getCallback(callback, asset -> { | |
368 | - if (asset == null) { | |
369 | - return ValidationResult.entityNotFound("Asset with requested id wasn't found!"); | |
370 | - } else { | |
371 | - if (!asset.getTenantId().equals(currentUser.getTenantId())) { | |
372 | - return ValidationResult.accessDenied("Asset doesn't belong to the current Tenant!"); | |
373 | - } else if (currentUser.isCustomerUser() && !asset.getCustomerId().equals(currentUser.getCustomerId())) { | |
374 | - return ValidationResult.accessDenied("Asset doesn't belong to the current Customer!"); | |
375 | - } else { | |
376 | - return ValidationResult.ok(); | |
377 | - } | |
378 | - } | |
379 | - })); | |
380 | - } | |
381 | - } | |
382 | - | |
383 | - | |
384 | - private void validateRuleChain(final SecurityUser currentUser, EntityId entityId, ValidationCallback callback) { | |
385 | - if (currentUser.isCustomerUser()) { | |
386 | - callback.onSuccess(ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); | |
387 | - } else { | |
388 | - ListenableFuture<RuleChain> ruleChainFuture = ruleChainService.findRuleChainByIdAsync(new RuleChainId(entityId.getId())); | |
389 | - Futures.addCallback(ruleChainFuture, getCallback(callback, ruleChain -> { | |
390 | - if (ruleChain == null) { | |
391 | - return ValidationResult.entityNotFound("Rule chain with requested id wasn't found!"); | |
392 | - } else { | |
393 | - if (currentUser.isTenantAdmin() && !ruleChain.getTenantId().equals(currentUser.getTenantId())) { | |
394 | - return ValidationResult.accessDenied("Rule chain doesn't belong to the current Tenant!"); | |
395 | - } else if (currentUser.isSystemAdmin() && !ruleChain.getTenantId().isNullUid()) { | |
396 | - return ValidationResult.accessDenied("Rule chain is not in system scope!"); | |
397 | - } else { | |
398 | - return ValidationResult.ok(); | |
399 | - } | |
400 | - } | |
401 | - })); | |
402 | - } | |
403 | - } | |
404 | - | |
405 | - private void validateCustomer(final SecurityUser currentUser, EntityId entityId, ValidationCallback callback) { | |
406 | - if (currentUser.isSystemAdmin()) { | |
407 | - callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); | |
408 | - } else { | |
409 | - ListenableFuture<Customer> customerFuture = customerService.findCustomerByIdAsync(new CustomerId(entityId.getId())); | |
410 | - Futures.addCallback(customerFuture, getCallback(callback, customer -> { | |
411 | - if (customer == null) { | |
412 | - return ValidationResult.entityNotFound("Customer with requested id wasn't found!"); | |
413 | - } else { | |
414 | - if (!customer.getTenantId().equals(currentUser.getTenantId())) { | |
415 | - return ValidationResult.accessDenied("Customer doesn't belong to the current Tenant!"); | |
416 | - } else if (currentUser.isCustomerUser() && !customer.getId().equals(currentUser.getCustomerId())) { | |
417 | - return ValidationResult.accessDenied("Customer doesn't relate to the currently authorized customer user!"); | |
418 | - } else { | |
419 | - return ValidationResult.ok(); | |
420 | - } | |
421 | - } | |
422 | - })); | |
423 | - } | |
424 | - } | |
425 | - | |
426 | - private void validateTenant(final SecurityUser currentUser, EntityId entityId, ValidationCallback callback) { | |
427 | - if (currentUser.isCustomerUser()) { | |
428 | - callback.onSuccess(ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); | |
429 | - } else if (currentUser.isSystemAdmin()) { | |
430 | - callback.onSuccess(ValidationResult.ok()); | |
431 | - } else { | |
432 | - ListenableFuture<Tenant> tenantFuture = tenantService.findTenantByIdAsync(new TenantId(entityId.getId())); | |
433 | - Futures.addCallback(tenantFuture, getCallback(callback, tenant -> { | |
434 | - if (tenant == null) { | |
435 | - return ValidationResult.entityNotFound("Tenant with requested id wasn't found!"); | |
436 | - } else if (!tenant.getId().equals(currentUser.getTenantId())) { | |
437 | - return ValidationResult.accessDenied("Tenant doesn't relate to the currently authorized user!"); | |
438 | - } else { | |
439 | - return ValidationResult.ok(); | |
440 | - } | |
441 | - })); | |
442 | - } | |
514 | + private void logAttributesUpdated(SecurityUser user, EntityId entityId, String scope, List<AttributeKvEntry> attributes, Throwable e) { | |
515 | + auditLogService.logEntityAction( | |
516 | + user.getTenantId(), | |
517 | + user.getCustomerId(), | |
518 | + user.getId(), | |
519 | + user.getName(), | |
520 | + (UUIDBased & EntityId) entityId, | |
521 | + null, | |
522 | + ActionType.ATTRIBUTES_UPDATED, | |
523 | + toException(e), | |
524 | + scope, | |
525 | + attributes); | |
443 | 526 | } |
444 | 527 | |
445 | - private <T> FutureCallback<T> getCallback(ValidationCallback callback, Function<T, ValidationResult> transformer) { | |
446 | - return new FutureCallback<T>() { | |
447 | - @Override | |
448 | - public void onSuccess(@Nullable T result) { | |
449 | - callback.onSuccess(transformer.apply(result)); | |
450 | - } | |
451 | 528 | |
452 | - @Override | |
453 | - public void onFailure(Throwable t) { | |
454 | - callback.onFailure(t); | |
455 | - } | |
456 | - }; | |
529 | + private void logAttributesRead(SecurityUser user, EntityId entityId, String scope, List<String> keys, Throwable e) { | |
530 | + auditLogService.logEntityAction( | |
531 | + user.getTenantId(), | |
532 | + user.getCustomerId(), | |
533 | + user.getId(), | |
534 | + user.getName(), | |
535 | + (UUIDBased & EntityId) entityId, | |
536 | + null, | |
537 | + ActionType.ATTRIBUTES_READ, | |
538 | + toException(e), | |
539 | + scope, | |
540 | + keys); | |
457 | 541 | } |
458 | 542 | |
459 | 543 | private ListenableFuture<List<AttributeKvEntry>> mergeAllAttributesFutures(List<ListenableFuture<List<AttributeKvEntry>>> futures) { |
... | ... | @@ -467,4 +551,40 @@ public class TelemetryController extends BaseController { |
467 | 551 | }, executor); |
468 | 552 | } |
469 | 553 | |
554 | + private List<String> toKeysList(String keys) { | |
555 | + List<String> keyList = null; | |
556 | + if (!StringUtils.isEmpty(keys)) { | |
557 | + keyList = Arrays.asList(keys.split(",")); | |
558 | + } | |
559 | + return keyList; | |
560 | + } | |
561 | + | |
562 | + private DeferredResult<ResponseEntity> getImmediateDeferredResult(String message, HttpStatus status) { | |
563 | + DeferredResult<ResponseEntity> result = new DeferredResult<>(); | |
564 | + result.setResult(new ResponseEntity<>(message, status)); | |
565 | + return result; | |
566 | + } | |
567 | + | |
568 | + private List<AttributeKvEntry> extractRequestAttributes(JsonNode jsonNode) { | |
569 | + long ts = System.currentTimeMillis(); | |
570 | + List<AttributeKvEntry> attributes = new ArrayList<>(); | |
571 | + jsonNode.fields().forEachRemaining(entry -> { | |
572 | + String key = entry.getKey(); | |
573 | + JsonNode value = entry.getValue(); | |
574 | + if (entry.getValue().isTextual()) { | |
575 | + attributes.add(new BaseAttributeKvEntry(new StringDataEntry(key, value.textValue()), ts)); | |
576 | + } else if (entry.getValue().isBoolean()) { | |
577 | + attributes.add(new BaseAttributeKvEntry(new BooleanDataEntry(key, value.booleanValue()), ts)); | |
578 | + } else if (entry.getValue().isDouble()) { | |
579 | + attributes.add(new BaseAttributeKvEntry(new DoubleDataEntry(key, value.doubleValue()), ts)); | |
580 | + } else if (entry.getValue().isNumber()) { | |
581 | + if (entry.getValue().isBigInteger()) { | |
582 | + throw new UncheckedApiException(new InvalidParametersException("Big integer values are not supported!")); | |
583 | + } else { | |
584 | + attributes.add(new BaseAttributeKvEntry(new LongDataEntry(key, value.longValue()), ts)); | |
585 | + } | |
586 | + } | |
587 | + }); | |
588 | + return attributes; | |
589 | + } | |
470 | 590 | } | ... | ... |
1 | 1 | /** |
2 | 2 | * Copyright © 2016-2018 The Thingsboard Authors |
3 | - * <p> | |
3 | + * | |
4 | 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | 5 | * you may not use this file except in compliance with the License. |
6 | 6 | * You may obtain a copy of the License at |
7 | - * <p> | |
8 | - * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | - * <p> | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | 10 | * Unless required by applicable law or agreed to in writing, software |
11 | 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
12 | 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ... | ... |
... | ... | @@ -48,59 +48,59 @@ import javax.servlet.http.HttpServletRequest; |
48 | 48 | @Slf4j |
49 | 49 | public class PluginApiController extends BaseController { |
50 | 50 | |
51 | - @SuppressWarnings("rawtypes") | |
52 | - @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") | |
53 | - @RequestMapping(value = "/{pluginToken}/**") | |
54 | - @ResponseStatus(value = HttpStatus.OK) | |
55 | - public DeferredResult<ResponseEntity> processRequest( | |
56 | - @PathVariable("pluginToken") String pluginToken, | |
57 | - RequestEntity<byte[]> requestEntity, | |
58 | - HttpServletRequest request) | |
59 | - throws ThingsboardException { | |
60 | - log.debug("[{}] Going to process requst uri: {}", pluginToken, requestEntity.getUrl()); | |
61 | - DeferredResult<ResponseEntity> result = new DeferredResult<ResponseEntity>(); | |
62 | - PluginMetaData pluginMd = pluginService.findPluginByApiToken(pluginToken); | |
63 | - if (pluginMd == null) { | |
64 | - result.setErrorResult(new PluginNotFoundException("Plugin with token: " + pluginToken + " not found!")); | |
65 | - } else { | |
66 | - TenantId tenantId = getCurrentUser().getTenantId(); | |
67 | - CustomerId customerId = getCurrentUser().getCustomerId(); | |
68 | - if (validatePluginAccess(pluginMd, tenantId, customerId)) { | |
69 | - if(tenantId != null && ModelConstants.NULL_UUID.equals(tenantId.getId())){ | |
70 | - tenantId = null; | |
71 | - } | |
72 | - UserId userId = getCurrentUser().getId(); | |
73 | - String userName = getCurrentUser().getName(); | |
74 | - PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(), | |
75 | - tenantId, customerId, userId, userName); | |
76 | - actorService.process(new BasicPluginRestMsg(securityCtx, new RestRequest(requestEntity, request), result)); | |
77 | - } else { | |
78 | - result.setResult(new ResponseEntity<>(HttpStatus.FORBIDDEN)); | |
79 | - } | |
80 | - | |
81 | - } | |
82 | - return result; | |
83 | - } | |
84 | - | |
85 | - public static boolean validatePluginAccess(PluginMetaData pluginMd, TenantId tenantId, CustomerId customerId) { | |
86 | - boolean systemAdministrator = tenantId == null || ModelConstants.NULL_UUID.equals(tenantId.getId()); | |
87 | - boolean tenantAdministrator = !systemAdministrator && (customerId == null || ModelConstants.NULL_UUID.equals(customerId.getId())); | |
88 | - boolean systemPlugin = ModelConstants.NULL_UUID.equals(pluginMd.getTenantId().getId()); | |
89 | - | |
90 | - boolean validUser = false; | |
91 | - if (systemPlugin) { | |
92 | - if (pluginMd.isPublicAccess() || systemAdministrator) { | |
93 | - // All users can access public system plugins. Only system | |
94 | - // users can access private system plugins | |
95 | - validUser = true; | |
96 | - } | |
97 | - } else { | |
98 | - if ((pluginMd.isPublicAccess() || tenantAdministrator) && tenantId != null && tenantId.equals(pluginMd.getTenantId())) { | |
99 | - // All tenant users can access public tenant plugins. Only tenant | |
100 | - // administrator can access private tenant plugins | |
101 | - validUser = true; | |
102 | - } | |
103 | - } | |
104 | - return validUser; | |
105 | - } | |
51 | +// @SuppressWarnings("rawtypes") | |
52 | +// @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") | |
53 | +// @RequestMapping(value = "/{pluginToken}/**") | |
54 | +// @ResponseStatus(value = HttpStatus.OK) | |
55 | +// public DeferredResult<ResponseEntity> processRequest( | |
56 | +// @PathVariable("pluginToken") String pluginToken, | |
57 | +// RequestEntity<byte[]> requestEntity, | |
58 | +// HttpServletRequest request) | |
59 | +// throws ThingsboardException { | |
60 | +// log.debug("[{}] Going to process requst uri: {}", pluginToken, requestEntity.getUrl()); | |
61 | +// DeferredResult<ResponseEntity> result = new DeferredResult<ResponseEntity>(); | |
62 | +// PluginMetaData pluginMd = pluginService.findPluginByApiToken(pluginToken); | |
63 | +// if (pluginMd == null) { | |
64 | +// result.setErrorResult(new PluginNotFoundException("Plugin with token: " + pluginToken + " not found!")); | |
65 | +// } else { | |
66 | +// TenantId tenantId = getCurrentUser().getTenantId(); | |
67 | +// CustomerId customerId = getCurrentUser().getCustomerId(); | |
68 | +// if (validatePluginAccess(pluginMd, tenantId, customerId)) { | |
69 | +// if(tenantId != null && ModelConstants.NULL_UUID.equals(tenantId.getId())){ | |
70 | +// tenantId = null; | |
71 | +// } | |
72 | +// UserId userId = getCurrentUser().getId(); | |
73 | +// String userName = getCurrentUser().getName(); | |
74 | +// PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(), | |
75 | +// tenantId, customerId, userId, userName); | |
76 | +// actorService.process(new BasicPluginRestMsg(securityCtx, new RestRequest(requestEntity, request), result)); | |
77 | +// } else { | |
78 | +// result.setResult(new ResponseEntity<>(HttpStatus.FORBIDDEN)); | |
79 | +// } | |
80 | +// | |
81 | +// } | |
82 | +// return result; | |
83 | +// } | |
84 | +// | |
85 | +// public static boolean validatePluginAccess(PluginMetaData pluginMd, TenantId tenantId, CustomerId customerId) { | |
86 | +// boolean systemAdministrator = tenantId == null || ModelConstants.NULL_UUID.equals(tenantId.getId()); | |
87 | +// boolean tenantAdministrator = !systemAdministrator && (customerId == null || ModelConstants.NULL_UUID.equals(customerId.getId())); | |
88 | +// boolean systemPlugin = ModelConstants.NULL_UUID.equals(pluginMd.getTenantId().getId()); | |
89 | +// | |
90 | +// boolean validUser = false; | |
91 | +// if (systemPlugin) { | |
92 | +// if (pluginMd.isPublicAccess() || systemAdministrator) { | |
93 | +// // All users can access public system plugins. Only system | |
94 | +// // users can access private system plugins | |
95 | +// validUser = true; | |
96 | +// } | |
97 | +// } else { | |
98 | +// if ((pluginMd.isPublicAccess() || tenantAdministrator) && tenantId != null && tenantId.equals(pluginMd.getTenantId())) { | |
99 | +// // All tenant users can access public tenant plugins. Only tenant | |
100 | +// // administrator can access private tenant plugins | |
101 | +// validUser = true; | |
102 | +// } | |
103 | +// } | |
104 | +// return validUser; | |
105 | +// } | |
106 | 106 | } | ... | ... |
application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java
renamed from
application/src/main/java/org/thingsboard/server/controller/plugin/PluginWebSocketHandler.java
1 | 1 | /** |
2 | 2 | * Copyright © 2016-2018 The Thingsboard Authors |
3 | - * | |
3 | + * <p> | |
4 | 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | 5 | * you may not use this file except in compliance with the License. |
6 | 6 | * You may obtain a copy of the License at |
7 | - * | |
8 | - * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | - * | |
7 | + * <p> | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * <p> | |
10 | 10 | * Unless required by applicable law or agreed to in writing, software |
11 | 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
12 | 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
... | ... | @@ -15,57 +15,42 @@ |
15 | 15 | */ |
16 | 16 | package org.thingsboard.server.controller.plugin; |
17 | 17 | |
18 | -import java.io.IOException; | |
19 | -import java.net.URI; | |
20 | -import java.security.InvalidParameterException; | |
21 | -import java.util.UUID; | |
22 | -import java.util.concurrent.ConcurrentHashMap; | |
23 | -import java.util.concurrent.ConcurrentMap; | |
24 | - | |
25 | 18 | import lombok.extern.slf4j.Slf4j; |
26 | 19 | import org.springframework.beans.factory.BeanCreationNotAllowedException; |
27 | -import org.springframework.context.annotation.Lazy; | |
28 | -import org.springframework.web.bind.annotation.RequestMapping; | |
29 | -import org.springframework.web.bind.annotation.RestController; | |
30 | -import org.thingsboard.server.actors.service.ActorService; | |
31 | -import org.thingsboard.server.common.data.id.UserId; | |
32 | -import org.thingsboard.server.config.WebSocketConfiguration; | |
33 | -import org.thingsboard.server.extensions.api.plugins.PluginConstants; | |
34 | -import org.thingsboard.server.service.security.model.SecurityUser; | |
35 | -import org.thingsboard.server.common.data.id.CustomerId; | |
36 | -import org.thingsboard.server.common.data.id.TenantId; | |
37 | -import org.thingsboard.server.common.data.plugin.PluginMetaData; | |
38 | -import org.thingsboard.server.dao.plugin.PluginService; | |
39 | -import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext; | |
40 | -import org.thingsboard.server.extensions.api.plugins.ws.BasicPluginWebsocketSessionRef; | |
41 | -import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef; | |
42 | -import org.thingsboard.server.extensions.api.plugins.ws.SessionEvent; | |
43 | -import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg; | |
44 | -import org.thingsboard.server.extensions.api.plugins.ws.msg.SessionEventPluginWebSocketMsg; | |
45 | -import org.thingsboard.server.extensions.api.plugins.ws.msg.TextPluginWebSocketMsg; | |
46 | -import org.slf4j.Logger; | |
47 | -import org.slf4j.LoggerFactory; | |
48 | 20 | import org.springframework.beans.factory.annotation.Autowired; |
21 | +import org.springframework.context.annotation.Lazy; | |
49 | 22 | import org.springframework.stereotype.Service; |
50 | 23 | import org.springframework.web.socket.CloseStatus; |
51 | 24 | import org.springframework.web.socket.TextMessage; |
52 | 25 | import org.springframework.web.socket.WebSocketSession; |
53 | 26 | import org.springframework.web.socket.handler.TextWebSocketHandler; |
27 | +import org.thingsboard.server.config.WebSocketConfiguration; | |
28 | +import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef; | |
29 | +import org.thingsboard.server.extensions.api.plugins.ws.SessionEvent; | |
30 | +import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg; | |
31 | +import org.thingsboard.server.extensions.api.plugins.ws.msg.TextPluginWebSocketMsg; | |
32 | +import org.thingsboard.server.service.security.model.SecurityUser; | |
33 | +import org.thingsboard.server.service.telemetry.TelemetryWebSocketMsgEndpoint; | |
34 | +import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; | |
35 | +import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; | |
36 | + | |
37 | +import java.io.IOException; | |
38 | +import java.net.URI; | |
39 | +import java.security.InvalidParameterException; | |
40 | +import java.util.UUID; | |
41 | +import java.util.concurrent.ConcurrentHashMap; | |
42 | +import java.util.concurrent.ConcurrentMap; | |
54 | 43 | |
55 | 44 | @Service |
56 | 45 | @Slf4j |
57 | -public class PluginWebSocketHandler extends TextWebSocketHandler implements PluginWebSocketMsgEndpoint { | |
46 | +public class TbWebSocketHandler extends TextWebSocketHandler implements PluginWebSocketMsgEndpoint, TelemetryWebSocketMsgEndpoint { | |
58 | 47 | |
59 | 48 | private static final ConcurrentMap<String, SessionMetaData> internalSessionMap = new ConcurrentHashMap<>(); |
60 | 49 | private static final ConcurrentMap<String, String> externalSessionMap = new ConcurrentHashMap<>(); |
61 | 50 | |
62 | 51 | @Autowired |
63 | 52 | @Lazy |
64 | - private ActorService actorService; | |
65 | - | |
66 | - @Autowired | |
67 | - @Lazy | |
68 | - private PluginService pluginService; | |
53 | + private TelemetryWebSocketService webSocketService; | |
69 | 54 | |
70 | 55 | @Override |
71 | 56 | public void handleTextMessage(WebSocketSession session, TextMessage message) { |
... | ... | @@ -73,7 +58,7 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug |
73 | 58 | log.info("[{}] Processing {}", session.getId(), message); |
74 | 59 | SessionMetaData sessionMd = internalSessionMap.get(session.getId()); |
75 | 60 | if (sessionMd != null) { |
76 | - actorService.process(new TextPluginWebSocketMsg(sessionMd.sessionRef, message.getPayload())); | |
61 | + webSocketService.handleWebSocketMsg(sessionMd.sessionRef, message.getPayload()); | |
77 | 62 | } else { |
78 | 63 | log.warn("[{}] Failed to find session", session.getId()); |
79 | 64 | session.close(CloseStatus.SERVER_ERROR.withReason("Session not found!")); |
... | ... | @@ -88,11 +73,11 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug |
88 | 73 | super.afterConnectionEstablished(session); |
89 | 74 | try { |
90 | 75 | String internalSessionId = session.getId(); |
91 | - PluginWebsocketSessionRef sessionRef = toRef(session); | |
76 | + TelemetryWebSocketSessionRef sessionRef = toRef(session); | |
92 | 77 | String externalSessionId = sessionRef.getSessionId(); |
93 | 78 | internalSessionMap.put(internalSessionId, new SessionMetaData(session, sessionRef)); |
94 | 79 | externalSessionMap.put(externalSessionId, internalSessionId); |
95 | - actorService.process(new SessionEventPluginWebSocketMsg(sessionRef, SessionEvent.onEstablished())); | |
80 | + processInWebSocketService(sessionRef, SessionEvent.onEstablished()); | |
96 | 81 | log.info("[{}][{}] Session is started", externalSessionId, session.getId()); |
97 | 82 | } catch (InvalidParameterException e) { |
98 | 83 | log.warn("[[{}] Failed to start session", session.getId(), e); |
... | ... | @@ -108,7 +93,7 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug |
108 | 93 | super.handleTransportError(session, tError); |
109 | 94 | SessionMetaData sessionMd = internalSessionMap.get(session.getId()); |
110 | 95 | if (sessionMd != null) { |
111 | - processInActorService(new SessionEventPluginWebSocketMsg(sessionMd.sessionRef, SessionEvent.onError(tError))); | |
96 | + processInWebSocketService(sessionMd.sessionRef, SessionEvent.onError(tError)); | |
112 | 97 | } else { |
113 | 98 | log.warn("[{}] Failed to find session", session.getId()); |
114 | 99 | } |
... | ... | @@ -121,20 +106,20 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug |
121 | 106 | SessionMetaData sessionMd = internalSessionMap.remove(session.getId()); |
122 | 107 | if (sessionMd != null) { |
123 | 108 | externalSessionMap.remove(sessionMd.sessionRef.getSessionId()); |
124 | - processInActorService(new SessionEventPluginWebSocketMsg(sessionMd.sessionRef, SessionEvent.onClosed())); | |
109 | + processInWebSocketService(sessionMd.sessionRef, SessionEvent.onClosed()); | |
125 | 110 | } |
126 | 111 | log.info("[{}] Session is closed", session.getId()); |
127 | 112 | } |
128 | 113 | |
129 | - private void processInActorService(SessionEventPluginWebSocketMsg msg) { | |
114 | + private void processInWebSocketService(TelemetryWebSocketSessionRef sessionRef, SessionEvent event) { | |
130 | 115 | try { |
131 | - actorService.process(msg); | |
116 | + webSocketService.handleWebSocketSessionEvent(sessionRef, event); | |
132 | 117 | } catch (BeanCreationNotAllowedException e) { |
133 | - log.warn("[{}] Failed to close session due to possible shutdown state", msg.getSessionRef().getSessionId()); | |
118 | + log.warn("[{}] Failed to close session due to possible shutdown state", sessionRef.getSessionId()); | |
134 | 119 | } |
135 | 120 | } |
136 | 121 | |
137 | - private PluginWebsocketSessionRef toRef(WebSocketSession session) throws IOException { | |
122 | + private TelemetryWebSocketSessionRef toRef(WebSocketSession session) throws IOException { | |
138 | 123 | URI sessionUri = session.getUri(); |
139 | 124 | String path = sessionUri.getPath(); |
140 | 125 | path = path.substring(WebSocketConfiguration.WS_PLUGIN_PREFIX.length()); |
... | ... | @@ -142,33 +127,20 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug |
142 | 127 | throw new IllegalArgumentException("URL should contain plugin token!"); |
143 | 128 | } |
144 | 129 | String[] pathElements = path.split("/"); |
145 | - String pluginToken = pathElements[0]; | |
146 | - // TODO: cache | |
147 | - PluginMetaData pluginMd = pluginService.findPluginByApiToken(pluginToken); | |
148 | - if (pluginMd == null) { | |
130 | + String serviceToken = pathElements[0]; | |
131 | + if (!"telemetry".equalsIgnoreCase(serviceToken)) { | |
149 | 132 | throw new InvalidParameterException("Can't find plugin with specified token!"); |
150 | 133 | } else { |
151 | 134 | SecurityUser currentUser = (SecurityUser) session.getAttributes().get(WebSocketConfiguration.WS_SECURITY_USER_ATTRIBUTE); |
152 | - TenantId tenantId = currentUser.getTenantId(); | |
153 | - CustomerId customerId = currentUser.getCustomerId(); | |
154 | - if (PluginApiController.validatePluginAccess(pluginMd, tenantId, customerId)) { | |
155 | - UserId userId = currentUser.getId(); | |
156 | - String userName = currentUser.getName(); | |
157 | - PluginApiCallSecurityContext securityCtx = new PluginApiCallSecurityContext(pluginMd.getTenantId(), pluginMd.getId(), tenantId, | |
158 | - currentUser.getCustomerId(), userId, userName); | |
159 | - return new BasicPluginWebsocketSessionRef(UUID.randomUUID().toString(), securityCtx, session.getUri(), session.getAttributes(), | |
160 | - session.getLocalAddress(), session.getRemoteAddress()); | |
161 | - } else { | |
162 | - throw new SecurityException("Current user is not allowed to use this plugin!"); | |
163 | - } | |
135 | + return new TelemetryWebSocketSessionRef(UUID.randomUUID().toString(), currentUser, session.getLocalAddress(), session.getRemoteAddress()); | |
164 | 136 | } |
165 | 137 | } |
166 | 138 | |
167 | 139 | private static class SessionMetaData { |
168 | 140 | private final WebSocketSession session; |
169 | - private final PluginWebsocketSessionRef sessionRef; | |
141 | + private final TelemetryWebSocketSessionRef sessionRef; | |
170 | 142 | |
171 | - public SessionMetaData(WebSocketSession session, PluginWebsocketSessionRef sessionRef) { | |
143 | + public SessionMetaData(WebSocketSession session, TelemetryWebSocketSessionRef sessionRef) { | |
172 | 144 | super(); |
173 | 145 | this.session = session; |
174 | 146 | this.sessionRef = sessionRef; |
... | ... | @@ -176,6 +148,41 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug |
176 | 148 | } |
177 | 149 | |
178 | 150 | @Override |
151 | + public void send(TelemetryWebSocketSessionRef sessionRef, String msg) throws IOException { | |
152 | + String externalId = sessionRef.getSessionId(); | |
153 | + log.debug("[{}] Processing {}", externalId, msg); | |
154 | + String internalId = externalSessionMap.get(externalId); | |
155 | + if (internalId != null) { | |
156 | + SessionMetaData sessionMd = internalSessionMap.get(internalId); | |
157 | + if (sessionMd != null) { | |
158 | + sessionMd.session.sendMessage(new TextMessage(msg)); | |
159 | + } else { | |
160 | + log.warn("[{}][{}] Failed to find session by internal id", externalId, internalId); | |
161 | + } | |
162 | + } else { | |
163 | + log.warn("[{}] Failed to find session by external id", externalId); | |
164 | + } | |
165 | + } | |
166 | + | |
167 | + @Override | |
168 | + public void close(TelemetryWebSocketSessionRef sessionRef) throws IOException { | |
169 | + String externalId = sessionRef.getSessionId(); | |
170 | + log.debug("[{}] Processing close request", externalId); | |
171 | + String internalId = externalSessionMap.get(externalId); | |
172 | + if (internalId != null) { | |
173 | + SessionMetaData sessionMd = internalSessionMap.get(internalId); | |
174 | + if (sessionMd != null) { | |
175 | + sessionMd.session.close(CloseStatus.NORMAL); | |
176 | + } else { | |
177 | + log.warn("[{}][{}] Failed to find session by internal id", externalId, internalId); | |
178 | + } | |
179 | + } else { | |
180 | + log.warn("[{}] Failed to find session by external id", externalId); | |
181 | + } | |
182 | + } | |
183 | + | |
184 | + //TODO: remove | |
185 | + @Override | |
179 | 186 | public void send(PluginWebsocketMsg<?> wsMsg) throws IOException { |
180 | 187 | PluginWebsocketSessionRef sessionRef = wsMsg.getSessionRef(); |
181 | 188 | String externalId = sessionRef.getSessionId(); |
... | ... | @@ -196,6 +203,7 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug |
196 | 203 | } |
197 | 204 | } |
198 | 205 | |
206 | + //TODO: remove | |
199 | 207 | @Override |
200 | 208 | public void close(PluginWebsocketSessionRef sessionRef) throws IOException { |
201 | 209 | String externalId = sessionRef.getSessionId(); |
... | ... | @@ -212,5 +220,4 @@ public class PluginWebSocketHandler extends TextWebSocketHandler implements Plug |
212 | 220 | log.warn("[{}] Failed to find session by external id", externalId); |
213 | 221 | } |
214 | 222 | } |
215 | - | |
216 | 223 | } | ... | ... |
1 | +package org.thingsboard.server.service.security; | |
2 | + | |
3 | +import com.google.common.base.Function; | |
4 | +import com.google.common.util.concurrent.FutureCallback; | |
5 | +import com.google.common.util.concurrent.Futures; | |
6 | +import com.google.common.util.concurrent.ListenableFuture; | |
7 | +import org.springframework.beans.factory.annotation.Autowired; | |
8 | +import org.springframework.http.HttpStatus; | |
9 | +import org.springframework.http.ResponseEntity; | |
10 | +import org.springframework.stereotype.Component; | |
11 | +import org.springframework.web.context.request.async.DeferredResult; | |
12 | +import org.thingsboard.server.actors.plugin.ValidationResult; | |
13 | +import org.thingsboard.server.common.data.Customer; | |
14 | +import org.thingsboard.server.common.data.Device; | |
15 | +import org.thingsboard.server.common.data.Tenant; | |
16 | +import org.thingsboard.server.common.data.asset.Asset; | |
17 | +import org.thingsboard.server.common.data.id.AssetId; | |
18 | +import org.thingsboard.server.common.data.id.CustomerId; | |
19 | +import org.thingsboard.server.common.data.id.DeviceId; | |
20 | +import org.thingsboard.server.common.data.id.EntityId; | |
21 | +import org.thingsboard.server.common.data.id.EntityIdFactory; | |
22 | +import org.thingsboard.server.common.data.id.RuleChainId; | |
23 | +import org.thingsboard.server.common.data.id.TenantId; | |
24 | +import org.thingsboard.server.common.data.rule.RuleChain; | |
25 | +import org.thingsboard.server.controller.ValidationCallback; | |
26 | +import org.thingsboard.server.dao.alarm.AlarmService; | |
27 | +import org.thingsboard.server.dao.asset.AssetService; | |
28 | +import org.thingsboard.server.dao.customer.CustomerService; | |
29 | +import org.thingsboard.server.dao.device.DeviceService; | |
30 | +import org.thingsboard.server.dao.rule.RuleChainService; | |
31 | +import org.thingsboard.server.dao.tenant.TenantService; | |
32 | +import org.thingsboard.server.dao.user.UserService; | |
33 | +import org.thingsboard.server.exception.ThingsboardException; | |
34 | +import org.thingsboard.server.extensions.api.exception.ToErrorResponseEntity; | |
35 | +import org.thingsboard.server.service.security.model.SecurityUser; | |
36 | + | |
37 | +import javax.annotation.Nullable; | |
38 | +import java.util.function.BiConsumer; | |
39 | + | |
40 | +/** | |
41 | + * Created by ashvayka on 27.03.18. | |
42 | + */ | |
43 | +@Component | |
44 | +public class AccessValidator { | |
45 | + | |
46 | + public static final String CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "Customer user is not allowed to perform this operation!"; | |
47 | + public static final String SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "System administrator is not allowed to perform this operation!"; | |
48 | + public static final String DEVICE_WITH_REQUESTED_ID_NOT_FOUND = "Device with requested id wasn't found!"; | |
49 | + | |
50 | + @Autowired | |
51 | + protected TenantService tenantService; | |
52 | + | |
53 | + @Autowired | |
54 | + protected CustomerService customerService; | |
55 | + | |
56 | + @Autowired | |
57 | + protected UserService userService; | |
58 | + | |
59 | + @Autowired | |
60 | + protected DeviceService deviceService; | |
61 | + | |
62 | + @Autowired | |
63 | + protected AssetService assetService; | |
64 | + | |
65 | + @Autowired | |
66 | + protected AlarmService alarmService; | |
67 | + | |
68 | + @Autowired | |
69 | + protected RuleChainService ruleChainService; | |
70 | + | |
71 | + public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, String entityType, String entityIdStr, | |
72 | + BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess) throws ThingsboardException { | |
73 | + return validateEntityAndCallback(currentUser, entityType, entityIdStr, onSuccess, (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR)); | |
74 | + } | |
75 | + | |
76 | + public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, String entityType, String entityIdStr, | |
77 | + BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess, | |
78 | + BiConsumer<DeferredResult<ResponseEntity>, Throwable> onFailure) throws ThingsboardException { | |
79 | + return validateEntityAndCallback(currentUser, EntityIdFactory.getByTypeAndId(entityType, entityIdStr), | |
80 | + onSuccess, onFailure); | |
81 | + } | |
82 | + | |
83 | + public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, EntityId entityId, | |
84 | + BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess) throws ThingsboardException { | |
85 | + return validateEntityAndCallback(currentUser, entityId, onSuccess, (result, t) -> handleError(t, result, HttpStatus.INTERNAL_SERVER_ERROR)); | |
86 | + } | |
87 | + | |
88 | + public DeferredResult<ResponseEntity> validateEntityAndCallback(SecurityUser currentUser, EntityId entityId, | |
89 | + BiConsumer<DeferredResult<ResponseEntity>, EntityId> onSuccess, | |
90 | + BiConsumer<DeferredResult<ResponseEntity>, Throwable> onFailure) throws ThingsboardException { | |
91 | + | |
92 | + final DeferredResult<ResponseEntity> response = new DeferredResult<>(); | |
93 | + | |
94 | + validate(currentUser, entityId, new ValidationCallback(response, | |
95 | + new FutureCallback<DeferredResult<ResponseEntity>>() { | |
96 | + @Override | |
97 | + public void onSuccess(@Nullable DeferredResult<ResponseEntity> result) { | |
98 | + onSuccess.accept(response, entityId); | |
99 | + } | |
100 | + | |
101 | + @Override | |
102 | + public void onFailure(Throwable t) { | |
103 | + onFailure.accept(response, t); | |
104 | + } | |
105 | + })); | |
106 | + | |
107 | + return response; | |
108 | + } | |
109 | + | |
110 | + public void validate(SecurityUser currentUser, EntityId entityId, ValidationCallback callback) { | |
111 | + switch (entityId.getEntityType()) { | |
112 | + case DEVICE: | |
113 | + validateDevice(currentUser, entityId, callback); | |
114 | + return; | |
115 | + case ASSET: | |
116 | + validateAsset(currentUser, entityId, callback); | |
117 | + return; | |
118 | + case RULE_CHAIN: | |
119 | + validateRuleChain(currentUser, entityId, callback); | |
120 | + return; | |
121 | + case CUSTOMER: | |
122 | + validateCustomer(currentUser, entityId, callback); | |
123 | + return; | |
124 | + case TENANT: | |
125 | + validateTenant(currentUser, entityId, callback); | |
126 | + return; | |
127 | + default: | |
128 | + //TODO: add support of other entities | |
129 | + throw new IllegalStateException("Not Implemented!"); | |
130 | + } | |
131 | + } | |
132 | + | |
133 | + private void validateDevice(final SecurityUser currentUser, EntityId entityId, ValidationCallback callback) { | |
134 | + if (currentUser.isSystemAdmin()) { | |
135 | + callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); | |
136 | + } else { | |
137 | + ListenableFuture<Device> deviceFuture = deviceService.findDeviceByIdAsync(new DeviceId(entityId.getId())); | |
138 | + Futures.addCallback(deviceFuture, getCallback(callback, device -> { | |
139 | + if (device == null) { | |
140 | + return ValidationResult.entityNotFound(DEVICE_WITH_REQUESTED_ID_NOT_FOUND); | |
141 | + } else { | |
142 | + if (!device.getTenantId().equals(currentUser.getTenantId())) { | |
143 | + return ValidationResult.accessDenied("Device doesn't belong to the current Tenant!"); | |
144 | + } else if (currentUser.isCustomerUser() && !device.getCustomerId().equals(currentUser.getCustomerId())) { | |
145 | + return ValidationResult.accessDenied("Device doesn't belong to the current Customer!"); | |
146 | + } else { | |
147 | + return ValidationResult.ok(); | |
148 | + } | |
149 | + } | |
150 | + })); | |
151 | + } | |
152 | + } | |
153 | + | |
154 | + private void validateAsset(final SecurityUser currentUser, EntityId entityId, ValidationCallback callback) { | |
155 | + if (currentUser.isSystemAdmin()) { | |
156 | + callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); | |
157 | + } else { | |
158 | + ListenableFuture<Asset> assetFuture = assetService.findAssetByIdAsync(new AssetId(entityId.getId())); | |
159 | + Futures.addCallback(assetFuture, getCallback(callback, asset -> { | |
160 | + if (asset == null) { | |
161 | + return ValidationResult.entityNotFound("Asset with requested id wasn't found!"); | |
162 | + } else { | |
163 | + if (!asset.getTenantId().equals(currentUser.getTenantId())) { | |
164 | + return ValidationResult.accessDenied("Asset doesn't belong to the current Tenant!"); | |
165 | + } else if (currentUser.isCustomerUser() && !asset.getCustomerId().equals(currentUser.getCustomerId())) { | |
166 | + return ValidationResult.accessDenied("Asset doesn't belong to the current Customer!"); | |
167 | + } else { | |
168 | + return ValidationResult.ok(); | |
169 | + } | |
170 | + } | |
171 | + })); | |
172 | + } | |
173 | + } | |
174 | + | |
175 | + | |
176 | + private void validateRuleChain(final SecurityUser currentUser, EntityId entityId, ValidationCallback callback) { | |
177 | + if (currentUser.isCustomerUser()) { | |
178 | + callback.onSuccess(ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); | |
179 | + } else { | |
180 | + ListenableFuture<RuleChain> ruleChainFuture = ruleChainService.findRuleChainByIdAsync(new RuleChainId(entityId.getId())); | |
181 | + Futures.addCallback(ruleChainFuture, getCallback(callback, ruleChain -> { | |
182 | + if (ruleChain == null) { | |
183 | + return ValidationResult.entityNotFound("Rule chain with requested id wasn't found!"); | |
184 | + } else { | |
185 | + if (currentUser.isTenantAdmin() && !ruleChain.getTenantId().equals(currentUser.getTenantId())) { | |
186 | + return ValidationResult.accessDenied("Rule chain doesn't belong to the current Tenant!"); | |
187 | + } else if (currentUser.isSystemAdmin() && !ruleChain.getTenantId().isNullUid()) { | |
188 | + return ValidationResult.accessDenied("Rule chain is not in system scope!"); | |
189 | + } else { | |
190 | + return ValidationResult.ok(); | |
191 | + } | |
192 | + } | |
193 | + })); | |
194 | + } | |
195 | + } | |
196 | + | |
197 | + private void validateCustomer(final SecurityUser currentUser, EntityId entityId, ValidationCallback callback) { | |
198 | + if (currentUser.isSystemAdmin()) { | |
199 | + callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); | |
200 | + } else { | |
201 | + ListenableFuture<Customer> customerFuture = customerService.findCustomerByIdAsync(new CustomerId(entityId.getId())); | |
202 | + Futures.addCallback(customerFuture, getCallback(callback, customer -> { | |
203 | + if (customer == null) { | |
204 | + return ValidationResult.entityNotFound("Customer with requested id wasn't found!"); | |
205 | + } else { | |
206 | + if (!customer.getTenantId().equals(currentUser.getTenantId())) { | |
207 | + return ValidationResult.accessDenied("Customer doesn't belong to the current Tenant!"); | |
208 | + } else if (currentUser.isCustomerUser() && !customer.getId().equals(currentUser.getCustomerId())) { | |
209 | + return ValidationResult.accessDenied("Customer doesn't relate to the currently authorized customer user!"); | |
210 | + } else { | |
211 | + return ValidationResult.ok(); | |
212 | + } | |
213 | + } | |
214 | + })); | |
215 | + } | |
216 | + } | |
217 | + | |
218 | + private void validateTenant(final SecurityUser currentUser, EntityId entityId, ValidationCallback callback) { | |
219 | + if (currentUser.isCustomerUser()) { | |
220 | + callback.onSuccess(ValidationResult.accessDenied(CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); | |
221 | + } else if (currentUser.isSystemAdmin()) { | |
222 | + callback.onSuccess(ValidationResult.ok()); | |
223 | + } else { | |
224 | + ListenableFuture<Tenant> tenantFuture = tenantService.findTenantByIdAsync(new TenantId(entityId.getId())); | |
225 | + Futures.addCallback(tenantFuture, getCallback(callback, tenant -> { | |
226 | + if (tenant == null) { | |
227 | + return ValidationResult.entityNotFound("Tenant with requested id wasn't found!"); | |
228 | + } else if (!tenant.getId().equals(currentUser.getTenantId())) { | |
229 | + return ValidationResult.accessDenied("Tenant doesn't relate to the currently authorized user!"); | |
230 | + } else { | |
231 | + return ValidationResult.ok(); | |
232 | + } | |
233 | + })); | |
234 | + } | |
235 | + } | |
236 | + | |
237 | + private <T> FutureCallback<T> getCallback(ValidationCallback callback, Function<T, ValidationResult> transformer) { | |
238 | + return new FutureCallback<T>() { | |
239 | + @Override | |
240 | + public void onSuccess(@Nullable T result) { | |
241 | + callback.onSuccess(transformer.apply(result)); | |
242 | + } | |
243 | + | |
244 | + @Override | |
245 | + public void onFailure(Throwable t) { | |
246 | + callback.onFailure(t); | |
247 | + } | |
248 | + }; | |
249 | + } | |
250 | + | |
251 | + public static void handleError(Throwable e, final DeferredResult<ResponseEntity> response, HttpStatus defaultErrorStatus) { | |
252 | + ResponseEntity responseEntity; | |
253 | + if (e != null && e instanceof ToErrorResponseEntity) { | |
254 | + responseEntity = ((ToErrorResponseEntity) e).toErrorResponseEntity(); | |
255 | + } else if (e != null && e instanceof IllegalArgumentException) { | |
256 | + responseEntity = new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); | |
257 | + } else { | |
258 | + responseEntity = new ResponseEntity<>(defaultErrorStatus); | |
259 | + } | |
260 | + response.setResult(responseEntity); | |
261 | + } | |
262 | +} | ... | ... |
1 | +package org.thingsboard.server.service.telemetry; | |
2 | + | |
3 | +import lombok.extern.slf4j.Slf4j; | |
4 | +import org.springframework.beans.factory.annotation.Autowired; | |
5 | +import org.springframework.stereotype.Service; | |
6 | +import org.thingsboard.server.common.data.id.EntityId; | |
7 | +import org.thingsboard.server.common.data.kv.AttributeKvEntry; | |
8 | +import org.thingsboard.server.common.data.kv.TsKvEntry; | |
9 | +import org.thingsboard.server.extensions.core.plugin.telemetry.sub.Subscription; | |
10 | + | |
11 | +import java.util.HashMap; | |
12 | +import java.util.List; | |
13 | +import java.util.Map; | |
14 | +import java.util.Set; | |
15 | + | |
16 | +/** | |
17 | + * Created by ashvayka on 27.03.18. | |
18 | + */ | |
19 | +@Service | |
20 | +@Slf4j | |
21 | +public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptionService { | |
22 | + | |
23 | + @Autowired | |
24 | + private TelemetryWebSocketService wsService; | |
25 | + | |
26 | + | |
27 | + private final Map<EntityId, Set<Subscription>> subscriptionsByEntityId = new HashMap<>(); | |
28 | + | |
29 | + private final Map<String, Map<Integer, Subscription>> subscriptionsByWsSessionId = new HashMap<>(); | |
30 | + | |
31 | + | |
32 | + | |
33 | + @Override | |
34 | + public void onAttributesUpdateFromServer(EntityId entityId, String scope, List<AttributeKvEntry> attributes) { | |
35 | + | |
36 | + } | |
37 | + | |
38 | + @Override | |
39 | + public void onTimeseriesUpdateFromServer(EntityId entityId, List<TsKvEntry> entries) { | |
40 | + | |
41 | + } | |
42 | +} | ... | ... |
1 | +package org.thingsboard.server.service.telemetry; | |
2 | + | |
3 | +import com.fasterxml.jackson.core.JsonProcessingException; | |
4 | +import com.fasterxml.jackson.databind.ObjectMapper; | |
5 | +import com.google.common.util.concurrent.FutureCallback; | |
6 | +import lombok.extern.slf4j.Slf4j; | |
7 | +import org.springframework.beans.factory.annotation.Autowired; | |
8 | +import org.springframework.stereotype.Service; | |
9 | +import org.springframework.util.StringUtils; | |
10 | +import org.thingsboard.server.common.data.DataConstants; | |
11 | +import org.thingsboard.server.common.data.id.EntityId; | |
12 | +import org.thingsboard.server.common.data.id.EntityIdFactory; | |
13 | +import org.thingsboard.server.common.data.kv.AttributeKvEntry; | |
14 | +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; | |
15 | +import org.thingsboard.server.common.data.kv.TsKvEntry; | |
16 | +import org.thingsboard.server.dao.attributes.AttributesService; | |
17 | +import org.thingsboard.server.dao.timeseries.TimeseriesService; | |
18 | +import org.thingsboard.server.extensions.api.exception.UnauthorizedException; | |
19 | +import org.thingsboard.server.extensions.api.plugins.PluginCallback; | |
20 | +import org.thingsboard.server.extensions.api.plugins.PluginContext; | |
21 | +import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef; | |
22 | +import org.thingsboard.server.extensions.api.plugins.ws.SessionEvent; | |
23 | +import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.AttributesSubscriptionCmd; | |
24 | +import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.SubscriptionCmd; | |
25 | +import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.TelemetryPluginCmd; | |
26 | +import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.TelemetryPluginCmdsWrapper; | |
27 | +import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionErrorCode; | |
28 | +import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState; | |
29 | +import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType; | |
30 | +import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionUpdate; | |
31 | + | |
32 | +import java.io.IOException; | |
33 | +import java.util.ArrayList; | |
34 | +import java.util.Arrays; | |
35 | +import java.util.Collections; | |
36 | +import java.util.HashMap; | |
37 | +import java.util.HashSet; | |
38 | +import java.util.List; | |
39 | +import java.util.Map; | |
40 | +import java.util.Optional; | |
41 | +import java.util.Set; | |
42 | +import java.util.concurrent.ConcurrentHashMap; | |
43 | +import java.util.concurrent.ConcurrentMap; | |
44 | +import java.util.stream.Collectors; | |
45 | + | |
46 | +/** | |
47 | + * Created by ashvayka on 27.03.18. | |
48 | + */ | |
49 | +@Service | |
50 | +@Slf4j | |
51 | +public class DefaultTelemetryWebSocketService implements TelemetryWebSocketService { | |
52 | + | |
53 | + private static final int UNKNOWN_SUBSCRIPTION_ID = 0; | |
54 | + private static final String PROCESSING_MSG = "[{}] Processing: {}"; | |
55 | + private static final ObjectMapper jsonMapper = new ObjectMapper(); | |
56 | + private static final String FAILED_TO_FETCH_DATA = "Failed to fetch data!"; | |
57 | + private static final String FAILED_TO_FETCH_ATTRIBUTES = "Failed to fetch attributes!"; | |
58 | + private static final String SESSION_META_DATA_NOT_FOUND = "Session meta-data not found!"; | |
59 | + | |
60 | + private final ConcurrentMap<String, WsSessionMetaData> wsSessionsMap = new ConcurrentHashMap<>(); | |
61 | + | |
62 | + @Autowired | |
63 | + private TelemetrySubscriptionService subscriptionManager; | |
64 | + | |
65 | + @Autowired | |
66 | + private TelemetryWebSocketMsgEndpoint msgEndpoint; | |
67 | + | |
68 | + @Autowired | |
69 | + private AttributesService attributesService; | |
70 | + | |
71 | + @Autowired | |
72 | + private TimeseriesService tsService; | |
73 | + | |
74 | + @Override | |
75 | + public void handleWebSocketSessionEvent(TelemetryWebSocketSessionRef sessionRef, SessionEvent event) { | |
76 | + String sessionId = sessionRef.getSessionId(); | |
77 | + log.debug(PROCESSING_MSG, sessionId, event); | |
78 | + switch (event.getEventType()) { | |
79 | + case ESTABLISHED: | |
80 | + wsSessionsMap.put(sessionId, new WsSessionMetaData(sessionRef)); | |
81 | + break; | |
82 | + case ERROR: | |
83 | + log.debug("[{}] Unknown websocket session error: {}. ", sessionId, event.getError().orElse(null)); | |
84 | + break; | |
85 | + case CLOSED: | |
86 | + wsSessionsMap.remove(sessionId); | |
87 | + subscriptionManager.cleanupLocalWsSessionSubscriptions(sessionRef, sessionId); | |
88 | + break; | |
89 | + } | |
90 | + } | |
91 | + | |
92 | + @Override | |
93 | + public void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg) { | |
94 | + if (log.isTraceEnabled()) { | |
95 | + log.trace("[{}] Processing: {}", sessionRef.getSessionId(), msg); | |
96 | + } | |
97 | + | |
98 | + try { | |
99 | + TelemetryPluginCmdsWrapper cmdsWrapper = jsonMapper.readValue(msg, TelemetryPluginCmdsWrapper.class); | |
100 | + if (cmdsWrapper != null) { | |
101 | + if (cmdsWrapper.getAttrSubCmds() != null) { | |
102 | + cmdsWrapper.getAttrSubCmds().forEach(cmd -> handleWsAttributesSubscriptionCmd(sessionRef, cmd)); | |
103 | + } | |
104 | + if (cmdsWrapper.getTsSubCmds() != null) { | |
105 | + cmdsWrapper.getTsSubCmds().forEach(cmd -> handleWsTimeseriesSubscriptionCmd(sessionRef, cmd)); | |
106 | + } | |
107 | + if (cmdsWrapper.getHistoryCmds() != null) { | |
108 | + cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(sessionRef, cmd)); | |
109 | + } | |
110 | + } | |
111 | + } catch (IOException e) { | |
112 | + log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e); | |
113 | + SubscriptionUpdate update = new SubscriptionUpdate(UNKNOWN_SUBSCRIPTION_ID, SubscriptionErrorCode.INTERNAL_ERROR, SESSION_META_DATA_NOT_FOUND); | |
114 | + sendWsMsg(sessionRef, update); | |
115 | + } | |
116 | + } | |
117 | + | |
118 | + private void handleWsAttributesSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, AttributesSubscriptionCmd cmd) { | |
119 | + String sessionId = sessionRef.getSessionId(); | |
120 | + log.debug("[{}] Processing: {}", sessionId, cmd); | |
121 | + | |
122 | + if (validateSessionMetadata(sessionRef, cmd, sessionId)) { | |
123 | + if (cmd.isUnsubscribe()) { | |
124 | + unsubscribe(sessionRef, cmd, sessionId); | |
125 | + } else if (validateSubscriptionCmd(sessionRef, cmd)) { | |
126 | + EntityId entityId = EntityIdFactory.getByTypeAndId(cmd.getEntityType(), cmd.getEntityId()); | |
127 | + log.debug("[{}] fetching latest attributes ({}) values for device: {}", sessionId, cmd.getKeys(), entityId); | |
128 | + Optional<Set<String>> keysOptional = getKeys(cmd); | |
129 | + if (keysOptional.isPresent()) { | |
130 | + List<String> keys = new ArrayList<>(keysOptional.get()); | |
131 | + handleWsAttributesSubscriptionByKeys(sessionRef, cmd, sessionId, entityId, keys); | |
132 | + } else { | |
133 | + handleWsAttributesSubscription(sessionRef, cmd, sessionId, entityId); | |
134 | + } | |
135 | + } | |
136 | + } | |
137 | + } | |
138 | + | |
139 | + private void handleWsAttributesSubscriptionByKeys(TelemetryWebSocketSessionRef sessionRef, | |
140 | + AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId, | |
141 | + List<String> keys) { | |
142 | + FutureCallback<List<AttributeKvEntry>> callback = new FutureCallback<List<AttributeKvEntry>>() { | |
143 | + @Override | |
144 | + public void onSuccess(List<AttributeKvEntry> data) { | |
145 | + List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList()); | |
146 | + sendWsMsg(sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData)); | |
147 | + | |
148 | + Map<String, Long> subState = new HashMap<>(keys.size()); | |
149 | + keys.forEach(key -> subState.put(key, 0L)); | |
150 | + attributesData.forEach(v -> subState.put(v.getKey(), v.getTs())); | |
151 | + | |
152 | + SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.ATTRIBUTES, false, subState, cmd.getScope()); | |
153 | + subscriptionManager.addLocalWsSubscription(sessionId, entityId, sub); | |
154 | + } | |
155 | + | |
156 | + @Override | |
157 | + public void onFailure(Throwable e) { | |
158 | + log.error(FAILED_TO_FETCH_ATTRIBUTES, e); | |
159 | + SubscriptionUpdate update; | |
160 | + if (UnauthorizedException.class.isInstance(e)) { | |
161 | + update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED, | |
162 | + SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg()); | |
163 | + } else { | |
164 | + update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, | |
165 | + FAILED_TO_FETCH_ATTRIBUTES); | |
166 | + } | |
167 | + sendWsMsg(sessionRef, update); | |
168 | + } | |
169 | + }; | |
170 | + | |
171 | + if (StringUtils.isEmpty(cmd.getScope())) { | |
172 | + //ValidationCallback? | |
173 | + ctx.loadAttributes(entityId, Arrays.asList(DataConstants.allScopes()), keys, callback); | |
174 | + } else { | |
175 | + ctx.loadAttributes(entityId, cmd.getScope(), keys, callback); | |
176 | + } | |
177 | + } | |
178 | + | |
179 | + private void handleWsAttributesSubscription(PluginContext ctx, PluginWebsocketSessionRef sessionRef, | |
180 | + AttributesSubscriptionCmd cmd, String sessionId, EntityId entityId) { | |
181 | + PluginCallback<List<AttributeKvEntry>> callback = new PluginCallback<List<AttributeKvEntry>>() { | |
182 | + @Override | |
183 | + public void onSuccess(PluginContext ctx, List<AttributeKvEntry> data) { | |
184 | + List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList()); | |
185 | + sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData)); | |
186 | + | |
187 | + Map<String, Long> subState = new HashMap<>(attributesData.size()); | |
188 | + attributesData.forEach(v -> subState.put(v.getKey(), v.getTs())); | |
189 | + | |
190 | + SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.ATTRIBUTES, true, subState, cmd.getScope()); | |
191 | + subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub); | |
192 | + } | |
193 | + | |
194 | + @Override | |
195 | + public void onFailure(PluginContext ctx, Exception e) { | |
196 | + log.error(FAILED_TO_FETCH_ATTRIBUTES, e); | |
197 | + SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, | |
198 | + FAILED_TO_FETCH_ATTRIBUTES); | |
199 | + sendWsMsg(ctx, sessionRef, update); | |
200 | + } | |
201 | + }; | |
202 | + | |
203 | + if (StringUtils.isEmpty(cmd.getScope())) { | |
204 | + ctx.loadAttributes(entityId, Arrays.asList(DataConstants.allScopes()), callback); | |
205 | + } else { | |
206 | + ctx.loadAttributes(entityId, cmd.getScope(), callback); | |
207 | + } | |
208 | + } | |
209 | + | |
210 | + private void unsubscribe(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { | |
211 | + if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { | |
212 | + subscriptionManager.cleanupLocalWsSessionSubscriptions(sessionRef, sessionId); | |
213 | + } else { | |
214 | + subscriptionManager.removeSubscription(sessionId, cmd.getCmdId()); | |
215 | + } | |
216 | + } | |
217 | + | |
218 | + private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd) { | |
219 | + if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { | |
220 | + SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, | |
221 | + "Device id is empty!"); | |
222 | + sendWsMsg(sessionRef, update); | |
223 | + return false; | |
224 | + } | |
225 | + return true; | |
226 | + } | |
227 | + | |
228 | + private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { | |
229 | + WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId); | |
230 | + if (sessionMD == null) { | |
231 | + log.warn("[{}] Session meta data not found. ", sessionId); | |
232 | + SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, | |
233 | + SESSION_META_DATA_NOT_FOUND); | |
234 | + sendWsMsg(sessionRef, update); | |
235 | + return false; | |
236 | + } else { | |
237 | + return true; | |
238 | + } | |
239 | + } | |
240 | + | |
241 | + private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, SubscriptionUpdate update) { | |
242 | + try { | |
243 | + msgEndpoint.send(sessionRef, jsonMapper.writeValueAsString(update)); | |
244 | + } catch (JsonProcessingException e) { | |
245 | + log.warn("[{}] Failed to encode reply: {}", sessionRef.getSessionId(), update, e); | |
246 | + } catch (IOException e) { | |
247 | + log.warn("[{}] Failed to send reply: {}", sessionRef.getSessionId(), update, e); | |
248 | + } | |
249 | + } | |
250 | + | |
251 | + private static Optional<Set<String>> getKeys(TelemetryPluginCmd cmd) { | |
252 | + if (!StringUtils.isEmpty(cmd.getKeys())) { | |
253 | + Set<String> keys = new HashSet<>(); | |
254 | + Collections.addAll(keys, cmd.getKeys().split(",")); | |
255 | + return Optional.of(keys); | |
256 | + } else { | |
257 | + return Optional.empty(); | |
258 | + } | |
259 | + } | |
260 | + | |
261 | +} | ... | ... |
application/src/main/java/org/thingsboard/server/service/telemetry/TelemetrySubscriptionService.java
0 → 100644
1 | +package org.thingsboard.server.service.telemetry; | |
2 | + | |
3 | +import org.thingsboard.server.common.data.id.EntityId; | |
4 | +import org.thingsboard.server.common.data.kv.AttributeKvEntry; | |
5 | +import org.thingsboard.server.common.data.kv.TsKvEntry; | |
6 | +import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState; | |
7 | + | |
8 | +import java.util.List; | |
9 | + | |
10 | +/** | |
11 | + * Created by ashvayka on 27.03.18. | |
12 | + */ | |
13 | +public interface TelemetrySubscriptionService { | |
14 | + | |
15 | + void onAttributesUpdateFromServer(EntityId entityId, String scope, List<AttributeKvEntry> attributes); | |
16 | + | |
17 | + void onTimeseriesUpdateFromServer(EntityId entityId, List<TsKvEntry> entries); | |
18 | + | |
19 | + void cleanupLocalWsSessionSubscriptions(TelemetryWebSocketSessionRef sessionRef, String sessionId); | |
20 | + | |
21 | + void removeSubscription(String sessionId, int cmdId); | |
22 | + | |
23 | + void addLocalWsSubscription(String sessionId, EntityId entityId, SubscriptionState sub); | |
24 | +} | ... | ... |
1 | +package org.thingsboard.server.service.telemetry; | |
2 | + | |
3 | +import java.io.IOException; | |
4 | + | |
5 | +/** | |
6 | + * Created by ashvayka on 27.03.18. | |
7 | + */ | |
8 | +public interface TelemetryWebSocketMsgEndpoint { | |
9 | + | |
10 | + void send(TelemetryWebSocketSessionRef sessionRef, String msg) throws IOException; | |
11 | + | |
12 | + void close(TelemetryWebSocketSessionRef sessionRef) throws IOException; | |
13 | + | |
14 | +} | ... | ... |
application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketService.java
0 → 100644
1 | +package org.thingsboard.server.service.telemetry; | |
2 | + | |
3 | +import org.thingsboard.server.extensions.api.plugins.ws.SessionEvent; | |
4 | + | |
5 | +/** | |
6 | + * Created by ashvayka on 27.03.18. | |
7 | + */ | |
8 | +public interface TelemetryWebSocketService { | |
9 | + | |
10 | + void handleWebSocketSessionEvent(TelemetryWebSocketSessionRef sessionRef, SessionEvent sessionEvent); | |
11 | + | |
12 | + void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg); | |
13 | +} | ... | ... |
application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketSessionRef.java
0 → 100644
1 | +package org.thingsboard.server.service.telemetry; | |
2 | + | |
3 | +import lombok.Getter; | |
4 | +import org.thingsboard.server.service.security.model.SecurityUser; | |
5 | + | |
6 | +import java.net.InetSocketAddress; | |
7 | +import java.util.Objects; | |
8 | + | |
9 | +/** | |
10 | + * Created by ashvayka on 27.03.18. | |
11 | + */ | |
12 | +public class TelemetryWebSocketSessionRef { | |
13 | + | |
14 | + private static final long serialVersionUID = 1L; | |
15 | + | |
16 | + @Getter | |
17 | + private final String sessionId; | |
18 | + @Getter | |
19 | + private final SecurityUser securityCtx; | |
20 | + @Getter | |
21 | + private final InetSocketAddress localAddress; | |
22 | + @Getter | |
23 | + private final InetSocketAddress remoteAddress; | |
24 | + | |
25 | + public TelemetryWebSocketSessionRef(String sessionId, SecurityUser securityCtx, InetSocketAddress localAddress, InetSocketAddress remoteAddress) { | |
26 | + this.sessionId = sessionId; | |
27 | + this.securityCtx = securityCtx; | |
28 | + this.localAddress = localAddress; | |
29 | + this.remoteAddress = remoteAddress; | |
30 | + } | |
31 | + | |
32 | + @Override | |
33 | + public boolean equals(Object o) { | |
34 | + if (this == o) return true; | |
35 | + if (o == null || getClass() != o.getClass()) return false; | |
36 | + TelemetryWebSocketSessionRef that = (TelemetryWebSocketSessionRef) o; | |
37 | + return Objects.equals(sessionId, that.sessionId); | |
38 | + } | |
39 | + | |
40 | + @Override | |
41 | + public int hashCode() { | |
42 | + return Objects.hash(sessionId); | |
43 | + } | |
44 | + | |
45 | + @Override | |
46 | + public String toString() { | |
47 | + return "TelemetryWebSocketSessionRef{" + | |
48 | + "sessionId='" + sessionId + '\'' + | |
49 | + ", localAddress=" + localAddress + | |
50 | + ", remoteAddress=" + remoteAddress + | |
51 | + '}'; | |
52 | + } | |
53 | +} | ... | ... |
application/src/main/java/org/thingsboard/server/service/telemetry/TelemetryWebSocketTextMsg.java
0 → 100644
1 | +package org.thingsboard.server.service.telemetry; | |
2 | + | |
3 | +import lombok.Data; | |
4 | +import lombok.Getter; | |
5 | +import org.thingsboard.server.service.security.model.SecurityUser; | |
6 | + | |
7 | +import java.net.InetSocketAddress; | |
8 | +import java.util.Objects; | |
9 | + | |
10 | +/** | |
11 | + * Created by ashvayka on 27.03.18. | |
12 | + */ | |
13 | +@Data | |
14 | +public class TelemetryWebSocketTextMsg { | |
15 | + | |
16 | + private final TelemetryWebSocketSessionRef sessionRef; | |
17 | + private final String payload; | |
18 | + | |
19 | +} | ... | ... |
application/src/main/java/org/thingsboard/server/service/telemetry/WsSessionMetaData.java
0 → 100644
1 | +package org.thingsboard.server.service.telemetry; | |
2 | + | |
3 | +import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef; | |
4 | + | |
5 | +/** | |
6 | + * Created by ashvayka on 27.03.18. | |
7 | + */ | |
8 | +public class WsSessionMetaData { | |
9 | + private TelemetryWebSocketSessionRef sessionRef; | |
10 | + private long lastActivityTime; | |
11 | + | |
12 | + public WsSessionMetaData(TelemetryWebSocketSessionRef sessionRef) { | |
13 | + super(); | |
14 | + this.sessionRef = sessionRef; | |
15 | + this.lastActivityTime = System.currentTimeMillis(); | |
16 | + } | |
17 | + | |
18 | + public TelemetryWebSocketSessionRef getSessionRef() { | |
19 | + return sessionRef; | |
20 | + } | |
21 | + | |
22 | + public void setSessionRef(TelemetryWebSocketSessionRef sessionRef) { | |
23 | + this.sessionRef = sessionRef; | |
24 | + } | |
25 | + | |
26 | + public long getLastActivityTime() { | |
27 | + return lastActivityTime; | |
28 | + } | |
29 | + | |
30 | + public void setLastActivityTime(long lastActivityTime) { | |
31 | + this.lastActivityTime = lastActivityTime; | |
32 | + } | |
33 | + | |
34 | + @Override | |
35 | + public String toString() { | |
36 | + return "WsSessionMetaData [sessionRef=" + sessionRef + ", lastActivityTime=" + lastActivityTime + "]"; | |
37 | + } | |
38 | +} | ... | ... |
... | ... | @@ -33,6 +33,10 @@ public class EntityIdFactory { |
33 | 33 | return getByTypeAndUuid(EntityType.valueOf(type), uuid); |
34 | 34 | } |
35 | 35 | |
36 | + public static EntityId getByTypeAndUuid(EntityType type, String uuid) { | |
37 | + return getByTypeAndUuid(type, UUID.fromString(uuid)); | |
38 | + } | |
39 | + | |
36 | 40 | public static EntityId getByTypeAndUuid(EntityType type, UUID uuid) { |
37 | 41 | switch (type) { |
38 | 42 | case TENANT: | ... | ... |
... | ... | @@ -344,9 +344,7 @@ public class SubscriptionManager { |
344 | 344 | } |
345 | 345 | |
346 | 346 | private void checkSubsciptionsPrevAddress(Set<Subscription> subscriptions) { |
347 | - Iterator<Subscription> subscriptionIterator = subscriptions.iterator(); | |
348 | - while (subscriptionIterator.hasNext()) { | |
349 | - Subscription s = subscriptionIterator.next(); | |
347 | + for (Subscription s : subscriptions) { | |
350 | 348 | if (s.isLocal()) { |
351 | 349 | if (s.getServer() != null) { |
352 | 350 | log.trace("[{}] Local subscription is no longer handled on remote server address [{}]", s.getWsSessionId(), s.getServer()); | ... | ... |
... | ... | @@ -243,27 +243,19 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler { |
243 | 243 | switch (attr.getDataType()) { |
244 | 244 | case BOOLEAN: |
245 | 245 | Optional<Boolean> booleanValue = attr.getBooleanValue(); |
246 | - if (booleanValue.isPresent()) { | |
247 | - dataBuilder.setBoolValue(booleanValue.get()); | |
248 | - } | |
246 | + booleanValue.ifPresent(dataBuilder::setBoolValue); | |
249 | 247 | break; |
250 | 248 | case LONG: |
251 | 249 | Optional<Long> longValue = attr.getLongValue(); |
252 | - if (longValue.isPresent()) { | |
253 | - dataBuilder.setLongValue(longValue.get()); | |
254 | - } | |
250 | + longValue.ifPresent(dataBuilder::setLongValue); | |
255 | 251 | break; |
256 | 252 | case DOUBLE: |
257 | 253 | Optional<Double> doubleValue = attr.getDoubleValue(); |
258 | - if (doubleValue.isPresent()) { | |
259 | - dataBuilder.setDoubleValue(doubleValue.get()); | |
260 | - } | |
254 | + doubleValue.ifPresent(dataBuilder::setDoubleValue); | |
261 | 255 | break; |
262 | 256 | case STRING: |
263 | 257 | Optional<String> stringValue = attr.getStrValue(); |
264 | - if (stringValue.isPresent()) { | |
265 | - dataBuilder.setStrValue(stringValue.get()); | |
266 | - } | |
258 | + stringValue.ifPresent(dataBuilder::setStrValue); | |
267 | 259 | break; |
268 | 260 | } |
269 | 261 | return dataBuilder; | ... | ... |