Browse Source

Lwm2m: backEnd: add add to LwM2MClient only resources (value)

pull/3980/head
nickAS21 6 years ago
parent
commit
ff56bd8863
  1. 2
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2MTransportContextServer.java
  2. 88
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2MTransportHandler.java
  3. 27
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2MTransportRequest.java
  4. 217
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2MTransportService.java
  5. 72
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2MClient.java
  6. 10
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResourceValue.java
  7. 6
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/secure/LwM2mInMemorySecurityStore.java
  8. 8
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java
  9. 27
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigServer.java

2
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2MTransportContextServer.java

@ -58,7 +58,7 @@ public class LwM2MTransportContextServer extends TransportContext {
private LwM2MTransportConfigServer ctxServer;
@Autowired
LwM2MTransportConfigServer lwM2MTransportConfigServer;
protected LwM2MTransportConfigServer lwM2MTransportConfigServer;
@Autowired
private TransportService transportService;

88
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2MTransportHandler.java

@ -27,7 +27,9 @@ import org.eclipse.leshan.core.node.LwM2mMultipleResource;
import org.eclipse.leshan.core.node.LwM2mNode;
import org.eclipse.leshan.core.node.LwM2mObject;
import org.eclipse.leshan.core.node.LwM2mObjectInstance;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.node.LwM2mSingleResource;
import org.eclipse.leshan.core.node.codec.CodecException;
import org.eclipse.leshan.core.util.Hex;
import org.eclipse.leshan.server.californium.LeshanServer;
import org.eclipse.leshan.server.californium.LeshanServerBuilder;
@ -53,7 +55,7 @@ import java.util.Optional;
@Slf4j
@Component("LwM2MTransportHandler")
@ConditionalOnExpression("('${service.type:null}'=='tb-transport' && '${transport.lwm2m.enabled:false}'=='true' )|| ('${service.type:null}'=='monolith' && '${transport.lwm2m.enabled}'=='true')")
public class LwM2MTransportHandler{
public class LwM2MTransportHandler {
// We choose a default timeout a bit higher to the MAX_TRANSMIT_WAIT(62-93s) which is the time from starting to
// send a Confirmable message to the time when an acknowledgement is no longer expected.
@ -132,8 +134,7 @@ public class LwM2MTransportHandler{
this.lhServerNoSecPskRpk.getRegistrationService().addListener(lwM2mServerListener.registrationListener);
this.lhServerNoSecPskRpk.getPresenceService().addListener(lwM2mServerListener.presenceListener);
this.lhServerNoSecPskRpk.getObservationService().addListener(lwM2mServerListener.observationListener);
}
catch (java.lang.NullPointerException e) {
} catch (java.lang.NullPointerException e) {
log.error("init [{}]", e.toString());
}
}
@ -151,31 +152,49 @@ public class LwM2MTransportHandler{
return coapConfig;
}
public static String getValueTypeToString (Object value, ResourceModel.Type type, int val) {
try{
// public static String getValueTypeToString (Object value, ResourceModel.Type type, int val) {
// try{
// switch (type) {
// case STRING: // String
// case OBJLNK: // ObjectLink
// return value.toString();
// case INTEGER: // Long
// return Long.toString((long) value);
// case BOOLEAN: // Boolean
// return Boolean.toString((Boolean) value);
// case FLOAT: // Double
// return Double.toString((Double) value);
// case TIME: // Date
// return Long.toString(((Date) value).getTime());
//// String DATE_FORMAT = "MMM d, yyyy HH:mm a";
//// DateFormat formatter = new SimpleDateFormat(DATE_FORMAT);
//// return formatter.format(new Date(Integer.toUnsignedLong((Integer) value)));
// case OPAQUE: // byte[] value, base64
// return Hex.encodeHexString((byte[])value);
// default:
// return null;
// }
// } catch (Exception e) {
// log.error(e.getStackTrace().toString());
// return null;
// }
// }
public static boolean equalsResourceValue(Object valueOld, Object valueNew, ResourceModel.Type type, LwM2mPath resourcePath) throws CodecException {
switch (type) {
case STRING: // String
case OBJLNK: // ObjectLink
return value.toString();
case INTEGER: // Long
return Long.toString((long) value);
case BOOLEAN: // Boolean
return Boolean.toString((Boolean) value);
case FLOAT: // Double
return Double.toString((Double) value);
case TIME: // Date
return Long.toString(((Date) value).getTime());
// String DATE_FORMAT = "MMM d, yyyy HH:mm a";
// DateFormat formatter = new SimpleDateFormat(DATE_FORMAT);
// return formatter.format(new Date(Integer.toUnsignedLong((Integer) value)));
case OPAQUE: // byte[] value, base64
return Hex.encodeHexString((byte[])value);
case BOOLEAN:
case INTEGER:
case FLOAT:
return String.valueOf(valueOld).equals(String.valueOf(valueNew));
case TIME:
return ((Date) valueOld).getTime() == ((Date) valueNew).getTime();
case STRING:
case OBJLNK:
return valueOld.equals(valueNew);
case OPAQUE:
return Hex.decodeHex(((String) valueOld).toCharArray()).equals(Hex.decodeHex(((String) valueNew).toCharArray()));
default:
return null;
}
} catch (Exception e) {
log.error(e.getStackTrace().toString());
return null;
throw new CodecException("Invalid value type for resource %s, type %s", resourcePath, type);
}
}
@ -198,19 +217,18 @@ public class LwM2MTransportHandler{
attrTelemetryObserveValue.setPostAttributeProfile(profilesConfigData.get(ATTRIBUTE).getAsJsonArray());
attrTelemetryObserveValue.setPostTelemetryProfile(profilesConfigData.get(TELEMETRY).getAsJsonArray());
attrTelemetryObserveValue.setPostObserveProfile(profilesConfigData.get(OBSERVE).getAsJsonArray());
return attrTelemetryObserveValue;
return attrTelemetryObserveValue;
}
/**
* @return deviceProfileBody with Observe&Attribute&Telemetry From Thingsboard
* Example: with pathResource (use only pathResource)
* property: "observeAttr"
* {"keyName": {
* "/3/0/1": "modelNumber",
* "/3/0/0": "manufacturer",
* "/3/0/2": "serialNumber"
* },
* "/3/0/1": "modelNumber",
* "/3/0/0": "manufacturer",
* "/3/0/2": "serialNumber"
* },
* "attribute":["/2/0/1","/3/0/9"],
* "telemetry":["/1/0/1","/2/0/1","/6/0/1"],
* "observe":["/2/0","/2/0/0","/4/0/2"]}
@ -312,12 +330,12 @@ public class LwM2MTransportHandler{
}
}
public static String splitCamelCaseString(String s){
public static String splitCamelCaseString(String s) {
LinkedList<String> linkedListOut = new LinkedList<>();
LinkedList<String> linkedList = new LinkedList<String>((Arrays.asList(s.split(" "))));
linkedList.forEach(str-> {
linkedList.forEach(str -> {
String strOut = str.replaceAll("\\W", "").replaceAll("_", "").toUpperCase();
if (strOut.length()>1) linkedListOut.add(strOut.charAt(0) + strOut.substring(1).toLowerCase());
if (strOut.length() > 1) linkedListOut.add(strOut.charAt(0) + strOut.substring(1).toLowerCase());
else linkedListOut.add(strOut);
});
linkedListOut.set(0, (linkedListOut.get(0).substring(0, 1).toLowerCase() + linkedListOut.get(0).substring(1)));

27
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2MTransportRequest.java

@ -123,10 +123,7 @@ public class LwM2MTransportRequest {
if (registration != null && resultIds.getObjectId() >= 0) {
DownlinkRequest request = null;
ContentFormat contentFormat = contentFormatParam != null ? ContentFormat.fromName(contentFormatParam.toUpperCase()) : null;
ResourceModel resource = (resultIds.getResourceId() !=null && lwM2MClient != null) ?
lwM2MClient.getModelObjects().get(resultIds.getObjectId()).getObjectModel().resources.get(resultIds.getResourceId()) : null;
ResourceModel.Type resType = (resource == null) ? null : resource.type;
boolean resMultiple = (resource == null) ? false : resource.multiple;
ResourceModel resource = service.context.getCtxServer().getResourceModel(resultIds);
timeoutInMs = timeoutInMs > 0 ? timeoutInMs : DEFAULT_TIMEOUT;
switch (typeOper) {
case GET_TYPE_OPER_READ:
@ -148,21 +145,23 @@ public class LwM2MTransportRequest {
request = new CancelObservationRequest(observation);
break;
case POST_TYPE_OPER_EXECUTE:
if (params != null && !resMultiple) {
if (params != null && resource != null && !resource.multiple) {
// request = new ExecuteRequest(target, LwM2MTransportHandler.getValueTypeToString(params, resType));
request = new ExecuteRequest(target, (String) this.converter.convertValue(params, resType, ResourceModel.Type.STRING, resultIds));
request = new ExecuteRequest(target, (String) this.converter.convertValue(params, resource.type, ResourceModel.Type.STRING, resultIds));
} else {
request = new ExecuteRequest(target);
}
break;
case POST_TYPE_OPER_WRITE_REPLACE:
// Request to write a <b>String Single-Instance Resource</b> using the TLV content format.
if (contentFormat.equals(ContentFormat.TLV) && !resMultiple) {
request = this.getWriteRequestSingleResource(null, resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId(), params, resType, registration);
}
// Mode.REPLACE && Request to write a <b>String Single-Instance Resource</b> using the given content format (TEXT, TLV, JSON)
else if (!contentFormat.equals(ContentFormat.TLV) && !resMultiple) {
request = this.getWriteRequestSingleResource(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId(), params, resType, registration);
if (resource != null) {
if (contentFormat.equals(ContentFormat.TLV) && !resource.multiple) {
request = this.getWriteRequestSingleResource(null, resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId(), params, resource.type, registration);
}
// Mode.REPLACE && Request to write a <b>String Single-Instance Resource</b> using the given content format (TEXT, TLV, JSON)
else if (!contentFormat.equals(ContentFormat.TLV) && !resource.multiple) {
request = this.getWriteRequestSingleResource(contentFormat, resultIds.getObjectId(), resultIds.getObjectInstanceId(), resultIds.getResourceId(), params, resource.type, registration);
}
}
break;
case PUT_TYPE_OPER_WRITE_UPDATE:
@ -217,10 +216,10 @@ public class LwM2MTransportRequest {
break;
default:
}
if (request != null) {
this.sendRequest(lwServer, registration, request, lwM2MClient, timeoutInMs, isDelayedUpdate);
}
else if (request == null && isDelayedUpdate) {
} else if (request == null && isDelayedUpdate) {
String msg = String.format(LOG_LW2M_ERROR + ": sendRequest: Resource path - %s msg No SendRequest to Client", target);
service.sentLogsToThingsboard(msg, registration.getId());
log.error("[{}] - [{}] No SendRequest", target);

217
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/LwM2MTransportService.java

@ -19,8 +19,8 @@ import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mMultipleResource;
import org.eclipse.leshan.core.node.LwM2mObject;
@ -55,12 +55,12 @@ import org.thingsboard.server.transport.lwm2m.server.client.ModelObject;
import org.thingsboard.server.transport.lwm2m.server.client.ResourceValue;
import org.thingsboard.server.transport.lwm2m.server.client.ResultsAnalyzerParameters;
import org.thingsboard.server.transport.lwm2m.server.secure.LwM2mInMemorySecurityStore;
import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@ -103,6 +103,8 @@ public class LwM2MTransportService {
private ExecutorService executorRegistered;
private ExecutorService executorUpdateRegistered;
private ExecutorService executorUnRegistered;
private LwM2mValueConverterImpl converter;
@Autowired
private TransportService transportService;
@ -118,13 +120,14 @@ public class LwM2MTransportService {
@PostConstruct
public void init() {
context.getScheduler().scheduleAtFixedRate(this::checkInactivityAndReportActivity, new Random().nextInt((int) context.getCtxServer().getSessionReportTimeout()), context.getCtxServer().getSessionReportTimeout(), TimeUnit.MILLISECONDS);
executorRegistered = Executors.newCachedThreadPool(
this.context.getScheduler().scheduleAtFixedRate(this::checkInactivityAndReportActivity, new Random().nextInt((int) context.getCtxServer().getSessionReportTimeout()), context.getCtxServer().getSessionReportTimeout(), TimeUnit.MILLISECONDS);
this.executorRegistered = Executors.newCachedThreadPool(
new NamedThreadFactory(String.format("LwM2M %s channel registered", SERVICE_CHANNEL)));
executorUpdateRegistered = Executors.newCachedThreadPool(
this.executorUpdateRegistered = Executors.newCachedThreadPool(
new NamedThreadFactory(String.format("LwM2M %s channel update registered", SERVICE_CHANNEL)));
executorUnRegistered = Executors.newCachedThreadPool(
this.executorUnRegistered = Executors.newCachedThreadPool(
new NamedThreadFactory(String.format("LwM2M %s channel un registered", SERVICE_CHANNEL)));
this.converter = new LwM2mValueConverterImpl();
}
/**
@ -148,7 +151,6 @@ public class LwM2MTransportService {
log.info("[{}] Client: onRegistered name ", registration.getEndpoint());
LwM2MClient lwM2MClient = lwM2mInMemorySecurityStore.updateInSessionsLwM2MClient(lwServer, registration);
if (lwM2MClient != null) {
lwM2MClient.setLwM2MTransportService(this);
lwM2MClient.setLwM2MTransportService(this);
lwM2MClient.setSessionUuid(UUID.randomUUID());
this.setLwM2MClient(lwServer, registration, lwM2MClient);
@ -183,6 +185,8 @@ public class LwM2MTransportService {
try {
SessionInfoProto sessionInfo = this.getValidateSessionInfo(registration.getId());
if (sessionInfo != null) {
// transportService.reportActivity(sessionInfo);
// transportService.registerAsyncSession(sessionInfo, new LwM2MSessionMsgListener(this, sessionInfo));
log.info("Client: [{}] updatedReg [{}] name [{}] profile ", registration.getId(), registration.getEndpoint(), sessionInfo.getDeviceType());
} else {
log.error("Client: [{}] updatedReg [{}] name [{}] sessionInfo ", registration.getId(), registration.getEndpoint(), null);
@ -199,7 +203,7 @@ public class LwM2MTransportService {
* !!! Warn: if have not finishing unReg, then this operation will be finished on next Client`s connect
*/
public void unReg(Registration registration, Collection<Observation> observations) {
executorUpdateRegistered.submit(() -> {
executorUnRegistered.submit(() -> {
try {
this.sentLogsToThingsboard(LOG_LW2M_INFO + ": Client unRegistration", registration.getId());
this.closeClientSession(registration);
@ -262,6 +266,7 @@ public class LwM2MTransportService {
}
/**
* #0 Add new ObjectModel to context
* Create new LwM2MClient for current session -> setModelClient...
* #1 Add all ObjectLinks (instance) to control the process of executing requests to the client
* to get the client model with current values
@ -272,6 +277,8 @@ public class LwM2MTransportService {
* @param lwM2MClient - object with All parameters off client
*/
private void setLwM2MClient(LeshanServer lwServer, Registration registration, LwM2MClient lwM2MClient) {
// #0
this.setNewObjectModels(lwServer, registration);
// #1
Arrays.stream(registration.getObjectLinks()).forEach(url -> {
LwM2mPath pathIds = new LwM2mPath(url.getUrl());
@ -289,6 +296,16 @@ public class LwM2MTransportService {
});
}
private void setNewObjectModels(LeshanServer lwServer, Registration registration) {
Arrays.stream(registration.getObjectLinks()).forEach(url -> {
LwM2mPath pathIds = new LwM2mPath(url.getUrl());
if (pathIds.isObjectInstance() && !pathIds.isResource() && !context.getCtxServer().getObjectModels().containsKey(pathIds.getObjectId())) {
ObjectModel model = lwServer.getModelProvider().getObjectModel(registration).getObjectModels().stream().filter(v -> v.id == pathIds.getObjectId()).collect(Collectors.toList()).get(0);
context.getCtxServer().getObjectModels().put(pathIds.getObjectId(), model);
}
});
}
/**
* @param registrationId - Id of Registration LwM2M Client
* @return - sessionInfo after access connect client
@ -396,8 +413,8 @@ public class LwM2MTransportService {
lwM2MClient.getDelayedRequests().forEach((k, v) -> {
List listV = new ArrayList<TransportProtos.KeyValueProto>();
listV.add(v.getKv());
this.putDelayedUpdateResourcesClient(lwM2MClient, lwM2MClient.getResourceValue(k), getJsonObject(listV).get(v.getKv().getKey()), k);
System.out.printf(" k: %s, v: %s%n, v1: %s%n", k, v.getKv().getStringV(), lwM2MClient.getResourceValue(k));
this.putDelayedUpdateResourcesClient(lwM2MClient, this.getResourceValueToString(lwM2MClient, k), getJsonObject(listV).get(v.getKv().getKey()), k);
System.out.printf(" k: %s, v: %s%n, v1: %s%n", k, v.getKv().getStringV(), this.getResourceValueToString(lwM2MClient, k));
});
lwM2MClient.getDelayedRequestsId().remove(attributesResponse.getRequestId());
if (lwM2MClient.getDelayedRequests().size() == 0) {
@ -427,10 +444,11 @@ public class LwM2MTransportService {
ConcurrentMap<String, String> keyNamesMap = new Gson().fromJson(profile.getPostKeyNameProfile().toString(), ConcurrentHashMap.class);
ConcurrentMap<String, String> keyNamesIsWritable = keyNamesMap.entrySet()
.stream()
.filter(e -> (attrSet.contains(e.getKey()) && lwM2MClient.getOperation(e.getKey()).isWritable()))
.filter(e -> (attrSet.contains(e.getKey()) && context.getCtxServer().getResourceModel(new LwM2mPath(e.getKey())) != null &&
context.getCtxServer().getResourceModel(new LwM2mPath(e.getKey())).operations.isWritable()))
.collect(Collectors.toConcurrentMap(Map.Entry::getKey, Map.Entry::getValue));
namesIsIsWritable.addAll(new HashSet<>(keyNamesIsWritable.values()));
keyNamesIsWritable.keySet().forEach(p -> namesIsIsWritable.add(lwM2MClient.getResourceName(p)));
keyNamesIsWritable.keySet().forEach(p -> namesIsIsWritable.add(this.getResourceName(p)));
return new ArrayList<>(namesIsIsWritable);
}
@ -501,7 +519,7 @@ public class LwM2MTransportService {
LwM2mPath pathIds = new LwM2mPath(p.getAsString().toString());
if (pathIds.isResource()) {
if (path == null || path.contains(p.getAsString())) {
this.addParameters(p.getAsString().toString(), attributes, registration, "attributes");
this.addParameters(p.getAsString().toString(), attributes, registration);
}
}
});
@ -510,7 +528,7 @@ public class LwM2MTransportService {
LwM2mPath pathIds = new LwM2mPath(p.getAsString().toString());
if (pathIds.isResource()) {
if (path == null || path.contains(p.getAsString())) {
this.addParameters(p.getAsString().toString(), telemetry, registration, "telemetry");
this.addParameters(p.getAsString().toString(), telemetry, registration);
}
}
});
@ -520,13 +538,15 @@ public class LwM2MTransportService {
* @param parameters - JsonObject attributes/telemetry
* @param registration - Registration LwM2M Client
*/
private void addParameters(String path, JsonObject parameters, Registration registration, String nameParam) {
JsonObject names = lwM2mInMemorySecurityStore.getProfiles().get(lwM2mInMemorySecurityStore.getSessions().get(registration.getId()).getProfileUuid()).getPostKeyNameProfile();
private void addParameters(String path, JsonObject parameters, Registration registration) {
LwM2MClient lwM2MClient = lwM2mInMemorySecurityStore.getSessions().get(registration.getId());
JsonObject names = lwM2mInMemorySecurityStore.getProfiles().get(lwM2MClient.getProfileUuid()).getPostKeyNameProfile();
String resName = String.valueOf(names.get(path));
if (resName != null && !resName.isEmpty()) {
String resValue = null;
try {
resValue = lwM2mInMemorySecurityStore.getSessions().get(registration.getId()).getResourceValue(path);
// resValue = lwM2mInMemorySecurityStore.getSessions().get(registration.getId()).getResourceValueString(path);
resValue = this.getResourceValueToString(lwM2MClient, path);
if (resValue != null) {
// log.info("addParameters Path: [{}] ResValue : [{}] nameParam [{}]", path, lwM2mInMemorySecurityStore.getSessions().get(registration.getId()).getResourceValue(path), nameParam);
parameters.addProperty(resName, resValue);
@ -577,7 +597,7 @@ public class LwM2MTransportService {
p.getAsString().toString() : null;
if (target != null) {
// #2
if (lwM2mInMemorySecurityStore.getSessions().get(registration.getId()).getResourceValue(target) != null) {
if (this.getResourceValueToString(lwM2mInMemorySecurityStore.getSessions().get(registration.getId()), target) != null) {
lwM2MTransportRequest.sendAllRequest(lwServer, registration, target, GET_TYPE_OPER_OBSERVE,
null, null, null, null, this.context.getCtxServer().getTimeout(),
false);
@ -635,7 +655,7 @@ public class LwM2MTransportService {
* @param path - observe
* @param response - observe
*/
@SneakyThrows
public void onObservationResponse(Registration registration, String path, ReadResponse response) {
if (response.getContent() != null) {
if (response.getContent() instanceof LwM2mObject) {
@ -655,13 +675,8 @@ public class LwM2MTransportService {
/**
* Sending observe value of resources to thingsboard
* #1 Return old Resource from ModelObject
* #2 Create new Resource with value from observation
* #3 Create new Resources from old Resources
* #4 Update new Resources (replace old Resource on new Resource)
* #5 Remove old Instance from modelClient
* #6 Create new Instance with new Resources values
* #7 Update modelClient.getModelObjects(idObject) (replace old Instance on new Instance)
* #1 Return old Value Resource from LwM2MClient
* #2 Update new Resources (replace old Resource Value on new Resource Value)
*
* @param registration - Registration LwM2M Client
* @param value - LwM2mSingleResource response.getContent()
@ -669,72 +684,38 @@ public class LwM2MTransportService {
* @param path - resource
*/
private void onObservationSetResourcesValue(Registration registration, Object value, Map<Integer, ?> values, String path) {
CountDownLatch respLatch = new CountDownLatch(1);
boolean isChange = false;
try {
CountDownLatch respLatch = new CountDownLatch(1);
try {
// #1
LwM2MClient lwM2MClient = lwM2mInMemorySecurityStore.getLwM2MClient(registration.getId());
LwM2mPath resultIds = new LwM2mPath(path);
log.warn("#0 nameDevice: [{}] resultIds: [{}] value: [{}], values: [{}] ", lwM2MClient.getDeviceName(), resultIds, value, values);
ResourceModel.Type resType = lwM2MClient.getModelObjects().get(resultIds.getObjectId()).getObjectModel().resources.get(resultIds.getResourceId()).type;
Map<Integer, LwM2mObjectInstance> instancesModelObject = (lwM2MClient.getModelObjects().get(resultIds.getObjectId()) != null) ? lwM2MClient.getModelObjects().get(resultIds.getObjectId()).getInstances() : null;
Map<Integer, LwM2mResource> resourcesOld = null;
try {
CountDownLatch respResLatch = new CountDownLatch(1);
try {
resourcesOld = (instancesModelObject != null &&
instancesModelObject.get(resultIds.getObjectInstanceId()) != null &&
instancesModelObject.get(resultIds.getObjectInstanceId()).getResources() != null) ? instancesModelObject.get(resultIds.getObjectInstanceId()).getResources() : null;
} finally {
respResLatch.countDown();
}
try {
respResLatch.await(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS);
} catch (InterruptedException ex) {
ex.printStackTrace();
log.error("#1_2 Update ResourcesValue after Observation in CountDownLatch is unsuccessfully path: [{}] value: [{}]", path, value);
}
} catch (Exception e) {
e.printStackTrace();
log.error("#1_2_1 Update ResourcesValue after Observation in CountDownLatch is unsuccessfully path: [{}] value: [{}]", path, value);
}
LwM2mResource resourceOld = (resourcesOld != null && resourcesOld.get(resultIds.getResourceId()) != null) ? resourcesOld.get(resultIds.getResourceId()) : null;
// #2
LwM2mResource resourceNew = null;
if ((resourceOld != null && resourceOld.isMultiInstances() && !resourceOld.getValues().equals(values)) ||
(resourceOld == null && value == null)) {
resourceNew = LwM2mMultipleResource.newResource(resultIds.getResourceId(), values, resType);
} else if ((resourceOld != null && !resourceOld.isMultiInstances() && !resourceOld.getValue().equals(values)) ||
(resourceOld == null && value != null)) {
resourceNew = LwM2mSingleResource.newResource(resultIds.getResourceId(), value, resType);
}
if (resourceNew != null) {
//#3
Map<Integer, LwM2mResource> resourcesNew = (resourcesOld == null) ? new HashMap<>() : new HashMap<>(resourcesOld);
// #4
if ((resourceOld != null)) resourcesNew.remove(resourceOld);
// #5
resourcesNew.put(resultIds.getResourceId(), resourceNew);
// #6
LwM2mObjectInstance instanceNew = new LwM2mObjectInstance(resultIds.getObjectInstanceId(), resourcesNew.values());
// #7
lwM2MClient.getModelObjects().get(resultIds.getObjectId()).removeInstance(resultIds.getObjectInstanceId());
instancesModelObject.put(resultIds.getObjectInstanceId(), instanceNew);
Set<String> paths = new HashSet<>();
paths.add(path);
this.updateAttrTelemetry(registration, false, paths);
}
} finally {
respLatch.countDown();
}
try {
respLatch.await(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS);
} catch (InterruptedException ex) {
ex.printStackTrace();
log.error("#1_1 Update ResourcesValue after Observation in CountDownLatch is unsuccessfully path: [{}] value: [{}]", path, value);
// #1
LwM2MClient lwM2MClient = lwM2mInMemorySecurityStore.getLwM2MClient(registration.getId());
LwM2mPath pathIds = new LwM2mPath(path);
log.warn("#0 nameDevice: [{}] resultIds: [{}] value: [{}], values: [{}] ", lwM2MClient.getDeviceName(), pathIds, value, values);
ResourceModel.Type resModelType = context.getCtxServer().getResourceModelType(pathIds);
ResourceValue resValueOld = lwM2MClient.getResources().get(path);
// #2
if (resValueOld.isMultiInstances() && !values.toString().equals(resValueOld.getResourceValue().toString())) {
ResourceValue resourceValue = new ResourceValue ( values, null, true);
lwM2MClient.getResources().put(path, resourceValue);
isChange = true;
} else if (!LwM2MTransportHandler.equalsResourceValue(resValueOld.getValue(), value, resModelType, pathIds)) {
ResourceValue resourceValue = new ResourceValue ( null, value, false);
lwM2MClient.getResources().put(path, resourceValue);
isChange = true;
}
} catch (Exception ignored) {
} finally {
respLatch.countDown();
}
try {
respLatch.await(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS);
} catch (InterruptedException ex) {
ex.printStackTrace();
log.error("#1_1 Update ResourcesValue after Observation in CountDownLatch is unsuccessfully path: [{}] value: [{}]", path, value);
}
if (isChange) {
Set<String> paths = new HashSet<>();
paths.add(path);
this.updateAttrTelemetry(registration, false, paths);
}
}
@ -762,19 +743,19 @@ public class LwM2MTransportService {
String value = de.getValue().getAsString();
LwM2MClient lwM2MClient = lwM2mInMemorySecurityStore.getSession(new UUID(sessionInfo.getSessionIdMSB(), sessionInfo.getSessionIdLSB())).entrySet().iterator().next().getValue();
AttrTelemetryObserveValue profile = lwM2mInMemorySecurityStore.getProfile(new UUID(sessionInfo.getDeviceProfileIdMSB(), sessionInfo.getDeviceProfileIdLSB()));
ResourceModel resourceModel = context.getCtxServer().getResourceModel(new LwM2mPath(path));
if (path != null && (this.validatePathInAttrProfile(profile, path) || this.validatePathInTelemetryProfile(profile, path))) {
if (lwM2MClient.getOperation(path).isWritable()) {
if (resourceModel != null && resourceModel.operations.isWritable()) {
lwM2MTransportRequest.sendAllRequest(lwM2MClient.getLwServer(), lwM2MClient.getRegistration(), path, POST_TYPE_OPER_WRITE_REPLACE,
ContentFormat.TLV.getName(), lwM2MClient, null, value, this.context.getCtxServer().getTimeout(),
false);
// log.info("[{}] path onAttributeUpdate", path);
} else {
log.error(LOG_LW2M_ERROR + ": Resource path - [{}] value - [{}] is not Writable and cannot be updated", path, value);
log.error("Resource path - [{}] value - [{}] is not Writable and cannot be updated", path, value);
String logMsg = String.format(LOG_LW2M_ERROR + ": attributeUpdate: Resource path - %s value - %s is not Writable and cannot be updated", path, value);
this.sentLogsToThingsboard(logMsg, lwM2MClient.getRegistration().getId());
}
} else {
log.error(LOG_LW2M_ERROR + ": Attribute name - [{}] value - [{}] is not present as attribute in profile and cannot be updated", de.getKey(), value);
log.error("Attribute name - [{}] value - [{}] is not present as attribute in profile and cannot be updated", de.getKey(), value);
String logMsg = String.format(LOG_LW2M_ERROR + ": attributeUpdate: attribute name - %s value - %s is not present as attribute in profile and cannot be updated", de.getKey(), value);
this.sentLogsToThingsboard(logMsg, lwM2MClient.getRegistration().getId());
}
@ -845,7 +826,8 @@ public class LwM2MTransportService {
Predicate<Map.Entry<Integer, ModelObject>> predicateObj = (obj -> {
return obj.getValue().getObjectModel().resources.entrySet().stream().filter(predicateRes).findFirst().isPresent();
});
Map.Entry<Integer, ModelObject> object = lwM2MClient.getModelObjects().entrySet().stream().filter(predicateObj).findFirst().get();
// Map.Entry<Integer, ModelObject> object = lwM2MClient.getModelObjects().entrySet().stream().filter(predicateObj).findFirst().get();
Map.Entry<Integer, ModelObject> object = null;
ModelObject modelObject = object.getValue();
LwM2mObjectInstance instance = modelObject.getInstances().entrySet().stream().findFirst().get().getValue();
ResourceModel resource = modelObject.getObjectModel().resources.entrySet().stream().filter(predicateRes).findFirst().get().getValue();
@ -867,7 +849,8 @@ public class LwM2MTransportService {
// ResultIds resultIds = new ResultIds(path);
LwM2mPath resultIds = new LwM2mPath(path);
LwM2MClient lwM2MClient = lwM2mInMemorySecurityStore.getLwM2MClient(registration.getId());
LwM2mResource resource = lwM2MClient.getModelObjects().get(resultIds.getObjectId()).getInstances().get(resultIds.getObjectInstanceId()).getResource(resultIds.getResourceId());
// LwM2mResource resource = lwM2MClient.getModelObjects().get(resultIds.getObjectId()).getInstances().get(resultIds.getObjectInstanceId()).getResource(resultIds.getResourceId());
LwM2mResource resource = null;
if (resource.isMultiInstances()) {
this.onObservationSetResourcesValue(registration, null, ((LwM2mSingleResource) request.getNode()).getValues(), path);
} else {
@ -1080,8 +1063,9 @@ public class LwM2MTransportService {
targets.forEach(target -> {
// ResultIds pathIds = new ResultIds(target);
LwM2mPath pathIds = new LwM2mPath(target);
if (pathIds.isResource() && lwM2MClient.getModelObjects().get(pathIds.getObjectId())
.getInstances().get(pathIds.getObjectInstanceId()).getResource(pathIds.getResourceId()).getValue() != null) {
// if (pathIds.isResource() && lwM2MClient.getModelObjects().get(pathIds.getObjectId())
// .getInstances().get(pathIds.getObjectInstanceId()).getResource(pathIds.getResourceId()).getValue() != null) {
if (pathIds.isResource()) {
if (GET_TYPE_OPER_READ.equals(typeOper)) {
lwM2MTransportRequest.sendAllRequest(lwServer, registration, target, typeOper,
ContentFormat.TLV.getName(), null, null, null, this.context.getCtxServer().getTimeout(),
@ -1098,32 +1082,17 @@ public class LwM2MTransportService {
private void cancelObserveIsValue(LeshanServer lwServer, Registration registration, Set<String> paramAnallyzer) {
LwM2MClient lwM2MClient = lwM2mInMemorySecurityStore.getLwM2MClient(registration.getId());
paramAnallyzer.forEach(p -> {
if (this.getResourceValue(lwM2MClient, p) != null) {
if (this.getResourceValue(lwM2MClient, new LwM2mPath(p)) != null) {
this.setCancelObservationRecourse(lwServer, registration, p);
}
}
);
}
private ResourceValue getResourceValue(LwM2MClient lwM2MClient, String path) {
private ResourceValue getResourceValue(LwM2MClient lwM2MClient, LwM2mPath pathIds) {
ResourceValue resourceValue = null;
// ResultIds pathIds = new ResultIds(path);
LwM2mPath pathIds = new LwM2mPath(path);
if (pathIds.isResource()) {
LwM2mResource resource = lwM2MClient.getModelObjects().get(pathIds.getObjectId()).getInstances().get(pathIds.getObjectInstanceId()).getResource(pathIds.getResourceId());
if (resource.isMultiInstances()) {
if (resource.getValues().size() > 0) {
resourceValue = new ResourceValue();
resourceValue.setMultiInstances(resource.isMultiInstances());
resourceValue.setValues(resource.getValues());
}
} else {
if (resource.getValue() != null) {
resourceValue = new ResourceValue();
resourceValue.setMultiInstances(resource.isMultiInstances());
resourceValue.setValue(resource.getValue());
}
}
resourceValue = lwM2MClient.getResources().get(pathIds.toString());
}
return resourceValue;
}
@ -1174,4 +1143,20 @@ public class LwM2MTransportService {
}
}
private String getResourceName(String path) {
LwM2mPath resultIds = new LwM2mPath(path);
return (context.getCtxServer().getObjectModels().get(resultIds.getObjectId()) != null) ?
context.getCtxServer().getObjectModels().get(resultIds.getObjectId()).resources.get(resultIds.getResourceId()).name : "";
}
/**
* @param path - path resource
* @return - value of Resource or null
*/
public String getResourceValueToString(LwM2MClient lwM2MClient, String path) {
LwM2mPath pathIds = new LwM2mPath(path);
ResourceValue resourceValue = this.getResourceValue(lwM2MClient, pathIds);
return (String) this.converter.convertValue(resourceValue.getResourceValue(), this.context.getCtxServer().getResourceModelType(pathIds), ResourceModel.Type.STRING, pathIds);
}
}

72
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2MClient.java

@ -18,12 +18,10 @@ package org.thingsboard.server.transport.lwm2m.server.client;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mObjectInstance;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.eclipse.leshan.core.response.LwM2mResponse;
import org.eclipse.leshan.core.response.ReadResponse;
import org.eclipse.leshan.core.util.Hex;
import org.eclipse.leshan.server.californium.LeshanServer;
import org.eclipse.leshan.server.registration.Registration;
import org.eclipse.leshan.server.security.SecurityInfo;
@ -38,8 +36,6 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import static org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE;
@Slf4j
@Data
public class LwM2MClient implements Cloneable {
@ -56,7 +52,7 @@ public class LwM2MClient implements Cloneable {
private Registration registration;
private ValidateDeviceCredentialsResponseMsg credentialsResponse;
private Map<String, String> attributes;
private Map<Integer, ModelObject> modelObjects;
private Map<String, ResourceValue> resources;
private Set<String> pendingRequests;
private Map<String, TransportProtos.TsKvProto> delayedRequests;
private Set<Integer> delayedRequestsId;
@ -67,15 +63,15 @@ public class LwM2MClient implements Cloneable {
return super.clone();
}
public LwM2MClient(String endPoint, String identity, SecurityInfo info, ValidateDeviceCredentialsResponseMsg credentialsResponse, Map<String, String> attributes, Map<Integer, ModelObject> modelObjects, UUID profileUuid) {
public LwM2MClient(String endPoint, String identity, SecurityInfo info, ValidateDeviceCredentialsResponseMsg credentialsResponse, Map<String, String> attributes, UUID profileUuid) {
this.endPoint = endPoint;
this.identity = identity;
this.info = info;
this.credentialsResponse = credentialsResponse;
this.attributes = (attributes != null && attributes.size() > 0) ? attributes : new ConcurrentHashMap<String, String>();
this.modelObjects = (modelObjects != null && modelObjects.size() > 0) ? modelObjects : new ConcurrentHashMap<Integer, ModelObject>();
this.pendingRequests = ConcurrentHashMap.newKeySet();
this.delayedRequests = new ConcurrentHashMap<>();
this.resources = new ConcurrentHashMap<>();
this.delayedRequestsId = ConcurrentHashMap.newKeySet();
this.profileUuid = profileUuid;
/**
@ -102,21 +98,29 @@ public class LwM2MClient implements Cloneable {
private void initValue() {
this.responses.forEach((key, resp) -> {
LwM2mPath pathIds = new LwM2mPath(key);
LwM2mPath pathIds = new LwM2mPath(key);
if (pathIds.isObject() || pathIds.isObjectInstance() || pathIds.isResource()) {
ObjectModel objectModel = this.lwServer.getModelProvider().getObjectModel(registration).getObjectModels().stream().filter(v -> v.id == pathIds.getObjectId()).collect(Collectors.toList()).get(0);
if (this.modelObjects.get(pathIds.getObjectId()) != null) {
this.modelObjects.get(pathIds.getObjectId()).getInstances().put(((ReadResponse) resp).getContent().getId(), (LwM2mObjectInstance) ((ReadResponse) resp).getContent());
} else {
Map<Integer, LwM2mObjectInstance> instances = new ConcurrentHashMap<>();
instances.put(((ReadResponse) resp).getContent().getId(), (LwM2mObjectInstance) ((ReadResponse) resp).getContent());
ModelObject modelObject = new ModelObject(objectModel, instances);
this.modelObjects.put(pathIds.getObjectId(), modelObject);
if (objectModel != null) {
((LwM2mObjectInstance)((ReadResponse)resp).getContent()).getResources().forEach((k, v) -> {
String rez = pathIds.toString() + "/" + k;
boolean ismulti = objectModel.resources.get(k).multiple;
if (objectModel.resources.get(k).multiple){
this.resources.put(rez, new ResourceValue(v.getValues(), null, true));
}
else {
this.resources.put(rez, new ResourceValue(null, v.getValue(), false));
}
});
}
}
});
}
/**
* if path != null
* @param path
*/
public void onSuccessOrErrorDelayedRequests(String path) {
if (path != null) this.delayedRequests.remove(path);
if (this.delayedRequests.size() == 0 && this.getDelayedRequestsId().size() == 0) {
@ -124,43 +128,5 @@ public class LwM2MClient implements Cloneable {
}
}
public ResourceModel.Operations getOperation(String path) {
LwM2mPath resultIds = new LwM2mPath(path);
return (this.getModelObjects().get(resultIds.getObjectId()) != null) ?
this.getModelObjects().get(resultIds.getObjectId()).getObjectModel().resources.get(resultIds.getResourceId()).operations :
ResourceModel.Operations.NONE;
}
public String getResourceName(String path) {
LwM2mPath resultIds = new LwM2mPath(path);
return (this.getModelObjects().get(resultIds.getObjectId()) != null) ? this.getModelObjects().get(resultIds.getObjectId()).getObjectModel().resources.get(resultIds.getResourceId()).name : "";
}
/**
* @param path - path resource
* @return - value of Resource or null
*/
public String getResourceValue(String path) {
String resValue = null;
LwM2mPath pathIds = new LwM2mPath(path);
ModelObject modelObject = this.getModelObjects().get(pathIds.getObjectId());
if (modelObject != null && modelObject.getInstances().get(pathIds.getObjectInstanceId()) != null) {
LwM2mObjectInstance instance = modelObject.getInstances().get(pathIds.getObjectInstanceId());
if (instance.getResource(pathIds.getResourceId()) != null) {
try {
resValue = instance.getResource(pathIds.getResourceId()).getType() == OPAQUE ?
Hex.encodeHexString((byte[]) instance.getResource(pathIds.getResourceId()).getValue()).toLowerCase() :
(instance.getResource(pathIds.getResourceId()).isMultiInstances()) ?
instance.getResource(pathIds.getResourceId()).getValues().toString() :
// getValueTypeToString(instance.getResource(pathIds.getResourceId()).getValue(), instance.getResource(pathIds.getResourceId()).getType());
(String) converter.convertValue(instance.getResource(pathIds.getResourceId()).getValue(), instance.getResource(pathIds.getResourceId()).getType(), ResourceModel.Type.STRING, pathIds);
} catch (Exception e) {
log.warn("getResourceValue [{}]", e.getStackTrace().toString());
}
}
}
return resValue;
}
}

10
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResourceValue.java

@ -23,4 +23,14 @@ public class ResourceValue {
Map<Integer, ?> values;
Object value;
boolean multiInstances;
public ResourceValue ( Map<Integer, ?> values, Object value, boolean multiInstances) {
this.values = values;
this.value = value;
this.multiInstances = multiInstances;
}
public Object getResourceValue() {
return this.multiInstances ? this.values : this.value;
}
}

6
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/secure/LwM2mInMemorySecurityStore.java

@ -179,11 +179,13 @@ public class LwM2mInMemorySecurityStore extends InMemorySecurityStore {
if (store.getSecurityInfo() != null) {
if (store.getSecurityMode() < DEFAULT_MODE.code) {
String endpoint = store.getSecurityInfo().getEndpoint();
sessions.put(endpoint, new LwM2MClient(endpoint, store.getSecurityInfo().getIdentity(), store.getSecurityInfo(), store.getMsg(), null, null, profileUuid));
// sessions.put(endpoint, new LwM2MClient(endpoint, store.getSecurityInfo().getIdentity(), store.getSecurityInfo(), store.getMsg(), null, null, profileUuid));
sessions.put(endpoint, new LwM2MClient(endpoint, store.getSecurityInfo().getIdentity(), store.getSecurityInfo(), store.getMsg(), null, profileUuid));
}
} else {
if (store.getSecurityMode() == NO_SEC.code && profileUuid != null)
sessions.put(identity, new LwM2MClient(identity, null, null, store.getMsg(), null, null, profileUuid));
// sessions.put(identity, new LwM2MClient(identity, null, null, store.getMsg(), null, null, profileUuid));
sessions.put(identity, new LwM2MClient(identity, null, null, store.getMsg(), null, profileUuid));
else {
log.error("Registration failed: FORBIDDEN/profileUuid/device [{}] , endpointId: [{}]", profileUuid, identity);
/**

8
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java

@ -26,6 +26,8 @@ import org.eclipse.leshan.core.util.StringUtils;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
@Slf4j
@ -120,6 +122,12 @@ public class LwM2mValueConverterImpl implements LwM2mValueConverter {
case INTEGER:
case FLOAT:
return String.valueOf(value);
case TIME:
// return Long.toString(((Date) value).getTime());
String DATE_FORMAT = "MMM d, yyyy HH:mm a";
Long timeValue = ((Date) value).getTime();
DateFormat formatter = new SimpleDateFormat(DATE_FORMAT);
return formatter.format(new Date(timeValue));
default:
break;
}

27
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/lwm2m/LwM2MTransportConfigServer.java

@ -20,6 +20,8 @@ import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.leshan.core.model.ObjectLoader;
import org.eclipse.leshan.core.model.ObjectModel;
import org.eclipse.leshan.core.model.ResourceModel;
import org.eclipse.leshan.core.node.LwM2mPath;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;
@ -33,6 +35,8 @@ import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
@ -173,6 +177,10 @@ public class LwM2MTransportConfigServer {
@Value("${transport.lwm2m.secure.redis_url:}")
private String redisUrl;
@Getter
@Setter
private Map<Integer, ObjectModel> objectModels;
@PostConstruct
public void init() {
modelsValue = ObjectLoader.loadDefault();
@ -188,6 +196,7 @@ public class LwM2MTransportConfigServer {
log.error(" [{}] Read Models", path.getAbsoluteFile());
}
getInKeyStore();
this.objectModels = new ConcurrentHashMap<Integer, ObjectModel>();
}
private File getPathModels() {
@ -238,4 +247,22 @@ public class LwM2MTransportConfigServer {
}
return FULL_FILE_PATH.toUri().getPath();
}
public ResourceModel getResourceModel(LwM2mPath pathIds) {
return (this.objectModels.size()>0 &&
this.objectModels.containsKey(pathIds.getObjectId()) &&
this.objectModels.get(pathIds.getObjectId()).resources.containsKey(pathIds.getResourceId())) ?
this.objectModels.get(pathIds.getObjectId()).resources.get(pathIds.getResourceId()) : null;
}
public ResourceModel.Type getResourceModelType(LwM2mPath pathIds) {
ResourceModel resource = this.getResourceModel(pathIds);
return (resource == null) ? null : resource.type;
}
public ResourceModel.Operations getOperation(LwM2mPath pathIds) {
ResourceModel resource = this.getResourceModel(pathIds);
return (resource == null) ? ResourceModel.Operations.NONE : resource.operations;
}
}

Loading…
Cancel
Save