committed by
GitHub
102 changed files with 5483 additions and 1504 deletions
@ -0,0 +1,296 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
* |
|||
* Copyright (c) 2016 Sierra Wireless and others. |
|||
* |
|||
* All rights reserved. This program and the accompanying materials |
|||
* are made available under the terms of the Eclipse Public License v2.0 |
|||
* and Eclipse Distribution License v1.0 which accompany this distribution. |
|||
* |
|||
* The Eclipse Public License is available at |
|||
* http://www.eclipse.org/legal/epl-v20.html
|
|||
* and the Eclipse Distribution License is available at |
|||
* http://www.eclipse.org/org/documents/edl-v10.html.
|
|||
* |
|||
* Contributors: |
|||
* Sierra Wireless - initial API and implementation |
|||
* Michał Wadowski (Orange) - Add Observe-Composite feature. |
|||
*/ |
|||
package org.eclipse.leshan.server.observation; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.eclipse.leshan.core.node.LwM2mPath; |
|||
import org.eclipse.leshan.core.observation.CompositeObservation; |
|||
import org.eclipse.leshan.core.observation.Observation; |
|||
import org.eclipse.leshan.core.observation.SingleObservation; |
|||
import org.eclipse.leshan.core.peer.LwM2mPeer; |
|||
import org.eclipse.leshan.core.response.ObserveCompositeResponse; |
|||
import org.eclipse.leshan.core.response.ObserveResponse; |
|||
import org.eclipse.leshan.server.endpoint.LwM2mServerEndpoint; |
|||
import org.eclipse.leshan.server.endpoint.LwM2mServerEndpointsProvider; |
|||
import org.eclipse.leshan.server.profile.ClientProfile; |
|||
import org.eclipse.leshan.server.registration.Registration; |
|||
import org.eclipse.leshan.server.registration.RegistrationStore; |
|||
import org.eclipse.leshan.server.registration.RegistrationUpdate; |
|||
import org.eclipse.leshan.server.registration.UpdatedRegistration; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.Collection; |
|||
import java.util.Collections; |
|||
import java.util.HashSet; |
|||
import java.util.List; |
|||
import java.util.Set; |
|||
import java.util.concurrent.CopyOnWriteArrayList; |
|||
|
|||
/** |
|||
* Implementation of the {@link ObservationService} accessing the persisted observation via the provided |
|||
* {@link RegistrationStore}. |
|||
* |
|||
* When a new observation is added or changed or canceled, the registered listeners are notified. |
|||
*/ |
|||
@Slf4j |
|||
public class ObservationServiceImpl implements ObservationService, LwM2mNotificationReceiver { |
|||
|
|||
private final Logger LOG = LoggerFactory.getLogger(ObservationServiceImpl.class); |
|||
|
|||
private final RegistrationStore registrationStore; |
|||
private final LwM2mServerEndpointsProvider endpointProvider; |
|||
private final boolean updateRegistrationOnNotification; |
|||
|
|||
private final List<ObservationListener> listeners = new CopyOnWriteArrayList<>();; |
|||
|
|||
/** |
|||
* Creates an instance of {@link ObservationServiceImpl} |
|||
*/ |
|||
public ObservationServiceImpl(RegistrationStore store, LwM2mServerEndpointsProvider endpointProvider) { |
|||
this(store, endpointProvider, false); |
|||
} |
|||
|
|||
/** |
|||
* Creates an instance of {@link ObservationServiceImpl} |
|||
* |
|||
* @param updateRegistrationOnNotification will activate registration update on observe notification. |
|||
* |
|||
* @since 1.1 |
|||
*/ |
|||
public ObservationServiceImpl(RegistrationStore store, LwM2mServerEndpointsProvider endpointProvider, |
|||
boolean updateRegistrationOnNotification) { |
|||
this.registrationStore = store; |
|||
this.updateRegistrationOnNotification = updateRegistrationOnNotification; |
|||
this.endpointProvider = endpointProvider; |
|||
} |
|||
|
|||
@Override |
|||
public int cancelObservations(Registration registration) { |
|||
// check registration id
|
|||
String registrationId = registration.getId(); |
|||
if (registrationId == null) |
|||
return 0; |
|||
|
|||
Collection<Observation> observations = registrationStore.removeObservations(registrationId); |
|||
if (observations == null) |
|||
return 0; |
|||
|
|||
for (Observation observation : observations) { |
|||
cancel(observation); |
|||
} |
|||
|
|||
return observations.size(); |
|||
} |
|||
|
|||
@Override |
|||
public int cancelObservations(Registration registration, String nodePath) { |
|||
if (registration == null || registration.getId() == null || nodePath == null || nodePath.isEmpty()) |
|||
return 0; |
|||
|
|||
Set<Observation> observations = getObservationsForCancel(registration.getId(), nodePath); |
|||
for (Observation observation : observations) { |
|||
cancelObservation(observation); |
|||
} |
|||
return observations.size(); |
|||
} |
|||
|
|||
@Override |
|||
public int cancelCompositeObservations(Registration registration, String[] nodePaths) { |
|||
if (registration == null || registration.getId() == null || nodePaths == null || nodePaths.length == 0) |
|||
return 0; |
|||
|
|||
Set<Observation> observations = getCompositeObservationsForCancel(registration.getId(), nodePaths); |
|||
for (Observation observation : observations) { |
|||
cancelObservation(observation); |
|||
} |
|||
return observations.size(); |
|||
} |
|||
|
|||
@Override |
|||
public void cancelObservation(Observation observation) { |
|||
if (observation == null) |
|||
return; |
|||
|
|||
registrationStore.removeObservation(observation.getRegistrationId(), observation.getId()); |
|||
cancel(observation); |
|||
} |
|||
|
|||
private void cancel(Observation observation) { |
|||
List<LwM2mServerEndpoint> endpoints = endpointProvider.getEndpoints(); |
|||
for (LwM2mServerEndpoint lwM2mEndpoint : endpoints) { |
|||
lwM2mEndpoint.cancelObservation(observation); |
|||
} |
|||
|
|||
for (ObservationListener listener : listeners) { |
|||
listener.cancelled(observation); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Set<Observation> getObservations(Registration registration) { |
|||
return getObservations(registration.getId()); |
|||
} |
|||
|
|||
private Set<Observation> getObservations(String registrationId) { |
|||
if (registrationId == null) |
|||
return Collections.emptySet(); |
|||
|
|||
return new HashSet<>(registrationStore.getObservations(registrationId)); |
|||
} |
|||
|
|||
private Set<Observation> getCompositeObservationsForCancel(String registrationId, String[] nodePaths) { |
|||
if (registrationId == null || nodePaths == null) |
|||
return Collections.emptySet(); |
|||
|
|||
// array of String to array of LWM2M path
|
|||
List<LwM2mPath> lwPaths = new ArrayList<>(nodePaths.length); |
|||
for (int i = 0; i < nodePaths.length; i++) { |
|||
lwPaths.add(new LwM2mPath(nodePaths[i])); |
|||
} |
|||
|
|||
// search composite-observation
|
|||
Set<Observation> result = new HashSet<>(); |
|||
for (Observation obs : getObservations(registrationId)) { |
|||
if (obs instanceof CompositeObservation) { |
|||
if (lwPaths.equals(((CompositeObservation) obs).getPaths())) { |
|||
result.add(obs); |
|||
} |
|||
} |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
private Set<Observation> getObservationsForCancel(String registrationId, String nodePath) { |
|||
if (registrationId == null || nodePath == null) |
|||
return Collections.emptySet(); |
|||
|
|||
Set<Observation> result = new HashSet<>(); |
|||
LwM2mPath lwPath = new LwM2mPath(nodePath); |
|||
for (Observation obs : getObservations(registrationId)) { |
|||
if (obs instanceof SingleObservation) { |
|||
LwM2mPath lwPathObs = ((SingleObservation) obs).getPath(); |
|||
if (lwPath.equals(lwPathObs) || lwPathObs.startWith(lwPath)) { // nodePath = "3", lwPathObs = "3/0/9": cancel for tne all lwPathObs
|
|||
result.add(obs); |
|||
} else if (!lwPath.equals(lwPathObs) && lwPath.startWith(lwPathObs)) { // nodePath = "3/0/9", lwPathObs = "3": error...
|
|||
String errorMsg = String.format( |
|||
"Unexpected error <cancelObservation>: There is registration with id [%s] existing observation [%s] includes input observation [%s]!", |
|||
registrationId, lwPathObs, lwPath); |
|||
throw new IllegalStateException(errorMsg); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
@Override |
|||
public void addListener(ObservationListener listener) { |
|||
listeners.add(listener); |
|||
} |
|||
|
|||
@Override |
|||
public void removeListener(ObservationListener listener) { |
|||
listeners.remove(listener); |
|||
} |
|||
|
|||
private Registration updateRegistrationOnRegistration(Observation observation, LwM2mPeer sender, |
|||
ClientProfile profile) { |
|||
if (updateRegistrationOnNotification) { |
|||
RegistrationUpdate regUpdate = new RegistrationUpdate(observation.getRegistrationId(), sender, null, null, |
|||
null, null, null, null, null, null, null, null); |
|||
UpdatedRegistration updatedRegistration = registrationStore.updateRegistration(regUpdate); |
|||
if (updatedRegistration == null || updatedRegistration.getUpdatedRegistration() == null) { |
|||
String errorMsg = String.format( |
|||
"Unexpected error: There is no registration with id %s for this observation %s", |
|||
observation.getRegistrationId(), observation); |
|||
LOG.error(errorMsg); |
|||
throw new IllegalStateException(errorMsg); |
|||
} |
|||
return updatedRegistration.getUpdatedRegistration(); |
|||
} |
|||
return profile.getRegistration(); |
|||
} |
|||
|
|||
// ********** NotificationListener interface **********//
|
|||
@Override |
|||
public void onNotification(SingleObservation observation, LwM2mPeer sender, ClientProfile profile, |
|||
ObserveResponse response) { |
|||
try { |
|||
Registration updatedRegistration = updateRegistrationOnRegistration(observation, sender, profile); |
|||
for (ObservationListener listener : listeners) { |
|||
listener.onResponse(observation, updatedRegistration, response); |
|||
} |
|||
} catch (Exception e) { |
|||
for (ObservationListener listener : listeners) { |
|||
listener.onError(observation, profile.getRegistration(), e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onNotification(CompositeObservation observation, LwM2mPeer sender, ClientProfile profile, |
|||
ObserveCompositeResponse response) { |
|||
try { |
|||
Registration updatedRegistration = updateRegistrationOnRegistration(observation, sender, profile); |
|||
for (ObservationListener listener : listeners) { |
|||
listener.onResponse(observation, updatedRegistration, response); |
|||
} |
|||
} catch (Exception e) { |
|||
for (ObservationListener listener : listeners) { |
|||
listener.onError(observation, profile.getRegistration(), e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onError(Observation observation, LwM2mPeer sender, ClientProfile profile, Exception error) { |
|||
for (ObservationListener listener : listeners) { |
|||
listener.onError(observation, profile.getRegistration(), error); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void newObservation(Observation observation, Registration registration) { |
|||
for (ObservationListener listener : listeners) { |
|||
listener.newObservation(observation, registration); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void cancelled(Observation observation) { |
|||
for (ObservationListener listener : listeners) { |
|||
listener.cancelled(observation); |
|||
} |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,147 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.transport.lwm2m.attributes; |
|||
|
|||
import org.eclipse.leshan.core.link.LinkParseException; |
|||
import org.eclipse.leshan.core.link.attributes.InvalidAttributeException; |
|||
import org.eclipse.leshan.core.link.lwm2m.attributes.DefaultLwM2mAttributeParser; |
|||
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute; |
|||
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; |
|||
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.params.ParameterizedTest; |
|||
import org.junit.jupiter.params.provider.Arguments; |
|||
import org.junit.jupiter.params.provider.MethodSource; |
|||
|
|||
import java.util.stream.Stream; |
|||
|
|||
import static org.junit.jupiter.api.Assertions.assertEquals; |
|||
import static org.junit.jupiter.api.Assertions.assertThrows; |
|||
|
|||
public class DefaultLwM2mAttributeParserTest { |
|||
|
|||
private final DefaultLwM2mAttributeParser parser = new DefaultLwM2mAttributeParser(); |
|||
|
|||
private static Stream<Arguments> validAttributes() throws InvalidAttributeException { |
|||
return Stream.of(//
|
|||
Arguments.of("pmin", LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD)), //
|
|||
Arguments.of("pmin=30", LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 30l)), //
|
|||
Arguments.of("pmax", LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD)), //
|
|||
Arguments.of("pmax=60", LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 60l)), //
|
|||
Arguments.of("dim=2", LwM2mAttributes.create(LwM2mAttributes.DIMENSION, 2l)), //
|
|||
Arguments.of("epmin", LwM2mAttributes.create(LwM2mAttributes.EVALUATE_MINIMUM_PERIOD)), //
|
|||
Arguments.of("epmin=30", LwM2mAttributes.create(LwM2mAttributes.EVALUATE_MINIMUM_PERIOD, 30l)), //
|
|||
Arguments.of("epmax", LwM2mAttributes.create(LwM2mAttributes.EVALUATE_MAXIMUM_PERIOD)), //
|
|||
Arguments.of("epmax=60", LwM2mAttributes.create(LwM2mAttributes.EVALUATE_MAXIMUM_PERIOD, 60l)), //
|
|||
Arguments.of("lt", LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN)), //
|
|||
Arguments.of("lt=30", LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, 30d)), //
|
|||
Arguments.of("lt=-30", LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, -30d)), //
|
|||
Arguments.of("lt=30.55", LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, 30.55)), //
|
|||
Arguments.of("lt=-30.55", LwM2mAttributes.create(LwM2mAttributes.LESSER_THAN, -30.55)), //
|
|||
Arguments.of("gt", LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN)), //
|
|||
Arguments.of("gt=60", LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 60d)), //
|
|||
Arguments.of("gt=-60", LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, -60d)), //
|
|||
Arguments.of("gt=60.55", LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 60.55)), //
|
|||
Arguments.of("gt=-60.55", LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, -60.55)), //
|
|||
Arguments.of("st", LwM2mAttributes.create(LwM2mAttributes.STEP)), //
|
|||
Arguments.of("st=60", LwM2mAttributes.create(LwM2mAttributes.STEP, 60d)), //
|
|||
Arguments.of("st=60.55", LwM2mAttributes.create(LwM2mAttributes.STEP, 60.55)) //
|
|||
); |
|||
} |
|||
|
|||
@ParameterizedTest(name = "Tests {index} : {0}") |
|||
@MethodSource("validAttributes") |
|||
public void check_one_valid_attribute(String attributeAsString, LwM2mAttribute<?> expectedValue) |
|||
throws LinkParseException, InvalidAttributeException { |
|||
LwM2mAttributeSet parsed = new LwM2mAttributeSet(parser.parseUriQuery(attributeAsString)); |
|||
LwM2mAttributeSet attResult = new LwM2mAttributeSet(expectedValue); |
|||
assertEquals(parsed, attResult); |
|||
} |
|||
|
|||
private static String[] invalidAttributes() { |
|||
return new String[] { //
|
|||
// MINIMUM_PERIOD
|
|||
"pmin=", //
|
|||
"pmin=1.9", //
|
|||
"pmin=-1.8", //
|
|||
"pmin=a", //
|
|||
// MAXIMUM_PERIOD
|
|||
"pmax=", //
|
|||
"pmax=-2", //
|
|||
"pmax=2.7", //
|
|||
"pmax=-2.6", //
|
|||
"pmax=bc", //
|
|||
// DIMENSION
|
|||
"dim", //
|
|||
"dim=", //
|
|||
"dim=-3", //
|
|||
"dim=3.5", //
|
|||
"dim=-3.4", //
|
|||
"dim=def", //
|
|||
// EVALUATE_MINIMUM_PERIOD
|
|||
"epmin=", //
|
|||
"epmin=-4", //
|
|||
"epmin=4.3", //
|
|||
"epmin=-4.2", //
|
|||
"epmin=ghij", //
|
|||
// EVALUATE_MAXIMUM_PERIOD
|
|||
"epmax=", //
|
|||
"epmax=-5", //
|
|||
"epmax=5.1", //
|
|||
"epmax=-5.9", //
|
|||
"epmax=klmno", //
|
|||
// LESSER_THAN
|
|||
"lt=", //
|
|||
"lt=pqrts", //
|
|||
"lt=0abc", //
|
|||
// GREATER_THAN
|
|||
"gt=", //
|
|||
"gt=uvwxyz", //
|
|||
"gt=0.xyz", //
|
|||
// STEP
|
|||
"st=", //
|
|||
"st=-6", //
|
|||
"st=-6.8", //
|
|||
// Multi-attributes
|
|||
"&pmin&pmax=60>=2&", //
|
|||
"&pmin&pmax=60>=2", //
|
|||
"pmin&pmax=60>=2&", //
|
|||
"pmin&pmax=60&>=2", //
|
|||
"pmin&pmax=60&>=2&&&", //
|
|||
"&&&pmin&pmax=60&>=2", //
|
|||
}; |
|||
} |
|||
|
|||
@ParameterizedTest(name = "Test {index} : {0}") |
|||
@MethodSource("invalidAttributes") |
|||
public void check_invalid_attributes(String invalidAttributesAsString) { |
|||
assertThrows(InvalidAttributeException.class, |
|||
() -> new LwM2mAttributeSet(parser.parseUriQuery(invalidAttributesAsString))); |
|||
|
|||
} |
|||
|
|||
@Test |
|||
public void check_multiple_valid_attributes() throws LinkParseException, InvalidAttributeException { |
|||
|
|||
LwM2mAttributeSet parsed = new LwM2mAttributeSet(parser.parseUriQuery("pmin&pmax=60>=2")); |
|||
LwM2mAttributeSet attResult = new LwM2mAttributeSet( //
|
|||
LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD), //
|
|||
LwM2mAttributes.create(LwM2mAttributes.MAXIMUM_PERIOD, 60l), //
|
|||
LwM2mAttributes.create(LwM2mAttributes.GREATER_THAN, 2d)); |
|||
|
|||
assertEquals(attResult, parsed); |
|||
} |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.transport.lwm2m.attributes; |
|||
|
|||
import org.eclipse.leshan.core.link.LinkParseException; |
|||
import org.eclipse.leshan.core.link.attributes.AttributeSet; |
|||
import org.eclipse.leshan.core.link.lwm2m.DefaultLwM2mLinkParser; |
|||
import org.eclipse.leshan.core.link.lwm2m.LwM2mLink; |
|||
import org.eclipse.leshan.core.link.lwm2m.LwM2mLinkParser; |
|||
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; |
|||
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; |
|||
import org.eclipse.leshan.core.node.LwM2mPath; |
|||
import org.junit.jupiter.api.Test; |
|||
|
|||
import static org.junit.jupiter.api.Assertions.assertEquals; |
|||
import static org.junit.jupiter.api.Assertions.assertThrowsExactly; |
|||
|
|||
public class DefaultLwM2mLinkParserTest { |
|||
|
|||
private final LwM2mLinkParser parser = new DefaultLwM2mLinkParser(); |
|||
|
|||
@Test |
|||
public void check_invalid_values() throws LinkParseException { |
|||
// first check it's OK with valid value (3/0/11 - "errorCodes")
|
|||
LwM2mLink[] parsed = parser.parseLwM2mLinkFromCoreLinkFormat("</3/0/11>;dim=255".getBytes(), null); |
|||
assertEquals(new LwM2mPath(3, 0, 11), parsed[0].getPath()); |
|||
AttributeSet attResult = new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.DIMENSION, 255l)); |
|||
assertEquals(attResult, parsed[0].getAttributes()); |
|||
|
|||
// then check an invalid one
|
|||
assertThrowsExactly(LinkParseException.class, () -> { |
|||
// dim should be between 0-255
|
|||
parser.parseLwM2mLinkFromCoreLinkFormat("</3/0/11>;dim=256".getBytes(), null); |
|||
}); |
|||
|
|||
// first check it's OK with valid value
|
|||
parsed = parser.parseLwM2mLinkFromCoreLinkFormat("</0/1>;ssid=1".getBytes(), null); |
|||
assertEquals(new LwM2mPath(0, 1), parsed[0].getPath()); |
|||
attResult = new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.SHORT_SERVER_ID, 1l)); |
|||
assertEquals(attResult, parsed[0].getAttributes()); |
|||
|
|||
// then check an invalid one
|
|||
assertThrowsExactly(LinkParseException.class, () -> { |
|||
// ssid should be between 1-65534
|
|||
parser.parseLwM2mLinkFromCoreLinkFormat("</0/1>;ssid=0".getBytes(), null); |
|||
}); |
|||
} |
|||
|
|||
@Test |
|||
public void check_attribute_with_no_value_failed() throws LinkParseException { |
|||
// first check it's OK with value
|
|||
LwM2mLink[] parsed = parser.parseLwM2mLinkFromCoreLinkFormat("</3/0/11>;pmin=200".getBytes(), null); |
|||
assertEquals(new LwM2mPath(3, 0, 11), parsed[0].getPath()); |
|||
AttributeSet attResult = new LwM2mAttributeSet(LwM2mAttributes.create(LwM2mAttributes.MINIMUM_PERIOD, 200l)); |
|||
assertEquals(attResult, parsed[0].getAttributes()); |
|||
|
|||
// then check an invalid one
|
|||
assertThrowsExactly(LinkParseException.class, () -> { |
|||
// pmin should be with value
|
|||
parser.parseLwM2mLinkFromCoreLinkFormat("</3/0/11>;pmin".getBytes(), null); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.transport.lwm2m.attributes; |
|||
|
|||
import org.eclipse.leshan.core.link.attributes.InvalidAttributeException; |
|||
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute; |
|||
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeModel; |
|||
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; |
|||
import org.junit.jupiter.params.ParameterizedTest; |
|||
import org.junit.jupiter.params.provider.Arguments; |
|||
import org.junit.jupiter.params.provider.MethodSource; |
|||
|
|||
import java.util.stream.Stream; |
|||
|
|||
import static org.junit.jupiter.api.Assertions.assertFalse; |
|||
import static org.junit.jupiter.api.Assertions.assertNotNull; |
|||
import static org.junit.jupiter.api.Assertions.assertNull; |
|||
import static org.junit.jupiter.api.Assertions.assertThrows; |
|||
|
|||
public class LwM2mAttributesTest { |
|||
|
|||
@ParameterizedTest(name = "Tests {index} : {0}") |
|||
@MethodSource("supportNullAttributes") |
|||
public void check_attribute_can_be_created_with_null_value(LwM2mAttributeModel<?> model) |
|||
throws InvalidAttributeException { |
|||
LwM2mAttribute<?> attribute = LwM2mAttributes.create(model); |
|||
assertNotNull(attribute); |
|||
assertFalse(attribute.hasValue()); |
|||
assertNull(attribute.getValue()); |
|||
attribute = LwM2mAttributes.create(model, null); |
|||
assertNotNull(attribute); |
|||
assertFalse(attribute.hasValue()); |
|||
assertNull(attribute.getValue()); |
|||
} |
|||
|
|||
@ParameterizedTest(name = "Tests {index} : {0}") |
|||
@MethodSource("doesntSupportAttributesWithoutValue") |
|||
public void check_attribute_can_not_be_created_without_value(LwM2mAttributeModel<?> model) { |
|||
assertThrows(UnsupportedOperationException.class, () -> LwM2mAttributes.create(model)); |
|||
} |
|||
|
|||
@ParameterizedTest(name = "Tests {index} : {0}") |
|||
@MethodSource("doesntSupportAttributesWithValueNull") |
|||
public void check_attribute_can_not_be_created_with_null(LwM2mAttributeModel<?> model) { |
|||
assertThrows(NullPointerException.class, () -> LwM2mAttributes.create(model, null)); |
|||
} |
|||
|
|||
private static Stream<Arguments> supportNullAttributes() throws InvalidAttributeException { |
|||
return Stream.of(//
|
|||
Arguments.of(LwM2mAttributes.MINIMUM_PERIOD), //
|
|||
Arguments.of(LwM2mAttributes.MAXIMUM_PERIOD), //
|
|||
Arguments.of(LwM2mAttributes.EVALUATE_MINIMUM_PERIOD), //
|
|||
Arguments.of(LwM2mAttributes.EVALUATE_MAXIMUM_PERIOD), //
|
|||
Arguments.of(LwM2mAttributes.LESSER_THAN), //
|
|||
Arguments.of(LwM2mAttributes.GREATER_THAN), //
|
|||
Arguments.of(LwM2mAttributes.STEP) //
|
|||
); |
|||
} |
|||
|
|||
private static Stream<Arguments> doesntSupportAttributesWithoutValue() throws InvalidAttributeException { |
|||
return Stream.of(//
|
|||
Arguments.of(LwM2mAttributes.ENABLER_VERSION), //
|
|||
Arguments.of(LwM2mAttributes.OBJECT_VERSION)//
|
|||
); |
|||
} |
|||
|
|||
private static Stream<Arguments> doesntSupportAttributesWithValueNull() throws InvalidAttributeException { |
|||
return Stream.of(//
|
|||
Arguments.of(LwM2mAttributes.DIMENSION), //
|
|||
Arguments.of(LwM2mAttributes.SHORT_SERVER_ID) //
|
|||
); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,732 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.transport.lwm2m.client; |
|||
|
|||
import org.eclipse.leshan.client.LwM2mClient; |
|||
import org.eclipse.leshan.client.resource.BaseObjectEnabler; |
|||
import org.eclipse.leshan.client.resource.DummyInstanceEnabler; |
|||
import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler; |
|||
import org.eclipse.leshan.client.resource.LwM2mInstanceEnablerFactory; |
|||
import org.eclipse.leshan.client.resource.listener.ResourceListener; |
|||
import org.eclipse.leshan.client.servers.LwM2mServer; |
|||
import org.eclipse.leshan.client.servers.ServersInfoExtractor; |
|||
import org.eclipse.leshan.client.util.LinkFormatHelper; |
|||
import org.eclipse.leshan.core.Destroyable; |
|||
import org.eclipse.leshan.core.LwM2mId; |
|||
import org.eclipse.leshan.core.Startable; |
|||
import org.eclipse.leshan.core.Stoppable; |
|||
import org.eclipse.leshan.core.link.lwm2m.LwM2mLink; |
|||
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttribute; |
|||
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributeSet; |
|||
import org.eclipse.leshan.core.link.lwm2m.attributes.LwM2mAttributes; |
|||
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; |
|||
import org.eclipse.leshan.core.node.LwM2mObjectInstance; |
|||
import org.eclipse.leshan.core.node.LwM2mPath; |
|||
import org.eclipse.leshan.core.node.LwM2mResource; |
|||
import org.eclipse.leshan.core.node.LwM2mResourceInstance; |
|||
import org.eclipse.leshan.core.request.BootstrapDeleteRequest; |
|||
import org.eclipse.leshan.core.request.BootstrapReadRequest; |
|||
import org.eclipse.leshan.core.request.BootstrapWriteRequest; |
|||
import org.eclipse.leshan.core.request.ContentFormat; |
|||
import org.eclipse.leshan.core.request.CreateRequest; |
|||
import org.eclipse.leshan.core.request.DeleteRequest; |
|||
import org.eclipse.leshan.core.request.DiscoverRequest; |
|||
import org.eclipse.leshan.core.request.DownlinkRequest; |
|||
import org.eclipse.leshan.core.request.ExecuteRequest; |
|||
import org.eclipse.leshan.core.request.ObserveRequest; |
|||
import org.eclipse.leshan.core.request.ReadRequest; |
|||
import org.eclipse.leshan.core.request.WriteAttributesRequest; |
|||
import org.eclipse.leshan.core.request.WriteRequest; |
|||
import org.eclipse.leshan.core.request.WriteRequest.Mode; |
|||
import org.eclipse.leshan.core.response.BootstrapDeleteResponse; |
|||
import org.eclipse.leshan.core.response.BootstrapReadResponse; |
|||
import org.eclipse.leshan.core.response.BootstrapWriteResponse; |
|||
import org.eclipse.leshan.core.response.CreateResponse; |
|||
import org.eclipse.leshan.core.response.DeleteResponse; |
|||
import org.eclipse.leshan.core.response.DiscoverResponse; |
|||
import org.eclipse.leshan.core.response.ExecuteResponse; |
|||
import org.eclipse.leshan.core.response.ObserveResponse; |
|||
import org.eclipse.leshan.core.response.ReadResponse; |
|||
import org.eclipse.leshan.core.response.WriteAttributesResponse; |
|||
import org.eclipse.leshan.core.response.WriteResponse; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.Arrays; |
|||
import java.util.Collection; |
|||
import java.util.Collections; |
|||
import java.util.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Map.Entry; |
|||
|
|||
public class TbLwm2mObjectEnabler extends BaseObjectEnabler implements Destroyable, Startable, Stoppable { |
|||
|
|||
private static Logger LOG = LoggerFactory.getLogger(DummyInstanceEnabler.class); |
|||
|
|||
protected Map<Integer, LwM2mInstanceEnabler> instances; |
|||
|
|||
protected LwM2mInstanceEnablerFactory instanceFactory; |
|||
protected ContentFormat defaultContentFormat; |
|||
|
|||
private LinkFormatHelper tbLinkFormatHelper; |
|||
protected Map<LwM2mPath, LwM2mAttributeSet> lwM2mAttributes; |
|||
public TbLwm2mObjectEnabler(int id, ObjectModel objectModel, Map<Integer, LwM2mInstanceEnabler> instances, |
|||
LwM2mInstanceEnablerFactory instanceFactory, ContentFormat defaultContentFormat) { |
|||
super(id, objectModel); |
|||
this.instances = new HashMap<>(instances); |
|||
; |
|||
this.instanceFactory = instanceFactory; |
|||
this.defaultContentFormat = defaultContentFormat; |
|||
for (Entry<Integer, LwM2mInstanceEnabler> entry : this.instances.entrySet()) { |
|||
instances.put(entry.getKey(), entry.getValue()); |
|||
listenInstance(entry.getValue(), entry.getKey()); |
|||
} |
|||
this.lwM2mAttributes = new HashMap<>(); |
|||
} |
|||
|
|||
public TbLwm2mObjectEnabler(int id, ObjectModel objectModel) { |
|||
super(id, objectModel); |
|||
} |
|||
|
|||
@Override |
|||
public synchronized List<Integer> getAvailableInstanceIds() { |
|||
List<Integer> ids = new ArrayList<>(instances.keySet()); |
|||
Collections.sort(ids); |
|||
return ids; |
|||
} |
|||
|
|||
@Override |
|||
public synchronized List<Integer> getAvailableResourceIds(int instanceId) { |
|||
LwM2mInstanceEnabler instanceEnabler = instances.get(instanceId); |
|||
if (instanceEnabler != null) { |
|||
return instanceEnabler.getAvailableResourceIds(getObjectModel()); |
|||
} else { |
|||
return Collections.emptyList(); |
|||
} |
|||
} |
|||
|
|||
public synchronized void addInstance(int instanceId, LwM2mInstanceEnabler newInstance) { |
|||
instances.put(instanceId, newInstance); |
|||
listenInstance(newInstance, instanceId); |
|||
fireInstancesAdded(instanceId); |
|||
} |
|||
|
|||
public synchronized LwM2mInstanceEnabler getInstance(int instanceId) { |
|||
return instances.get(instanceId); |
|||
} |
|||
|
|||
public synchronized LwM2mInstanceEnabler removeInstance(int instanceId) { |
|||
LwM2mInstanceEnabler removedInstance = instances.remove(instanceId); |
|||
if (removedInstance != null) { |
|||
fireInstancesRemoved(removedInstance.getId()); |
|||
} |
|||
return removedInstance; |
|||
} |
|||
|
|||
@Override |
|||
protected CreateResponse doCreate(LwM2mServer server, CreateRequest request) { |
|||
if (!getObjectModel().multiple && instances.size() > 0) { |
|||
return CreateResponse.badRequest("an instance already exist for this single instance object"); |
|||
} |
|||
|
|||
if (request.unknownObjectInstanceId()) { |
|||
// create instance
|
|||
LwM2mInstanceEnabler newInstance = createInstance(server, getObjectModel().multiple ? null : 0, |
|||
request.getResources()); |
|||
|
|||
// add new instance to this object
|
|||
instances.put(newInstance.getId(), newInstance); |
|||
listenInstance(newInstance, newInstance.getId()); |
|||
fireInstancesAdded(newInstance.getId()); |
|||
|
|||
return CreateResponse |
|||
.success(new LwM2mPath(request.getPath().getObjectId(), newInstance.getId()).toString()); |
|||
} else { |
|||
List<LwM2mObjectInstance> instanceNodes = request.getObjectInstances(); |
|||
|
|||
// checks single object instances
|
|||
if (!getObjectModel().multiple) { |
|||
if (request.getObjectInstances().size() > 1) { |
|||
return CreateResponse.badRequest("can not create several instances on this single instance object"); |
|||
} |
|||
if (request.getObjectInstances().get(0).getId() != 0) { |
|||
return CreateResponse.badRequest("single instance object must use 0 as ID"); |
|||
} |
|||
} |
|||
// ensure instance does not already exists
|
|||
for (LwM2mObjectInstance instance : instanceNodes) { |
|||
if (instances.containsKey(instance.getId())) { |
|||
return CreateResponse.badRequest(String.format("instance %d already exists", instance.getId())); |
|||
} |
|||
} |
|||
|
|||
// create the new instances
|
|||
int[] instanceIds = new int[request.getObjectInstances().size()]; |
|||
int i = 0; |
|||
for (LwM2mObjectInstance instance : request.getObjectInstances()) { |
|||
// create instance
|
|||
LwM2mInstanceEnabler newInstance = createInstance(server, instance.getId(), |
|||
instance.getResources().values()); |
|||
|
|||
// add new instance to this object
|
|||
instances.put(newInstance.getId(), newInstance); |
|||
listenInstance(newInstance, newInstance.getId()); |
|||
|
|||
// store instance ids
|
|||
instanceIds[i] = newInstance.getId(); |
|||
i++; |
|||
} |
|||
fireInstancesAdded(instanceIds); |
|||
return CreateResponse.success(); |
|||
} |
|||
} |
|||
|
|||
protected LwM2mInstanceEnabler createInstance(LwM2mServer server, Integer instanceId, |
|||
Collection<LwM2mResource> resources) { |
|||
// create the new instance
|
|||
LwM2mInstanceEnabler newInstance = instanceFactory.create(getObjectModel(), instanceId, instances.keySet()); |
|||
newInstance.setLwM2mClient(getLwm2mClient()); |
|||
|
|||
// add/write resource
|
|||
for (LwM2mResource resource : resources) { |
|||
newInstance.write(server, true, resource.getId(), resource); |
|||
} |
|||
|
|||
return newInstance; |
|||
} |
|||
|
|||
@Override |
|||
protected ReadResponse doRead(LwM2mServer server, ReadRequest request) { |
|||
LwM2mPath path = request.getPath(); |
|||
|
|||
// Manage Object case
|
|||
if (path.isObject()) { |
|||
List<LwM2mObjectInstance> lwM2mObjectInstances = new ArrayList<>(); |
|||
for (LwM2mInstanceEnabler instance : instances.values()) { |
|||
ReadResponse response = instance.read(server); |
|||
if (response.isSuccess()) { |
|||
lwM2mObjectInstances.add((LwM2mObjectInstance) response.getContent()); |
|||
} |
|||
} |
|||
return ReadResponse.success(new LwM2mObject(getId(), lwM2mObjectInstances)); |
|||
} |
|||
|
|||
// Manage Instance case
|
|||
LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId()); |
|||
if (instance == null) |
|||
return ReadResponse.notFound(); |
|||
|
|||
if (path.getResourceId() == null) { |
|||
return instance.read(server); |
|||
} |
|||
|
|||
// Manage Resource case
|
|||
if (path.getResourceInstanceId() == null) { |
|||
return instance.read(server, path.getResourceId()); |
|||
} |
|||
|
|||
// Manage Resource Instance case
|
|||
return instance.read(server, path.getResourceId(), path.getResourceInstanceId()); |
|||
} |
|||
|
|||
@Override |
|||
protected BootstrapReadResponse doRead(LwM2mServer server, BootstrapReadRequest request) { |
|||
// Basic implementation we delegate to classic Read Request
|
|||
ReadResponse response = doRead(server, |
|||
new ReadRequest(request.getContentFormat(), request.getPath(), request.getCoapRequest())); |
|||
return new BootstrapReadResponse(response.getCode(), response.getContent(), response.getErrorMessage()); |
|||
} |
|||
|
|||
@Override |
|||
protected ObserveResponse doObserve(final LwM2mServer server, final ObserveRequest request) { |
|||
final LwM2mPath path = request.getPath(); |
|||
|
|||
// Manage Object case
|
|||
if (path.isObject()) { |
|||
List<LwM2mObjectInstance> lwM2mObjectInstances = new ArrayList<>(); |
|||
for (LwM2mInstanceEnabler instance : instances.values()) { |
|||
ReadResponse response = instance.observe(server); |
|||
if (response.isSuccess()) { |
|||
lwM2mObjectInstances.add((LwM2mObjectInstance) response.getContent()); |
|||
} |
|||
} |
|||
return ObserveResponse.success(new LwM2mObject(getId(), lwM2mObjectInstances)); |
|||
} |
|||
|
|||
// Manage Instance case
|
|||
final LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId()); |
|||
if (instance == null) |
|||
return ObserveResponse.notFound(); |
|||
|
|||
if (path.getResourceId() == null) { |
|||
return instance.observe(server); |
|||
} |
|||
|
|||
// Manage Resource case
|
|||
if (path.getResourceInstanceId() == null) { |
|||
return instance.observe(server, path.getResourceId()); |
|||
} |
|||
|
|||
// Manage Resource Instance case
|
|||
return instance.observe(server, path.getResourceId(), path.getResourceInstanceId()); |
|||
} |
|||
|
|||
@Override |
|||
protected WriteResponse doWrite(LwM2mServer server, WriteRequest request) { |
|||
LwM2mPath path = request.getPath(); |
|||
|
|||
// Manage Instance case
|
|||
LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId()); |
|||
if (instance == null) |
|||
return WriteResponse.notFound(); |
|||
|
|||
if (path.isObjectInstance()) { |
|||
return instance.write(server, request.isReplaceRequest(), (LwM2mObjectInstance) request.getNode()); |
|||
} |
|||
|
|||
// Manage Resource case
|
|||
if (path.getResourceInstanceId() == null) { |
|||
return instance.write(server, request.isReplaceRequest(), path.getResourceId(), |
|||
(LwM2mResource) request.getNode()); |
|||
} |
|||
|
|||
// Manage Resource Instance case
|
|||
return instance.write(server, false, path.getResourceId(), path.getResourceInstanceId(), |
|||
((LwM2mResourceInstance) request.getNode())); |
|||
} |
|||
|
|||
@Override |
|||
protected BootstrapWriteResponse doWrite(LwM2mServer server, BootstrapWriteRequest request) { |
|||
LwM2mPath path = request.getPath(); |
|||
|
|||
// Manage Object case
|
|||
if (path.isObject()) { |
|||
for (LwM2mObjectInstance instanceNode : ((LwM2mObject) request.getNode()).getInstances().values()) { |
|||
LwM2mInstanceEnabler instanceEnabler = instances.get(instanceNode.getId()); |
|||
if (instanceEnabler == null) { |
|||
doCreate(server, new CreateRequest(path.getObjectId(), instanceNode)); |
|||
} else { |
|||
doWrite(server, new WriteRequest(Mode.REPLACE, path.getObjectId(), instanceEnabler.getId(), |
|||
instanceNode.getResources().values())); |
|||
} |
|||
} |
|||
return BootstrapWriteResponse.success(); |
|||
} |
|||
|
|||
// Manage Instance case
|
|||
if (path.isObjectInstance()) { |
|||
LwM2mObjectInstance instanceNode = (LwM2mObjectInstance) request.getNode(); |
|||
LwM2mInstanceEnabler instanceEnabler = instances.get(path.getObjectInstanceId()); |
|||
if (instanceEnabler == null) { |
|||
doCreate(server, new CreateRequest(path.getObjectId(), instanceNode)); |
|||
} else { |
|||
doWrite(server, new WriteRequest(Mode.REPLACE, request.getContentFormat(), path.getObjectId(), |
|||
path.getObjectInstanceId(), instanceNode.getResources().values())); |
|||
} |
|||
return BootstrapWriteResponse.success(); |
|||
} |
|||
|
|||
// Manage resource case
|
|||
LwM2mResource resource = (LwM2mResource) request.getNode(); |
|||
LwM2mInstanceEnabler instanceEnabler = instances.get(path.getObjectInstanceId()); |
|||
if (instanceEnabler == null) { |
|||
doCreate(server, new CreateRequest(path.getObjectId(), |
|||
new LwM2mObjectInstance(path.getObjectInstanceId(), resource))); |
|||
} else { |
|||
instanceEnabler.write(server, true, path.getResourceId(), resource); |
|||
} |
|||
return BootstrapWriteResponse.success(); |
|||
} |
|||
|
|||
@Override |
|||
protected ExecuteResponse doExecute(LwM2mServer server, ExecuteRequest request) { |
|||
LwM2mPath path = request.getPath(); |
|||
LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId()); |
|||
if (instance == null) { |
|||
return ExecuteResponse.notFound(); |
|||
} |
|||
return instance.execute(server, path.getResourceId(), request.getArguments()); |
|||
} |
|||
|
|||
@Override |
|||
protected DeleteResponse doDelete(LwM2mServer server, DeleteRequest request) { |
|||
LwM2mInstanceEnabler deletedInstance = instances.remove(request.getPath().getObjectInstanceId()); |
|||
if (deletedInstance != null) { |
|||
deletedInstance.onDelete(server); |
|||
fireInstancesRemoved(deletedInstance.getId()); |
|||
return DeleteResponse.success(); |
|||
} |
|||
return DeleteResponse.notFound(); |
|||
} |
|||
|
|||
@Override |
|||
public BootstrapDeleteResponse doDelete(LwM2mServer server, BootstrapDeleteRequest request) { |
|||
if (request.getPath().isRoot() || request.getPath().isObject()) { |
|||
if (id == LwM2mId.SECURITY) { |
|||
// For security object, we clean everything except bootstrap Server account.
|
|||
|
|||
// Get bootstrap account and store removed instances ids
|
|||
Entry<Integer, LwM2mInstanceEnabler> bootstrapServerAccount = null; |
|||
int[] instanceIds = new int[instances.size()]; |
|||
int i = 0; |
|||
for (Entry<Integer, LwM2mInstanceEnabler> instance : instances.entrySet()) { |
|||
if (ServersInfoExtractor.isBootstrapServer(instance.getValue())) { |
|||
bootstrapServerAccount = instance; |
|||
} else { |
|||
// Store instance ids
|
|||
instanceIds[i] = instance.getKey(); |
|||
i++; |
|||
} |
|||
} |
|||
// Clear everything
|
|||
instances.clear(); |
|||
|
|||
// Put bootstrap account again
|
|||
if (bootstrapServerAccount != null) { |
|||
instances.put(bootstrapServerAccount.getKey(), bootstrapServerAccount.getValue()); |
|||
} |
|||
|
|||
fireInstancesRemoved(instanceIds); |
|||
return BootstrapDeleteResponse.success(); |
|||
} else if (id == LwM2mId.OSCORE) { |
|||
// For OSCORE object, we clean everything except OSCORE object link to bootstrap Server account.
|
|||
|
|||
// Get bootstrap account
|
|||
LwM2mObjectInstance bootstrapInstance = ServersInfoExtractor.getBootstrapSecurityInstance( |
|||
getLwm2mClient().getObjectTree().getObjectEnabler(LwM2mId.SECURITY)); |
|||
// Get OSCORE instance ID associated to it
|
|||
Integer bootstrapOscoreInstanceId = bootstrapInstance != null |
|||
? ServersInfoExtractor.getOscoreSecurityMode(bootstrapInstance) |
|||
: null; |
|||
|
|||
// if bootstrap server use OSCORE,
|
|||
// search the OSCORE instance for this ID and store removed instances ids
|
|||
if (bootstrapOscoreInstanceId != null) { |
|||
Entry<Integer, LwM2mInstanceEnabler> bootstrapServerOscore = null; |
|||
int[] instanceIds = new int[instances.size()]; |
|||
int i = 0; |
|||
for (Entry<Integer, LwM2mInstanceEnabler> instance : instances.entrySet()) { |
|||
if (bootstrapOscoreInstanceId.equals(instance.getKey())) { |
|||
bootstrapServerOscore = instance; |
|||
} else { |
|||
// Store instance ids
|
|||
instanceIds[i] = instance.getKey(); |
|||
i++; |
|||
} |
|||
} |
|||
|
|||
// Clear everything
|
|||
instances.clear(); |
|||
|
|||
// Put bootstrap OSCORE instance again
|
|||
if (bootstrapServerOscore != null) { |
|||
instances.put(bootstrapServerOscore.getKey(), bootstrapServerOscore.getValue()); |
|||
} |
|||
fireInstancesRemoved(instanceIds); |
|||
return BootstrapDeleteResponse.success(); |
|||
} |
|||
// else delete everything.
|
|||
} |
|||
|
|||
// In all other cases, just delete everything
|
|||
instances.clear(); |
|||
// fired instances removed
|
|||
int[] instanceIds = new int[instances.size()]; |
|||
int i = 0; |
|||
for (Entry<Integer, LwM2mInstanceEnabler> instance : instances.entrySet()) { |
|||
instanceIds[i] = instance.getKey(); |
|||
i++; |
|||
} |
|||
fireInstancesRemoved(instanceIds); |
|||
|
|||
return BootstrapDeleteResponse.success(); |
|||
} else if (request.getPath().isObjectInstance()) { |
|||
if (id == LwM2mId.SECURITY) { |
|||
// For security object, deleting bootstrap Server account is not allowed
|
|||
LwM2mInstanceEnabler instance = instances.get(request.getPath().getObjectInstanceId()); |
|||
if (instance == null) { |
|||
return BootstrapDeleteResponse |
|||
.badRequest(String.format("Instance %s not found", request.getPath())); |
|||
} else if (ServersInfoExtractor.isBootstrapServer(instance)) { |
|||
return BootstrapDeleteResponse.badRequest("bootstrap server can not be deleted"); |
|||
} |
|||
} else if (id == LwM2mId.OSCORE) { |
|||
// For OSCORE object, deleting instance linked to Bootstrap account is not allowed
|
|||
|
|||
// Get bootstrap instance
|
|||
LwM2mObjectInstance bootstrapInstance = ServersInfoExtractor.getBootstrapSecurityInstance( |
|||
getLwm2mClient().getObjectTree().getObjectEnabler(LwM2mId.SECURITY)); |
|||
// Get OSCORE instance ID associated to it
|
|||
Integer bootstrapOscoreInstanceId = bootstrapInstance != null |
|||
? ServersInfoExtractor.getOscoreSecurityMode(bootstrapInstance) |
|||
: null; |
|||
|
|||
if (bootstrapOscoreInstanceId != null |
|||
&& bootstrapOscoreInstanceId.equals(request.getPath().getObjectInstanceId())) { |
|||
return BootstrapDeleteResponse |
|||
.badRequest("OSCORE instance linked to bootstrap server can not be deleted"); |
|||
} |
|||
} |
|||
if (null != instances.remove(request.getPath().getObjectInstanceId())) { |
|||
fireInstancesRemoved(request.getPath().getObjectInstanceId()); |
|||
return BootstrapDeleteResponse.success(); |
|||
} else { |
|||
return BootstrapDeleteResponse.badRequest(String.format("Instance %s not found", request.getPath())); |
|||
} |
|||
} |
|||
return BootstrapDeleteResponse.badRequest(String.format("unexcepted path %s", request.getPath())); |
|||
} |
|||
|
|||
protected void listenInstance(LwM2mInstanceEnabler instance, final int instanceId) { |
|||
instance.addResourceListener(new ResourceListener() { |
|||
@Override |
|||
public void resourceChanged(LwM2mPath... paths) { |
|||
for (LwM2mPath path : paths) { |
|||
if (!isValid(instanceId, path)) { |
|||
LOG.warn("InstanceEnabler ({}) of object ({}) try to raise a change of {} which seems invalid.", |
|||
instanceId, getId(), path); |
|||
} |
|||
} |
|||
fireResourcesChanged(paths); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
protected boolean isValid(int instanceId, LwM2mPath pathToValidate) { |
|||
if (!(pathToValidate.isResource() || pathToValidate.isResourceInstance())) |
|||
return false; |
|||
|
|||
if (pathToValidate.getObjectId() != getId()) { |
|||
return false; |
|||
} |
|||
|
|||
if (pathToValidate.getObjectInstanceId() != instanceId) { |
|||
return false; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
@Override |
|||
public ContentFormat getDefaultEncodingFormat(DownlinkRequest<?> request) { |
|||
return defaultContentFormat; |
|||
} |
|||
|
|||
@Override |
|||
public void init(LwM2mClient client, LinkFormatHelper linkFormatHelper) { |
|||
super.init(client, linkFormatHelper); |
|||
this.tbLinkFormatHelper = linkFormatHelper; |
|||
for (LwM2mInstanceEnabler instanceEnabler : instances.values()) { |
|||
instanceEnabler.setLwM2mClient(client); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void destroy() { |
|||
for (LwM2mInstanceEnabler instanceEnabler : instances.values()) { |
|||
if (instanceEnabler instanceof Destroyable) { |
|||
((Destroyable) instanceEnabler).destroy(); |
|||
} else if (instanceEnabler instanceof Stoppable) { |
|||
((Stoppable) instanceEnabler).stop(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void start() { |
|||
for (LwM2mInstanceEnabler instanceEnabler : instances.values()) { |
|||
if (instanceEnabler instanceof Startable) { |
|||
((Startable) instanceEnabler).start(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void stop() { |
|||
for (LwM2mInstanceEnabler instanceEnabler : instances.values()) { |
|||
if (instanceEnabler instanceof Stoppable) { |
|||
((Stoppable) instanceEnabler).stop(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public synchronized WriteAttributesResponse writeAttributes(LwM2mServer server, WriteAttributesRequest request) { |
|||
// execute is not supported for bootstrap
|
|||
if (server.isLwm2mBootstrapServer()) { |
|||
return WriteAttributesResponse.methodNotAllowed(); |
|||
} |
|||
// return WriteAttributesResponse.internalServerError("not implemented");
|
|||
return doWriteAttributes(server, request); |
|||
} |
|||
|
|||
/** |
|||
* <NOTIFICATION> Class Attributes |
|||
* - pmin (def = 0(sec)) Integer Resource/Object Instance/Object Readable Resource |
|||
* - pmax (def = -- ) Integer Resource/Object Instance/Object Readable Resource |
|||
* - Greater Than gt (def = -- ) Float Resource Numerical&Readable Resource |
|||
* - Less Than lt (def = -- ) Float Resource Numerical&Readable Resource |
|||
* - Step st (def = -- ) Float Resource Numerical&Readable Resource |
|||
*/ |
|||
public WriteAttributesResponse doWriteAttributes(LwM2mServer server, WriteAttributesRequest request) { |
|||
LwM2mPath lwM2mPath = request.getPath(); |
|||
LwM2mAttributeSet attributeSet = lwM2mAttributes.get(lwM2mPath); |
|||
Map <String, LwM2mAttribute<?>> attributes = new HashMap<>(); |
|||
|
|||
for (LwM2mAttribute attr : request.getAttributes().getLwM2mAttributes()) { |
|||
if (attr.getName().equals("pmax") || attr.getName().equals("pmin")) { |
|||
if (lwM2mPath.isObject() || lwM2mPath.isObjectInstance() || lwM2mPath.isResource()) { |
|||
attributes.put(attr.getName(), attr); |
|||
} else { |
|||
return WriteAttributesResponse.badRequest("Attribute " + attr.getName() + " can be used for only Resource/Object Instance/Object."); |
|||
} |
|||
} else if (attr.getName().equals("gt") || attr.getName().equals("lt") || attr.getName().equals("st")) { |
|||
if (lwM2mPath.isResource()) { |
|||
attributes.put(attr.getName(), attr); |
|||
} else { |
|||
return WriteAttributesResponse.badRequest("Attribute " + attr.getName() + " can be used for only Resource."); |
|||
} |
|||
} |
|||
} |
|||
if (attributes.size()>0){ |
|||
if (attributeSet == null) { |
|||
attributeSet = new LwM2mAttributeSet(attributes.values()); |
|||
} else { |
|||
Iterable<LwM2mAttribute<?>> lwM2mAttributeIterable = attributeSet.getLwM2mAttributes(); |
|||
Map <String, LwM2mAttribute<?>> attributesOld = new HashMap<>(); |
|||
for (LwM2mAttribute<?> attr : lwM2mAttributeIterable) { |
|||
attributesOld.put(attr.getName(), attr); |
|||
} |
|||
attributesOld.putAll(attributes); |
|||
attributeSet = new LwM2mAttributeSet(attributesOld.values()); |
|||
} |
|||
lwM2mAttributes.put(lwM2mPath, attributeSet); |
|||
return WriteAttributesResponse.success(); |
|||
} |
|||
return WriteAttributesResponse.internalServerError("not implemented"); |
|||
} |
|||
|
|||
@Override |
|||
public synchronized DiscoverResponse discover(LwM2mServer server, DiscoverRequest request) { |
|||
|
|||
if (server.isLwm2mBootstrapServer()) { |
|||
// discover is not supported for bootstrap
|
|||
return DiscoverResponse.methodNotAllowed(); |
|||
} |
|||
|
|||
if (id == LwM2mId.SECURITY || id == LwM2mId.OSCORE) { |
|||
return DiscoverResponse.notFound(); |
|||
} |
|||
return doDiscover(server, request); |
|||
|
|||
} |
|||
|
|||
protected DiscoverResponse doDiscover(LwM2mServer server, DiscoverRequest request) { |
|||
|
|||
LwM2mPath path = request.getPath(); |
|||
if (path.isObject()) { |
|||
LwM2mLink[] ObjectLinks = linkUpdateAttributes(this.tbLinkFormatHelper.getObjectDescription(this, null), server); |
|||
return DiscoverResponse.success(ObjectLinks); |
|||
|
|||
} else if (path.isObjectInstance()) { |
|||
// Manage discover on instance
|
|||
if (!getAvailableInstanceIds().contains(path.getObjectInstanceId())) |
|||
return DiscoverResponse.notFound(); |
|||
|
|||
LwM2mLink[] instanceLink = linkUpdateAttributes(this.tbLinkFormatHelper.getInstanceDescription(this, path.getObjectInstanceId(), null), server); |
|||
return DiscoverResponse.success(instanceLink); |
|||
|
|||
} else if (path.isResource()) { |
|||
// Manage discover on resource
|
|||
if (!getAvailableInstanceIds().contains(path.getObjectInstanceId())) |
|||
return DiscoverResponse.notFound(); |
|||
|
|||
ResourceModel resourceModel = getObjectModel().resources.get(path.getResourceId()); |
|||
if (resourceModel == null) |
|||
return DiscoverResponse.notFound(); |
|||
|
|||
if (!getAvailableResourceIds(path.getObjectInstanceId()).contains(path.getResourceId())) |
|||
return DiscoverResponse.notFound(); |
|||
|
|||
LwM2mLink resourceLink = linkAddAttribute( |
|||
this.tbLinkFormatHelper.getResourceDescription(this, path.getObjectInstanceId(), path.getResourceId(), null), |
|||
server); |
|||
return DiscoverResponse.success(new LwM2mLink[] { resourceLink }); |
|||
} |
|||
return DiscoverResponse.badRequest(null); |
|||
} |
|||
|
|||
private LwM2mLink[] linkUpdateAttributes(LwM2mLink[] links, LwM2mServer server) { |
|||
return Arrays.stream(links) |
|||
.map(link -> linkAddAttribute(link, server)) |
|||
.toArray(LwM2mLink[]::new); |
|||
} |
|||
|
|||
private LwM2mLink linkAddAttribute(LwM2mLink link, LwM2mServer server) { |
|||
|
|||
LwM2mAttributeSet lwM2mAttributeSetDop = null; |
|||
if (this.lwM2mAttributes.get(link.getPath())!= null){ |
|||
lwM2mAttributeSetDop = this.lwM2mAttributes.get(link.getPath()); |
|||
} |
|||
LwM2mAttribute resourceAttributeDim = getResourceAttributes (server, link.getPath()); |
|||
|
|||
Map <String, LwM2mAttribute<?>> attributes = new HashMap<>(); |
|||
if (link.getAttributes() != null) { |
|||
for (LwM2mAttribute attr : link.getAttributes().getLwM2mAttributes()) { |
|||
attributes.put(attr.getName(), attr); |
|||
} |
|||
} |
|||
if (lwM2mAttributeSetDop != null) { |
|||
for (LwM2mAttribute attr : lwM2mAttributeSetDop.getLwM2mAttributes()) { |
|||
attributes.put(attr.getName(), attr); |
|||
} |
|||
} |
|||
if (resourceAttributeDim != null) { |
|||
attributes.put(resourceAttributeDim.getName(), resourceAttributeDim); |
|||
} |
|||
return new LwM2mLink(link.getRootPath(), link.getPath(), attributes.values()); |
|||
} |
|||
|
|||
protected LwM2mAttribute getResourceAttributes (LwM2mServer server, LwM2mPath path) { |
|||
ResourceModel resourceModel = getObjectModel().resources.get(path.getResourceId()); |
|||
if (path.isResource() && resourceModel.multiple) { |
|||
return getResourceAttributeDim(path, server); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
protected LwM2mAttribute getResourceAttributeDim(LwM2mPath path, LwM2mServer server) { |
|||
LwM2mInstanceEnabler instance = instances.get(path.getObjectInstanceId()); |
|||
try { |
|||
ReadResponse readResponse = instance.read(server, path.getResourceId()); |
|||
if (readResponse.getCode().getCode()==205 && readResponse.getContent() instanceof LwM2mMultipleResource) { |
|||
long valueDim = ((LwM2mMultipleResource)readResponse.getContent()).getInstances().size(); |
|||
return LwM2mAttributes.create(LwM2mAttributes.DIMENSION, valueDim); |
|||
} else { |
|||
return null; |
|||
} |
|||
} catch (Exception e ){ |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
@ -0,0 +1,71 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.transport.lwm2m.client; |
|||
|
|||
import org.eclipse.leshan.client.resource.BaseInstanceEnablerFactory; |
|||
import org.eclipse.leshan.client.resource.LwM2mInstanceEnabler; |
|||
import org.eclipse.leshan.client.resource.LwM2mObjectEnabler; |
|||
import org.eclipse.leshan.client.resource.ObjectsInitializer; |
|||
import org.eclipse.leshan.core.model.LwM2mModel; |
|||
import org.eclipse.leshan.core.model.ObjectModel; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
public class TbObjectsInitializer extends ObjectsInitializer { |
|||
|
|||
|
|||
public TbObjectsInitializer(LwM2mModel model) { |
|||
super(model); |
|||
} |
|||
|
|||
public List<LwM2mObjectEnabler> create(int... objectId) { |
|||
List<LwM2mObjectEnabler> enablers = new ArrayList<>(); |
|||
for (int anObjectId : objectId) { |
|||
LwM2mObjectEnabler objectEnabler = create(anObjectId); |
|||
if (objectEnabler != null) |
|||
enablers.add(objectEnabler); |
|||
} |
|||
return enablers; |
|||
} |
|||
|
|||
public LwM2mObjectEnabler create(int objectId) { |
|||
ObjectModel objectModel = model.getObjectModel(objectId); |
|||
if (objectModel == null) { |
|||
throw new IllegalArgumentException( |
|||
"Cannot create object for id " + objectId + " because no model is defined for this id."); |
|||
} |
|||
return createNodeEnabler(objectModel); |
|||
} |
|||
|
|||
protected LwM2mObjectEnabler createNodeEnabler(ObjectModel objectModel) { |
|||
Map<Integer, LwM2mInstanceEnabler> instances = new HashMap<>(); |
|||
LwM2mInstanceEnabler[] newInstances = createInstances(objectModel); |
|||
for (LwM2mInstanceEnabler instance : newInstances) { |
|||
// set id if not already set
|
|||
if (instance.getId() == null) { |
|||
int id = BaseInstanceEnablerFactory.generateNewInstanceId(instances.keySet()); |
|||
instance.setId(id); |
|||
} |
|||
instance.setModel(objectModel); |
|||
instances.put(instance.getId(), instance); |
|||
} |
|||
return new TbLwm2mObjectEnabler(objectModel.id, objectModel, instances, getFactoryFor(objectModel), |
|||
getContentFormat(objectModel.id)); |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.transport.lwm2m.rpc; |
|||
|
|||
import org.junit.Before; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
|
|||
@DaoSqlTest |
|||
public abstract class AbstractRpcLwM2MIntegrationObserveTest extends AbstractRpcLwM2MIntegrationTest{ |
|||
private final String[] RESOURCES_RPC_MULTIPLE_19 = new String[]{"0.xml", "1.xml", "2.xml", "3.xml", "5.xml", "6.xml", "9.xml", "19.xml", "3303.xml"}; |
|||
|
|||
public AbstractRpcLwM2MIntegrationObserveTest() { |
|||
setResources(this.RESOURCES_RPC_MULTIPLE_19); |
|||
} |
|||
|
|||
@Before |
|||
public void initTest () throws Exception { |
|||
awaitObserveReadAll(2, deviceId); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,487 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.transport.lwm2m.rpc.sql; |
|||
|
|||
import com.fasterxml.jackson.databind.node.ObjectNode; |
|||
import org.eclipse.leshan.core.ResponseCode; |
|||
import org.junit.Test; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationObserveTest; |
|||
|
|||
import static org.junit.Assert.assertEquals; |
|||
import static org.junit.Assert.assertFalse; |
|||
import static org.junit.Assert.assertTrue; |
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_0; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_1; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_15; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_2; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_5; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_9; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_0_0; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_19_1_0; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_14; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3_9; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_INSTANCE_ID_0; |
|||
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; |
|||
|
|||
public class RpcLwm2MIntegrationObserveCompositeTest extends AbstractRpcLwM2MIntegrationObserveTest { |
|||
|
|||
|
|||
/** |
|||
* ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9", "19/1/0/0"]} - Ok |
|||
* @throws Exception |
|||
*/ |
|||
@Test |
|||
public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_LwM2mResourceInstance() throws Exception { |
|||
sendCancelObserveAllWithAwait(deviceId); |
|||
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; |
|||
String expectedIdVer5_0_5= objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; |
|||
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; |
|||
String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0; |
|||
String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + idVer_3_0_9 + "\", \"" + expectedIdVer19_1_0_0 + "\"]"; |
|||
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
String actualValues = rpcActualResult.get("value").asText(); |
|||
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_7) + "=LwM2mSingleResource")); |
|||
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_3) + "=LwM2mSingleResource")); |
|||
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_5) + "=LwM2mSingleResource")); |
|||
assertTrue(actualValues.contains(fromVersionedIdToObjectId(idVer_3_0_9) + "=LwM2mSingleResource")); |
|||
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer19_1_0_0) + "=LwM2mResourceInstance")); |
|||
} |
|||
|
|||
/** |
|||
* ObserveComposite {"ids":["19/1/0/0", "5/0"]} - Ok |
|||
* @throws Exception |
|||
*/ |
|||
@Test |
|||
public void testObserveComposite_ObjectInstanceWithOtherObjectResourceInstance_Result_CONTENT_Ok() throws Exception { |
|||
sendCancelObserveAllWithAwait(deviceId); |
|||
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0; |
|||
String expectedIdVer5_0 = objectInstanceIdVer_5; |
|||
String expectedIds = "[\"" + expectedIdVer19_1_0 + "\", \"" + expectedIdVer5_0 + "\"]"; |
|||
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
String actual= rpcActualResult.get("value").asText(); |
|||
assertTrue(actual.contains(fromVersionedIdToObjectId(expectedIdVer19_1_0) + "=LwM2mMultipleResource")); |
|||
assertTrue(actual.contains(fromVersionedIdToObjectId(expectedIdVer5_0) + "=LwM2mObjectInstance")); |
|||
} |
|||
|
|||
/** |
|||
* ObserveComposite {"ids":["5/0/7", "5/0/2"]} - Ok |
|||
* "5/0/2" - Execute^ result == null |
|||
* @throws Exception |
|||
*/ |
|||
@Test |
|||
public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_If_Error_Null() throws Exception { |
|||
// sendCancelObserveAllWithAwait(deviceId);
|
|||
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; |
|||
String expectedIdVer5_0_2 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2; |
|||
String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_2 + "\"]"; |
|||
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
String actualValues = rpcActualResult.get("value").asText(); |
|||
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_7) + "=LwM2mSingleResource")); |
|||
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_2) + "=null")); |
|||
} |
|||
|
|||
|
|||
/** |
|||
* ObserveComposite {"ids":["5/0/7", "5/0/2"]} - Ok |
|||
* "5/0" contains "5/0/2" |
|||
* @throws Exception |
|||
*/ |
|||
@Test |
|||
public void testObserveComposite_Result_BAD_REQUEST_ONE_PATH_CONTAINCE_OTHER() throws Exception { |
|||
sendCancelObserveAllWithAwait(deviceId); |
|||
String expectedIdVer5_0 = objectInstanceIdVer_5; |
|||
String expectedIdVer5_0_2 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_2; |
|||
String expectedIds = "[\"" + expectedIdVer5_0 + "\", \"" + expectedIdVer5_0_2 + "\"]"; |
|||
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); |
|||
String actual= rpcActualResult.get("error").asText(); |
|||
String expected = "Invalid path list : /5/0 and /5/0/2 are overlapped paths"; |
|||
assertTrue(expected.equals(actual)); |
|||
} |
|||
|
|||
/** |
|||
* Previous -> "3/0/9" |
|||
* ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9"]} - CONTENT |
|||
* @throws Exception |
|||
*/ |
|||
@Test |
|||
public void testObserveCompositeThereAreObservationOneResource_Result_CONTENT_Value_ObservationAddIfAbsent() throws Exception { |
|||
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; |
|||
String expectedIdVer5_0_5= objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; |
|||
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; |
|||
String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + idVer_3_0_9 + "\"]"; |
|||
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
String expectedResult = "/3/0/9=LwM2mSingleResource [id=9"; |
|||
assertTrue(rpcActualResult.get("value").asText().contains(expectedResult)); |
|||
} |
|||
|
|||
/** |
|||
* ObserveComposite {"ids":["5/0/7", "5/0/5", "5/0/3", "3/0/9", "19/1/0"]} - Ok |
|||
* @throws Exception |
|||
*/ |
|||
@Test |
|||
public void testObserveCompositeAnyResources_Result_CONTENT_Value_LwM2mSingleResource_LwM2mMultipleResource() throws Exception { |
|||
sendCancelObserveAllWithAwait(deviceId); |
|||
|
|||
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; |
|||
String expectedIdVer5_0_5= objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; |
|||
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; |
|||
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0; |
|||
String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + idVer_3_0_9 + "\", \"" + expectedIdVer19_1_0 + "\"]"; |
|||
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
String actualValues = rpcActualResult.get("value").asText(); |
|||
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_7) + "=LwM2mSingleResource")); |
|||
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_3) + "=LwM2mSingleResource")); |
|||
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_5 + "=LwM2mSingleResource"))); |
|||
assertTrue(actualValues.contains(fromVersionedIdToObjectId(idVer_3_0_9) + "=LwM2mSingleResource")); |
|||
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer19_1_0) + "=LwM2mMultipleResource")); |
|||
} |
|||
|
|||
/** |
|||
* ObserveComposite with keyName {"keys":["batteryLevel", "UtfOffset", "dataRead", "dataWrite"]} - Ok |
|||
* @throws Exception |
|||
*/ |
|||
@Test |
|||
public void testObserveCompositeWithKeyName_Result_CONTENT_Value_SingleResources() throws Exception { |
|||
sendCancelObserveAllWithAwait(deviceId); |
|||
|
|||
String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9; |
|||
String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; |
|||
String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0; |
|||
String expectedKey19_1_0 = RESOURCE_ID_NAME_19_1_0; |
|||
String expectedKeys = "[\"" + expectedKey3_0_9 + "\", \"" + expectedKey3_0_14 + "\", \"" + expectedKey19_0_0 + "\", \"" + expectedKey19_1_0 + "\"]"; |
|||
String actualResult = sendCompositeRPCByKeys("ObserveComposite", expectedKeys); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
String actualValues = rpcActualResult.get("value").asText(); |
|||
String expectedIdVer3_0_14 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; |
|||
String expectedIdVer19_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_0; |
|||
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0; |
|||
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer3_0_14))); |
|||
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer19_0_0))); |
|||
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer19_1_0))); |
|||
assertTrue(actualValues.contains(fromVersionedIdToObjectId(idVer_3_0_9))); |
|||
} |
|||
|
|||
/** |
|||
* ObserveComposite with keyName {"keys":["batteryLevel", "UtfOffset", "dataRead", "dataWrite"]} - - BAD_REQUEST |
|||
* @throws Exception |
|||
*/ |
|||
@Test |
|||
public void testObserveCompositeWithKeyNameThereAreObservationOneResource_Result_CONTENT_Value_ObservationAddIfAbsent() throws Exception { |
|||
String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9; |
|||
String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; |
|||
String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0; |
|||
String expectedKey19_1_0 = RESOURCE_ID_NAME_19_1_0; |
|||
String expectedKeys = "[\"" + expectedKey3_0_9 + "\", \"" + expectedKey3_0_14 + "\", \"" + expectedKey19_0_0 + "\", \"" + expectedKey19_1_0 + "\"]"; |
|||
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0; |
|||
String actualResult = sendCompositeRPCByKeys("ObserveComposite", expectedKeys); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
String actual = rpcActualResult.get("value").asText(); |
|||
assertTrue(actual.contains(fromVersionedIdToObjectId(expectedIdVer19_1_0) + "=LwM2mMultipleResource")); |
|||
} |
|||
|
|||
/** |
|||
* ObserveReadAll |
|||
* {"result":"CONTENT","value":"[\"CompositeObservation: [/19/1/0\",\"/19/0/0\",\"/3/0/14\",\"/3/0/9]\"]"} - Ok |
|||
* @throws Exception |
|||
*/ |
|||
@Test |
|||
public void testObserveReadAll_AfterCompositeObservation_Result_CONTENT_Value_SingleObservation_Only() throws Exception { |
|||
sendCancelObserveAllWithAwait(deviceId); |
|||
|
|||
String expectedKey3_0_9 = RESOURCE_ID_NAME_3_9; |
|||
String expectedKey3_0_14 = RESOURCE_ID_NAME_3_14; |
|||
String expectedKey19_0_0 = RESOURCE_ID_NAME_19_0_0; |
|||
String expectedKey19_1_0 = RESOURCE_ID_NAME_19_1_0; |
|||
String expectedKeys = "[\"" + expectedKey3_0_9 + "\", \"" + expectedKey3_0_14 + "\", \"" + expectedKey19_0_0 + "\", \"" + expectedKey19_1_0 + "\"]"; |
|||
String actualResult = sendCompositeRPCByKeys("ObserveComposite", expectedKeys); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null); |
|||
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); |
|||
String actualValues = rpcActualResultReadAll.get("value").asText(); |
|||
String expectedIdVer3_0_14 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; |
|||
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0; |
|||
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_3_0_9))); |
|||
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer3_0_14))); |
|||
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_1_0))); |
|||
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_19_0_0))); |
|||
} |
|||
|
|||
/** |
|||
* ObserveReadAll |
|||
* {"result":"CONTENT","value":"{"result":"CONTENT","value":"["SingleObservation:/3/0/9","SingleObservation:/3/0/14","SingleObservation:/19/1/0/0","SingleObservation:/19/0/0"]"} - Ok |
|||
* @throws Exception |
|||
*/ |
|||
@Test |
|||
public void testObserveReadAll_Result_CONTENT_Value_SingleObservation_Only() throws Exception { |
|||
sendCancelObserveAllWithAwait(deviceId); |
|||
|
|||
String expectedIdVer3_0_14 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; |
|||
String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0; |
|||
String actualResult3_0_9 = sendObserve("Observe", idVer_3_0_9); |
|||
ObjectNode rpcActualResult3_0_9 = JacksonUtil.fromString(actualResult3_0_9, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult3_0_9.get("result").asText()); |
|||
String actualResult3_0_14 = sendObserve("Observe", expectedIdVer3_0_14); |
|||
ObjectNode rpcActualResult3_0_14 = JacksonUtil.fromString(actualResult3_0_14, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult3_0_14.get("result").asText()); |
|||
String actualResult19_1_0_0 = sendObserve("Observe", expectedIdVer19_1_0_0); |
|||
ObjectNode rpcActualResult19_1_0_0 = JacksonUtil.fromString(actualResult19_1_0_0, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult19_1_0_0.get("result").asText()); |
|||
String actualResult19_0_0 = sendObserve("Observe", idVer_19_0_0); |
|||
ObjectNode rpcActualResult19_0_0 = JacksonUtil.fromString(actualResult19_0_0, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult19_0_0.get("result").asText()); |
|||
String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null); |
|||
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); |
|||
String actualValues = rpcActualResultReadAll.get("value").asText(); |
|||
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_3_0_9))); |
|||
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer3_0_14))); |
|||
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer19_1_0_0))); |
|||
assertTrue(actualValues.contains("SingleObservation:" + fromVersionedIdToObjectId(idVer_19_0_0))); |
|||
} |
|||
|
|||
/** |
|||
* ObserveReadAll |
|||
* {"result":"CONTENT","value":"[\"CompositeObservation: [/19/1/0\",\"/19/0/0\",\"/3/0/14\",\"/3/0/9]\"]"} - Ok |
|||
* @throws Exception |
|||
*/ |
|||
@Test |
|||
public void testObserveReadAll_AfterCompositeObservation_WithResourceNotReadable_Result_CONTENT_Value_SingleObservation_Only() throws Exception { |
|||
sendCancelObserveAllWithAwait(deviceId); |
|||
|
|||
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; |
|||
String expectedIdVer5_0_2= objectInstanceIdVer_5 + "/" + RESOURCE_ID_2; |
|||
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; |
|||
String expectedIdVer19_1_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0; |
|||
String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_2 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + idVer_3_0_9 + "\", \"" + expectedIdVer19_1_0 + "\"]"; |
|||
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
String actualValues = rpcActualResult.get("value").asText(); |
|||
|
|||
assertTrue(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_2) + "=null")); |
|||
|
|||
String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null); |
|||
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); |
|||
actualValues = rpcActualResultReadAll.get("value").asText(); |
|||
|
|||
assertFalse(actualValues.contains(fromVersionedIdToObjectId(expectedIdVer5_0_2))); |
|||
|
|||
} |
|||
|
|||
/** |
|||
* ObserveComposite {"ids":["/5/0/7", "/5/0/5", "/5/0/3", "/3/0/9", "/19/1/0/0"]} - Ok |
|||
* ObserveCompositeCancel {"ids":["/5/0/7", "/5/0/5", "/5/0/3", "/3/0/9", "/19/1/0/0"]} - Ok |
|||
* @throws Exception |
|||
*/ |
|||
@Test |
|||
public void testObserveCompositeAnyResources_Result_CONTENT_CancelObserveComposite_This_Result_Content_Count_5() throws Exception { |
|||
sendCancelObserveAllWithAwait(deviceId); |
|||
// ObserveComposite
|
|||
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; |
|||
String expectedIdVer5_0_5= objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; |
|||
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; |
|||
String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0; |
|||
String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + idVer_3_0_9 + "\", \"" + expectedIdVer19_1_0_0 + "\"]"; |
|||
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
// ObserveCompositeCancel
|
|||
actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds); |
|||
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
assertEquals("5", rpcActualResult.get("value").asText()); |
|||
|
|||
assertEquals(0, (Object) getCntObserveAll(deviceId)); |
|||
} |
|||
|
|||
/** |
|||
* ObserveComposite {"ids":["/3", "/5/0/3", "/19/1/0/0"]} - Ok |
|||
* ObserveCompositeCancel {"ids":["/3", "/5/0/3", "/19/1/0/0"]} - Ok |
|||
* @throws Exception |
|||
*/ |
|||
@Test |
|||
public void testObserveCompositeOneObjectAnyResources_Result_CONTENT_CancelObserveComposite_This_Result_Content_Count_3() throws Exception { |
|||
sendCancelObserveAllWithAwait(deviceId); |
|||
// ObserveComposite
|
|||
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; |
|||
String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0; |
|||
String expectedIds = "[\"" + idVer_3_0_9 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + expectedIdVer19_1_0_0 + "\"]"; |
|||
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
// ObserveCompositeCancel
|
|||
actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds); |
|||
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
assertEquals("3", rpcActualResult.get("value").asText()); |
|||
|
|||
assertEquals(0, (Object) getCntObserveAll(deviceId)); |
|||
} |
|||
|
|||
/** |
|||
* ObserveComposite {"ids":["/3/0/9", "/3/0/14", "/5/0/3", "/19/1/0/0"]} - Ok |
|||
* ObserveCompositeCancel {"ids":["/3", "/19/1/0/0"]} - Ok |
|||
* last Observation |
|||
* @throws Exception |
|||
*/ |
|||
@Test |
|||
public void testObserveCompositeAnyResources_Result_CONTENT_CancelObserveComposite_OneObjectAnyResource_Result_Content_Count_4() throws Exception { |
|||
sendCancelObserveAllWithAwait(deviceId); |
|||
// ObserveComposite
|
|||
String expectedIdVer5_0_7 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_7; |
|||
String expectedIdVer5_0_5 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_5; |
|||
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; |
|||
String expectedIdVer3_0_9 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_9; |
|||
String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0; |
|||
String expectedIds = "[\"" + expectedIdVer5_0_7 + "\", \"" + expectedIdVer5_0_5 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + expectedIdVer3_0_9 + "\", \"" + expectedIdVer19_1_0_0 + "\"]"; |
|||
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
awaitObserveReadAll(5, deviceId); |
|||
|
|||
// ObserveCompositeCancel
|
|||
expectedIds = "[\"" + objectInstanceIdVer_5 + "\", \"" + expectedIdVer19_1_0_0 + "\"]"; |
|||
actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds); |
|||
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
assertEquals("4", rpcActualResult.get("value").asText()); |
|||
|
|||
String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null); |
|||
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); |
|||
String actualValues = rpcActualResultReadAll.get("value").asText(); |
|||
assertEquals("[\"SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer3_0_9) + "\"]", actualValues); |
|||
} |
|||
|
|||
/** |
|||
* ObserveComposite {"ids":["/3", "/5/0/3", "/19/1/0/0"]} - Ok |
|||
* ObserveCompositeCancel {"ids":["/3/0/9", "/5/0/3", "/19/1/0/0"} -> BAD_REQUEST |
|||
* ObserveCompositeCancel {"ids":["/3"} -> CONTENT |
|||
*/ |
|||
@Test |
|||
public void testObserveOneObjectAnyResources_Result_CONTENT_Cancel_OneResourceFromObjectAnyResource_Result_BAD_REQUEST_Cancel_OneObject_Result_CONTENT() throws Exception { |
|||
sendCancelObserveAllWithAwait(deviceId); |
|||
// ObserveComposite
|
|||
sendCancelObserveAllWithAwait(deviceId); |
|||
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; |
|||
String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0; |
|||
String expectedIds = "[\"" + objectIdVer_3 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + expectedIdVer19_1_0_0 + "\"]"; |
|||
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
|
|||
// ObserveCompositeCancel
|
|||
expectedIds = "[\"" + expectedIdVer19_1_0_0 + "\", \"" + idVer_3_0_9 + "\"]"; |
|||
actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds); |
|||
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); |
|||
String expectedValue = "for observation path " + fromVersionedIdToObjectId(idVer_3_0_9) + ", that includes this observation path " + fromVersionedIdToObjectId(objectIdVer_3); |
|||
assertTrue(rpcActualResult.get("error").asText().contains(expectedValue)); |
|||
|
|||
// ObserveCompositeCancel
|
|||
expectedIds = "[\"" + objectIdVer_3 + "\", \"" + expectedIdVer19_1_0_0 + "\"]"; |
|||
actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds); |
|||
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
assertEquals("2", rpcActualResult.get("value").asText()); |
|||
|
|||
String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null); |
|||
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); |
|||
String actualValues = rpcActualResultReadAll.get("value").asText(); |
|||
assertEquals("[\"SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer5_0_3) + "\"]", actualValues); |
|||
} |
|||
|
|||
|
|||
/** |
|||
* ObserveComposite {"ids":["/3/0/9", "/3/0/14", "/5/0/3", "/3/0/15", "/19/1/0/0"]} - Ok |
|||
* ObserveCancel {"id":"/3/0/9"} -> INTERNAL_SERVER_ERROR |
|||
* ObserveCompositeCancel {"ids":["/3/0/9", "/19/1/0/0", "/3]} - Ok |
|||
* last Observation |
|||
* @throws Exception |
|||
*/ |
|||
@Test |
|||
public void testObserveCompositeAnyResources_Result_CONTENT_CancelObserveComposite_OneResource_OneObjectAnyResource_Result_Content_Count_4() throws Exception { |
|||
sendCancelObserveAllWithAwait(deviceId); |
|||
// ObserveComposite
|
|||
sendCancelObserveAllWithAwait(deviceId); |
|||
String expectedIdVer3_0_14 = objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_14; |
|||
String expectedIdVer3_0_15= objectIdVer_3 + "/" + OBJECT_INSTANCE_ID_0 + "/" + RESOURCE_ID_15; |
|||
String expectedIdVer5_0_3 = objectInstanceIdVer_5 + "/" + RESOURCE_ID_3; |
|||
String expectedIdVer19_1_0_0 = objectIdVer_19 + "/" + OBJECT_INSTANCE_ID_1 + "/" + RESOURCE_ID_0 + "/" + RESOURCE_INSTANCE_ID_0; |
|||
String expectedIds = "[\"" + idVer_3_0_9 + "\", \"" + expectedIdVer3_0_14 + "\", \"" + expectedIdVer5_0_3 + "\", \"" + expectedIdVer3_0_15 + "\", \"" + expectedIdVer19_1_0_0 + "\"]"; |
|||
String actualResult = sendCompositeRPCByIds("ObserveComposite", expectedIds); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
// ObserveCompositeCancel
|
|||
expectedIds = "[\"" + idVer_3_0_9 + "\", \"" + expectedIdVer19_1_0_0 + "\", \"" + objectIdVer_3 + "\"]"; |
|||
actualResult = sendCompositeRPCByIds("ObserveCompositeCancel", expectedIds); |
|||
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
assertEquals("4", rpcActualResult.get("value").asText()); |
|||
|
|||
String actualResultReadAll = sendCompositeRPCByKeys("ObserveReadAll", null); |
|||
ObjectNode rpcActualResultReadAll = JacksonUtil.fromString(actualResultReadAll, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResultReadAll.get("result").asText()); |
|||
String actualValues = rpcActualResultReadAll.get("value").asText(); |
|||
assertEquals("[\"SingleObservation:" + fromVersionedIdToObjectId(expectedIdVer5_0_3) + "\"]", actualValues); |
|||
} |
|||
|
|||
|
|||
private String sendObserve(String method, String params) throws Exception { |
|||
String sendRpcRequest; |
|||
if (params == null) { |
|||
sendRpcRequest = "{\"method\": \"" + method + "\"}"; |
|||
} |
|||
else { |
|||
sendRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"id\": \"" + params + "\"}}"; |
|||
} |
|||
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, sendRpcRequest, String.class, status().isOk()); |
|||
} |
|||
|
|||
private String sendCompositeRPCByIds(String method, String paths) throws Exception { |
|||
String setRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"ids\":" + paths + "}}"; |
|||
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); |
|||
} |
|||
|
|||
private String sendCompositeRPCByKeys(String method, String keys) throws Exception { |
|||
String setRpcRequest = "{\"method\": \"" + method + "\", \"params\": {\"keys\":" + keys + "}}"; |
|||
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); |
|||
} |
|||
} |
|||
@ -0,0 +1,249 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.transport.lwm2m.rpc.sql; |
|||
|
|||
import com.fasterxml.jackson.databind.node.ObjectNode; |
|||
import org.eclipse.leshan.core.ResponseCode; |
|||
import org.junit.Test; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; |
|||
|
|||
import static org.junit.Assert.assertEquals; |
|||
import static org.junit.Assert.assertTrue; |
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_6; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_7; |
|||
|
|||
|
|||
public class RpcLwm2mIntegrationDiscoverWriteAttributesTest extends AbstractRpcLwM2MIntegrationTest { |
|||
|
|||
/** |
|||
* WriteAttributes {"id":"/3_1.2/0/6","attributes":{"pmax":100, "pmin":10}} |
|||
* if not implemented: |
|||
* {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"} |
|||
* if implemented: |
|||
* {"result":"BAD_REQUEST","error":"Attribute pmax can be used for only Resource/Object Instance/Object."} |
|||
*/ |
|||
@Test |
|||
public void testWriteAttributesResourceWithParametersByResourceInstanceId_Result_BAD_REQUEST() throws Exception { |
|||
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6 + "/1"; |
|||
String expectedValue = "{\"pmax\":100, \"pmin\":10}"; |
|||
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); |
|||
String expected = "Attribute pmax can be used for only Resource/Object Instance/Object."; |
|||
String actual = rpcActualResult.get("error").asText(); |
|||
assertTrue(actual.equals(expected)); |
|||
} |
|||
/** |
|||
* WriteAttributes {"id":"/3_1.2/0/6","attributes":{"pmax":100, "pmin":10}} |
|||
* if not implemented: |
|||
* {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"} |
|||
* if implemented: |
|||
* {"result":"BAD_REQUEST","error":"Attribute pmax can be used for only Resource/Object Instance/Object."} |
|||
*/ |
|||
@Test |
|||
public void testWriteAttributeResourceDimWithParametersByResourceId_Result_BAD_REQUEST() throws Exception { |
|||
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6; |
|||
String expectedValue = "{\"dim\":3}"; |
|||
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); |
|||
String expected = "Attribute dim is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request."; |
|||
String actual = rpcActualResult.get("error").asText(); |
|||
assertTrue(actual.equals(expected)); |
|||
} |
|||
|
|||
@Test |
|||
public void testWriteAttributesResourceVerWithParametersById_Result_BAD_REQUEST() throws Exception { |
|||
String expectedPath = objectIdVer_3; |
|||
String expectedValue = "{\"ver\":1.3}"; |
|||
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); |
|||
String expected = "Attribute ver is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request."; |
|||
String actual = rpcActualResult.get("error").asText(); |
|||
assertTrue(actual.equals(expected)); |
|||
} |
|||
|
|||
@Test |
|||
public void testWriteAttributesResourceServerUriWithParametersById_Result_BAD_REQUEST() throws Exception { |
|||
String expectedPath = objectInstanceIdVer_1; |
|||
String actualResult = sendRPCReadById(expectedPath); |
|||
String expectedValue = "{\"uri\":\"coaps://localhost:5690\"}"; |
|||
actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); |
|||
String expected = "Attribute uri is of class PROPERTIES but only NOTIFICATION attribute can be used in WRITE ATTRIBUTE request."; |
|||
String actual = rpcActualResult.get("error").asText(); |
|||
assertTrue(actual.equals(expected)); |
|||
} |
|||
|
|||
/** |
|||
* <PROPERTIES> Class Attributes |
|||
* Dimension dim Integer [0:255] |
|||
* Number of instances existing for a Multiple-Instance Resource |
|||
* <ObjectID>3</ObjectID> |
|||
* <Item ID="6"> |
|||
* <Name>Available Power Sources</Name> |
|||
* <Operations>R</Operations> |
|||
* <MultipleInstances>Multiple</MultipleInstances> |
|||
* <Type>Integer</Type> |
|||
* <RangeEnumeration>0..7</RangeEnumeration> |
|||
* WriteAttributes implemented: Discover {"id":"3/0/6"} -> 'dim' = 3 |
|||
* "ver" only for objectId |
|||
*/ |
|||
@Test |
|||
public void testReadDIM_3_0_6_Only_R () throws Exception { |
|||
String path = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6; |
|||
String actualResult = sendDiscover(path); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
String expected = "</3/0/6>;dim=3"; |
|||
assertTrue(rpcActualResult.get("value").asText().equals(expected)); |
|||
|
|||
} |
|||
|
|||
|
|||
/** |
|||
* <PROPERTIES> Class Attributes |
|||
* Object Version ver Object |
|||
* Provide the version of the associated Object. |
|||
* "ver" only for objectId |
|||
*/ |
|||
@Test |
|||
public void testReadVer () throws Exception { |
|||
String path = objectIdVer_3; |
|||
String actualResult = sendDiscover(path); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
String expected = "</3>;ver=1.2"; |
|||
assertTrue(rpcActualResult.get("value").asText().contains(expected)); |
|||
} |
|||
|
|||
/** |
|||
* WriteAttributes {"id":"/3/0/14","attributes":{"pmax":100, "pmin":10}} |
|||
* if not implemented: |
|||
* {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"} |
|||
* if implemented: |
|||
* {"result":"CHANGED"} |
|||
*/ |
|||
@Test |
|||
public void testWriteAttributesResourceWithParametersById_Result_CHANGED() throws Exception { |
|||
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; |
|||
String expectedValue = "{\"pmax\":100, \"pmin\":10}"; |
|||
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); |
|||
// result changed
|
|||
actualResult = sendDiscover(expectedPath); |
|||
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
String expected = "</3/0/14>;pmax=100;pmin=10"; |
|||
assertTrue(rpcActualResult.get("value").asText().contains(expected)); |
|||
} |
|||
|
|||
/** |
|||
* <NOTIFICATION> Class Attributes |
|||
* Minimum/Maximum Period pmin/pmax |
|||
* Notes: The Minimum Period Attribute: |
|||
* -- indicates the minimum time in seconds the LwM2M Client MUST wait between two notifications. If a notification of an observed Resource is supposed to be generated but it is before pmin expiry, notification MUST be sent as soon as pmin expires. In the absence of this parameter, the Minimum Period is defined by the Default Minimum Period set in the LwM2M Server Account. |
|||
* Notes: The Maximum Period Attribute: |
|||
* -- indicates the maximum time in seconds the LwM2M Client MAY wait between two notifications. When this "Maximum Period" expires after the last notification, a new notification MUST be sent. In the absence of this parameter, the "Maximum Period" is defined by the Default Maximum Period when set in the LwM2M Server Account or considered as 0 otherwise. The value of 0, means pmax MUST be ignored. The maximum period parameter MUST be greater than the minimum period parameter otherwise pmax will be ignored for the Resource to which such inconsistent timing conditions are applied. |
|||
* Greater Than gt Resource |
|||
* Less Than lt Resource |
|||
* Step st Resource |
|||
* |
|||
* Object Id = 1 |
|||
* Default Minimum Period Id = 2 300 or 0 |
|||
* Default Maximum Period Id = 3 6000 or "-" |
|||
* </3/0>;pmax=65, </3/0/1>, <3/0/2>, </3/0/3>, </3/0/4>, |
|||
* <3/0/6>;dim=8,<3/0/7>;gt=50;lt=42.2;st=0.5,<3/0/8>;... |
|||
*/ |
|||
@Test |
|||
public void testWriteAttributesPeriodLtGt () throws Exception { |
|||
String expectedPath = objectInstanceIdVer_3; |
|||
String expectedValue = "{\"pmax\":60}"; |
|||
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); |
|||
expectedPath = objectInstanceIdVer_3; |
|||
expectedValue = "{\"pmax\":65}"; |
|||
actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); |
|||
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); |
|||
expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_7; |
|||
expectedValue ="{\"gt\":50, \"lt\":42.2, \"st\":0.5}"; |
|||
actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); |
|||
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CHANGED.getName(), rpcActualResult.get("result").asText()); |
|||
// ObjectId
|
|||
expectedPath = objectIdVer_3; |
|||
actualResult = sendDiscover(expectedPath); |
|||
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
// String expected = "</3>;ver=1.2,</3/0>;pmax=60,</3/0/0>,</3/0/1>,</3/0/2>,</3/0/3>,</3/0/6>;dim=3,</3/0/7>;st=0.5;lt=42.2;gt=50.0,</3/0/8>,</3/0/9>,</3/0/10>,</3/0/11>;dim=1,</3/0/13>,</3/0/14>,</3/0/15>,</3/0/16>,</3/0/17>,</3/0/18>,</3/0/19>,</3/0/20>,</3/0/21>";
|
|||
String expected = "</3>;ver=1.2,</3/0>;pmax=65"; |
|||
assertTrue(rpcActualResult.get("value").asText().contains(expected)); |
|||
expected = "</3/0/6>;dim=3,</3/0/7>;st=0.5;lt=42.2;gt=50.0"; |
|||
assertTrue(rpcActualResult.get("value").asText().contains(expected)); |
|||
// ObjectInstanceId
|
|||
expectedPath = objectInstanceIdVer_3; |
|||
actualResult = sendDiscover(expectedPath); |
|||
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
expected = "</3/0>;pmax=65"; |
|||
assertTrue(rpcActualResult.get("value").asText().contains(expected)); |
|||
expected = "</3/0/6>;dim=3,</3/0/7>;st=0.5;lt=42.2;gt=50.0"; |
|||
assertTrue(rpcActualResult.get("value").asText().contains(expected)); |
|||
// ResourceId
|
|||
expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6; |
|||
actualResult = sendDiscover(expectedPath); |
|||
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
expected = "</3/0/6>;dim=3"; |
|||
assertTrue(rpcActualResult.get("value").asText().contains(expected)); |
|||
expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_7; |
|||
actualResult = sendDiscover(expectedPath); |
|||
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); |
|||
expected = "</3/0/7>;st=0.5;lt=42.2;gt=50.0"; |
|||
assertTrue(rpcActualResult.get("value").asText().contains(expected)); |
|||
// ResourceInstanceId
|
|||
expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_6+ "/1"; |
|||
actualResult = sendDiscover(expectedPath); |
|||
rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.INTERNAL_SERVER_ERROR.getName(), rpcActualResult.get("result").asText()); |
|||
expected = "InvalidRequestException: Discover request cannot target resource instance path: /3/0/6/1"; |
|||
assertTrue(rpcActualResult.get("error").asText().contains(expected)); |
|||
} |
|||
|
|||
private String sendRPCExecuteWithValueById(String path, String value) throws Exception { |
|||
String setRpcRequest = "{\"method\": \"WriteAttributes\", \"params\": {\"id\": \"" + path + "\", \"attributes\": " + value + " }}"; |
|||
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); |
|||
} |
|||
|
|||
private String sendRPCReadById(String path) throws Exception { |
|||
String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}"; |
|||
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); |
|||
} |
|||
|
|||
private String sendDiscover(String path) throws Exception { |
|||
String setRpcRequest = "{\"method\": \"Discover\", \"params\": {\"id\": \"" + path + "\"}}"; |
|||
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); |
|||
} |
|||
} |
|||
@ -1,56 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.transport.lwm2m.rpc.sql; |
|||
|
|||
import com.fasterxml.jackson.databind.node.ObjectNode; |
|||
import org.eclipse.leshan.core.ResponseCode; |
|||
import org.junit.Test; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; |
|||
|
|||
import static org.junit.Assert.assertEquals; |
|||
import static org.junit.Assert.assertTrue; |
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_14; |
|||
|
|||
|
|||
public class RpcLwm2mIntegrationWriteAttributesTest extends AbstractRpcLwM2MIntegrationTest { |
|||
|
|||
/** |
|||
* WriteAttributes {"id":"/3/0/14","attributes":{"pmax":100, "pmin":10}} |
|||
* if not implemented: |
|||
* {"result":"INTERNAL_SERVER_ERROR","error":"not implemented"} |
|||
* if implemented: |
|||
* {"result":"CHANGED"} |
|||
*/ |
|||
@Test |
|||
public void testWriteAttributesResourceWithParametersById_Result_INTERNAL_SERVER_ERROR() throws Exception { |
|||
String expectedPath = objectInstanceIdVer_3 + "/" + RESOURCE_ID_14; |
|||
String expectedValue = "{\"pmax\":100, \"pmin\":10}"; |
|||
String actualResult = sendRPCExecuteWithValueById(expectedPath, expectedValue); |
|||
ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); |
|||
assertEquals(ResponseCode.INTERNAL_SERVER_ERROR.getName(), rpcActualResult.get("result").asText()); |
|||
String expected = "not implemented"; |
|||
String actual = rpcActualResult.get("error").asText(); |
|||
assertTrue(actual.equals(expected)); |
|||
} |
|||
|
|||
private String sendRPCExecuteWithValueById(String path, String value) throws Exception { |
|||
String setRpcRequest = "{\"method\": \"WriteAttributes\", \"params\": {\"id\": \"" + path + "\", \"attributes\": " + value + " }}"; |
|||
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,129 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.common.data.util; |
|||
|
|||
|
|||
import org.eclipse.leshan.core.LwM2m.LwM2mVersion; |
|||
import org.eclipse.leshan.core.model.DDFFileValidator; |
|||
import org.eclipse.leshan.core.model.InvalidDDFFileException; |
|||
import org.eclipse.leshan.core.util.Validate; |
|||
import org.w3c.dom.Node; |
|||
import org.xml.sax.SAXException; |
|||
import org.xml.sax.SAXNotRecognizedException; |
|||
import org.xml.sax.SAXNotSupportedException; |
|||
|
|||
import javax.xml.XMLConstants; |
|||
import javax.xml.transform.Source; |
|||
import javax.xml.transform.dom.DOMSource; |
|||
import javax.xml.transform.stream.StreamSource; |
|||
import javax.xml.validation.Schema; |
|||
import javax.xml.validation.SchemaFactory; |
|||
import javax.xml.validation.Validator; |
|||
import java.io.IOException; |
|||
import java.io.InputStream; |
|||
|
|||
/** |
|||
* A DDF File Validator. |
|||
* <p> |
|||
* Validate a DDF File against the embedded LWM2M schema. |
|||
* <p> |
|||
* Support LWM2M version 1.0 and 1.1. |
|||
*/ |
|||
|
|||
public class TbDefaultDDFFileValidator implements DDFFileValidator { |
|||
private static String LWM2M_V1_0_SCHEMA_PATH = "/schemas/LWM2M.xsd"; |
|||
private static String LWM2M_V1_1_SCHEMA_PATH = "/schemas/LWM2M-v1_1.xsd"; |
|||
|
|||
private final String schema; |
|||
|
|||
/** |
|||
* Create a {@link DDFFileValidator} using the LWM2M v1.1 schema. |
|||
*/ |
|||
public TbDefaultDDFFileValidator() { |
|||
this(LwM2mVersion.V1_1); |
|||
} |
|||
|
|||
/** |
|||
* Create a {@link DDFFileValidator} using schema corresponding to LWM2M {@link LwM2mVersion}. |
|||
*/ |
|||
public TbDefaultDDFFileValidator(LwM2mVersion version) { |
|||
Validate.notNull(version, "version must not be null"); |
|||
if (LwM2mVersion.V1_0.equals(version)) { |
|||
schema = LWM2M_V1_0_SCHEMA_PATH; |
|||
} else if (LwM2mVersion.V1_1.equals(version)) { |
|||
schema = LWM2M_V1_1_SCHEMA_PATH; |
|||
} else { |
|||
throw new IllegalStateException(String.format("Unsupported version %s", version)); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void validate(Node xmlToValidate) throws InvalidDDFFileException { |
|||
try { |
|||
validate(new DOMSource(xmlToValidate)); |
|||
} catch (SAXException | IOException e) { |
|||
throw new InvalidDDFFileException(e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Validate a XML {@link Source} against the embedded LWM2M Schema. |
|||
* |
|||
* @param xmlToValidate an XML source to validate |
|||
* @throws SAXException see {@link Validator#validate(Source)} |
|||
* @throws IOException see {@link Validator#validate(Source)} |
|||
*/ |
|||
public void validate(Source xmlToValidate) throws SAXException, IOException { |
|||
Validator validator = getEmbeddedLwM2mSchema().newValidator(); |
|||
validator.validate(xmlToValidate); |
|||
} |
|||
|
|||
/** |
|||
* Get the Embedded the LWM2M.xsd Schema. |
|||
* |
|||
* @throws SAXException see {@link SchemaFactory#newSchema(Source)} |
|||
*/ |
|||
protected Schema getEmbeddedLwM2mSchema() throws SAXException { |
|||
InputStream inputStream = DDFFileValidator.class.getResourceAsStream(schema); |
|||
Source source = new StreamSource(inputStream); |
|||
SchemaFactory schemaFactory = createSchemaFactory(); |
|||
return schemaFactory.newSchema(source); |
|||
} |
|||
|
|||
protected SchemaFactory createSchemaFactory() { |
|||
SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); |
|||
// SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);
|
|||
try { |
|||
// Create Safe SchemaFactory (not vulnerable to XXE Attacks)
|
|||
// --------------------------------------------------------
|
|||
// There is several recommendation from different source we try to apply all, even if some are maybe
|
|||
// redundant.
|
|||
|
|||
// from :
|
|||
// https://semgrep.dev/docs/cheat-sheets/java-xxe/
|
|||
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); |
|||
|
|||
// from :
|
|||
// https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#schemafactory
|
|||
// factory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
|
|||
// factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
|
|||
|
|||
} catch (SAXNotRecognizedException | SAXNotSupportedException e) { |
|||
throw new IllegalStateException("Unable to create SchemaFactory", e); |
|||
} |
|||
return factory; |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.transport.coap.callback; |
|||
|
|||
import org.eclipse.californium.core.coap.Response; |
|||
import org.eclipse.californium.core.server.resources.CoapExchange; |
|||
import org.thingsboard.server.common.transport.TransportServiceCallback; |
|||
|
|||
public class CoapResponseCallback implements TransportServiceCallback<Void> { |
|||
|
|||
protected final CoapExchange exchange; |
|||
protected final Response onSuccessResponse; |
|||
protected final Response onFailureResponse; |
|||
|
|||
public CoapResponseCallback(CoapExchange exchange, Response onSuccessResponse, Response onFailureResponse) { |
|||
this.exchange = exchange; |
|||
this.onSuccessResponse = onSuccessResponse; |
|||
this.onFailureResponse = onFailureResponse; |
|||
} |
|||
|
|||
/** |
|||
* @param msg |
|||
*/ |
|||
@Override |
|||
public void onSuccess(Void msg) { |
|||
this.onSuccessResponse.setConfirmable(isConRequest()); |
|||
exchange.respond(this.onSuccessResponse); |
|||
} |
|||
|
|||
/** |
|||
* @param e |
|||
*/ |
|||
@Override |
|||
public void onError(Throwable e) { |
|||
exchange.respond(onFailureResponse); |
|||
} |
|||
|
|||
protected boolean isConRequest() { |
|||
return exchange.advanced().getRequest().isConfirmable(); |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.transport.lwm2m.server.downlink.composite; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; |
|||
import org.thingsboard.server.transport.lwm2m.server.downlink.AbstractTbLwM2MRequestCallback; |
|||
import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService; |
|||
|
|||
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LOG_LWM2M_INFO; |
|||
|
|||
@Slf4j |
|||
public class TbLwM2MCancelObserveCompositeCallback extends AbstractTbLwM2MRequestCallback<TbLwM2MCancelObserveCompositeRequest, Integer> { |
|||
|
|||
private final String [] versionedIds; |
|||
|
|||
public TbLwM2MCancelObserveCompositeCallback(LwM2MTelemetryLogService logService, LwM2mClient client, String [] versionedIds) { |
|||
super(logService, client); |
|||
this.versionedIds = versionedIds; |
|||
} |
|||
|
|||
@Override |
|||
public void onSuccess(TbLwM2MCancelObserveCompositeRequest request, Integer canceledSubscriptionsCount) { |
|||
log.trace("[{}] Cancel composite observation of [{}] successful: {}", client.getEndpoint(), this.versionedIds, canceledSubscriptionsCount); |
|||
logService.log(client, String.format("[%s]: Cancel Composite Observe for [%s] successful. Result: [%s]", LOG_LWM2M_INFO, this.versionedIds, canceledSubscriptionsCount)); |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.transport.lwm2m.server.downlink.composite; |
|||
|
|||
import lombok.Builder; |
|||
import org.thingsboard.server.transport.lwm2m.server.LwM2MOperationType; |
|||
|
|||
public class TbLwM2MCancelObserveCompositeRequest extends AbstractTbLwM2MTargetedDownlinkCompositeRequest { |
|||
|
|||
@Builder |
|||
private TbLwM2MCancelObserveCompositeRequest(String [] versionedIds, long timeout) { |
|||
super(versionedIds, timeout); |
|||
} |
|||
|
|||
@Override |
|||
public LwM2MOperationType getType() { |
|||
return LwM2MOperationType.OBSERVE_COMPOSITE_CANCEL; |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.transport.lwm2m.server.downlink.composite; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.eclipse.leshan.core.request.ObserveCompositeRequest; |
|||
import org.eclipse.leshan.core.response.ObserveCompositeResponse; |
|||
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; |
|||
import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MUplinkTargetedCallback; |
|||
import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService; |
|||
import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; |
|||
|
|||
@Slf4j |
|||
public class TbLwM2MObserveCompositeCallback extends TbLwM2MUplinkTargetedCallback<ObserveCompositeRequest, ObserveCompositeResponse> { |
|||
|
|||
public TbLwM2MObserveCompositeCallback(LwM2mUplinkMsgHandler handler, LwM2MTelemetryLogService logService, LwM2mClient client, String[] versionedIds) { |
|||
super(handler, logService, client, versionedIds); |
|||
} |
|||
|
|||
@Override |
|||
public void onSuccess(ObserveCompositeRequest request, ObserveCompositeResponse response) { |
|||
super.onSuccess(request, response); |
|||
handler.onUpdateValueAfterReadCompositeResponse(client.getRegistration(), response); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.transport.lwm2m.server.downlink.composite; |
|||
|
|||
import lombok.Builder; |
|||
import lombok.Getter; |
|||
import org.eclipse.leshan.core.request.ContentFormat; |
|||
import org.eclipse.leshan.core.response.ObserveCompositeResponse; |
|||
import org.thingsboard.server.transport.lwm2m.server.LwM2MOperationType; |
|||
import org.thingsboard.server.transport.lwm2m.server.downlink.HasContentFormat; |
|||
|
|||
import java.util.Optional; |
|||
|
|||
public class TbLwM2MObserveCompositeRequest extends AbstractTbLwM2MTargetedDownlinkCompositeRequest<ObserveCompositeResponse> implements HasContentFormat { |
|||
|
|||
|
|||
private final Optional<ContentFormat> requestContentFormatOpt; |
|||
|
|||
@Getter |
|||
private final ContentFormat responseContentFormat; |
|||
|
|||
@Builder |
|||
private TbLwM2MObserveCompositeRequest(String [] versionedIds, long timeout, ContentFormat requestContentFormat, ContentFormat responseContentFormat) { |
|||
super(versionedIds, timeout); |
|||
this.requestContentFormatOpt = Optional.ofNullable(requestContentFormat); |
|||
this.responseContentFormat = responseContentFormat; |
|||
} |
|||
|
|||
@Override |
|||
public LwM2MOperationType getType() { |
|||
return LwM2MOperationType.OBSERVE_COMPOSITE; |
|||
} |
|||
|
|||
@Override |
|||
public Optional<ContentFormat> getRequestContentFormat() { |
|||
return this.requestContentFormatOpt; |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.transport.lwm2m.server.rpc.composite; |
|||
|
|||
import org.eclipse.leshan.core.ResponseCode; |
|||
import org.thingsboard.server.common.transport.TransportService; |
|||
import org.thingsboard.server.gen.transport.TransportProtos; |
|||
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; |
|||
import org.thingsboard.server.transport.lwm2m.server.downlink.DownlinkRequestCallback; |
|||
import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MCancelObserveCompositeRequest; |
|||
import org.thingsboard.server.transport.lwm2m.server.rpc.LwM2MRpcResponseBody; |
|||
import org.thingsboard.server.transport.lwm2m.server.rpc.RpcDownlinkRequestCallbackProxy; |
|||
|
|||
public class RpcCancelObserveCompositeCallback extends RpcDownlinkRequestCallbackProxy<TbLwM2MCancelObserveCompositeRequest, Integer> { |
|||
|
|||
public RpcCancelObserveCompositeCallback(TransportService transportService, LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, DownlinkRequestCallback<TbLwM2MCancelObserveCompositeRequest, Integer> callback) { |
|||
super(transportService, client, requestMsg, callback); |
|||
} |
|||
|
|||
@Override |
|||
protected void sendRpcReplyOnSuccess(Integer response) { |
|||
reply(LwM2MRpcResponseBody.builder().result(ResponseCode.CONTENT.getName()).value(response.toString()).build()); |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.transport.lwm2m.server.rpc.composite; |
|||
|
|||
import org.eclipse.leshan.core.request.LwM2mRequest; |
|||
import org.eclipse.leshan.core.response.ObserveCompositeResponse; |
|||
import org.thingsboard.server.common.transport.TransportService; |
|||
import org.thingsboard.server.gen.transport.TransportProtos; |
|||
import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; |
|||
import org.thingsboard.server.transport.lwm2m.server.downlink.DownlinkRequestCallback; |
|||
import org.thingsboard.server.transport.lwm2m.server.rpc.RpcLwM2MDownlinkCallback; |
|||
|
|||
import java.util.Optional; |
|||
|
|||
import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.contentToString; |
|||
|
|||
public class RpcObserveResponseCompositeCallback<R extends LwM2mRequest<T>, T extends ObserveCompositeResponse> extends RpcLwM2MDownlinkCallback<R, T> { |
|||
|
|||
public RpcObserveResponseCompositeCallback(TransportService transportService, LwM2mClient client, TransportProtos.ToDeviceRpcRequestMsg requestMsg, DownlinkRequestCallback<R, T> callback) { |
|||
super(transportService, client, requestMsg, callback); |
|||
} |
|||
|
|||
@Override |
|||
protected Optional<String> serializeSuccessfulResponse(T response) { |
|||
return contentToString(response.getContent()); |
|||
} |
|||
} |
|||
@ -0,0 +1,619 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.transport.lwm2m.server.store; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import com.fasterxml.jackson.databind.node.ObjectNode; |
|||
import org.eclipse.californium.core.coap.Token; |
|||
import org.eclipse.californium.core.network.RandomTokenGenerator; |
|||
import org.eclipse.californium.core.network.TokenGenerator; |
|||
import org.eclipse.californium.core.network.TokenGenerator.Scope; |
|||
import org.eclipse.leshan.core.Destroyable; |
|||
import org.eclipse.leshan.core.Startable; |
|||
import org.eclipse.leshan.core.Stoppable; |
|||
import org.eclipse.leshan.core.model.ObjectModel; |
|||
import org.eclipse.leshan.core.model.ResourceModel; |
|||
import org.eclipse.leshan.core.node.LwM2mPath; |
|||
import org.eclipse.leshan.core.observation.CompositeObservation; |
|||
import org.eclipse.leshan.core.observation.Observation; |
|||
import org.eclipse.leshan.core.observation.ObservationIdentifier; |
|||
import org.eclipse.leshan.core.observation.SingleObservation; |
|||
import org.eclipse.leshan.core.peer.LwM2mIdentity; |
|||
import org.eclipse.leshan.core.request.ContentFormat; |
|||
import org.eclipse.leshan.core.util.NamedThreadFactory; |
|||
import org.eclipse.leshan.server.registration.Deregistration; |
|||
import org.eclipse.leshan.server.registration.ExpirationListener; |
|||
import org.eclipse.leshan.server.registration.Registration; |
|||
import org.eclipse.leshan.server.registration.RegistrationStore; |
|||
import org.eclipse.leshan.server.registration.RegistrationUpdate; |
|||
import org.eclipse.leshan.server.registration.UpdatedRegistration; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; |
|||
import org.thingsboard.server.transport.lwm2m.server.LwM2mVersionedModelProvider; |
|||
|
|||
import java.net.InetSocketAddress; |
|||
import java.util.ArrayList; |
|||
import java.util.Collection; |
|||
import java.util.Collections; |
|||
import java.util.HashMap; |
|||
import java.util.HashSet; |
|||
import java.util.Iterator; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Objects; |
|||
import java.util.Set; |
|||
import java.util.concurrent.Executors; |
|||
import java.util.concurrent.ScheduledExecutorService; |
|||
import java.util.concurrent.ScheduledFuture; |
|||
import java.util.concurrent.TimeUnit; |
|||
import java.util.concurrent.atomic.AtomicReference; |
|||
import java.util.concurrent.locks.ReadWriteLock; |
|||
import java.util.concurrent.locks.ReentrantReadWriteLock; |
|||
|
|||
import static org.eclipse.leshan.core.californium.ObserveUtil.CTX_CF_OBERSATION; |
|||
import static org.eclipse.leshan.core.californium.ObserveUtil.extractSerializedObservation; |
|||
|
|||
public class TbInMemoryRegistrationStore implements RegistrationStore, Startable, Stoppable, Destroyable { |
|||
private final Logger LOG = LoggerFactory.getLogger(TbInMemoryRegistrationStore.class); |
|||
|
|||
// Data structure
|
|||
private final Map<String /* end-point */, Registration> regsByEp = new HashMap<>(); |
|||
private final Map<InetSocketAddress, Registration> regsByAddr = new HashMap<>(); |
|||
private final Map<String /* reg-id */, Registration> regsByRegId = new HashMap<>(); |
|||
private final Map<LwM2mIdentity, Registration> regsByIdentity = new HashMap<>(); |
|||
private final Map<ObservationIdentifier, Observation> obsByToken = new HashMap<>(); |
|||
private final Map<String, Set<ObservationIdentifier>> tokensByRegId = new HashMap<>(); |
|||
|
|||
private final ReadWriteLock lock = new ReentrantReadWriteLock(); |
|||
|
|||
// Listener use to notify when a registration expires
|
|||
private ExpirationListener expirationListener; |
|||
|
|||
private final ScheduledExecutorService schedExecutor; |
|||
private ScheduledFuture<?> cleanerTask; |
|||
private boolean started = false; |
|||
private final long cleanPeriod; // in seconds
|
|||
|
|||
private final LwM2MTransportServerConfig config; |
|||
|
|||
private final LwM2mVersionedModelProvider modelProvider; |
|||
|
|||
private TokenGenerator tokenGenerator; |
|||
|
|||
public TbInMemoryRegistrationStore() { |
|||
this(null, 2, null); // default clean period : 2s
|
|||
} |
|||
|
|||
public TbInMemoryRegistrationStore(LwM2MTransportServerConfig config, long cleanPeriodInSec, LwM2mVersionedModelProvider modelProvider) { |
|||
this(config, Executors.newScheduledThreadPool(1, |
|||
new NamedThreadFactory(String.format("TbInMemoryRegistrationStore Cleaner (%ds)", cleanPeriodInSec))), |
|||
cleanPeriodInSec, modelProvider); |
|||
} |
|||
|
|||
public TbInMemoryRegistrationStore(LwM2MTransportServerConfig config, ScheduledExecutorService schedExecutor, long cleanPeriodInSec, LwM2mVersionedModelProvider modelProvider) { |
|||
this.schedExecutor = schedExecutor; |
|||
this.cleanPeriod = cleanPeriodInSec; |
|||
this.modelProvider = modelProvider; |
|||
this.config = config; |
|||
} |
|||
|
|||
/* *************** Leshan Registration API **************** */ |
|||
|
|||
@Override |
|||
public Deregistration addRegistration(Registration registration) { |
|||
try { |
|||
lock.writeLock().lock(); |
|||
|
|||
Registration registrationRemoved = regsByEp.put(registration.getEndpoint(), registration); |
|||
regsByRegId.put(registration.getId(), registration); |
|||
regsByIdentity.put(registration.getClientTransportData().getIdentity(), registration); |
|||
// If a registration is already associated to this address we don't care as we only want to keep the most
|
|||
// recent binding.
|
|||
regsByAddr.put(registration.getSocketAddress(), registration); |
|||
if (registrationRemoved != null) { |
|||
Collection<Observation> observationsRemoved = unsafeRemoveAllObservations(registrationRemoved.getId()); |
|||
if (!registrationRemoved.getSocketAddress().equals(registration.getSocketAddress())) { |
|||
removeFromMap(regsByAddr, registrationRemoved.getSocketAddress(), registrationRemoved); |
|||
} |
|||
if (!registrationRemoved.getId().equals(registration.getId())) { |
|||
removeFromMap(regsByRegId, registrationRemoved.getId(), registrationRemoved); |
|||
} |
|||
if (!registrationRemoved.getClientTransportData().getIdentity() |
|||
.equals(registration.getClientTransportData().getIdentity())) { |
|||
removeFromMap(regsByIdentity, registrationRemoved.getClientTransportData().getIdentity(), |
|||
registrationRemoved); |
|||
} |
|||
return new Deregistration(registrationRemoved, observationsRemoved); |
|||
} |
|||
} finally { |
|||
lock.writeLock().unlock(); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
@Override |
|||
public UpdatedRegistration updateRegistration(RegistrationUpdate update) { |
|||
try { |
|||
lock.writeLock().lock(); |
|||
|
|||
Registration registration = getRegistration(update.getRegistrationId()); |
|||
if (registration == null) { |
|||
return null; |
|||
} else { |
|||
Registration updatedRegistration = update.update(registration); |
|||
regsByEp.put(updatedRegistration.getEndpoint(), updatedRegistration); |
|||
// If registration is already associated to this address we don't care as we only want to keep the most
|
|||
// recent binding.
|
|||
regsByAddr.put(updatedRegistration.getSocketAddress(), updatedRegistration); |
|||
if (!registration.getSocketAddress().equals(updatedRegistration.getSocketAddress())) { |
|||
removeFromMap(regsByAddr, registration.getSocketAddress(), registration); |
|||
} |
|||
regsByIdentity.put(updatedRegistration.getClientTransportData().getIdentity(), updatedRegistration); |
|||
if (!registration.getClientTransportData().getIdentity() |
|||
.equals(updatedRegistration.getClientTransportData().getIdentity())) { |
|||
removeFromMap(regsByIdentity, registration.getClientTransportData().getIdentity(), registration); |
|||
} |
|||
|
|||
regsByRegId.put(updatedRegistration.getId(), updatedRegistration); |
|||
|
|||
return new UpdatedRegistration(registration, updatedRegistration); |
|||
} |
|||
} finally { |
|||
lock.writeLock().unlock(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Registration getRegistration(String registrationId) { |
|||
try { |
|||
lock.readLock().lock(); |
|||
return regsByRegId.get(registrationId); |
|||
} finally { |
|||
lock.readLock().unlock(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Registration getRegistrationByEndpoint(String endpoint) { |
|||
try { |
|||
lock.readLock().lock(); |
|||
return regsByEp.get(endpoint); |
|||
} finally { |
|||
lock.readLock().unlock(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Registration getRegistrationByAdress(InetSocketAddress address) { |
|||
try { |
|||
lock.readLock().lock(); |
|||
return regsByAddr.get(address); |
|||
} finally { |
|||
lock.readLock().unlock(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Registration getRegistrationByIdentity(LwM2mIdentity identity) { |
|||
try { |
|||
lock.readLock().lock(); |
|||
return regsByIdentity.get(identity); |
|||
} finally { |
|||
lock.readLock().unlock(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Iterator<Registration> getAllRegistrations() { |
|||
try { |
|||
lock.readLock().lock(); |
|||
return new ArrayList<>(regsByEp.values()).iterator(); |
|||
} finally { |
|||
lock.readLock().unlock(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Deregistration removeRegistration(String registrationId) { |
|||
try { |
|||
lock.writeLock().lock(); |
|||
|
|||
Registration registration = getRegistration(registrationId); |
|||
if (registration != null) { |
|||
Collection<Observation> observationsRemoved = unsafeRemoveAllObservations(registration.getId()); |
|||
regsByEp.remove(registration.getEndpoint()); |
|||
removeFromMap(regsByAddr, registration.getSocketAddress(), registration); |
|||
removeFromMap(regsByRegId, registration.getId(), registration); |
|||
removeFromMap(regsByIdentity, registration.getClientTransportData().getIdentity(), registration); |
|||
return new Deregistration(registration, observationsRemoved); |
|||
} |
|||
return null; |
|||
} finally { |
|||
lock.writeLock().unlock(); |
|||
} |
|||
} |
|||
|
|||
/* *************** Leshan Observation API **************** */ |
|||
|
|||
@Override |
|||
public Collection<Observation> addObservation(String registrationId, Observation observation, boolean addIfAbsent) { |
|||
List<Observation> removed = new ArrayList<>(); |
|||
try { |
|||
lock.writeLock().lock(); |
|||
|
|||
if (!regsByRegId.containsKey(registrationId)) { |
|||
throw new IllegalStateException(String.format( |
|||
"can not add observation %s there is no registration with id %s", observation, registrationId)); |
|||
} |
|||
|
|||
if (observation instanceof SingleObservation) { |
|||
if (validateObserveResource(((SingleObservation)observation).getPath(), registrationId)) { |
|||
updateSingleObservation(registrationId, (SingleObservation) observation, addIfAbsent, removed); |
|||
// cancel existing observations for the same path and registration id.
|
|||
cancelObservation (observation, registrationId, removed); |
|||
} |
|||
} else { |
|||
ContentFormat ct = ((CompositeObservation) observation).getResponseContentFormat(); |
|||
Map<String, String> ctx = observation.getContext(); |
|||
String serializedObservation = extractSerializedObservation(observation); |
|||
JsonNode nodeSerObs = JacksonUtil.toJsonNode(serializedObservation); |
|||
((CompositeObservation)observation).getPaths().forEach(path -> { |
|||
if (validateObserveResource(path, registrationId)) { |
|||
String serializedObs = createSerializedSingleObservation(nodeSerObs, path.toString()); |
|||
Observation singleObservation = createSingleObservation(registrationId, path, ct, ctx, serializedObs, getTokenGenerator()); |
|||
updateSingleObservation(registrationId, (SingleObservation) singleObservation, addIfAbsent, removed); |
|||
// cancel existing observations for the same path and registration id.
|
|||
cancelObservation (singleObservation, registrationId, removed); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
} finally { |
|||
lock.writeLock().unlock(); |
|||
} |
|||
|
|||
return removed; |
|||
} |
|||
|
|||
private boolean validateObserveResource(LwM2mPath path, String registrationId){ |
|||
// check if the resource is readable.
|
|||
if (path.isResource() || path.isResourceInstance()) { |
|||
ObjectModel objectModel = modelProvider.getObjectModel(getRegistration(registrationId)).getObjectModel(path.getObjectId()); |
|||
ResourceModel resourceModel = objectModel == null ? null : objectModel.resources.get(path.getResourceId()); |
|||
if (resourceModel == null) { |
|||
return false; |
|||
} else if (!resourceModel.operations.isReadable()) { |
|||
return false; |
|||
} else if (path.isResourceInstance() && !resourceModel.multiple) { |
|||
return false; |
|||
} |
|||
} |
|||
return true; |
|||
} |
|||
|
|||
private void updateSingleObservation (String registrationId, SingleObservation observation, boolean addIfAbsent, List<Observation> removed) { |
|||
// Absorption by existing Observations
|
|||
|
|||
|
|||
Observation previousObservation = null; |
|||
SingleObservation existingObservation = null; |
|||
|
|||
ObservationIdentifier id = observation.getId(); |
|||
if (addIfAbsent) { |
|||
if (!obsByToken.containsKey(id)) { |
|||
existingObservation = validateByAbsorptionExistingObservations(observation); |
|||
if (existingObservation == null) { |
|||
obsByToken.put(id, observation); |
|||
} else if (!existingObservation.getPath().equals(observation.getPath())){ |
|||
obsByToken.put(id, observation); |
|||
previousObservation = obsByToken.get(existingObservation.getId()); |
|||
} |
|||
} |
|||
} else { |
|||
previousObservation = obsByToken.put(id, observation); |
|||
} |
|||
if (!tokensByRegId.containsKey(registrationId)) { |
|||
tokensByRegId.put(registrationId, new HashSet<ObservationIdentifier>()); |
|||
} |
|||
|
|||
if (existingObservation == null || !existingObservation.getPath().equals(observation.getPath())) { |
|||
tokensByRegId.get(registrationId).add(id); |
|||
} |
|||
|
|||
// log any collisions
|
|||
if (addIfAbsent && previousObservation != null) { |
|||
if (!existingObservation.getPath().equals(observation.getPath())) { |
|||
removed.add(previousObservation); |
|||
LOG.warn("Token collision ? observation [{}] will be replaced by observation [{}], that this observation includes input observation [{}]!", |
|||
previousObservation, observation, observation); |
|||
} else { |
|||
LOG.warn("Token collision ? existing observation [{}] includes input observation [{}]", |
|||
existingObservation, observation); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private SingleObservation validateByAbsorptionExistingObservations (SingleObservation observation) { |
|||
LwM2mPath pathObservation = observation.getPath(); |
|||
AtomicReference<SingleObservation> result = new AtomicReference<>(); |
|||
obsByToken.values().stream().forEach(obs -> { |
|||
LwM2mPath pathObs = ((SingleObservation)obs).getPath(); |
|||
if ((!pathObservation.equals(pathObs) && pathObs.startWith(pathObservation)) || // pathObs = "3/0/9"-> pathObservation = "3"
|
|||
(pathObservation.equals(pathObs) && !observation.getId().equals(obs.getId()))) { |
|||
result.set((SingleObservation)obs); |
|||
} else if (!pathObservation.equals(pathObs) && pathObservation.startWith(pathObs)) { // pathObs = "3" -> pathObservation = "3/0/9"
|
|||
result.set(observation); |
|||
} |
|||
}); |
|||
return result.get(); |
|||
} |
|||
|
|||
private TokenGenerator getTokenGenerator(){ |
|||
if (this.tokenGenerator == null) { |
|||
this.tokenGenerator = new RandomTokenGenerator(config.getCoapConfig()); |
|||
} |
|||
return this.tokenGenerator; |
|||
} |
|||
|
|||
public static SingleObservation createSingleObservation(String registrationId, LwM2mPath target, ContentFormat ct, |
|||
Map<String, String> ctx, String serializedObservation, TokenGenerator tokenGenerator) { |
|||
Token token = tokenGenerator.createToken(Scope.SHORT_TERM); |
|||
Map<String, String> protocolData = Collections.emptyMap(); |
|||
if (serializedObservation != null) { |
|||
protocolData = new HashMap<>(); |
|||
protocolData.put(CTX_CF_OBERSATION, serializedObservation); |
|||
} |
|||
return new SingleObservation(new ObservationIdentifier(token.getBytes()), registrationId, target, ct, ctx, protocolData); |
|||
} |
|||
|
|||
public static String createSerializedSingleObservation(JsonNode nodeSerObs, String path){ |
|||
if (nodeSerObs.has("context")){ |
|||
((ObjectNode) nodeSerObs.get("context")).put("leshan-path", path + "\n"); |
|||
return JacksonUtil.toString(nodeSerObs); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
@Override |
|||
public Observation removeObservation(String registrationId, ObservationIdentifier observationId) { |
|||
try { |
|||
lock.writeLock().lock(); |
|||
Observation observation = unsafeGetObservation(observationId); |
|||
if (observation != null && registrationId.equals(observation.getRegistrationId())) { |
|||
unsafeRemoveObservation(observationId); |
|||
return observation; |
|||
} |
|||
return null; |
|||
} finally { |
|||
lock.writeLock().unlock(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Observation getObservation(String registrationId, ObservationIdentifier observationId) { |
|||
try { |
|||
lock.readLock().lock(); |
|||
Observation observation = unsafeGetObservation(observationId); |
|||
if (observation != null && registrationId.equals(observation.getRegistrationId())) { |
|||
return observation; |
|||
} |
|||
return null; |
|||
} finally { |
|||
lock.readLock().unlock(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public Observation getObservation(ObservationIdentifier observationId) { |
|||
try { |
|||
lock.readLock().lock(); |
|||
Observation observation = unsafeGetObservation(observationId); |
|||
if (observation != null) { |
|||
return observation; |
|||
} |
|||
return null; |
|||
} finally { |
|||
lock.readLock().unlock(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Prepare for Cancel one Observation |
|||
* @param registrationId |
|||
* @return |
|||
*/ |
|||
@Override |
|||
public Collection<Observation> getObservations(String registrationId) { |
|||
try { |
|||
lock.readLock().lock(); |
|||
return unsafeGetObservations(registrationId); |
|||
} finally { |
|||
lock.readLock().unlock(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* CancelAllObservation |
|||
* @param registrationId |
|||
* @return |
|||
*/ |
|||
|
|||
@Override |
|||
public Collection<Observation> removeObservations(String registrationId) { |
|||
try { |
|||
lock.writeLock().lock(); |
|||
return unsafeRemoveAllObservations(registrationId); |
|||
} finally { |
|||
lock.writeLock().unlock(); |
|||
} |
|||
} |
|||
|
|||
/* *************** Observation utility functions **************** */ |
|||
|
|||
private Observation unsafeGetObservation(ObservationIdentifier token) { |
|||
Observation obs = obsByToken.get(token); |
|||
return obs; |
|||
} |
|||
|
|||
private void cancelObservation (Observation observation, String registrationId, List<Observation> removed) { |
|||
for (Observation obs : unsafeGetObservations(registrationId)) { |
|||
cancelExistingObservation(observation, obs, removed); |
|||
} |
|||
} |
|||
|
|||
private void cancelExistingObservation(Observation observation, Observation obs, List<Observation> removed) { |
|||
LwM2mPath pathObservation = ((SingleObservation)observation).getPath(); |
|||
LwM2mPath pathObs = ((SingleObservation)obs).getPath(); |
|||
if ((!pathObservation.equals(pathObs) && pathObs.startWith(pathObservation)) || // pathObservation = "3", pathObs = "3/0/9"
|
|||
(pathObservation.equals(pathObs) && !observation.getId().equals(obs.getId()))) { |
|||
unsafeRemoveObservation(obs.getId()); |
|||
removed.add(obs); |
|||
} else if (!pathObservation.equals(pathObs) && pathObservation.startWith(pathObs)) { // pathObservation = "3/0/9", pathObs = "3"
|
|||
unsafeRemoveObservation(observation.getId()); |
|||
} |
|||
} |
|||
|
|||
private void unsafeRemoveObservation(ObservationIdentifier observationId) { |
|||
Observation removed = obsByToken.remove(observationId); |
|||
if (removed != null) { |
|||
String registrationId = removed.getRegistrationId(); |
|||
Set<ObservationIdentifier> tokens = tokensByRegId.get(registrationId); |
|||
tokens.remove(observationId); |
|||
if (tokens.isEmpty()) { |
|||
tokensByRegId.remove(registrationId); |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
/** |
|||
* CancelAllObservation |
|||
* @param registrationId |
|||
* @return |
|||
*/ |
|||
private Collection<Observation> unsafeRemoveAllObservations(String registrationId) { |
|||
Collection<Observation> removed = new ArrayList<>(); |
|||
Set<ObservationIdentifier> ids = tokensByRegId.get(registrationId); |
|||
if (ids != null) { |
|||
for (ObservationIdentifier id : ids) { |
|||
Observation observationRemoved = obsByToken.remove(id); |
|||
if (observationRemoved != null) { |
|||
removed.add(observationRemoved); |
|||
} |
|||
} |
|||
} |
|||
tokensByRegId.remove(registrationId); |
|||
return removed; |
|||
} |
|||
|
|||
private Collection<Observation> unsafeGetObservations(String registrationId) { |
|||
Collection<Observation> result = new ArrayList<>(); |
|||
Set<ObservationIdentifier> ids = tokensByRegId.get(registrationId); |
|||
if (ids != null) { |
|||
for (ObservationIdentifier id : ids) { |
|||
Observation obs = unsafeGetObservation(id); |
|||
if (obs != null) { |
|||
result.add(obs); |
|||
} |
|||
} |
|||
} |
|||
return result; |
|||
} |
|||
/* *************** Expiration handling **************** */ |
|||
|
|||
@Override |
|||
public void setExpirationListener(ExpirationListener listener) { |
|||
this.expirationListener = listener; |
|||
} |
|||
|
|||
/** |
|||
* start the registration store, will start regular cleanup of dead registrations. |
|||
*/ |
|||
@Override |
|||
public synchronized void start() { |
|||
if (!started) { |
|||
started = true; |
|||
cleanerTask = schedExecutor.scheduleAtFixedRate(new TbInMemoryRegistrationStore.Cleaner(), cleanPeriod, cleanPeriod, TimeUnit.SECONDS); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Stop the underlying cleanup of the registrations. |
|||
*/ |
|||
@Override |
|||
public synchronized void stop() { |
|||
if (started) { |
|||
started = false; |
|||
if (cleanerTask != null) { |
|||
cleanerTask.cancel(false); |
|||
cleanerTask = null; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Destroy "cleanup" scheduler. |
|||
*/ |
|||
@Override |
|||
public synchronized void destroy() { |
|||
started = false; |
|||
schedExecutor.shutdownNow(); |
|||
try { |
|||
schedExecutor.awaitTermination(5, TimeUnit.SECONDS); |
|||
} catch (InterruptedException e) { |
|||
LOG.warn("Destroying InMemoryRegistrationStore was interrupted.", e); |
|||
} |
|||
} |
|||
|
|||
private class Cleaner implements Runnable { |
|||
|
|||
@Override |
|||
public void run() { |
|||
try { |
|||
Collection<Registration> allRegs = new ArrayList<>(); |
|||
try { |
|||
lock.readLock().lock(); |
|||
allRegs.addAll(regsByEp.values()); |
|||
} finally { |
|||
lock.readLock().unlock(); |
|||
} |
|||
|
|||
for (Registration reg : allRegs) { |
|||
if (!reg.isAlive()) { |
|||
// force de-registration
|
|||
Deregistration removedRegistration = removeRegistration(reg.getId()); |
|||
expirationListener.registrationExpired(removedRegistration.getRegistration(), |
|||
removedRegistration.getObservations()); |
|||
} |
|||
} |
|||
} catch (Exception e) { |
|||
LOG.warn("Unexpected Exception while registration cleaning", e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// boolean remove(Object key, Object value) exist only since java8
|
|||
// So this method is here only while we want to support java 7
|
|||
protected <K, V> boolean removeFromMap(Map<K, V> map, K key, V value) { |
|||
if (map.containsKey(key) && Objects.equals(map.get(key), value)) { |
|||
map.remove(key); |
|||
return true; |
|||
} else |
|||
return false; |
|||
} |
|||
} |
|||
@ -0,0 +1,81 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.msa; |
|||
|
|||
import com.fasterxml.jackson.databind.node.ObjectNode; |
|||
import org.eclipse.californium.core.CoapClient; |
|||
import org.eclipse.californium.core.coap.MediaTypeRegistry; |
|||
import org.eclipse.californium.core.config.CoapConfig; |
|||
import org.eclipse.californium.elements.config.Configuration; |
|||
import org.eclipse.californium.elements.config.Configuration.ModuleDefinitionsProvider; |
|||
import org.eclipse.californium.elements.config.IntegerDefinition; |
|||
import org.eclipse.californium.elements.config.TcpConfig; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.msg.session.FeatureType; |
|||
|
|||
public abstract class AbstractCoapClientTest extends AbstractContainerTest{ |
|||
|
|||
private static final String COAP_BASE_URL = "coap://localhost:5683/api/v1/"; |
|||
private static final long CLIENT_REQUEST_TIMEOUT = 60000L; |
|||
|
|||
|
|||
private static final String COAP_CLIENT_TEST = "COAP_CLIENT_TEST."; |
|||
private static final IntegerDefinition COAP_PORT_DEF = CoapConfig.COAP_PORT; |
|||
|
|||
private static final ModuleDefinitionsProvider MODULE_DEFINITIONS_PROVIDER = new ModuleDefinitionsProvider() { |
|||
|
|||
@Override |
|||
public String getModule() { |
|||
return COAP_CLIENT_TEST; |
|||
} |
|||
|
|||
@Override |
|||
public void applyDefinitions(Configuration config) { |
|||
TcpConfig.register(); |
|||
config.set(COAP_PORT_DEF, 5683); |
|||
} |
|||
}; |
|||
|
|||
protected CoapClient client; |
|||
|
|||
protected byte[] createCoapClientAndPublish(String deviceName) throws Exception { |
|||
String provisionRequestMsg = createTestProvisionMessage(deviceName); |
|||
Configuration.addDefaultModule(MODULE_DEFINITIONS_PROVIDER); |
|||
String featureTokenUrl = COAP_BASE_URL + FeatureType.PROVISION.name().toLowerCase(); |
|||
client = new CoapClient(featureTokenUrl); |
|||
return client.setTimeout(CLIENT_REQUEST_TIMEOUT) |
|||
.post(provisionRequestMsg.getBytes(), MediaTypeRegistry.APPLICATION_JSON) |
|||
.getPayload(); |
|||
} |
|||
|
|||
protected void disconnect() { |
|||
if (client != null) { |
|||
client.shutdown(); |
|||
} |
|||
} |
|||
|
|||
private String createTestProvisionMessage(String deviceName) { |
|||
ObjectNode provisionRequest = JacksonUtil.newObjectNode(); |
|||
provisionRequest.put("provisionDeviceKey", TEST_PROVISION_DEVICE_KEY); |
|||
provisionRequest.put("provisionDeviceSecret", TEST_PROVISION_DEVICE_SECRET); |
|||
if (deviceName != null) { |
|||
provisionRequest.put("deviceName", deviceName); |
|||
} |
|||
return provisionRequest.toString(); |
|||
} |
|||
} |
|||
|
|||
|
|||
@ -1,122 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.msa; |
|||
|
|||
import org.eclipse.californium.core.CoapClient; |
|||
import org.eclipse.californium.core.CoapHandler; |
|||
import org.eclipse.californium.core.CoapObserveRelation; |
|||
import org.eclipse.californium.core.CoapResponse; |
|||
import org.eclipse.californium.core.coap.CoAP; |
|||
import org.eclipse.californium.core.coap.MediaTypeRegistry; |
|||
import org.eclipse.californium.core.coap.Request; |
|||
import org.eclipse.californium.elements.exception.ConnectorException; |
|||
import org.thingsboard.server.common.msg.session.FeatureType; |
|||
|
|||
import java.io.IOException; |
|||
|
|||
public class TestCoapClient { |
|||
|
|||
private static final String COAP_BASE_URL = "coap://localhost:5683/api/v1/"; |
|||
private static final long CLIENT_REQUEST_TIMEOUT = 60000L; |
|||
|
|||
private final CoapClient client; |
|||
|
|||
public TestCoapClient(){ |
|||
this.client = createClient(); |
|||
} |
|||
|
|||
public TestCoapClient(String accessToken, FeatureType featureType) { |
|||
this.client = createClient(getFeatureTokenUrl(accessToken, featureType)); |
|||
} |
|||
|
|||
public TestCoapClient(String featureTokenUrl) { |
|||
this.client = createClient(featureTokenUrl); |
|||
} |
|||
|
|||
public void connectToCoap(String accessToken) { |
|||
setURI(accessToken, null); |
|||
} |
|||
|
|||
public void connectToCoap(String accessToken, FeatureType featureType) { |
|||
setURI(accessToken, featureType); |
|||
} |
|||
|
|||
public void disconnect() { |
|||
if (client != null) { |
|||
client.shutdown(); |
|||
} |
|||
} |
|||
|
|||
public CoapResponse postMethod(String requestBody) throws ConnectorException, IOException { |
|||
return this.postMethod(requestBody.getBytes()); |
|||
} |
|||
|
|||
public CoapResponse postMethod(byte[] requestBodyBytes) throws ConnectorException, IOException { |
|||
return client.setTimeout(CLIENT_REQUEST_TIMEOUT).post(requestBodyBytes, MediaTypeRegistry.APPLICATION_JSON); |
|||
} |
|||
|
|||
public void postMethod(CoapHandler handler, String payload, int format) { |
|||
client.post(handler, payload, format); |
|||
} |
|||
|
|||
public void postMethod(CoapHandler handler, byte[] payload, int format) { |
|||
client.post(handler, payload, format); |
|||
} |
|||
|
|||
public CoapResponse getMethod() throws ConnectorException, IOException { |
|||
return client.setTimeout(CLIENT_REQUEST_TIMEOUT).get(); |
|||
} |
|||
|
|||
public CoapObserveRelation getObserveRelation(TestCoapClientCallback callback){ |
|||
Request request = Request.newGet().setObserve(); |
|||
request.setType(CoAP.Type.CON); |
|||
return client.observe(request, callback); |
|||
} |
|||
|
|||
public void setURI(String featureTokenUrl) { |
|||
if (client == null) { |
|||
throw new RuntimeException("Failed to connect! CoapClient is not initialized!"); |
|||
} |
|||
client.setURI(featureTokenUrl); |
|||
} |
|||
|
|||
public void setURI(String accessToken, FeatureType featureType) { |
|||
if (featureType == null){ |
|||
featureType = FeatureType.ATTRIBUTES; |
|||
} |
|||
setURI(getFeatureTokenUrl(accessToken, featureType)); |
|||
} |
|||
|
|||
private CoapClient createClient() { |
|||
return new CoapClient(); |
|||
} |
|||
|
|||
private CoapClient createClient(String featureTokenUrl) { |
|||
return new CoapClient(featureTokenUrl); |
|||
} |
|||
|
|||
public static String getFeatureTokenUrl(FeatureType featureType) { |
|||
return COAP_BASE_URL + featureType.name().toLowerCase(); |
|||
} |
|||
|
|||
public static String getFeatureTokenUrl(String token, FeatureType featureType) { |
|||
return COAP_BASE_URL + token + "/" + featureType.name().toLowerCase(); |
|||
} |
|||
|
|||
public static String getFeatureTokenUrl(String token, FeatureType featureType, int requestId) { |
|||
return COAP_BASE_URL + token + "/" + featureType.name().toLowerCase() + "/" + requestId; |
|||
} |
|||
} |
|||
@ -1,68 +0,0 @@ |
|||
/** |
|||
* Copyright © 2016-2024 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.msa; |
|||
|
|||
import lombok.Data; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.eclipse.californium.core.CoapHandler; |
|||
import org.eclipse.californium.core.CoapResponse; |
|||
import org.eclipse.californium.core.coap.CoAP; |
|||
|
|||
import java.util.concurrent.CountDownLatch; |
|||
|
|||
@Slf4j |
|||
@Data |
|||
public class TestCoapClientCallback implements CoapHandler { |
|||
|
|||
protected final CountDownLatch latch; |
|||
protected Integer observe; |
|||
protected byte[] payloadBytes; |
|||
protected CoAP.ResponseCode responseCode; |
|||
|
|||
public TestCoapClientCallback() { |
|||
this.latch = new CountDownLatch(1); |
|||
} |
|||
|
|||
public TestCoapClientCallback(int subscribeCount) { |
|||
this.latch = new CountDownLatch(subscribeCount); |
|||
} |
|||
|
|||
public Integer getObserve() { |
|||
return observe; |
|||
} |
|||
|
|||
public byte[] getPayloadBytes() { |
|||
return payloadBytes; |
|||
} |
|||
|
|||
public CoAP.ResponseCode getResponseCode() { |
|||
return responseCode; |
|||
} |
|||
|
|||
@Override |
|||
public void onLoad(CoapResponse response) { |
|||
observe = response.getOptions().getObserve(); |
|||
payloadBytes = response.getPayload(); |
|||
responseCode = response.getCode(); |
|||
latch.countDown(); |
|||
} |
|||
|
|||
@Override |
|||
public void onError() { |
|||
log.warn("Command Response Ack Error, No connect"); |
|||
} |
|||
|
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue