275 changed files with 8569 additions and 8844 deletions
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
@ -0,0 +1,126 @@ |
|||
/** |
|||
* 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.service.subscription; |
|||
|
|||
import ch.qos.logback.classic.Logger; |
|||
import ch.qos.logback.classic.spi.ILoggingEvent; |
|||
import ch.qos.logback.core.read.ListAppender; |
|||
import com.google.common.util.concurrent.Futures; |
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import com.google.common.util.concurrent.ListeningExecutorService; |
|||
import com.google.common.util.concurrent.MoreExecutors; |
|||
import org.apache.commons.lang3.RandomStringUtils; |
|||
import org.junit.jupiter.api.AfterEach; |
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.slf4j.LoggerFactory; |
|||
import org.springframework.test.util.ReflectionTestUtils; |
|||
import org.thingsboard.server.cache.limits.RateLimitService; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.limit.LimitedApi; |
|||
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; |
|||
import org.thingsboard.server.gen.transport.TransportProtos; |
|||
import org.thingsboard.server.queue.discovery.PartitionService; |
|||
import org.thingsboard.server.service.ws.WebSocketSessionRef; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.HashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.UUID; |
|||
import java.util.concurrent.Executors; |
|||
|
|||
import static org.junit.jupiter.api.Assertions.assertFalse; |
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.ArgumentMatchers.eq; |
|||
import static org.mockito.ArgumentMatchers.nullable; |
|||
import static org.mockito.Mockito.mock; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
public class DefaultTbLocalSubscriptionServiceTest { |
|||
|
|||
ListAppender<ILoggingEvent> testLogAppender; |
|||
TbLocalSubscriptionService subscriptionService; |
|||
|
|||
@BeforeEach |
|||
public void setUp() throws Exception { |
|||
Logger logger = (Logger) LoggerFactory.getLogger(DefaultTbLocalSubscriptionService.class); |
|||
testLogAppender = new ListAppender<>(); |
|||
testLogAppender.start(); |
|||
logger.addAppender(testLogAppender); |
|||
|
|||
RateLimitService rateLimitService = mock(); |
|||
when(rateLimitService.checkRateLimit(eq(LimitedApi.WS_SUBSCRIPTIONS), any(Object.class), nullable(String.class))).thenReturn(true); |
|||
PartitionService partitionService = mock(); |
|||
when(partitionService.resolve(any(), any(), any())).thenReturn(TopicPartitionInfo.builder().build()); |
|||
subscriptionService = new DefaultTbLocalSubscriptionService(mock(), mock(), mock(), partitionService, mock(), mock(), mock(), rateLimitService); |
|||
ReflectionTestUtils.setField(subscriptionService, "serviceId", "serviceId"); |
|||
} |
|||
|
|||
@AfterEach |
|||
public void tearDown() { |
|||
if (testLogAppender != null) { |
|||
testLogAppender.stop(); |
|||
Logger logger = (Logger) LoggerFactory.getLogger(DefaultTbLocalSubscriptionService.class); |
|||
logger.detachAppender(testLogAppender); |
|||
} |
|||
} |
|||
|
|||
@Test |
|||
public void addSubscriptionConcurrentModificationTest() throws Exception { |
|||
ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10)); |
|||
TenantId tenantId = new TenantId(UUID.randomUUID()); |
|||
DeviceId deviceId = new DeviceId(UUID.randomUUID()); |
|||
WebSocketSessionRef sessionRef = mock(); |
|||
ReflectionTestUtils.setField(subscriptionService, "subscriptionUpdateExecutor", executorService); |
|||
|
|||
List<ListenableFuture<?>> futures = new ArrayList<>(); |
|||
|
|||
try { |
|||
subscriptionService.onCoreStartupMsg(TransportProtos.CoreStartupMsg.newBuilder().addAllPartitions(List.of(0)).getDefaultInstanceForType()); |
|||
for (int i = 0; i < 50; i++) { |
|||
futures.add(executorService.submit(() -> subscriptionService.addSubscription(createSubscription(tenantId, deviceId), sessionRef))); |
|||
} |
|||
Futures.allAsList(futures).get(); |
|||
} finally { |
|||
executorService.shutdownNow(); |
|||
} |
|||
|
|||
List<ILoggingEvent> logs = testLogAppender.list; |
|||
boolean exceptionLogged = logs.stream() |
|||
.filter(event -> event.getThrowableProxy() != null) |
|||
.map(event -> event.getThrowableProxy().getClassName()) |
|||
.anyMatch(log -> log.equals("java.util.ConcurrentModificationException")); |
|||
|
|||
assertFalse(exceptionLogged, "Detected ConcurrentModificationException!"); |
|||
} |
|||
|
|||
private TbSubscription<?> createSubscription(TenantId tenantId, EntityId entityId) { |
|||
Map<String, Long> keys = new HashMap<>(); |
|||
for (int i = 0; i < 50; i++) { |
|||
keys.put(RandomStringUtils.randomAlphanumeric(5), 1L); |
|||
} |
|||
return TbAttributeSubscription.builder() |
|||
.tenantId(tenantId) |
|||
.entityId(entityId) |
|||
.subscriptionId(1) |
|||
.sessionId(RandomStringUtils.randomAlphanumeric(5)) |
|||
.keyStates(keys) |
|||
.build(); |
|||
} |
|||
} |
|||
@ -0,0 +1,186 @@ |
|||
/** |
|||
* 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.service.subscription; |
|||
|
|||
import org.junit.jupiter.api.Test; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.id.EntityId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; |
|||
|
|||
import java.util.Collection; |
|||
import java.util.HashSet; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Set; |
|||
import java.util.UUID; |
|||
import java.util.stream.Collectors; |
|||
|
|||
import static org.junit.jupiter.api.Assertions.assertEquals; |
|||
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.assertTrue; |
|||
|
|||
public class TbEntityLocalSubsInfoTest { |
|||
|
|||
@Test |
|||
public void addTest() { |
|||
Set<TbAttributeSubscription> expectedSubs = new HashSet<>(); |
|||
TbEntityLocalSubsInfo subsInfo = createSubsInfo(); |
|||
TenantId tenantId = subsInfo.getTenantId(); |
|||
EntityId entityId = subsInfo.getEntityId(); |
|||
TbAttributeSubscription attrSubscription1 = TbAttributeSubscription.builder() |
|||
.sessionId("session1") |
|||
.tenantId(tenantId) |
|||
.entityId(entityId) |
|||
.keyStates(Map.of("key1", 1L, "key2", 2L)) |
|||
.build(); |
|||
expectedSubs.add(attrSubscription1); |
|||
TbEntitySubEvent created = subsInfo.add(attrSubscription1); |
|||
assertFalse(subsInfo.isEmpty()); |
|||
assertNotNull(created); |
|||
assertEquals(expectedSubs, subsInfo.getSubs()); |
|||
checkEvent(created, expectedSubs, ComponentLifecycleEvent.CREATED); |
|||
|
|||
assertNull(subsInfo.add(attrSubscription1)); |
|||
|
|||
TbAttributeSubscription attrSubscription2 = TbAttributeSubscription.builder() |
|||
.sessionId("session2") |
|||
.tenantId(tenantId) |
|||
.entityId(entityId) |
|||
.keyStates(Map.of("key3", 3L, "key4", 4L)) |
|||
.build(); |
|||
expectedSubs.add(attrSubscription2); |
|||
TbEntitySubEvent updated = subsInfo.add(attrSubscription2); |
|||
assertNotNull(updated); |
|||
|
|||
assertEquals(expectedSubs, subsInfo.getSubs()); |
|||
checkEvent(updated, expectedSubs, ComponentLifecycleEvent.UPDATED); |
|||
} |
|||
|
|||
@Test |
|||
public void removeTest() { |
|||
Set<TbAttributeSubscription> expectedSubs = new HashSet<>(); |
|||
TbEntityLocalSubsInfo subsInfo = createSubsInfo(); |
|||
TenantId tenantId = subsInfo.getTenantId(); |
|||
EntityId entityId = subsInfo.getEntityId(); |
|||
TbAttributeSubscription attrSubscription1 = TbAttributeSubscription.builder() |
|||
.sessionId("session1") |
|||
.tenantId(tenantId) |
|||
.entityId(entityId) |
|||
.keyStates(Map.of("key1", 1L, "key2", 2L)) |
|||
.build(); |
|||
|
|||
TbAttributeSubscription attrSubscription2 = TbAttributeSubscription.builder() |
|||
.sessionId("session2") |
|||
.tenantId(tenantId) |
|||
.entityId(entityId) |
|||
.keyStates(Map.of("key3", 3L, "key4", 4L)) |
|||
.build(); |
|||
|
|||
expectedSubs.add(attrSubscription1); |
|||
expectedSubs.add(attrSubscription2); |
|||
|
|||
subsInfo.add(attrSubscription1); |
|||
subsInfo.add(attrSubscription2); |
|||
|
|||
assertEquals(expectedSubs, subsInfo.getSubs()); |
|||
|
|||
TbEntitySubEvent updatedEvent = subsInfo.remove(attrSubscription1); |
|||
expectedSubs.remove(attrSubscription1); |
|||
assertNotNull(updatedEvent); |
|||
assertEquals(expectedSubs, subsInfo.getSubs()); |
|||
checkEvent(updatedEvent, expectedSubs, ComponentLifecycleEvent.UPDATED); |
|||
|
|||
TbEntitySubEvent deletedEvent = subsInfo.remove(attrSubscription2); |
|||
expectedSubs.remove(attrSubscription2); |
|||
assertNotNull(deletedEvent); |
|||
assertEquals(expectedSubs, subsInfo.getSubs()); |
|||
checkEvent(deletedEvent, expectedSubs, ComponentLifecycleEvent.DELETED); |
|||
|
|||
assertTrue(subsInfo.isEmpty()); |
|||
} |
|||
|
|||
@Test |
|||
public void removeAllTest() { |
|||
TbEntityLocalSubsInfo subsInfo = createSubsInfo(); |
|||
TenantId tenantId = subsInfo.getTenantId(); |
|||
EntityId entityId = subsInfo.getEntityId(); |
|||
TbAttributeSubscription attrSubscription1 = TbAttributeSubscription.builder() |
|||
.sessionId("session1") |
|||
.tenantId(tenantId) |
|||
.entityId(entityId) |
|||
.keyStates(Map.of("key1", 1L, "key2", 2L)) |
|||
.build(); |
|||
|
|||
TbAttributeSubscription attrSubscription2 = TbAttributeSubscription.builder() |
|||
.sessionId("session2") |
|||
.tenantId(tenantId) |
|||
.entityId(entityId) |
|||
.keyStates(Map.of("key3", 3L, "key4", 4L)) |
|||
.build(); |
|||
|
|||
TbAttributeSubscription attrSubscription3 = TbAttributeSubscription.builder() |
|||
.sessionId("session3") |
|||
.tenantId(tenantId) |
|||
.entityId(entityId) |
|||
.keyStates(Map.of("key5", 5L, "key6", 6L)) |
|||
.build(); |
|||
|
|||
subsInfo.add(attrSubscription1); |
|||
subsInfo.add(attrSubscription2); |
|||
subsInfo.add(attrSubscription3); |
|||
|
|||
assertFalse(subsInfo.isEmpty()); |
|||
|
|||
TbEntitySubEvent updatedEvent = subsInfo.removeAll(List.of(attrSubscription1, attrSubscription2)); |
|||
assertNotNull(updatedEvent); |
|||
checkEvent(updatedEvent, Set.of(attrSubscription3), ComponentLifecycleEvent.UPDATED); |
|||
|
|||
assertFalse(subsInfo.isEmpty()); |
|||
|
|||
TbEntitySubEvent deletedEvent = subsInfo.removeAll(List.of(attrSubscription3)); |
|||
assertNotNull(deletedEvent); |
|||
checkEvent(deletedEvent, null, ComponentLifecycleEvent.DELETED); |
|||
|
|||
assertTrue(subsInfo.isEmpty()); |
|||
} |
|||
|
|||
private TbEntityLocalSubsInfo createSubsInfo() { |
|||
return new TbEntityLocalSubsInfo(new TenantId(UUID.randomUUID()), new DeviceId(UUID.randomUUID())); |
|||
} |
|||
|
|||
private void checkEvent(TbEntitySubEvent event, Set<TbAttributeSubscription> expectedSubs, ComponentLifecycleEvent expectedType) { |
|||
assertEquals(expectedType, event.getType()); |
|||
TbSubscriptionsInfo info = event.getInfo(); |
|||
if (event.getType() == ComponentLifecycleEvent.DELETED) { |
|||
assertNull(info); |
|||
return; |
|||
} |
|||
assertNotNull(info); |
|||
assertFalse(info.notifications); |
|||
assertFalse(info.alarms); |
|||
assertFalse(info.attrAllKeys); |
|||
assertFalse(info.tsAllKeys); |
|||
assertNull(info.tsKeys); |
|||
assertEquals(getAttrKeys(expectedSubs), info.attrKeys); |
|||
} |
|||
|
|||
private Set<String> getAttrKeys(Set<TbAttributeSubscription> attributeSubscriptions) { |
|||
return attributeSubscriptions.stream().map(s -> s.getKeyStates().keySet()).flatMap(Collection::stream).collect(Collectors.toSet()); |
|||
} |
|||
} |
|||
@ -1,732 +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.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; |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
@ -1,71 +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.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,73 @@ |
|||
/** |
|||
* 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.ota.sql; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.junit.Test; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.common.data.DeviceProfile; |
|||
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; |
|||
import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; |
|||
import org.thingsboard.server.common.data.kv.TsKvEntry; |
|||
import org.thingsboard.server.transport.lwm2m.ota.AbstractOtaLwM2MIntegrationTest; |
|||
|
|||
import java.util.List; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
import static org.assertj.core.api.Assertions.assertThat; |
|||
import static org.awaitility.Awaitility.await; |
|||
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADED; |
|||
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADING; |
|||
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.INITIATED; |
|||
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.QUEUED; |
|||
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATED; |
|||
import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.VERIFIED; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; |
|||
|
|||
@Slf4j |
|||
public class Ota9LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { |
|||
|
|||
/** |
|||
* => Start -> INITIAL (State=0) -> DOWNLOAD STARTED; |
|||
* => PKG / URI Write -> DOWNLOAD STARTED (Res=1 (Downloading) && State=1) -> DOWNLOADED |
|||
* => PKG Written -> DOWNLOADED (Res=1 Initial && State=2) -> DELIVERED; |
|||
* => PKG integrity verified -> DELIVERED (Res=3 (Successfully Downloaded and package integrity verified) && State=3) -> INSTALLED; |
|||
* => Install -> INSTALLED (Res=2 SW successfully installed) && State=4) -> Start |
|||
* |
|||
* */ |
|||
@Test |
|||
public void testSoftwareUpdateByObject9() throws Exception { |
|||
Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA9, getBootstrapServerCredentialsNoSec(NONE)); |
|||
DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + this.CLIENT_ENDPOINT_OTA9, transportConfiguration); |
|||
LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(this.CLIENT_ENDPOINT_OTA9)); |
|||
final Device device = createLwm2mDevice(deviceCredentials, this.CLIENT_ENDPOINT_OTA9, deviceProfile.getId()); |
|||
createNewClient(SECURITY_NO_SEC, null, false, this.CLIENT_ENDPOINT_OTA9); |
|||
awaitObserveReadAll(4, device.getId().getId().toString()); |
|||
|
|||
device.setSoftwareId(createSoftware(deviceProfile.getId()).getId()); |
|||
final Device savedDevice = doPost("/api/device", device, Device.class); //sync call
|
|||
|
|||
assertThat(savedDevice).as("saved device").isNotNull(); |
|||
assertThat(getDeviceFromAPI(device.getId().getId())).as("fetched device").isEqualTo(savedDevice); |
|||
|
|||
expectedStatuses = List.of( |
|||
QUEUED, INITIATED, DOWNLOADING, DOWNLOADING, DOWNLOADING, DOWNLOADED, VERIFIED, UPDATED); |
|||
List<TsKvEntry> ts = await("await on timeseries") |
|||
.atMost(TIMEOUT, TimeUnit.SECONDS) |
|||
.until(() -> getFwSwStateTelemetryFromAPI(device.getId().getId(), "sw_state"), this::predicateForStatuses); |
|||
log.warn("Object9: Got the ts: {}", ts); |
|||
} |
|||
} |
|||
@ -0,0 +1,98 @@ |
|||
/** |
|||
* 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.JsonNode; |
|||
import com.fasterxml.jackson.databind.node.ArrayNode; |
|||
import com.fasterxml.jackson.databind.node.ObjectNode; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; |
|||
import java.util.concurrent.atomic.AtomicReference; |
|||
import static java.util.concurrent.TimeUnit.SECONDS; |
|||
import static org.awaitility.Awaitility.await; |
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.OBJECT_INSTANCE_ID_12; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_0; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_TS_1; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_VALUE_0; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_3303_12_5700_VALUE_1; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_NAME_3303_12_5700; |
|||
import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS; |
|||
|
|||
@Slf4j |
|||
public class RpcLwm2mIntegrationReadCollectedValueTest extends AbstractRpcLwM2MIntegrationTest { |
|||
|
|||
/** |
|||
* Read {"id":"/3303/12/5700"} |
|||
* Trigger a Send operation from the client with multiple values for the same resource as a payload |
|||
* acked "[{"bn":"/3303/12/5700","bt":1724".. 116 bytes] |
|||
* 2 values for the resource /3303/12/5700 should be stored with: |
|||
* - timestamps1 = Instance.now() + RESOURCE_ID_VALUE_3303_12_5700_1 |
|||
* - timestamps2 = (timestamps1 + 3 sec) + RESOURCE_ID_VALUE_3303_12_5700_2 |
|||
* @throws Exception |
|||
*/ |
|||
@Test |
|||
public void testReadSingleResource_sendFromClient_CollectedValue() throws Exception { |
|||
// init test
|
|||
int cntValues = 2; |
|||
int resourceId = 5700; |
|||
String expectedIdVer = objectIdVer_3303 + "/" + OBJECT_INSTANCE_ID_12 + "/" + resourceId; |
|||
sendRPCById(expectedIdVer); |
|||
|
|||
// verify time start/end send CollectedValue;
|
|||
await().atMost(40, SECONDS).until(() -> RESOURCE_ID_3303_12_5700_TS_0 > 0 |
|||
&& RESOURCE_ID_3303_12_5700_TS_1 > 0); |
|||
|
|||
// verify result read: verify count value: 1-2: send CollectedValue;
|
|||
AtomicReference<ObjectNode> actualValues = new AtomicReference<>(); |
|||
await().atMost(40, SECONDS).until(() -> { |
|||
actualValues.set(doGetAsync( |
|||
"/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?keys=" |
|||
+ RESOURCE_ID_NAME_3303_12_5700 |
|||
+ "&startTs=" + (RESOURCE_ID_3303_12_5700_TS_0 - RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS) |
|||
+ "&endTs=" + (RESOURCE_ID_3303_12_5700_TS_1 + RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS) |
|||
+ "&interval=0&limit=100&useStrictDataTypes=false", |
|||
ObjectNode.class)); |
|||
return actualValues.get() != null && actualValues.get().size() > 0 |
|||
&& actualValues.get().get(RESOURCE_ID_NAME_3303_12_5700).size() >= cntValues && verifyTs(actualValues); |
|||
}); |
|||
} |
|||
|
|||
private boolean verifyTs(AtomicReference<ObjectNode> actualValues) { |
|||
String expectedVal_0 = String.valueOf(RESOURCE_ID_3303_12_5700_VALUE_0); |
|||
String expectedVal_1 = String.valueOf(RESOURCE_ID_3303_12_5700_VALUE_1); |
|||
ArrayNode actual = (ArrayNode) actualValues.get().get(RESOURCE_ID_NAME_3303_12_5700); |
|||
long actualTS0 = 0; |
|||
long actualTS1 = 0; |
|||
for (JsonNode tsNode : actual) { |
|||
if (tsNode.get("value").asText().equals(expectedVal_0)) { |
|||
actualTS0 = tsNode.get("ts").asLong(); |
|||
} else if (tsNode.get("value").asText().equals(expectedVal_1)) { |
|||
actualTS1 = tsNode.get("ts").asLong(); |
|||
} |
|||
} |
|||
return actualTS0 >= RESOURCE_ID_3303_12_5700_TS_0 |
|||
&& actualTS1 <= RESOURCE_ID_3303_12_5700_TS_1 |
|||
&& (actualTS1 - actualTS0) >= RESOURCE_ID_VALUE_3303_12_5700_DELTA_TS; |
|||
} |
|||
|
|||
private String sendRPCById(String path) throws Exception { |
|||
String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}"; |
|||
return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); |
|||
} |
|||
} |
|||
@ -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.coap.efento.utils; |
|||
|
|||
public enum PulseCounterType { |
|||
|
|||
WATER_CNT_ACC("water_cnt_acc_", 100), |
|||
PULSE_CNT_ACC("pulse_cnt_acc_", 1000), |
|||
ELEC_METER_ACC("elec_meter_acc_", 1000), |
|||
PULSE_CNT_ACC_WIDE("pulse_cnt_acc_wide_", 1000000); |
|||
|
|||
private final String prefix; |
|||
private final int majorResolution; |
|||
|
|||
PulseCounterType(String prefix, int majorResolution) { |
|||
this.prefix = prefix; |
|||
this.majorResolution = majorResolution; |
|||
} |
|||
|
|||
public String getPrefix() { |
|||
return prefix; |
|||
} |
|||
|
|||
public int getMajorResolution() { |
|||
return majorResolution; |
|||
} |
|||
} |
|||
@ -0,0 +1,115 @@ |
|||
/** |
|||
* 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.dao.sqlts; |
|||
|
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import org.apache.commons.lang3.RandomStringUtils; |
|||
import org.junit.Assert; |
|||
import org.junit.Test; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.thingsboard.server.common.data.id.DeviceId; |
|||
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
|||
import org.thingsboard.server.common.data.kv.StringDataEntry; |
|||
import org.thingsboard.server.common.data.kv.TsKvEntry; |
|||
import org.thingsboard.server.dao.service.AbstractServiceTest; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
|
|||
import static org.junit.jupiter.api.Assertions.assertEquals; |
|||
import static org.junit.jupiter.api.Assertions.assertNotNull; |
|||
import static org.junit.jupiter.api.Assertions.assertNull; |
|||
import static org.junit.jupiter.api.Assertions.assertTrue; |
|||
|
|||
@DaoSqlTest |
|||
public class SqlTimeseriesLatestDaoTest extends AbstractServiceTest { |
|||
|
|||
@Autowired |
|||
private TimeseriesLatestDao timeseriesLatestDao; |
|||
|
|||
@Test |
|||
public void saveLatestTest() throws Exception { |
|||
DeviceId deviceId = new DeviceId(UUID.randomUUID()); |
|||
var entry = createEntry("key", 1000); |
|||
Long version = timeseriesLatestDao.saveLatest(tenantId, deviceId, entry).get(); |
|||
assertNotNull(version); |
|||
assertTrue(version > 0); |
|||
|
|||
TsKvEntry foundEntry = timeseriesLatestDao.findLatest(tenantId, deviceId, "key").get(); |
|||
assertNotNull(foundEntry); |
|||
equalsIgnoreVersion(entry, foundEntry); |
|||
assertEquals(version, foundEntry.getVersion()); |
|||
|
|||
var updatedEntry = createEntry("key", 2000); |
|||
Long updatedVersion = timeseriesLatestDao.saveLatest(tenantId, deviceId, updatedEntry).get(); |
|||
assertNotNull(updatedVersion); |
|||
assertTrue(updatedVersion > version); |
|||
|
|||
foundEntry = timeseriesLatestDao.findLatest(tenantId, deviceId, "key").get(); |
|||
assertNotNull(foundEntry); |
|||
equalsIgnoreVersion(updatedEntry, foundEntry); |
|||
assertEquals(updatedVersion, foundEntry.getVersion()); |
|||
|
|||
var oldEntry = createEntry("key", 1); |
|||
Long oldVersion = timeseriesLatestDao.saveLatest(tenantId, deviceId, oldEntry).get(); |
|||
assertNull(oldVersion); |
|||
|
|||
foundEntry = timeseriesLatestDao.findLatest(tenantId, deviceId, "key").get(); |
|||
assertNotNull(foundEntry); |
|||
equalsIgnoreVersion(updatedEntry, foundEntry); |
|||
assertEquals(updatedVersion, foundEntry.getVersion()); |
|||
} |
|||
|
|||
@Test |
|||
public void updateWithOldTsTest() throws Exception { |
|||
DeviceId deviceId = new DeviceId(UUID.randomUUID()); |
|||
int n = 50; |
|||
for (int i = 0; i < n; i++) { |
|||
timeseriesLatestDao.saveLatest(tenantId, deviceId, createEntry("key_" + i, System.currentTimeMillis())); |
|||
} |
|||
|
|||
List<ListenableFuture<Long>> futures = new ArrayList<>(n); |
|||
|
|||
for (int i = 0; i < n; i++) { |
|||
long ts = i % 2 == 0 ? System.currentTimeMillis() : 1000; |
|||
futures.add(timeseriesLatestDao.saveLatest(tenantId, deviceId, createEntry("key_" + i, ts))); |
|||
} |
|||
|
|||
for (int i = 0; i < futures.size(); i++) { |
|||
Long version = futures.get(i).get(); |
|||
if (i % 2 == 0) { |
|||
assertNotNull(version); |
|||
assertTrue(version > 0); |
|||
} else { |
|||
assertNull(version); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private TsKvEntry createEntry(String key, long ts) { |
|||
return new BasicTsKvEntry(ts, new StringDataEntry(key, RandomStringUtils.random(10))); |
|||
} |
|||
|
|||
private void equalsIgnoreVersion(TsKvEntry expected, TsKvEntry actual) { |
|||
Assert.assertEquals(expected.getKey(), actual.getKey()); |
|||
Assert.assertEquals(expected.getValue(), actual.getValue()); |
|||
Assert.assertEquals(expected.getTs(), actual.getTs()); |
|||
} |
|||
|
|||
} |
|||
File diff suppressed because one or more lines are too long
@ -1,68 +1,69 @@ |
|||
{ |
|||
"root": true, |
|||
"ignorePatterns": [ |
|||
"projects/**/*" |
|||
], |
|||
"overrides": [ |
|||
{ |
|||
"files": [ |
|||
"*.ts" |
|||
], |
|||
"parserOptions": { |
|||
"project": [ |
|||
"tsconfig.json", |
|||
"e2e/tsconfig.json" |
|||
], |
|||
"createDefaultProgram": true |
|||
}, |
|||
"extends": [ |
|||
"plugin:@angular-eslint/ng-cli-compat", |
|||
"plugin:@angular-eslint/ng-cli-compat--formatting-add-on", |
|||
"plugin:@angular-eslint/template/process-inline-templates" |
|||
], |
|||
"rules": { |
|||
"@typescript-eslint/explicit-member-accessibility": [ |
|||
"off", |
|||
{ |
|||
"accessibility": "explicit" |
|||
} |
|||
], |
|||
"arrow-parens": [ |
|||
"off", |
|||
"always" |
|||
], |
|||
"@angular-eslint/component-selector": [ |
|||
"error", |
|||
{ |
|||
"prefix": [ "tb" ] |
|||
} |
|||
], |
|||
"id-blacklist": [ |
|||
"error", |
|||
"any", |
|||
"Number", |
|||
"String", |
|||
"string", |
|||
"Boolean", |
|||
"boolean", |
|||
"Undefined", |
|||
"undefined" |
|||
], |
|||
"import/order": "off", |
|||
"@typescript-eslint/member-ordering": "off", |
|||
"no-underscore-dangle": "off", |
|||
"@typescript-eslint/naming-convention": "off", |
|||
"jsdoc/newline-after-description": 0 |
|||
} |
|||
}, |
|||
{ |
|||
"files": [ |
|||
"*.html" |
|||
], |
|||
"extends": [ |
|||
"plugin:@angular-eslint/template/recommended" |
|||
], |
|||
"rules": {} |
|||
} |
|||
] |
|||
"root": true, |
|||
"ignorePatterns": [ |
|||
"projects/**/*" |
|||
], |
|||
"overrides": [ |
|||
{ |
|||
"files": [ |
|||
"*.ts", |
|||
"*.tsx" |
|||
], |
|||
"parserOptions": { |
|||
"project": [ |
|||
"tsconfig.json" |
|||
], |
|||
"createDefaultProgram": true |
|||
}, |
|||
"extends": [ |
|||
"plugin:@angular-eslint/recommended", |
|||
"plugin:@angular-eslint/template/process-inline-templates" |
|||
], |
|||
"rules": { |
|||
"@typescript-eslint/explicit-member-accessibility": [ |
|||
"off", |
|||
{ |
|||
"accessibility": "explicit" |
|||
} |
|||
], |
|||
"arrow-parens": [ |
|||
"off", |
|||
"always" |
|||
], |
|||
"@angular-eslint/component-selector": [ |
|||
"error", |
|||
{ |
|||
"prefix": [ |
|||
"tb" |
|||
] |
|||
} |
|||
], |
|||
"id-blacklist": [ |
|||
"error", |
|||
"any", |
|||
"Number", |
|||
"String", |
|||
"string", |
|||
"Boolean", |
|||
"boolean", |
|||
"Undefined", |
|||
"undefined" |
|||
], |
|||
"import/order": "off", |
|||
"@typescript-eslint/member-ordering": "off", |
|||
"no-underscore-dangle": "off", |
|||
"@typescript-eslint/naming-convention": "off", |
|||
"jsdoc/newline-after-description": 0 |
|||
} |
|||
}, |
|||
{ |
|||
"files": [ |
|||
"*.html" |
|||
], |
|||
"extends": [ |
|||
"plugin:@angular-eslint/template/recommended" |
|||
], |
|||
"rules": {} |
|||
} |
|||
] |
|||
} |
|||
|
|||
@ -1,43 +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. |
|||
*/ |
|||
// Protractor configuration file, see link for more information
|
|||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
|||
|
|||
const { SpecReporter } = require("jasmine-spec-reporter"); |
|||
|
|||
exports.config = { |
|||
allScriptsTimeout: 11000, |
|||
specs: [ |
|||
"./src/**/*.e2e-spec.ts", |
|||
], |
|||
capabilities: { |
|||
"browserName": "chrome", |
|||
}, |
|||
directConnect: true, |
|||
baseUrl: "http://localhost:4200/", |
|||
framework: "jasmine", |
|||
jasmineNodeOpts: { |
|||
showColors: true, |
|||
defaultTimeoutInterval: 30000, |
|||
print: function() {}, |
|||
}, |
|||
onPrepare() { |
|||
require("ts-node").register({ |
|||
project: require("path").join(__dirname, "./tsconfig.e2e.json"), |
|||
}); |
|||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); |
|||
}, |
|||
}; |
|||
@ -1,39 +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.
|
|||
///
|
|||
|
|||
import { AppPage } from './app.po'; |
|||
import { browser, logging } from 'protractor'; |
|||
|
|||
describe('workspace-project App', () => { |
|||
let page: AppPage; |
|||
|
|||
beforeEach(() => { |
|||
page = new AppPage(); |
|||
}); |
|||
|
|||
it('should display welcome message', () => { |
|||
page.navigateTo(); |
|||
expect(page.getTitleText()).toEqual('Welcome to tb-license-server!'); |
|||
}); |
|||
|
|||
afterEach(async () => { |
|||
// Assert that there are no errors emitted from the browser
|
|||
const logs = await browser.manage().logs().get(logging.Type.BROWSER); |
|||
expect(logs).not.toContain(jasmine.objectContaining({ |
|||
level: logging.Level.SEVERE, |
|||
} as logging.Entry)); |
|||
}); |
|||
}); |
|||
@ -1,27 +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.
|
|||
///
|
|||
|
|||
import { browser, by, element } from 'protractor'; |
|||
|
|||
export class AppPage { |
|||
navigateTo() { |
|||
return browser.get(browser.baseUrl) as Promise<any>; |
|||
} |
|||
|
|||
getTitleText() { |
|||
return element(by.css('tb-root h1')).getText() as Promise<string>; |
|||
} |
|||
} |
|||
@ -1,13 +0,0 @@ |
|||
{ |
|||
"extends": "../tsconfig.json", |
|||
"compilerOptions": { |
|||
"outDir": "../out-tsc/app", |
|||
"module": "commonjs", |
|||
"target": "es5", |
|||
"types": [ |
|||
"jasmine", |
|||
"jasminewd2", |
|||
"node" |
|||
] |
|||
} |
|||
} |
|||
@ -0,0 +1,130 @@ |
|||
/* |
|||
* 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. |
|||
*/ |
|||
|
|||
const fs = require('fs'); |
|||
const path = require('path'); |
|||
|
|||
const materialIconDir = path.join('.', 'src', 'assets', 'metadata'); |
|||
const mdiMetadata = path.join('.', 'node_modules', '@mdi', 'svg', 'meta.json'); |
|||
|
|||
async function init() { |
|||
const iconsBundle = JSON.parse(await fs.promises.readFile(path.join(materialIconDir, 'material-icons.json'))); |
|||
|
|||
await getMaterialIconMetadataAndUpdated(iconsBundle); |
|||
await getMDIMetadataAndUpdated(iconsBundle); |
|||
|
|||
await fs.promises.writeFile(path.join(materialIconDir, 'material-icons.json'), JSON.stringify(iconsBundle), 'utf8') |
|||
} |
|||
|
|||
async function getMaterialIconMetadataAndUpdated(iconsBundle){ |
|||
const iconsResponse = await fetch('https://fonts.google.com/metadata/icons?key=material_symbols&incomplete=true'); |
|||
const iconsText = await iconsResponse.text(); |
|||
const clearText = iconsText.substring(iconsText.indexOf("\n") + 1); |
|||
|
|||
const icons = JSON.parse(clearText).icons; |
|||
|
|||
let prevItem; |
|||
const filterIcons = icons.filter((item) => { |
|||
if (prevItem?.name !== item.name && !item.unsupported_families.includes('Material Icons')) { |
|||
prevItem = item; |
|||
return true; |
|||
} |
|||
return false; |
|||
}); |
|||
|
|||
filterIcons.forEach((item, index) => { |
|||
const findItem = iconsBundle.find((el) => el.name === item.name); |
|||
if (!findItem) { |
|||
let prevIndexIcon = 0; |
|||
if (index === 0) { |
|||
prevIndexIcon = 45; |
|||
} else { |
|||
let iteration = 0; |
|||
while (prevIndexIcon < 45) { |
|||
iteration++; |
|||
const prevIconName = filterIcons[index - iteration].name; |
|||
prevIndexIcon = findPreviousIcon(iconsBundle, prevIconName); |
|||
} |
|||
} |
|||
if (prevIndexIcon >= 0) { |
|||
iconsBundle.splice(prevIndexIcon + 1, 0, {name:item.name, tags:item.tags}); |
|||
} |
|||
console.log('Not found icon:', item.name); |
|||
console.count('Not found material icon'); |
|||
return; |
|||
} |
|||
if (JSON.stringify(item.tags) !== JSON.stringify(findItem.tags)) { |
|||
findItem.tags = item.tags; |
|||
console.log('Difference tags in', item.name); |
|||
console.count('Difference tags in material icon'); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
async function getMDIMetadataAndUpdated(iconsBundle){ |
|||
const mdiBundle = JSON.parse(await fs.promises.readFile(mdiMetadata)); |
|||
|
|||
iconsBundle |
|||
.filter(item => item.name.startsWith('mdi:')) |
|||
.forEach(item => { |
|||
const iconName = item.name.substring(item.name.indexOf(":") + 1); |
|||
const findItem = mdiBundle.find((el) => el.name === iconName); |
|||
if (!findItem) { |
|||
console.error('Delete icon:', item.name); |
|||
} |
|||
}); |
|||
|
|||
|
|||
mdiBundle.forEach((item, index) => { |
|||
const iconName = `mdi:${item.name}` |
|||
let iconTags = item.tags; |
|||
const iconAliases = item.aliases.map(item => item.replaceAll('-', ' ')); |
|||
if (!iconTags.length && item.aliases.length) { |
|||
iconTags = iconAliases; |
|||
} else if (item.aliases.length) { |
|||
iconTags = iconTags.concat(iconAliases); |
|||
} |
|||
iconTags = iconTags.map(item => item.toLowerCase()); |
|||
|
|||
const findItem = iconsBundle.find((el) => el.name === iconName); |
|||
if (!findItem) { |
|||
let prevIndexIcon; |
|||
if (index === 0) { |
|||
prevIndexIcon = iconsBundle.findIndex(item => item.name.startsWith('mdi:')) |
|||
} else { |
|||
const prevIconName = `mdi:${mdiBundle[index - 1].name}`; |
|||
prevIndexIcon = findPreviousIcon(iconsBundle, prevIconName); |
|||
} |
|||
if (prevIndexIcon >= 0) { |
|||
iconsBundle.splice(prevIndexIcon + 1, 0, {name:iconName, tags:iconTags}); |
|||
} |
|||
console.log('Not found icon:', iconName); |
|||
console.count('Not found mdi icon'); |
|||
return; |
|||
} |
|||
if (JSON.stringify(iconTags) !== JSON.stringify(findItem.tags)) { |
|||
findItem.tags = iconTags; |
|||
console.log('Difference tags in', iconName); |
|||
console.count('Difference tags in mdi icon'); |
|||
} |
|||
}); |
|||
} |
|||
|
|||
function findPreviousIcon(iconsBundle, findName) { |
|||
return iconsBundle.findIndex(item => item.name === findName); |
|||
} |
|||
|
|||
init(); |
|||
@ -1,8 +1,8 @@ |
|||
diff --git a/node_modules/@angular/core/fesm2020/core.mjs b/node_modules/@angular/core/fesm2020/core.mjs
|
|||
index e9a9b75..17044d9 100755
|
|||
--- a/node_modules/@angular/core/fesm2020/core.mjs
|
|||
+++ b/node_modules/@angular/core/fesm2020/core.mjs
|
|||
@@ -11053,13 +11053,13 @@ function findDirectiveDefMatches(tView, tNode) {
|
|||
diff --git a/node_modules/@angular/core/fesm2022/core.mjs b/node_modules/@angular/core/fesm2022/core.mjs
|
|||
index 0fa881f..b844dfa 100755
|
|||
--- a/node_modules/@angular/core/fesm2022/core.mjs
|
|||
+++ b/node_modules/@angular/core/fesm2022/core.mjs
|
|||
@@ -12868,13 +12868,13 @@ function findDirectiveDefMatches(tView, tNode) {
|
|||
if (isNodeMatchingSelectorList(tNode, def.selectors, /* isProjectionMode */ false)) { |
|||
matches || (matches = []); |
|||
if (isComponentDef(def)) { |
|||
File diff suppressed because one or more lines are too long
@ -1,51 +0,0 @@ |
|||
diff --git a/node_modules/@mat-datetimepicker/core/esm2020/datetimepicker/clock.mjs b/node_modules/@mat-datetimepicker/core/esm2020/datetimepicker/clock.mjs
|
|||
index e3457ea..a069460 100644
|
|||
--- a/node_modules/@mat-datetimepicker/core/esm2020/datetimepicker/clock.mjs
|
|||
+++ b/node_modules/@mat-datetimepicker/core/esm2020/datetimepicker/clock.mjs
|
|||
@@ -259,9 +259,9 @@ export class MatDatetimepickerClockComponent {
|
|||
value = 0; |
|||
} |
|||
// Don't close the minutes view if an invalid minute is clicked. |
|||
- if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
|
|||
- return;
|
|||
- }
|
|||
+ // if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
|
|||
+ // return;
|
|||
+ // }
|
|||
date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), value); |
|||
} |
|||
this._timeChanged = true; |
|||
diff --git a/node_modules/@mat-datetimepicker/core/fesm2015/mat-datetimepicker-core.mjs b/node_modules/@mat-datetimepicker/core/fesm2015/mat-datetimepicker-core.mjs
|
|||
index 7699ff6..01aad13 100644
|
|||
--- a/node_modules/@mat-datetimepicker/core/fesm2015/mat-datetimepicker-core.mjs
|
|||
+++ b/node_modules/@mat-datetimepicker/core/fesm2015/mat-datetimepicker-core.mjs
|
|||
@@ -951,9 +951,9 @@ class MatDatetimepickerClockComponent {
|
|||
value = 0; |
|||
} |
|||
// Don't close the minutes view if an invalid minute is clicked. |
|||
- if (!((_b = this._minutes.find((m) => (m === null || m === void 0 ? void 0 : m['value']) === value)) === null || _b === void 0 ? void 0 : _b['enabled'])) {
|
|||
- return;
|
|||
- }
|
|||
+ // if (!((_b = this._minutes.find((m) => (m === null || m === void 0 ? void 0 : m['value']) === value)) === null || _b === void 0 ? void 0 : _b['enabled'])) {
|
|||
+ // return;
|
|||
+ // }
|
|||
date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), value); |
|||
} |
|||
this._timeChanged = true; |
|||
diff --git a/node_modules/@mat-datetimepicker/core/fesm2020/mat-datetimepicker-core.mjs b/node_modules/@mat-datetimepicker/core/fesm2020/mat-datetimepicker-core.mjs
|
|||
index 809a57d..f712b84 100644
|
|||
--- a/node_modules/@mat-datetimepicker/core/fesm2020/mat-datetimepicker-core.mjs
|
|||
+++ b/node_modules/@mat-datetimepicker/core/fesm2020/mat-datetimepicker-core.mjs
|
|||
@@ -946,9 +946,9 @@ class MatDatetimepickerClockComponent {
|
|||
value = 0; |
|||
} |
|||
// Don't close the minutes view if an invalid minute is clicked. |
|||
- if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
|
|||
- return;
|
|||
- }
|
|||
+ // if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
|
|||
+ // return;
|
|||
+ // }
|
|||
date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), value); |
|||
} |
|||
this._timeChanged = true; |
|||
@ -0,0 +1,34 @@ |
|||
diff --git a/node_modules/@mat-datetimepicker/core/esm2022/datetimepicker/clock.mjs b/node_modules/@mat-datetimepicker/core/esm2022/datetimepicker/clock.mjs
|
|||
index 7ecfae7..08363d3 100644
|
|||
--- a/node_modules/@mat-datetimepicker/core/esm2022/datetimepicker/clock.mjs
|
|||
+++ b/node_modules/@mat-datetimepicker/core/esm2022/datetimepicker/clock.mjs
|
|||
@@ -259,9 +259,9 @@ export class MatDatetimepickerClockComponent {
|
|||
value = 0; |
|||
} |
|||
// Don't close the minutes view if an invalid minute is clicked. |
|||
- if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
|
|||
- return;
|
|||
- }
|
|||
+ // if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
|
|||
+ // return;
|
|||
+ // }
|
|||
date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), value); |
|||
} |
|||
this._timeChanged = true; |
|||
diff --git a/node_modules/@mat-datetimepicker/core/fesm2022/mat-datetimepicker-core.mjs b/node_modules/@mat-datetimepicker/core/fesm2022/mat-datetimepicker-core.mjs
|
|||
index 00f4a52..df688e3 100644
|
|||
--- a/node_modules/@mat-datetimepicker/core/fesm2022/mat-datetimepicker-core.mjs
|
|||
+++ b/node_modules/@mat-datetimepicker/core/fesm2022/mat-datetimepicker-core.mjs
|
|||
@@ -946,9 +946,9 @@ class MatDatetimepickerClockComponent {
|
|||
value = 0; |
|||
} |
|||
// Don't close the minutes view if an invalid minute is clicked. |
|||
- if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
|
|||
- return;
|
|||
- }
|
|||
+ // if (!this._minutes.find((m) => m?.['value'] === value)?.['enabled']) {
|
|||
+ // return;
|
|||
+ // }
|
|||
date = this._adapter.createDatetime(this._adapter.getYear(this.activeDate), this._adapter.getMonth(this.activeDate), this._adapter.getDate(this.activeDate), this._adapter.getHour(this.activeDate), value); |
|||
} |
|||
this._timeChanged = true; |
|||
@ -1,15 +1,15 @@ |
|||
diff --git a/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs b/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs
|
|||
index cf4e220..df51c91 100644
|
|||
--- a/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs
|
|||
+++ b/node_modules/angular-gridster2/fesm2020/angular-gridster2.mjs
|
|||
diff --git a/node_modules/angular-gridster2/fesm2022/angular-gridster2.mjs b/node_modules/angular-gridster2/fesm2022/angular-gridster2.mjs
|
|||
index 0dcd873..e99b602 100644
|
|||
--- a/node_modules/angular-gridster2/fesm2022/angular-gridster2.mjs
|
|||
+++ b/node_modules/angular-gridster2/fesm2022/angular-gridster2.mjs
|
|||
@@ -666,8 +666,8 @@ class GridsterRenderer {
|
|||
renderer.setStyle(el, DirTypes.LTR ? 'margin-right' : 'margin-left', ''); |
|||
} |
|||
else { |
|||
- const x = Math.round(this.gridster.curColWidth * item.x);
|
|||
- const y = Math.round(this.gridster.curRowHeight * item.y);
|
|||
+ const x = this.gridster.curColWidth * item.x;
|
|||
+ const y = this.gridster.curRowHeight * item.y;
|
|||
const width = this.gridster.curColWidth * item.cols - this.gridster.$options.margin; |
|||
const height = this.gridster.curRowHeight * item.rows - this.gridster.$options.margin; |
|||
// set the cell style |
|||
renderer.setStyle(el, DirTypes.LTR ? 'margin-right' : 'margin-left', ''); |
|||
} |
|||
else { |
|||
- const x = Math.round(this.gridster.curColWidth * item.x);
|
|||
- const y = Math.round(this.gridster.curRowHeight * item.y);
|
|||
+ const x = this.gridster.curColWidth * item.x;
|
|||
+ const y = this.gridster.curRowHeight * item.y;
|
|||
const width = this.gridster.curColWidth * item.cols - this.gridster.$options.margin; |
|||
const height = this.gridster.curRowHeight * item.rows - this.gridster.$options.margin; |
|||
// set the cell style |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue