From fe2becb7cfb721cd5e449a6a5a3b30e0a04c1154 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 15 Nov 2017 13:04:11 +0200 Subject: [PATCH 01/57] Added check for updated TS --- ui/src/app/api/subscription.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/app/api/subscription.js b/ui/src/app/api/subscription.js index 4786bf9835..022a31cbed 100644 --- a/ui/src/app/api/subscription.js +++ b/ui/src/app/api/subscription.js @@ -654,8 +654,9 @@ export default class Subscription { if (!sourceData.data.length) { update = false; } else if (prevData && prevData[0] && prevData[0].length > 1 && sourceData.data.length > 0) { + var prevTs = prevData[0][0]; var prevValue = prevData[0][1]; - if (prevValue === sourceData.data[0][1]) { + if (prevTs === sourceData.data[0][0] && prevValue === sourceData.data[0][1]) { update = false; } } From 101097f9471879eea2d86c2e3fcba3fa94620bd7 Mon Sep 17 00:00:00 2001 From: Vitaliy Paromsiy Date: Wed, 22 Nov 2017 21:46:57 +0200 Subject: [PATCH 02/57] Api Quota --- .../src/main/resources/thingsboard.yml | 12 +++ common/transport/pom.xml | 12 +++ .../server/common/transport/quota/Clock.java | 45 ++++++++++ .../quota/HostRequestLimitPolicy.java | 38 ++++++++ .../quota/HostRequestsQuotaService.java | 76 ++++++++++++++++ .../common/transport/quota/QuotaService.java | 25 ++++++ .../inmemory/HostRequestIntervalRegistry.java | 67 ++++++++++++++ .../quota/inmemory/IntervalCount.java | 68 ++++++++++++++ .../inmemory/IntervalRegistryCleaner.java | 66 ++++++++++++++ .../inmemory/IntervalRegistryLogger.java | 88 +++++++++++++++++++ .../common/transport/quota/ClockTest.java | 66 ++++++++++++++ .../quota/HostRequestLimitPolicyTest.java | 46 ++++++++++ .../quota/HostRequestsQuotaServiceTest.java | 64 ++++++++++++++ .../HostRequestIntervalRegistryTest.java | 57 ++++++++++++ .../quota/inmemory/IntervalCountTest.java | 65 ++++++++++++++ .../inmemory/IntervalRegistryLoggerTest.java | 61 +++++++++++++ .../transport/coap/CoapTransportResource.java | 13 ++- .../transport/coap/CoapTransportService.java | 27 +++--- .../server/transport/coap/CoapServerTest.java | 6 ++ .../transport/http/DeviceApiController.java | 58 +++++++++--- .../transport/mqtt/MqttTransportHandler.java | 73 ++++++++------- .../mqtt/MqttTransportServerInitializer.java | 20 ++--- .../transport/mqtt/MqttTransportService.java | 7 +- 23 files changed, 990 insertions(+), 70 deletions(-) create mode 100644 common/transport/src/main/java/org/thingsboard/server/common/transport/quota/Clock.java create mode 100644 common/transport/src/main/java/org/thingsboard/server/common/transport/quota/HostRequestLimitPolicy.java create mode 100644 common/transport/src/main/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaService.java create mode 100644 common/transport/src/main/java/org/thingsboard/server/common/transport/quota/QuotaService.java create mode 100644 common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java create mode 100644 common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalCount.java create mode 100644 common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryCleaner.java create mode 100644 common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java create mode 100644 common/transport/src/test/java/org/thingsboard/server/common/transport/quota/ClockTest.java create mode 100644 common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestLimitPolicyTest.java create mode 100644 common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaServiceTest.java create mode 100644 common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistryTest.java create mode 100644 common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalCountTest.java create mode 100644 common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLoggerTest.java diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index e1bad01cce..657e28e173 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -107,6 +107,18 @@ coap: adaptor: "${COAP_ADAPTOR_NAME:JsonCoapAdaptor}" timeout: "${COAP_TIMEOUT:10000}" +#Quota parameters +quota: + host: + limit: "${QUOTA_HOST_LIMIT:10000}" + intervalMs: "${QUOTA_HOST_INTERVAL_MS:60000}" + ttlMs: "${QUOTA_HOST_TTL_MS:60000}" + cleanPeriodMs: "${QUOTA_HOST_CLEAN_PERIOD_MS:300000}" + enabled: "${QUOTA_HOST_ENABLED:false}" + log: + topSize: 10 + intervalMin: 2 + database: type: "${DATABASE_TYPE:sql}" # cassandra OR sql diff --git a/common/transport/pom.xml b/common/transport/pom.xml index f609e9a2c8..3243131e1c 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -74,6 +74,18 @@ mockito-all test + + org.springframework + spring-context + + + com.google.guava + guava + + + org.apache.commons + commons-lang3 + diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/Clock.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/Clock.java new file mode 100644 index 0000000000..e832354b1e --- /dev/null +++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/Clock.java @@ -0,0 +1,45 @@ +/** + * Copyright © 2016-2017 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.transport.quota; + +/** + * @author Vitaliy Paromskiy + * @version 1.0 + */ +public final class Clock { + + private static long time = 0L; + + private Clock() { + } + + + public static long millis() { + return time == 0 ? System.currentTimeMillis() : time; + } + + public static void setMillis(long millis) { + time = millis; + } + + public static void shift(long delta) { + time += delta; + } + + public static void reset() { + time = 0; + } +} diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/HostRequestLimitPolicy.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/HostRequestLimitPolicy.java new file mode 100644 index 0000000000..83d664123e --- /dev/null +++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/HostRequestLimitPolicy.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2017 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.transport.quota; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * @author Vitaliy Paromskiy + * @version 1.0 + */ +@Component +public class HostRequestLimitPolicy { + + private final long limit; + + public HostRequestLimitPolicy(@Value("${quota.host.limit}") long limit) { + this.limit = limit; + } + + public boolean isValid(long currentValue) { + return currentValue <= limit; + } + +} diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaService.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaService.java new file mode 100644 index 0000000000..7ef4df258f --- /dev/null +++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaService.java @@ -0,0 +1,76 @@ +/** + * Copyright © 2016-2017 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.transport.quota; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.transport.quota.inmemory.HostRequestIntervalRegistry; +import org.thingsboard.server.common.transport.quota.inmemory.IntervalRegistryCleaner; +import org.thingsboard.server.common.transport.quota.inmemory.IntervalRegistryLogger; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +/** + * @author Vitaliy Paromskiy + * @version 1.0 + */ +@Service +@Slf4j +public class HostRequestsQuotaService implements QuotaService { + + private final HostRequestIntervalRegistry requestRegistry; + private final HostRequestLimitPolicy requestsPolicy; + private final IntervalRegistryCleaner registryCleaner; + private final IntervalRegistryLogger registryLogger; + private final boolean enabled; + + public HostRequestsQuotaService(HostRequestIntervalRegistry requestRegistry, HostRequestLimitPolicy requestsPolicy, + IntervalRegistryCleaner registryCleaner, IntervalRegistryLogger registryLogger, + @Value("${quota.host.enabled}") boolean enabled) { + this.requestRegistry = requestRegistry; + this.requestsPolicy = requestsPolicy; + this.registryCleaner = registryCleaner; + this.registryLogger = registryLogger; + this.enabled = enabled; + } + + @PostConstruct + public void init() { + if (enabled) { + registryCleaner.schedule(); + registryLogger.schedule(); + } + } + + @PreDestroy + public void close() { + if (enabled) { + registryCleaner.stop(); + registryLogger.stop(); + } + } + + @Override + public boolean isQuotaExceeded(String key) { + if (enabled) { + long count = requestRegistry.tick(key); + return requestsPolicy.isValid(count); + } + return false; + } +} diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/QuotaService.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/QuotaService.java new file mode 100644 index 0000000000..cea5db63d1 --- /dev/null +++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/QuotaService.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2017 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.transport.quota; + +/** + * @author Vitaliy Paromskiy + * @version 1.0 + */ +public interface QuotaService { + + boolean isQuotaExceeded(String key); +} diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java new file mode 100644 index 0000000000..0f5da2b1a8 --- /dev/null +++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java @@ -0,0 +1,67 @@ +/** + * Copyright © 2016-2017 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.transport.quota.inmemory; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * @author Vitaliy Paromskiy + * @version 1.0 + */ +@Component +@Slf4j +public class HostRequestIntervalRegistry { + + private final Map hostCounts = new ConcurrentHashMap<>(); + private final long intervalDurationMs; + private final long ttlMs; + + public HostRequestIntervalRegistry(@Value("${quota.host.intervalMs}") long intervalDurationMs, + @Value("${quota.host.ttlMs}") long ttlMs) { + this.intervalDurationMs = intervalDurationMs; + this.ttlMs = ttlMs; + } + + @PostConstruct + public void init() { + if (ttlMs < intervalDurationMs) { + log.warn("TTL for IntervalRegistry [{}] smaller than interval duration [{}]", ttlMs, intervalDurationMs); + } + } + + public long tick(String clientHostId) { + IntervalCount intervalCount = hostCounts.computeIfAbsent(clientHostId, s -> new IntervalCount(intervalDurationMs)); + return intervalCount.resetIfExpiredAndTick(); + } + + public void clean() { + hostCounts.entrySet().removeIf(entry -> entry.getValue().silenceDuration() > ttlMs); + } + + public Map getContent() { + return hostCounts.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + interval -> interval.getValue().getCount())); + } +} diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalCount.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalCount.java new file mode 100644 index 0000000000..8301b8e6f1 --- /dev/null +++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalCount.java @@ -0,0 +1,68 @@ +/** + * Copyright © 2016-2017 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.transport.quota.inmemory; + + +import org.thingsboard.server.common.transport.quota.Clock; + +import java.util.concurrent.atomic.LongAdder; + +/** + * @author Vitaliy Paromskiy + * @version 1.0 + */ +public class IntervalCount { + + private final LongAdder adder = new LongAdder(); + private final long intervalDurationMs; + private volatile long startTime; + private volatile long lastTickTime; + + public IntervalCount(long intervalDurationMs) { + this.intervalDurationMs = intervalDurationMs; + startTime = Clock.millis(); + } + + public long resetIfExpiredAndTick() { + if (isExpired()) { + reset(); + } + tick(); + return adder.sum(); + } + + public long silenceDuration() { + return Clock.millis() - lastTickTime; + } + + public long getCount() { + return adder.sum(); + } + + private void tick() { + adder.add(1); + lastTickTime = Clock.millis(); + } + + private void reset() { + adder.reset(); + startTime = Clock.millis(); + } + + private boolean isExpired() { + return (Clock.millis() - startTime) > intervalDurationMs; + } +} diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryCleaner.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryCleaner.java new file mode 100644 index 0000000000..1e2076a0b0 --- /dev/null +++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryCleaner.java @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2017 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.transport.quota.inmemory; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.annotation.PreDestroy; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * @author Vitaliy Paromskiy + * @version 1.0 + */ +@Component +@Slf4j +public class IntervalRegistryCleaner { + + private final HostRequestIntervalRegistry intervalRegistry; + private final long cleanPeriodMs; + private ScheduledExecutorService executor; + + public IntervalRegistryCleaner(HostRequestIntervalRegistry intervalRegistry, @Value("${quota.host.cleanPeriodMs}") long cleanPeriodMs) { + this.intervalRegistry = intervalRegistry; + this.cleanPeriodMs = cleanPeriodMs; + } + + public void schedule() { + if (executor != null) { + throw new IllegalStateException("Registry Cleaner already scheduled"); + } + executor = Executors.newSingleThreadScheduledExecutor(); + executor.scheduleAtFixedRate(this::clean, cleanPeriodMs, cleanPeriodMs, TimeUnit.MILLISECONDS); + } + + public void stop() { + if (executor != null) { + executor.shutdown(); + } + } + + public void clean() { + try { + intervalRegistry.clean(); + } catch (RuntimeException ex) { + log.error("Could not clear Interval Registry", ex); + } + } + +} diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java new file mode 100644 index 0000000000..91797d4422 --- /dev/null +++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java @@ -0,0 +1,88 @@ +/** + * Copyright © 2016-2017 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.transport.quota.inmemory; + +import com.google.common.collect.MinMaxPriorityQueue; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.Comparator; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author Vitaliy Paromskiy + * @version 1.0 + */ +@Component +@Slf4j +public class IntervalRegistryLogger { + + private final int topSize; + private final HostRequestIntervalRegistry intervalRegistry; + private final long logIntervalMin; + private ScheduledExecutorService executor; + + public IntervalRegistryLogger(@Value("${quota.log.topSize}") int topSize, @Value("${quota.log.intervalMin}") long logIntervalMin, + HostRequestIntervalRegistry intervalRegistry) { + this.topSize = topSize; + this.logIntervalMin = logIntervalMin; + this.intervalRegistry = intervalRegistry; + } + + public void schedule() { + if (executor != null) { + throw new IllegalStateException("Registry Cleaner already scheduled"); + } + executor = Executors.newSingleThreadScheduledExecutor(); + executor.scheduleAtFixedRate(this::logStatistic, logIntervalMin, logIntervalMin, TimeUnit.MINUTES); + } + + public void stop() { + if (executor != null) { + executor.shutdown(); + } + } + + public void logStatistic() { + Map top = getTopElements(intervalRegistry.getContent()); + log(top); + } + + protected Map getTopElements(Map countMap) { + MinMaxPriorityQueue> topQueue = MinMaxPriorityQueue + .orderedBy(Comparator.comparing((Function, Long>) Map.Entry::getValue).reversed()) + .maximumSize(topSize) + .create(countMap.entrySet()); + + return topQueue.stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private void log(Map top) { + StringBuilder builder = new StringBuilder("Quota Statistic : "); + for (Map.Entry host : top.entrySet()) { + builder.append(host.getKey()).append(" : ").append(host.getValue()); + } + + log.info(builder.toString()); + } +} diff --git a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/ClockTest.java b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/ClockTest.java new file mode 100644 index 0000000000..6ed5445aec --- /dev/null +++ b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/ClockTest.java @@ -0,0 +1,66 @@ +/** + * Copyright © 2016-2017 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.transport.quota; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Vitaliy Paromskiy + * @version 1.0 + */ +public class ClockTest { + + @Before + public void init() { + Clock.reset(); + } + + @After + public void clear() { + Clock.reset(); + } + + @Test + public void defaultClockUseSystemTime() { + assertFalse(Clock.millis() > System.currentTimeMillis()); + } + + @Test + public void timeCanBeSet() { + Clock.setMillis(100L); + assertEquals(100L, Clock.millis()); + } + + @Test + public void clockCanBeReseted() { + Clock.setMillis(100L); + assertEquals(100L, Clock.millis()); + Clock.reset(); + assertFalse(Clock.millis() > System.currentTimeMillis()); + } + + @Test + public void timeIsShifted() { + Clock.setMillis(100L); + Clock.shift(50L); + assertEquals(150L, Clock.millis()); + } + +} \ No newline at end of file diff --git a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestLimitPolicyTest.java b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestLimitPolicyTest.java new file mode 100644 index 0000000000..f28d17c5f9 --- /dev/null +++ b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestLimitPolicyTest.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2017 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.transport.quota; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Vitaliy Paromskiy + * @version 1.0 + */ +public class HostRequestLimitPolicyTest { + + private HostRequestLimitPolicy limitPolicy = new HostRequestLimitPolicy(10L); + + @Test + public void ifCurrentValueLessThenLimitItIsValid() { + assertTrue(limitPolicy.isValid(9)); + } + + @Test + public void ifCurrentValueEqualsToLimitItIsValid() { + assertTrue(limitPolicy.isValid(10)); + } + + @Test + public void ifCurrentValueGreaterThenLimitItIsValid() { + assertFalse(limitPolicy.isValid(11)); + } + +} \ No newline at end of file diff --git a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaServiceTest.java b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaServiceTest.java new file mode 100644 index 0000000000..475fcccead --- /dev/null +++ b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaServiceTest.java @@ -0,0 +1,64 @@ +/** + * Copyright © 2016-2017 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.transport.quota; + +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.transport.quota.inmemory.HostRequestIntervalRegistry; +import org.thingsboard.server.common.transport.quota.inmemory.IntervalRegistryCleaner; +import org.thingsboard.server.common.transport.quota.inmemory.IntervalRegistryLogger; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.*; + +/** + * @author Vitaliy Paromskiy + * @version 1.0 + */ +public class HostRequestsQuotaServiceTest { + + private HostRequestsQuotaService quotaService; + + private HostRequestIntervalRegistry requestRegistry = mock(HostRequestIntervalRegistry.class); + private HostRequestLimitPolicy requestsPolicy = mock(HostRequestLimitPolicy.class); + private IntervalRegistryCleaner registryCleaner = mock(IntervalRegistryCleaner.class); + private IntervalRegistryLogger registryLogger = mock(IntervalRegistryLogger.class); + + @Before + public void init() { + quotaService = new HostRequestsQuotaService(requestRegistry, requestsPolicy, registryCleaner, registryLogger, true); + } + + @Test + public void hostQuotaValidated() { + when(requestRegistry.tick("key")).thenReturn(10L); + when(requestsPolicy.isValid(10L)).thenReturn(true); + + assertTrue(quotaService.isQuotaExceeded("key")); + + verify(requestRegistry).tick("key"); + verify(requestsPolicy).isValid(10L); + verifyNoMoreInteractions(requestRegistry, requestsPolicy); + } + + @Test + public void serviceCanBeDisabled() { + quotaService = new HostRequestsQuotaService(requestRegistry, requestsPolicy, registryCleaner, registryLogger, false); + assertFalse(quotaService.isQuotaExceeded("key")); + verifyNoMoreInteractions(requestRegistry, requestsPolicy); + } +} \ No newline at end of file diff --git a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistryTest.java b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistryTest.java new file mode 100644 index 0000000000..cb21ccebd5 --- /dev/null +++ b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistryTest.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2017 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.transport.quota.inmemory; + +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vitaliy Paromskiy + * @version 1.0 + */ +public class HostRequestIntervalRegistryTest { + + private HostRequestIntervalRegistry registry; + + @Before + public void init() { + registry = new HostRequestIntervalRegistry(10000L, 100L); + } + + @Test + public void newHostCreateNewInterval() { + assertEquals(1L, registry.tick("host1")); + } + + @Test + public void existingHostUpdated() { + registry.tick("aaa"); + assertEquals(1L, registry.tick("bbb")); + assertEquals(2L, registry.tick("aaa")); + } + + @Test + public void expiredIntervalsCleaned() throws InterruptedException { + registry.tick("aaa"); + Thread.sleep(150L); + registry.tick("bbb"); + registry.clean(); + assertEquals(1L, registry.tick("aaa")); + assertEquals(2L, registry.tick("bbb")); + } +} \ No newline at end of file diff --git a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalCountTest.java b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalCountTest.java new file mode 100644 index 0000000000..7bdcafde3c --- /dev/null +++ b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalCountTest.java @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2017 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.transport.quota.inmemory; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.transport.quota.Clock; + +import static org.junit.Assert.assertEquals; + +/** + * @author Vitaliy Paromskiy + * @version 1.0 + */ +public class IntervalCountTest { + + @Before + public void init() { + Clock.setMillis(1000L); + } + + @After + public void clear() { + Clock.reset(); + } + + @Test + public void ticksInSameIntervalAreSummed() { + IntervalCount intervalCount = new IntervalCount(100L); + assertEquals(1L, intervalCount.resetIfExpiredAndTick()); + Clock.shift(100); + assertEquals(2L, intervalCount.resetIfExpiredAndTick()); + } + + @Test + public void oldDataCleanedWhenIntervalExpired() { + IntervalCount intervalCount = new IntervalCount(100L); + assertEquals(1L, intervalCount.resetIfExpiredAndTick()); + Clock.shift(101); + assertEquals(1L, intervalCount.resetIfExpiredAndTick()); + } + + @Test + public void silenceDurationCalculatedFromLastTick() { + IntervalCount intervalCount = new IntervalCount(100L); + assertEquals(1L, intervalCount.resetIfExpiredAndTick()); + Clock.shift(10L); + assertEquals(10L, intervalCount.silenceDuration()); + } + +} \ No newline at end of file diff --git a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLoggerTest.java b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLoggerTest.java new file mode 100644 index 0000000000..cc25b4c402 --- /dev/null +++ b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLoggerTest.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2017 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.transport.quota.inmemory; + +import com.google.common.collect.ImmutableMap; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +/** + * @author Vitaliy Paromskiy + * @version 1.0 + */ +public class IntervalRegistryLoggerTest { + + private IntervalRegistryLogger logger; + + private HostRequestIntervalRegistry requestRegistry = mock(HostRequestIntervalRegistry.class); + + @Before + public void init() { + logger = new IntervalRegistryLogger(3, 10, requestRegistry); + } + + @Test + public void onlyMaxHostsCollected() { + Map map = ImmutableMap.of("a", 8L, "b", 3L, "c", 1L, "d", 3L); + Map actual = logger.getTopElements(map); + Map expected = ImmutableMap.of("a", 8L, "b", 3L, "d", 3L); + + assertEquals(expected, actual); + } + + @Test + public void emptyMapProcessedCorrectly() { + Map map = Collections.emptyMap(); + Map actual = logger.getTopElements(map); + Map expected = Collections.emptyMap(); + + assertEquals(expected, actual); + } + +} \ No newline at end of file diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index 834a911e77..2ffbf05f6e 100644 --- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.msg.session.*; import org.thingsboard.server.common.transport.SessionMsgProcessor; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.auth.DeviceAuthService; +import org.thingsboard.server.common.transport.quota.QuotaService; import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor; import org.thingsboard.server.transport.coap.session.CoapExchangeObserverProxy; import org.thingsboard.server.transport.coap.session.CoapSessionCtx; @@ -51,13 +52,16 @@ public class CoapTransportResource extends CoapResource { private final CoapTransportAdaptor adaptor; private final SessionMsgProcessor processor; private final DeviceAuthService authService; + private final QuotaService quotaService; private final Field observerField; private final long timeout; - public CoapTransportResource(SessionMsgProcessor processor, DeviceAuthService authService, CoapTransportAdaptor adaptor, String name, long timeout) { + public CoapTransportResource(SessionMsgProcessor processor, DeviceAuthService authService, CoapTransportAdaptor adaptor, String name, + long timeout, QuotaService quotaService) { super(name); this.processor = processor; this.authService = authService; + this.quotaService = quotaService; this.adaptor = adaptor; this.timeout = timeout; // This is important to turn off existing observable logic in @@ -70,6 +74,13 @@ public class CoapTransportResource extends CoapResource { @Override public void handleGET(CoapExchange exchange) { + if(quotaService.isQuotaExceeded(exchange.getSourceAddress().getHostAddress())) { + log.warn("Missing feature type parameter"); + log.warn("COAP Quota exceeded for [{}:{}] . Disconnect", exchange.getSourceAddress().getHostAddress(), exchange.getSourcePort()); + exchange.respond(ResponseCode.BAD_REQUEST); + return; + } + Optional featureType = getFeatureType(exchange.advanced().getRequest()); if (!featureType.isPresent()) { log.trace("Missing feature type parameter"); diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java index a78c718d4f..7dcf77c036 100644 --- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java +++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java @@ -15,26 +15,24 @@ */ package org.thingsboard.server.transport.coap; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.UnknownHostException; - -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; - import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; import org.eclipse.californium.core.CoapServer; import org.eclipse.californium.core.network.CoapEndpoint; -import org.thingsboard.server.common.transport.SessionMsgProcessor; -import org.thingsboard.server.common.transport.auth.DeviceAuthService; -import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.transport.SessionMsgProcessor; +import org.thingsboard.server.common.transport.auth.DeviceAuthService; +import org.thingsboard.server.common.transport.quota.QuotaService; +import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; @Service("CoapTransportService") @Slf4j @@ -54,6 +52,9 @@ public class CoapTransportService { @Autowired(required = false) private DeviceAuthService authService; + @Autowired(required = false) + private QuotaService quotaService; + @Value("${coap.bind_address}") private String host; @@ -83,7 +84,7 @@ public class CoapTransportService { private void createResources() { CoapResource api = new CoapResource(API); - api.add(new CoapTransportResource(processor, authService, adaptor, V1, timeout)); + api.add(new CoapTransportResource(processor, authService, adaptor, V1, timeout, quotaService)); server.add(api); } diff --git a/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java b/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java index 706af80ec9..5ac589099a 100644 --- a/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java +++ b/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java @@ -50,6 +50,7 @@ import org.thingsboard.server.common.msg.session.*; import org.thingsboard.server.common.transport.SessionMsgProcessor; import org.thingsboard.server.common.transport.auth.DeviceAuthResult; import org.thingsboard.server.common.transport.auth.DeviceAuthService; +import org.thingsboard.server.common.transport.quota.QuotaService; import java.util.ArrayList; import java.util.List; @@ -131,6 +132,11 @@ public class CoapServerTest { } }; } + + @Bean + public static QuotaService quotaService() { + return key -> false; + } } @Autowired diff --git a/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java index 70767afdfc..c799dcf7b7 100644 --- a/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java +++ b/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java @@ -35,10 +35,11 @@ import org.thingsboard.server.common.msg.session.FromDeviceMsg; import org.thingsboard.server.common.transport.SessionMsgProcessor; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.common.transport.auth.DeviceAuthService; +import org.thingsboard.server.common.transport.quota.QuotaService; import org.thingsboard.server.transport.http.session.HttpSessionCtx; +import javax.servlet.http.HttpServletRequest; import java.util.Arrays; -import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -59,11 +60,18 @@ public class DeviceApiController { @Autowired(required = false) private DeviceAuthService authService; + @Autowired(required = false) + private QuotaService quotaService; + @RequestMapping(value = "/{deviceToken}/attributes", method = RequestMethod.GET, produces = "application/json") public DeferredResult getDeviceAttributes(@PathVariable("deviceToken") String deviceToken, @RequestParam(value = "clientKeys", required = false, defaultValue = "") String clientKeys, - @RequestParam(value = "sharedKeys", required = false, defaultValue = "") String sharedKeys) { + @RequestParam(value = "sharedKeys", required = false, defaultValue = "") String sharedKeys, + HttpServletRequest httpRequest) { DeferredResult responseWriter = new DeferredResult(); + if (quotaExceeded(httpRequest, responseWriter)) { + return responseWriter; + } HttpSessionCtx ctx = getHttpSessionCtx(responseWriter); if (ctx.login(new DeviceTokenCredentials(deviceToken))) { GetAttributesRequest request; @@ -84,8 +92,11 @@ public class DeviceApiController { @RequestMapping(value = "/{deviceToken}/attributes", method = RequestMethod.POST) public DeferredResult postDeviceAttributes(@PathVariable("deviceToken") String deviceToken, - @RequestBody String json) { + @RequestBody String json, HttpServletRequest request) { DeferredResult responseWriter = new DeferredResult(); + if (quotaExceeded(request, responseWriter)) { + return responseWriter; + } HttpSessionCtx ctx = getHttpSessionCtx(responseWriter); if (ctx.login(new DeviceTokenCredentials(deviceToken))) { try { @@ -101,8 +112,11 @@ public class DeviceApiController { @RequestMapping(value = "/{deviceToken}/telemetry", method = RequestMethod.POST) public DeferredResult postTelemetry(@PathVariable("deviceToken") String deviceToken, - @RequestBody String json) { + @RequestBody String json, HttpServletRequest request) { DeferredResult responseWriter = new DeferredResult(); + if (quotaExceeded(request, responseWriter)) { + return responseWriter; + } HttpSessionCtx ctx = getHttpSessionCtx(responseWriter); if (ctx.login(new DeviceTokenCredentials(deviceToken))) { try { @@ -118,15 +132,20 @@ public class DeviceApiController { @RequestMapping(value = "/{deviceToken}/rpc", method = RequestMethod.GET, produces = "application/json") public DeferredResult subscribeToCommands(@PathVariable("deviceToken") String deviceToken, - @RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout) { - return subscribe(deviceToken, timeout, new RpcSubscribeMsg()); + @RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout, + HttpServletRequest request) { + + return subscribe(deviceToken, timeout, new RpcSubscribeMsg(), request); } @RequestMapping(value = "/{deviceToken}/rpc/{requestId}", method = RequestMethod.POST) public DeferredResult replyToCommand(@PathVariable("deviceToken") String deviceToken, @PathVariable("requestId") Integer requestId, - @RequestBody String json) { + @RequestBody String json, HttpServletRequest request) { DeferredResult responseWriter = new DeferredResult(); + if (quotaExceeded(request, responseWriter)) { + return responseWriter; + } HttpSessionCtx ctx = getHttpSessionCtx(responseWriter); if (ctx.login(new DeviceTokenCredentials(deviceToken))) { try { @@ -143,8 +162,11 @@ public class DeviceApiController { @RequestMapping(value = "/{deviceToken}/rpc", method = RequestMethod.POST) public DeferredResult postRpcRequest(@PathVariable("deviceToken") String deviceToken, - @RequestBody String json) { + @RequestBody String json, HttpServletRequest httpRequest) { DeferredResult responseWriter = new DeferredResult(); + if (quotaExceeded(httpRequest, responseWriter)) { + return responseWriter; + } HttpSessionCtx ctx = getHttpSessionCtx(responseWriter); if (ctx.login(new DeviceTokenCredentials(deviceToken))) { try { @@ -163,12 +185,17 @@ public class DeviceApiController { @RequestMapping(value = "/{deviceToken}/attributes/updates", method = RequestMethod.GET, produces = "application/json") public DeferredResult subscribeToAttributes(@PathVariable("deviceToken") String deviceToken, - @RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout) { - return subscribe(deviceToken, timeout, new AttributesSubscribeMsg()); + @RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout, + HttpServletRequest httpRequest) { + + return subscribe(deviceToken, timeout, new AttributesSubscribeMsg(), httpRequest); } - private DeferredResult subscribe(String deviceToken, long timeout, FromDeviceMsg msg) { + private DeferredResult subscribe(String deviceToken, long timeout, FromDeviceMsg msg, HttpServletRequest httpRequest) { DeferredResult responseWriter = new DeferredResult(); + if (quotaExceeded(httpRequest, responseWriter)) { + return responseWriter; + } HttpSessionCtx ctx = getHttpSessionCtx(responseWriter, timeout); if (ctx.login(new DeviceTokenCredentials(deviceToken))) { try { @@ -195,4 +222,13 @@ public class DeviceApiController { processor.process(new BasicToDeviceActorSessionMsg(ctx.getDevice(), msg)); } + private boolean quotaExceeded(HttpServletRequest request, DeferredResult responseWriter) { + if (quotaService.isQuotaExceeded(request.getRemoteAddr())) { + log.warn("REST Quota exceeded for [{}] . Disconnect", request.getRemoteAddr()); + responseWriter.setResult(new ResponseEntity<>(HttpStatus.BANDWIDTH_LIMIT_EXCEEDED)); + return true; + } + return false; + } + } diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index 2e6abfde96..6fd559bdbf 100644 --- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -16,7 +16,6 @@ package org.thingsboard.server.transport.mqtt; import com.fasterxml.jackson.databind.JsonNode; -import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.mqtt.*; @@ -36,18 +35,18 @@ import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg; import org.thingsboard.server.common.transport.SessionMsgProcessor; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.auth.DeviceAuthService; +import org.thingsboard.server.common.transport.quota.QuotaService; import org.thingsboard.server.dao.EncryptionUtil; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; -import org.thingsboard.server.transport.mqtt.session.GatewaySessionCtx; import org.thingsboard.server.transport.mqtt.session.DeviceSessionCtx; +import org.thingsboard.server.transport.mqtt.session.GatewaySessionCtx; import org.thingsboard.server.transport.mqtt.util.SslUtil; import javax.net.ssl.SSLPeerUnverifiedException; import javax.security.cert.X509Certificate; import java.net.InetSocketAddress; -import java.net.SocketAddress; import java.util.ArrayList; import java.util.List; @@ -72,13 +71,14 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private final DeviceService deviceService; private final DeviceAuthService authService; private final RelationService relationService; + private final QuotaService quotaService; private final SslHandler sslHandler; private volatile boolean connected; private volatile InetSocketAddress address; private volatile GatewaySessionCtx gatewaySessionCtx; public MqttTransportHandler(SessionMsgProcessor processor, DeviceService deviceService, DeviceAuthService authService, RelationService relationService, - MqttTransportAdaptor adaptor, SslHandler sslHandler) { + MqttTransportAdaptor adaptor, SslHandler sslHandler, QuotaService quotaService) { this.processor = processor; this.deviceService = deviceService; this.relationService = relationService; @@ -87,6 +87,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement this.deviceSessionCtx = new DeviceSessionCtx(processor, authService, adaptor); this.sessionId = deviceSessionCtx.getSessionId().toUidStr(); this.sslHandler = sslHandler; + this.quotaService = quotaService; } @Override @@ -102,35 +103,43 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement if (msg.fixedHeader() == null) { log.info("[{}:{}] Invalid message received", address.getHostName(), address.getPort()); processDisconnect(ctx); - } else { - deviceSessionCtx.setChannel(ctx); - switch (msg.fixedHeader().messageType()) { - case CONNECT: - processConnect(ctx, (MqttConnectMessage) msg); - break; - case PUBLISH: - processPublish(ctx, (MqttPublishMessage) msg); - break; - case SUBSCRIBE: - processSubscribe(ctx, (MqttSubscribeMessage) msg); - break; - case UNSUBSCRIBE: - processUnsubscribe(ctx, (MqttUnsubscribeMessage) msg); - break; - case PINGREQ: - if (checkConnected(ctx)) { - ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0))); - } - break; - case DISCONNECT: - if (checkConnected(ctx)) { - processDisconnect(ctx); - } - break; - default: - break; - } + return; + } + + if (quotaService.isQuotaExceeded(address.getHostName())) { + log.warn("MQTT Quota exceeded for [{}:{}] . Disconnect", address.getHostName(), address.getPort()); + processDisconnect(ctx); + return; } + + deviceSessionCtx.setChannel(ctx); + switch (msg.fixedHeader().messageType()) { + case CONNECT: + processConnect(ctx, (MqttConnectMessage) msg); + break; + case PUBLISH: + processPublish(ctx, (MqttPublishMessage) msg); + break; + case SUBSCRIBE: + processSubscribe(ctx, (MqttSubscribeMessage) msg); + break; + case UNSUBSCRIBE: + processUnsubscribe(ctx, (MqttUnsubscribeMessage) msg); + break; + case PINGREQ: + if (checkConnected(ctx)) { + ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(PINGRESP, false, AT_MOST_ONCE, false, 0))); + } + break; + case DISCONNECT: + if (checkConnected(ctx)) { + processDisconnect(ctx); + } + break; + default: + break; + } + } private void processPublish(ChannelHandlerContext ctx, MqttPublishMessage mqttMsg) { diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java index 1469290837..93812a80a8 100644 --- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java +++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java @@ -15,27 +15,19 @@ */ package org.thingsboard.server.transport.mqtt; -import io.netty.buffer.ByteBufAllocator; -import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.mqtt.MqttDecoder; import io.netty.handler.codec.mqtt.MqttEncoder; -import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslHandler; -import io.netty.handler.ssl.util.SelfSignedCertificate; -import org.springframework.beans.factory.annotation.Value; import org.thingsboard.server.common.transport.SessionMsgProcessor; import org.thingsboard.server.common.transport.auth.DeviceAuthService; +import org.thingsboard.server.common.transport.quota.QuotaService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; -import javax.net.ssl.SSLException; -import java.security.cert.CertificateException; - /** * @author Andrew Shvayka */ @@ -49,16 +41,18 @@ public class MqttTransportServerInitializer extends ChannelInitializer Date: Wed, 29 Nov 2017 18:53:32 +0200 Subject: [PATCH 03/57] Fixed quota exceeded condition check. --- .../server/common/transport/quota/HostRequestsQuotaService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaService.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaService.java index 7ef4df258f..0914dd6d44 100644 --- a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaService.java +++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaService.java @@ -69,7 +69,7 @@ public class HostRequestsQuotaService implements QuotaService { public boolean isQuotaExceeded(String key) { if (enabled) { long count = requestRegistry.tick(key); - return requestsPolicy.isValid(count); + return !requestsPolicy.isValid(count); } return false; } From 35b2feaaf1fd859af6f680b9d2e210627e669a4a Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 29 Nov 2017 19:07:11 +0200 Subject: [PATCH 04/57] Ignore localhost during API limits check. --- .../transport/quota/inmemory/HostRequestIntervalRegistry.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java index 0f5da2b1a8..9f8f9465c2 100644 --- a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java +++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java @@ -50,6 +50,9 @@ public class HostRequestIntervalRegistry { } public long tick(String clientHostId) { + if ("localhost".equals(clientHostId) || "127.0.0.1".equals(clientHostId)) { + return 0; + } IntervalCount intervalCount = hostCounts.computeIfAbsent(clientHostId, s -> new IntervalCount(intervalDurationMs)); return intervalCount.resetIfExpiredAndTick(); } From 8e2dfc3ea4856f677f52090f2e1890cb4d2181a1 Mon Sep 17 00:00:00 2001 From: oleg Date: Fri, 29 Dec 2017 10:35:53 +0200 Subject: [PATCH 05/57] "Timeseries - Flot" widget improvements --- ui/src/app/widget/lib/flot-widget.js | 79 +++++++++++++++++----------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/ui/src/app/widget/lib/flot-widget.js b/ui/src/app/widget/lib/flot-widget.js index 59787cf920..4e5f99f919 100644 --- a/ui/src/app/widget/lib/flot-widget.js +++ b/ui/src/app/widget/lib/flot-widget.js @@ -238,19 +238,30 @@ export default class TbFlot { if (this.ticksFormatterFunction) { return this.ticksFormatterFunction(value); } - var factor = this.tickDecimals ? Math.pow(10, this.tickDecimals) : 1, - formatted = "" + Math.round(value * factor) / factor; - if (this.tickDecimals != null) { - var decimal = formatted.indexOf("."), - precision = decimal === -1 ? 0 : formatted.length - decimal - 1; - - if (precision < this.tickDecimals) { - formatted = (precision ? formatted : formatted + ".") + ("" + factor).substr(1, this.tickDecimals - precision); - } + if (this.tickDecimals) { + value = value.toFixed(this.tickDecimals); } - formatted += ' ' + this.tickUnits; - return formatted; - } + if (this.tickUnits) { + value = value + ' ' + this.tickUnits; + } + if (this.tickSize) { + return value; + } + + // var factor = this.tickDecimals ? Math.pow(10, this.tickDecimals) : 1, + // formatted = "" + Math.round(value * factor) / factor; + // if (this.tickDecimals != null) { + // var decimal = formatted.indexOf("."), + // precision = decimal === -1 ? 0 : formatted.length - decimal - 1; + // + // if (precision < this.tickDecimals) { + // formatted = (precision ? formatted : formatted + ".") + ("" + factor).substr(1, this.tickDecimals - precision); + // } + // } + // formatted += ' ' + this.tickUnits; + return value; + + }; this.yaxis.tickFormatter = ctx.yAxisTickFormatter; @@ -262,6 +273,8 @@ export default class TbFlot { this.yaxis.labelFont.color = this.yaxis.font.color; this.yaxis.labelFont.size = this.yaxis.font.size+2; this.yaxis.labelFont.weight = "bold"; + this.yaxis.tickSize = settings.yaxis.tickSize || null; + this.yaxis.tickDecimals = settings.yaxis.tickDecimals || null; if (settings.yaxis.ticksFormatter && settings.yaxis.ticksFormatter.length) { try { this.yaxis.ticksFormatterFunction = new Function('value', settings.yaxis.ticksFormatter); @@ -489,7 +502,7 @@ export default class TbFlot { var yaxis = angular.copy(this.yaxis); var label = keySettings.axisTitle && keySettings.axisTitle.length ? keySettings.axisTitle : yaxis.label; - var tickDecimals = angular.isDefined(keySettings.axisTickDecimals) ? keySettings.axisTickDecimals : 0; + //var tickDecimals = angular.isDefined(keySettings.axisTickDecimals) ? keySettings.axisTickDecimals : 0; var position = keySettings.axisPosition && keySettings.axisPosition.length ? keySettings.axisPosition : "left"; var min = angular.isDefined(keySettings.axisMin) ? keySettings.axisMin : yaxis.min; @@ -499,7 +512,6 @@ export default class TbFlot { yaxis.min = min; yaxis.max = max; yaxis.tickUnits = units; - yaxis.tickDecimals = tickDecimals; yaxis.alignTicksWithAxis = position == "right" ? 1 : null; yaxis.position = position; @@ -904,6 +916,11 @@ export default class TbFlot { "type": "number", "default": null }, + "tickSize": { + "title": "Step size between ticks", + "type": "number", + "default": null + }, "showLabels": { "title": "Show labels", "type": "boolean", @@ -928,7 +945,13 @@ export default class TbFlot { "title": "Ticks formatter function, f(value)", "type": "string", "default": "" + }, + "tickDecimals": { + "title": "The number of decimals to display", + "type": "number", + "default": 0 } + } } }, @@ -986,6 +1009,8 @@ export default class TbFlot { "items": [ "yaxis.min", "yaxis.max", + "yaxis.tickSize", + "yaxis.tickDecimals", "yaxis.showLabels", "yaxis.title", "yaxis.titleAngle", @@ -1010,24 +1035,24 @@ export default class TbFlot { static datakeySettingsSchema(defaultShowLines) { return { - "schema": { + "schema": { "type": "object", - "title": "DataKeySettings", - "properties": { + "title": "DataKeySettings", + "properties": { "showLines": { "title": "Show lines", - "type": "boolean", - "default": defaultShowLines + "type": "boolean", + "default": defaultShowLines }, "fillLines": { "title": "Fill lines", - "type": "boolean", - "default": false + "type": "boolean", + "default": false }, "showPoints": { "title": "Show points", - "type": "boolean", - "default": false + "type": "boolean", + "default": false }, "tooltipValueFormatter": { "title": "Tooltip value format function, f(value)", @@ -1054,11 +1079,6 @@ export default class TbFlot { "type": "string", "default": "" }, - "axisTickDecimals": { - "title": "Axis tick number of digits after floating point", - "type": "number", - "default": 0 - }, "axisPosition": { "title": "Axis position", "type": "string", @@ -1072,7 +1092,7 @@ export default class TbFlot { }, "required": ["showLines", "fillLines", "showPoints"] }, - "form": [ + "form": [ "showLines", "fillLines", "showPoints", @@ -1084,7 +1104,6 @@ export default class TbFlot { "axisMin", "axisMax", "axisTitle", - "axisTickDecimals", { "key": "axisPosition", "type": "rc-select", From bdca585d9091322a9c8bc2248025777ce3a72a87 Mon Sep 17 00:00:00 2001 From: mp-loki Date: Fri, 5 Jan 2018 21:38:58 -0500 Subject: [PATCH 06/57] client.keygen.sh script fix --- tools/src/main/shell/client.keygen.sh | 4 ++-- tools/src/main/shell/keygen.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/src/main/shell/client.keygen.sh b/tools/src/main/shell/client.keygen.sh index 2e83ef0ef0..bb59077ba7 100755 --- a/tools/src/main/shell/client.keygen.sh +++ b/tools/src/main/shell/client.keygen.sh @@ -74,13 +74,13 @@ echo "Generating SSL Key Pair..." keytool -genkeypair -v \ -alias $CLIENT_KEY_ALIAS \ - -dname "CN=$DOMAIN_SUFFIX, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" \ -keystore $CLIENT_FILE_PREFIX.jks \ -keypass $CLIENT_KEY_PASSWORD \ -storepass $CLIENT_KEYSTORE_PASSWORD \ -keyalg RSA \ -keysize 2048 \ - -validity 9999 + -validity 9999 \ + -dname "CN=$DOMAIN_SUFFIX, OU=$ORGANIZATIONAL_UNIT, O=$ORGANIZATION, L=$CITY, ST=$STATE_OR_PROVINCE, C=$TWO_LETTER_COUNTRY_CODE" echo "Converting keystore to pkcs12" keytool -importkeystore \ diff --git a/tools/src/main/shell/keygen.properties b/tools/src/main/shell/keygen.properties index 8dd11f2ea4..a01b782a74 100644 --- a/tools/src/main/shell/keygen.properties +++ b/tools/src/main/shell/keygen.properties @@ -17,7 +17,7 @@ DOMAIN_SUFFIX="$(hostname)" ORGANIZATIONAL_UNIT=Thingsboard ORGANIZATION=Thingsboard -CITY=San Francisco +CITY=SF STATE_OR_PROVINCE=CA TWO_LETTER_COUNTRY_CODE=US From 7e0d4df80858be80776bcf0f87bb149e8164218d Mon Sep 17 00:00:00 2001 From: oleg Date: Thu, 11 Jan 2018 18:08:36 +0200 Subject: [PATCH 07/57] Attribute subscription --- .../core/plugin/telemetry/SubscriptionManager.java | 9 +++++++-- .../telemetry/handlers/TelemetryRpcMsgHandler.java | 3 ++- .../telemetry/handlers/TelemetryWebsocketMsgHandler.java | 8 ++++---- .../core/plugin/telemetry/sub/Subscription.java | 4 ++++ .../core/plugin/telemetry/sub/SubscriptionState.java | 1 + extensions-core/src/main/proto/telemetry.proto | 1 + 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java index d137e10fbd..bad1678d77 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java @@ -33,6 +33,7 @@ import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionU import java.util.*; import java.util.function.Function; +import java.util.function.Predicate; /** * @author Andrew Shvayka @@ -174,9 +175,13 @@ public class SubscriptionManager { } public void onLocalSubscriptionUpdate(PluginContext ctx, EntityId entityId, SubscriptionType type, Function> f) { + onLocalSubscriptionUpdate(ctx, entityId, s -> type == s.getType(), f); + } + + public void onLocalSubscriptionUpdate(PluginContext ctx, EntityId entityId, Predicate filter, Function> f) { Set deviceSubscriptions = subscriptionsByEntityId.get(entityId); if (deviceSubscriptions != null) { - deviceSubscriptions.stream().filter(s -> type == s.getType()).forEach(s -> { + deviceSubscriptions.stream().filter(filter).forEach(s -> { String sessionId = s.getWsSessionId(); List subscriptionUpdate = f.apply(s); if (!subscriptionUpdate.isEmpty()) { @@ -206,7 +211,7 @@ public class SubscriptionManager { public void onAttributesUpdateFromServer(PluginContext ctx, EntityId entityId, String scope, List attributes) { Optional serverAddress = ctx.resolve(entityId); if (!serverAddress.isPresent()) { - onLocalSubscriptionUpdate(ctx, entityId, SubscriptionType.ATTRIBUTES, s -> { + onLocalSubscriptionUpdate(ctx, entityId, s -> SubscriptionType.ATTRIBUTES == s.getType() && scope.equals(s.getScope()), s -> { List subscriptionUpdate = new ArrayList(); for (AttributeKvEntry kv : attributes) { if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) { diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java index ba5d61080e..874c480f0d 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java @@ -114,7 +114,7 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler { } Map statesMap = proto.getKeyStatesList().stream().collect(Collectors.toMap(SubscriptionKetStateProto::getKey, SubscriptionKetStateProto::getTs)); Subscription subscription = new Subscription( - new SubscriptionState(proto.getSessionId(), proto.getSubscriptionId(), EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()), SubscriptionType.valueOf(proto.getType()), proto.getAllKeys(), statesMap), + new SubscriptionState(proto.getSessionId(), proto.getSubscriptionId(), EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()), SubscriptionType.valueOf(proto.getType()), proto.getAllKeys(), statesMap, proto.getScope()), false, msg.getServerAddress()); subscriptionManager.addRemoteWsSubscription(ctx, msg.getServerAddress(), proto.getSessionId(), subscription); } @@ -127,6 +127,7 @@ public class TelemetryRpcMsgHandler implements RpcMsgHandler { builder.setEntityId(cmd.getEntityId().getId().toString()); builder.setType(cmd.getType().name()); builder.setAllKeys(cmd.isAllKeys()); + builder.setScope(cmd.getScope()); cmd.getKeyStates().entrySet().forEach(e -> builder.addKeyStates(SubscriptionKetStateProto.newBuilder().setKey(e.getKey()).setTs(e.getValue()).build())); ctx.sendPluginRpcMsg(new RpcMsg(address, SUBSCRIPTION_CLAZZ, builder.build().toByteArray())); } diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java index 7b0e6d8fcd..6f02c9a5f2 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java @@ -131,7 +131,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { keys.forEach(key -> subState.put(key, 0L)); attributesData.forEach(v -> subState.put(v.getKey(), v.getTs())); - SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.ATTRIBUTES, false, subState); + SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.ATTRIBUTES, false, subState, cmd.getScope()); subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub); } @@ -168,7 +168,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { Map subState = new HashMap<>(attributesData.size()); attributesData.forEach(v -> subState.put(v.getKey(), v.getTs())); - SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.ATTRIBUTES, true, subState); + SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.ATTRIBUTES, true, subState, cmd.getScope()); subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub); } @@ -234,7 +234,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data)); Map subState = new HashMap<>(data.size()); data.forEach(v -> subState.put(v.getKey(), v.getTs())); - SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.TIMESERIES, true, subState); + SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.TIMESERIES, true, subState, cmd.getScope()); subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub); } @@ -262,7 +262,7 @@ public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler { Map subState = new HashMap<>(keys.size()); keys.forEach(key -> subState.put(key, startTs)); data.forEach(v -> subState.put(v.getKey(), v.getTs())); - SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.TIMESERIES, false, subState); + SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), entityId, SubscriptionType.TIMESERIES, false, subState, cmd.getScope()); subscriptionManager.addLocalWsSubscription(ctx, sessionId, entityId, sub); } diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java index 1285cfa903..360b018d14 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java @@ -51,6 +51,10 @@ public class Subscription { return getSub().getType(); } + public String getScope() { + return getSub().getScope(); + } + public boolean isAllKeys() { return getSub().isAllKeys(); } diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java index 5e15fda502..7e0a2ba48f 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java @@ -33,6 +33,7 @@ public class SubscriptionState { @Getter private final SubscriptionType type; @Getter private final boolean allKeys; @Getter private final Map keyStates; + @Getter private final String scope; @Override public boolean equals(Object o) { diff --git a/extensions-core/src/main/proto/telemetry.proto b/extensions-core/src/main/proto/telemetry.proto index 2bfef5908b..59c5c14fd5 100644 --- a/extensions-core/src/main/proto/telemetry.proto +++ b/extensions-core/src/main/proto/telemetry.proto @@ -27,6 +27,7 @@ message SubscriptionProto { string type = 5; bool allKeys = 6; repeated SubscriptionKetStateProto keyStates = 7; + string scope = 8; } message SubscriptionUpdateProto { From 2920a1a73ea76b788835d527fa1f2264ae1a41c6 Mon Sep 17 00:00:00 2001 From: oleg Date: Fri, 12 Jan 2018 19:03:59 +0200 Subject: [PATCH 08/57] Dashboard page jittering --- ui/src/app/common/utils.service.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/src/app/common/utils.service.js b/ui/src/app/common/utils.service.js index 085a28bad5..54d85ec6fe 100644 --- a/ui/src/app/common/utils.service.js +++ b/ui/src/app/common/utils.service.js @@ -553,7 +553,9 @@ function Utils($mdColorPalette, $rootScope, $window, $translate, $q, $timeout, t var aspect = imageAspectMap[urlHashCode]; if (angular.isUndefined(aspect)) { var testImage = document.createElement('img'); // eslint-disable-line - testImage.style.visibility = 'hidden'; + testImage.style.position = 'absolute'; + testImage.style.left = '-99999px'; + testImage.style.top = '-99999px'; testImage.onload = function() { aspect = testImage.width / testImage.height; document.body.removeChild(testImage); //eslint-disable-line From a8ed801e503bb1d0a42437f8c6a9610194ee4334 Mon Sep 17 00:00:00 2001 From: oleg Date: Mon, 15 Jan 2018 17:01:51 +0200 Subject: [PATCH 09/57] UI:Bug. 'Flot state' widget --- ui/src/app/widget/lib/flot-widget.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/ui/src/app/widget/lib/flot-widget.js b/ui/src/app/widget/lib/flot-widget.js index 59787cf920..392c8736de 100644 --- a/ui/src/app/widget/lib/flot-widget.js +++ b/ui/src/app/widget/lib/flot-widget.js @@ -405,9 +405,13 @@ export default class TbFlot { } } series.lines = { - fill: keySettings.fillLines === true, - show: this.chartType === 'line' ? keySettings.showLines !== false : keySettings.showLines === true + fill: keySettings.fillLines === true }; + if (this.chartType === 'line' || this.chartType === 'state') { + series.lines.show = keySettings.showLines !== false + } else { + series.lines.show = keySettings.showLines === true; + } if (angular.isDefined(keySettings.lineWidth)) { series.lines.lineWidth = keySettings.lineWidth; @@ -1016,18 +1020,18 @@ export default class TbFlot { "properties": { "showLines": { "title": "Show lines", - "type": "boolean", - "default": defaultShowLines + "type": "boolean", + "default": defaultShowLines }, "fillLines": { "title": "Fill lines", - "type": "boolean", - "default": false + "type": "boolean", + "default": false }, "showPoints": { "title": "Show points", - "type": "boolean", - "default": false + "type": "boolean", + "default": false }, "tooltipValueFormatter": { "title": "Tooltip value format function, f(value)", From c683b9c076df5e39d2814ce8adb27dcf0622ec64 Mon Sep 17 00:00:00 2001 From: oleg Date: Wed, 10 Jan 2018 13:31:49 +0200 Subject: [PATCH 10/57] UI:Fix 'Timeseries - Flot' widget (the legend breaks tooltips) --- ui/src/app/widget/lib/flot-widget.js | 81 ++++++++++++++++++---------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/ui/src/app/widget/lib/flot-widget.js b/ui/src/app/widget/lib/flot-widget.js index 4e5f99f919..981cdd1e5b 100644 --- a/ui/src/app/widget/lib/flot-widget.js +++ b/ui/src/app/widget/lib/flot-widget.js @@ -238,29 +238,23 @@ export default class TbFlot { if (this.ticksFormatterFunction) { return this.ticksFormatterFunction(value); } - if (this.tickDecimals) { - value = value.toFixed(this.tickDecimals); + if (angular.isNumber(this.tickDecimals)) { + var factor = this.tickDecimals ? Math.pow(10, this.tickDecimals) : 1, + formatted = "" + Math.round(value * factor) / factor; + if (this.tickDecimals != null) { + var decimal = formatted.indexOf("."), + precision = decimal === -1 ? 0 : formatted.length - decimal - 1; + + if (precision < this.tickDecimals) { + value = (precision ? formatted : formatted + ".") + ("" + factor).substr(1, this.tickDecimals - precision); + } + } } if (this.tickUnits) { value = value + ' ' + this.tickUnits; } - if (this.tickSize) { - return value; - } - // var factor = this.tickDecimals ? Math.pow(10, this.tickDecimals) : 1, - // formatted = "" + Math.round(value * factor) / factor; - // if (this.tickDecimals != null) { - // var decimal = formatted.indexOf("."), - // precision = decimal === -1 ? 0 : formatted.length - decimal - 1; - // - // if (precision < this.tickDecimals) { - // formatted = (precision ? formatted : formatted + ".") + ("" + factor).substr(1, this.tickDecimals - precision); - // } - // } - // formatted += ' ' + this.tickUnits; return value; - }; this.yaxis.tickFormatter = ctx.yAxisTickFormatter; @@ -273,8 +267,16 @@ export default class TbFlot { this.yaxis.labelFont.color = this.yaxis.font.color; this.yaxis.labelFont.size = this.yaxis.font.size+2; this.yaxis.labelFont.weight = "bold"; - this.yaxis.tickSize = settings.yaxis.tickSize || null; - this.yaxis.tickDecimals = settings.yaxis.tickDecimals || null; + if (angular.isNumber(settings.yaxis.tickSize)) { + this.yaxis.tickSize = settings.yaxis.tickSize; + } else { + this.yaxis.tickSize = null; + } + if (angular.isNumber(settings.yaxis.tickDecimals)) { + this.yaxis.tickDecimals = settings.yaxis.tickDecimals + } else { + this.yaxis.tickDecimals = null; + } if (settings.yaxis.ticksFormatter && settings.yaxis.ticksFormatter.length) { try { this.yaxis.ticksFormatterFunction = new Function('value', settings.yaxis.ticksFormatter); @@ -500,9 +502,19 @@ export default class TbFlot { createYAxis(keySettings, units) { var yaxis = angular.copy(this.yaxis); + var tickDecimals, tickSize; var label = keySettings.axisTitle && keySettings.axisTitle.length ? keySettings.axisTitle : yaxis.label; - //var tickDecimals = angular.isDefined(keySettings.axisTickDecimals) ? keySettings.axisTickDecimals : 0; + if (angular.isNumber(keySettings.axisTickDecimals)) { + tickDecimals = keySettings.axisTickDecimals; + } else { + tickDecimals = yaxis.tickDecimals; + } + if (angular.isNumber(keySettings.axisTickSize)) { + tickSize = keySettings.axisTickSize; + } else { + tickSize = yaxis.tickSize; + } var position = keySettings.axisPosition && keySettings.axisPosition.length ? keySettings.axisPosition : "left"; var min = angular.isDefined(keySettings.axisMin) ? keySettings.axisMin : yaxis.min; @@ -512,6 +524,8 @@ export default class TbFlot { yaxis.min = min; yaxis.max = max; yaxis.tickUnits = units; + yaxis.tickDecimals = tickDecimals; + yaxis.tickSize = tickSize; yaxis.alignTicksWithAxis = position == "right" ? 1 : null; yaxis.position = position; @@ -557,7 +571,7 @@ export default class TbFlot { } } yaxis.hidden = hidden; - var newIndex = -1; + var newIndex = 1; if (!yaxis.hidden) { this.options.yaxes.push(yaxis); newIndex = this.options.yaxes.length; @@ -916,11 +930,6 @@ export default class TbFlot { "type": "number", "default": null }, - "tickSize": { - "title": "Step size between ticks", - "type": "number", - "default": null - }, "showLabels": { "title": "Show labels", "type": "boolean", @@ -950,8 +959,12 @@ export default class TbFlot { "title": "The number of decimals to display", "type": "number", "default": 0 + }, + "tickSize": { + "title": "Step size between ticks", + "type": "number", + "default": null } - } } }, @@ -1009,8 +1022,8 @@ export default class TbFlot { "items": [ "yaxis.min", "yaxis.max", - "yaxis.tickSize", "yaxis.tickDecimals", + "yaxis.tickSize", "yaxis.showLabels", "yaxis.title", "yaxis.titleAngle", @@ -1079,6 +1092,16 @@ export default class TbFlot { "type": "string", "default": "" }, + "axisTickDecimals": { + "title": "Axis tick number of digits after floating point", + "type": "number", + "default": 0 + }, + "axisTickSize": { + "title": "Axis step size between ticks", + "type": "number", + "default": null + }, "axisPosition": { "title": "Axis position", "type": "string", @@ -1104,6 +1127,8 @@ export default class TbFlot { "axisMin", "axisMax", "axisTitle", + "axisTickDecimals", + "axisTickSize", { "key": "axisPosition", "type": "rc-select", From ec6b926219b86d8c9ba1707e829d47eeae5505fe Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 19 Jan 2018 18:59:57 +0200 Subject: [PATCH 11/57] Update flot-widget.js --- ui/src/app/widget/lib/flot-widget.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/ui/src/app/widget/lib/flot-widget.js b/ui/src/app/widget/lib/flot-widget.js index 981cdd1e5b..718da76e9d 100644 --- a/ui/src/app/widget/lib/flot-widget.js +++ b/ui/src/app/widget/lib/flot-widget.js @@ -238,23 +238,22 @@ export default class TbFlot { if (this.ticksFormatterFunction) { return this.ticksFormatterFunction(value); } - if (angular.isNumber(this.tickDecimals)) { - var factor = this.tickDecimals ? Math.pow(10, this.tickDecimals) : 1, - formatted = "" + Math.round(value * factor) / factor; - if (this.tickDecimals != null) { - var decimal = formatted.indexOf("."), - precision = decimal === -1 ? 0 : formatted.length - decimal - 1; - - if (precision < this.tickDecimals) { - value = (precision ? formatted : formatted + ".") + ("" + factor).substr(1, this.tickDecimals - precision); - } + + var factor = this.tickDecimals ? Math.pow(10, this.tickDecimals) : 1, + formatted = "" + Math.round(value * factor) / factor; + if (this.tickDecimals != null) { + var decimal = formatted.indexOf("."), + precision = decimal === -1 ? 0 : formatted.length - decimal - 1; + + if (precision < this.tickDecimals) { + formatted = (precision ? formatted : formatted + ".") + ("" + factor).substr(1, this.tickDecimals - precision); } } if (this.tickUnits) { - value = value + ' ' + this.tickUnits; + formatted += ' ' + this.tickUnits; } - - return value; + + return formatted; }; this.yaxis.tickFormatter = ctx.yAxisTickFormatter; @@ -1445,4 +1444,4 @@ export default class TbFlot { } } -/* eslint-enable angular/angularelement */ \ No newline at end of file +/* eslint-enable angular/angularelement */ From f66e4864cd667d96ce1d89cfb5d438a747bec2b8 Mon Sep 17 00:00:00 2001 From: oleg Date: Tue, 23 Jan 2018 19:17:55 +0200 Subject: [PATCH 12/57] UI:Fix. Timeseries table > divider wrong behavior --- ui/src/app/widget/lib/timeseries-table-widget.tpl.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/app/widget/lib/timeseries-table-widget.tpl.html b/ui/src/app/widget/lib/timeseries-table-widget.tpl.html index 0913483476..349edbac5e 100644 --- a/ui/src/app/widget/lib/timeseries-table-widget.tpl.html +++ b/ui/src/app/widget/lib/timeseries-table-widget.tpl.html @@ -41,7 +41,7 @@ - + From 74aa9ca36974a0deb39a783f51e685da3aa7a3c5 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 1 Feb 2018 10:45:38 +0200 Subject: [PATCH 13/57] Deprecated map widget fix. --- ui/src/app/widget/lib/map-widget.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/app/widget/lib/map-widget.js b/ui/src/app/widget/lib/map-widget.js index ef86381438..ab2e6d83ae 100644 --- a/ui/src/app/widget/lib/map-widget.js +++ b/ui/src/app/widget/lib/map-widget.js @@ -276,7 +276,7 @@ export default class TbMapWidget { this.locationsSettings[i].useMarkerImage = true; var url = this.ctx.settings.markerImage; var size = this.ctx.settings.markerImageSize || 34; - this.locationSettings.currentImage = { + this.locationSettings[i].currentImage = { url: url, size: size }; From 6447a0badaa588ea024501d15932cf451d811f54 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 1 Feb 2018 11:01:49 +0200 Subject: [PATCH 14/57] Deprecated map widget fix. --- ui/src/app/widget/lib/map-widget.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/app/widget/lib/map-widget.js b/ui/src/app/widget/lib/map-widget.js index ab2e6d83ae..db04023ff0 100644 --- a/ui/src/app/widget/lib/map-widget.js +++ b/ui/src/app/widget/lib/map-widget.js @@ -276,7 +276,7 @@ export default class TbMapWidget { this.locationsSettings[i].useMarkerImage = true; var url = this.ctx.settings.markerImage; var size = this.ctx.settings.markerImageSize || 34; - this.locationSettings[i].currentImage = { + this.locationsSettings[i].currentImage = { url: url, size: size }; From f6da85e9ae08fafa7a5f23a4e888997689556944 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 1 Feb 2018 18:14:06 +0200 Subject: [PATCH 15/57] IE11/Edge fix for iframe mode detection. --- ui/src/app/app.run.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ui/src/app/app.run.js b/ui/src/app/app.run.js index 2ab5fe2bd5..7d6c31f07b 100644 --- a/ui/src/app/app.run.js +++ b/ui/src/app/app.run.js @@ -20,7 +20,13 @@ import UrlHandler from './url.handler'; export default function AppRun($rootScope, $window, $injector, $location, $log, $state, $mdDialog, $filter, loginService, userService, $translate) { $window.Flow = Flow; - var frame = $window.frameElement; + var frame = null; + try { + frame = $window.frameElement; + } catch(e) { + // ie11 fix + } + var unauthorizedDialog = null; var forbiddenDialog = null; From d727a8a3b4770a47557b087ac87dd824db202240 Mon Sep 17 00:00:00 2001 From: Andrew Shvayka Date: Wed, 7 Feb 2018 16:43:24 +0200 Subject: [PATCH 16/57] Caching of main requests related to relations and GWs API (#567) * added enable/disable condition for transport protocols * fix coap tests * configured relations cache * minor fix * Improvements * delete devices caching --- .../src/main/resources/thingsboard.yml | 16 ++ .../server/common/data/CacheConstants.java | 2 + dao/pom.xml | 8 + .../server/dao/cache/CacheSpecs.java | 24 +++ ...eviousDeviceCredentialsIdKeyGenerator.java | 4 +- .../dao/cache/ServiceCacheConfiguration.java | 91 ++++------ .../server/dao/device/DeviceService.java | 2 +- .../server/dao/device/DeviceServiceImpl.java | 36 ++-- .../dao/relation/BaseRelationService.java | 169 +++++++++++++++--- .../BaseDeviceCredentialsCacheTest.java | 17 +- .../dao/service/BaseRelationCacheTest.java | 101 +++++++++++ .../service/nosql/RelationCacheNoSqlTest.java | 23 +++ .../dao/service/sql/RelationCacheSqlTest.java | 23 +++ .../resources/application-test.properties | 11 +- pom.xml | 6 + .../transport/coap/CoapTransportService.java | 4 +- .../transport/mqtt/MqttTransportService.java | 2 + .../mqtt/session/GatewaySessionCtx.java | 19 +- 18 files changed, 431 insertions(+), 127 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/cache/CacheSpecs.java create mode 100644 dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java create mode 100644 dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationCacheNoSqlTest.java create mode 100644 dao/src/test/java/org/thingsboard/server/dao/service/sql/RelationCacheSqlTest.java diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 18e35c6744..5e1cfde664 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -77,6 +77,8 @@ http: # MQTT server parameters mqtt: + # Enable/disable mqtt transport protocol. + enabled: "${MQTT_ENABLED:true}" bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}" bind_port: "${MQTT_BIND_PORT:1883}" adaptor: "${MQTT_ADAPTOR_NAME:JsonMqttAdaptor}" @@ -102,6 +104,8 @@ mqtt: # CoAP server parameters coap: + # Enable/disable coap transport protocol. + enabled: "${COAP_ENABLED:true}" bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}" bind_port: "${COAP_BIND_PORT:5683}" adaptor: "${COAP_ADAPTOR_NAME:JsonCoapAdaptor}" @@ -208,6 +212,18 @@ cache: policy: "${CACHE_DEVICE_CREDENTIAL_MAX_SIZE_POLICY:PER_NODE}" size: "${CACHE_DEVICE_CREDENTIAL_MAX_SIZE_SIZE:1000000}" +caching: + specs: + relations: + timeToLiveInMinutes: 1440 + maxSize: 100000 + deviceCredentials: + timeToLiveInMinutes: 1440 + maxSize: 100000 + devices: + timeToLiveInMinutes: 1440 + maxSize: 100000 + # Check new version updates parameters updates: # Enable/disable updates checking. diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index dfcc63ffea..f6fd9a93e8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -17,4 +17,6 @@ package org.thingsboard.server.common.data; public class CacheConstants { public static final String DEVICE_CREDENTIALS_CACHE = "deviceCredentials"; + public static final String RELATIONS_CACHE = "relations"; + public static final String DEVICE_CACHE = "devices"; } diff --git a/dao/pom.xml b/dao/pom.xml index d9463e4f9c..75d69343e5 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -148,6 +148,10 @@ com.hazelcast hazelcast + + com.github.ben-manes.caffeine + caffeine + com.hazelcast hazelcast-spring @@ -174,6 +178,10 @@ hsqldb test + + org.springframework + spring-context-support + diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/CacheSpecs.java b/dao/src/main/java/org/thingsboard/server/dao/cache/CacheSpecs.java new file mode 100644 index 0000000000..82cbebf375 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/CacheSpecs.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2017 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.cache; + +import lombok.Data; + +@Data +public class CacheSpecs { + private Integer timeToLiveInMinutes; + private Integer maxSize; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/PreviousDeviceCredentialsIdKeyGenerator.java b/dao/src/main/java/org/thingsboard/server/dao/cache/PreviousDeviceCredentialsIdKeyGenerator.java index aa4004343d..6b4fccdde1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/PreviousDeviceCredentialsIdKeyGenerator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/PreviousDeviceCredentialsIdKeyGenerator.java @@ -23,6 +23,8 @@ import java.lang.reflect.Method; public class PreviousDeviceCredentialsIdKeyGenerator implements KeyGenerator { + private static final String NOT_VALID_DEVICE = "notValidDeviceCredentialsId"; + @Override public Object generate(Object o, Method method, Object... objects) { DeviceCredentialsService deviceCredentialsService = (DeviceCredentialsService) o; @@ -33,6 +35,6 @@ public class PreviousDeviceCredentialsIdKeyGenerator implements KeyGenerator { return oldDeviceCredentials.getCredentialsId(); } } - return null; + return NOT_VALID_DEVICE; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cache/ServiceCacheConfiguration.java b/dao/src/main/java/org/thingsboard/server/dao/cache/ServiceCacheConfiguration.java index 35391785d0..ba21cb204c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cache/ServiceCacheConfiguration.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cache/ServiceCacheConfiguration.java @@ -15,76 +15,57 @@ */ package org.thingsboard.server.dao.cache; -import com.hazelcast.config.*; -import com.hazelcast.core.Hazelcast; -import com.hazelcast.core.HazelcastInstance; -import com.hazelcast.instance.GroupProperty; -import com.hazelcast.spring.cache.HazelcastCacheManager; -import com.hazelcast.zookeeper.ZookeeperDiscoveryProperties; -import com.hazelcast.zookeeper.ZookeeperDiscoveryStrategyFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.Ticker; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCache; import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.support.SimpleCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.thingsboard.server.common.data.CacheConstants; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; @Configuration +@ConfigurationProperties(prefix = "caching") @EnableCaching -@ConditionalOnProperty(prefix = "cache", value = "enabled", havingValue = "true") +@Data public class ServiceCacheConfiguration { - private static final String HAZELCAST_CLUSTER_NAME = "hazelcast"; - - @Value("${cache.device_credentials.max_size.size}") - private Integer cacheDeviceCredentialsMaxSizeSize; - @Value("${cache.device_credentials.max_size.policy}") - private String cacheDeviceCredentialsMaxSizePolicy; - @Value("${cache.device_credentials.time_to_live}") - private Integer cacheDeviceCredentialsTTL; - - @Value("${zk.enabled}") - private boolean zkEnabled; - @Value("${zk.url}") - private String zkUrl; - @Value("${zk.zk_dir}") - private String zkDir; + private Map specs; @Bean - public HazelcastInstance hazelcastInstance() { - Config config = new Config(); - - if (zkEnabled) { - addZkConfig(config); + public CacheManager cacheManager() { + SimpleCacheManager manager = new SimpleCacheManager(); + if (specs != null) { + List caches = + specs.entrySet().stream() + .map(entry -> buildCache(entry.getKey(), + entry.getValue())) + .collect(Collectors.toList()); + manager.setCaches(caches); } - - config.addMapConfig(createDeviceCredentialsCacheConfig()); - - return Hazelcast.newHazelcastInstance(config); + return manager; } - private void addZkConfig(Config config) { - config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(false); - config.setProperty(GroupProperty.DISCOVERY_SPI_ENABLED.getName(), Boolean.TRUE.toString()); - DiscoveryStrategyConfig discoveryStrategyConfig = new DiscoveryStrategyConfig(new ZookeeperDiscoveryStrategyFactory()); - discoveryStrategyConfig.addProperty(ZookeeperDiscoveryProperties.ZOOKEEPER_URL.key(), zkUrl); - discoveryStrategyConfig.addProperty(ZookeeperDiscoveryProperties.ZOOKEEPER_PATH.key(), zkDir); - discoveryStrategyConfig.addProperty(ZookeeperDiscoveryProperties.GROUP.key(), HAZELCAST_CLUSTER_NAME); - config.getNetworkConfig().getJoin().getDiscoveryConfig().addDiscoveryStrategyConfig(discoveryStrategyConfig); + private CaffeineCache buildCache(String name, CacheSpecs cacheSpec) { + final Caffeine caffeineBuilder + = Caffeine.newBuilder() + .expireAfterWrite(cacheSpec.getTimeToLiveInMinutes(), TimeUnit.MINUTES) + .maximumSize(cacheSpec.getMaxSize()) + .ticker(ticker()); + return new CaffeineCache(name, caffeineBuilder.build()); } - private MapConfig createDeviceCredentialsCacheConfig() { - MapConfig deviceCredentialsCacheConfig = new MapConfig(CacheConstants.DEVICE_CREDENTIALS_CACHE); - deviceCredentialsCacheConfig.setTimeToLiveSeconds(cacheDeviceCredentialsTTL); - deviceCredentialsCacheConfig.setEvictionPolicy(EvictionPolicy.LRU); - deviceCredentialsCacheConfig.setMaxSizeConfig( - new MaxSizeConfig( - cacheDeviceCredentialsMaxSizeSize, - MaxSizeConfig.MaxSizePolicy.valueOf(cacheDeviceCredentialsMaxSizePolicy)) - ); - return deviceCredentialsCacheConfig; + @Bean + public Ticker ticker() { + return Ticker.systemTicker(); } @Bean @@ -92,8 +73,4 @@ public class ServiceCacheConfiguration { return new PreviousDeviceCredentialsIdKeyGenerator(); } - @Bean - public CacheManager cacheManager() { - return new HazelcastCacheManager(hazelcastInstance()); - } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java index 3b0c5ec124..c285c79d9c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceService.java @@ -34,7 +34,7 @@ public interface DeviceService { ListenableFuture findDeviceByIdAsync(DeviceId deviceId); - Optional findDeviceByTenantIdAndName(TenantId tenantId, String name); + Device findDeviceByTenantIdAndName(TenantId tenantId, String name); Device saveDevice(Device device); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index e762a0c57d..d7663f49f0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -22,6 +22,10 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.*; @@ -33,12 +37,12 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.tenant.TenantDao; @@ -47,6 +51,7 @@ import javax.annotation.Nullable; import java.util.*; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.CacheConstants.DEVICE_CACHE; import static org.thingsboard.server.dao.DaoUtil.toUUIDs; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; import static org.thingsboard.server.dao.service.Validator.*; @@ -71,6 +76,9 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @Autowired private DeviceCredentialsService deviceCredentialsService; + @Autowired + private CacheManager cacheManager; + @Override public Device findDeviceById(DeviceId deviceId) { log.trace("Executing findDeviceById [{}]", deviceId); @@ -85,18 +93,16 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe return deviceDao.findByIdAsync(deviceId.getId()); } + @Cacheable(cacheNames = DEVICE_CACHE, key = "{#tenantId, #name}") @Override - public Optional findDeviceByTenantIdAndName(TenantId tenantId, String name) { + public Device findDeviceByTenantIdAndName(TenantId tenantId, String name) { log.trace("Executing findDeviceByTenantIdAndName [{}][{}]", tenantId, name); validateId(tenantId, INCORRECT_TENANT_ID + tenantId); Optional deviceOpt = deviceDao.findDeviceByTenantIdAndName(tenantId.getId(), name); - if (deviceOpt.isPresent()) { - return Optional.of(deviceOpt.get()); - } else { - return Optional.empty(); - } + return deviceOpt.orElse(null); } + @CacheEvict(cacheNames = DEVICE_CACHE, key = "{#device.tenantId, #device.name}") @Override public Device saveDevice(Device device) { log.trace("Executing saveDevice [{}]", device); @@ -129,12 +135,18 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe @Override public void deleteDevice(DeviceId deviceId) { log.trace("Executing deleteDevice [{}]", deviceId); + Cache cache = cacheManager.getCache(DEVICE_CACHE); validateId(deviceId, INCORRECT_DEVICE_ID + deviceId); DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(deviceId); if (deviceCredentials != null) { deviceCredentialsService.deleteDeviceCredentials(deviceCredentials); } deleteEntityRelations(deviceId); + Device device = deviceDao.findById(deviceId.getId()); + List list = new ArrayList<>(); + list.add(device.getTenantId()); + list.add(device.getName()); + cache.evict(list); deviceDao.removeById(deviceId.getId()); } @@ -190,7 +202,7 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe validateId(customerId, INCORRECT_CUSTOMER_ID + customerId); validateString(type, "Incorrect type " + type); validatePageLink(pageLink, INCORRECT_PAGE_LINK + pageLink); - List devices = deviceDao.findDevicesByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); + List devices = deviceDao.findDevicesByTenantIdAndCustomerIdAndType(tenantId.getId(), customerId.getId(), type, pageLink); return new TextPageData<>(devices, pageLink); } @@ -244,10 +256,10 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe validateId(tenantId, INCORRECT_TENANT_ID + tenantId); ListenableFuture> tenantDeviceTypes = deviceDao.findTenantDeviceTypesAsync(tenantId.getId()); return Futures.transform(tenantDeviceTypes, - (Function, List>) deviceTypes -> { - deviceTypes.sort(Comparator.comparing(EntitySubtype::getType)); - return deviceTypes; - }); + (Function, List>) deviceTypes -> { + deviceTypes.sort(Comparator.comparing(EntitySubtype::getType)); + return deviceTypes; + }); } private DataValidator deviceValidator = diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index 86afdb488d..f89d0eb059 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -21,6 +21,11 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.id.EntityId; @@ -34,6 +39,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.function.BiConsumer; +import static org.thingsboard.server.common.data.CacheConstants.RELATIONS_CACHE; + /** * Created by ashvayka on 28.04.17. */ @@ -47,6 +54,9 @@ public class BaseRelationService implements RelationService { @Autowired private EntityService entityService; + @Autowired + private CacheManager cacheManager; + @Override public ListenableFuture checkRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { log.trace("Executing checkRelation [{}][{}][{}][{}]", from, to, relationType, typeGroup); @@ -54,6 +64,7 @@ public class BaseRelationService implements RelationService { return relationDao.checkRelation(from, to, relationType, typeGroup); } + @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType}") @Override public ListenableFuture getRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { log.trace("Executing EntityRelation [{}][{}][{}][{}]", from, to, relationType, typeGroup); @@ -61,6 +72,12 @@ public class BaseRelationService implements RelationService { return relationDao.getRelation(from, to, relationType, typeGroup); } + @Caching(evict = { + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.from"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.to"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type}") + }) @Override public boolean saveRelation(EntityRelation relation) { log.trace("Executing saveRelation [{}]", relation); @@ -68,6 +85,12 @@ public class BaseRelationService implements RelationService { return relationDao.saveRelation(relation); } + @Caching(evict = { + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.from"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.to"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type}") + }) @Override public ListenableFuture saveRelationAsync(EntityRelation relation) { log.trace("Executing saveRelationAsync [{}]", relation); @@ -75,6 +98,13 @@ public class BaseRelationService implements RelationService { return relationDao.saveRelationAsync(relation); } + @Caching(evict = { + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.from"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.to"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type}") + }) @Override public boolean deleteRelation(EntityRelation relation) { log.trace("Executing deleteRelation [{}]", relation); @@ -82,6 +112,13 @@ public class BaseRelationService implements RelationService { return relationDao.deleteRelation(relation); } + @Caching(evict = { + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.from"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.type}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#relation.to"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.to, #relation.type}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#relation.from, #relation.to, #relation.type}") + }) @Override public ListenableFuture deleteRelationAsync(EntityRelation relation) { log.trace("Executing deleteRelationAsync [{}]", relation); @@ -89,6 +126,13 @@ public class BaseRelationService implements RelationService { return relationDao.deleteRelationAsync(relation); } + @Caching(evict = { + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#from"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#to"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType}") + }) @Override public boolean deleteRelation(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { log.trace("Executing deleteRelation [{}][{}][{}][{}]", from, to, relationType, typeGroup); @@ -96,6 +140,13 @@ public class BaseRelationService implements RelationService { return relationDao.deleteRelation(from, to, relationType, typeGroup); } + @Caching(evict = { + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#from"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "#to"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType}"), + @CacheEvict(cacheNames = RELATIONS_CACHE, key = "{#from, #to, #relationType}") + }) @Override public ListenableFuture deleteRelationAsync(EntityId from, EntityId to, String relationType, RelationTypeGroup typeGroup) { log.trace("Executing deleteRelationAsync [{}][{}][{}][{}]", from, to, relationType, typeGroup); @@ -105,23 +156,17 @@ public class BaseRelationService implements RelationService { @Override public boolean deleteEntityRelations(EntityId entity) { + Cache cache = cacheManager.getCache(RELATIONS_CACHE); log.trace("Executing deleteEntityRelations [{}]", entity); validate(entity); - List>> inboundRelationsList = new ArrayList<>(); + List>> inboundRelationsListTo = new ArrayList<>(); for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { - inboundRelationsList.add(relationDao.findAllByTo(entity, typeGroup)); + inboundRelationsListTo.add(relationDao.findAllByTo(entity, typeGroup)); } - ListenableFuture>> inboundRelations = Futures.allAsList(inboundRelationsList); - ListenableFuture> inboundDeletions = Futures.transform(inboundRelations, new Function>, List>() { - @Override - public List apply(List> relations) { - List results = new ArrayList<>(); - for (List relationList : relations) { - relationList.stream().forEach(relation -> results.add(relationDao.deleteRelation(relation))); - } - return results; - } - }); + ListenableFuture>> inboundRelationsTo = Futures.allAsList(inboundRelationsListTo); + ListenableFuture> inboundDeletions = Futures.transform(inboundRelationsTo, (List> relations) -> + getBooleans(relations, cache, true)); + ListenableFuture inboundFuture = Futures.transform(inboundDeletions, getListToBooleanFunction()); boolean inboundDeleteResult = false; try { @@ -129,12 +174,39 @@ public class BaseRelationService implements RelationService { } catch (InterruptedException | ExecutionException e) { log.error("Error deleting entity inbound relations", e); } + + List>> inboundRelationsListFrom = new ArrayList<>(); + for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { + inboundRelationsListFrom.add(relationDao.findAllByFrom(entity, typeGroup)); + } + ListenableFuture>> inboundRelationsFrom = Futures.allAsList(inboundRelationsListFrom); + Futures.transform(inboundRelationsFrom, (Function>, List>) relations -> + getBooleans(relations, cache, false)); + boolean outboundDeleteResult = relationDao.deleteOutboundRelations(entity); return inboundDeleteResult && outboundDeleteResult; } + private List getBooleans(List> relations, Cache cache, boolean isRemove) { + List results = new ArrayList<>(); + for (List relationList : relations) { + relationList.stream().forEach(relation -> { + checkFromDeleteSync(cache, results, relation, isRemove); + }); + } + return results; + } + + private void checkFromDeleteSync(Cache cache, List results, EntityRelation relation, boolean isRemove) { + if (isRemove) { + results.add(relationDao.deleteRelation(relation)); + } + cacheEviction(relation, relation.getTo(), cache); + } + @Override public ListenableFuture deleteEntityRelationsAsync(EntityId entity) { + Cache cache = cacheManager.getCache(RELATIONS_CACHE); log.trace("Executing deleteEntityRelationsAsync [{}]", entity); validate(entity); List>> inboundRelationsList = new ArrayList<>(); @@ -142,24 +214,61 @@ public class BaseRelationService implements RelationService { inboundRelationsList.add(relationDao.findAllByTo(entity, typeGroup)); } ListenableFuture>> inboundRelations = Futures.allAsList(inboundRelationsList); - ListenableFuture> inboundDeletions = Futures.transform(inboundRelations, new AsyncFunction>, List>() { - @Override - public ListenableFuture> apply(List> relations) throws Exception { - List> results = new ArrayList<>(); - for (List relationList : relations) { - relationList.stream().forEach(relation -> results.add(relationDao.deleteRelationAsync(relation))); - } - return Futures.allAsList(results); - } + ListenableFuture> inboundDeletions = Futures.transform(inboundRelations, + (AsyncFunction>, List>) relations -> { + List> results = getListenableFutures(relations, cache, true); + return Futures.allAsList(results); }); ListenableFuture inboundFuture = Futures.transform(inboundDeletions, getListToBooleanFunction()); - ListenableFuture outboundFuture = relationDao.deleteOutboundRelationsAsync(entity); + List>> inboundRelationsList1 = new ArrayList<>(); + for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { + inboundRelationsList1.add(relationDao.findAllByTo(entity, typeGroup)); + } + ListenableFuture>> inboundRelations1 = Futures.allAsList(inboundRelationsList1); + Futures.transform(inboundRelations1, (AsyncFunction>, List>) relations -> { + List> results = getListenableFutures(relations, cache, false); + return Futures.allAsList(results); + }); + ListenableFuture outboundFuture = relationDao.deleteOutboundRelationsAsync(entity); return Futures.transform(Futures.allAsList(Arrays.asList(inboundFuture, outboundFuture)), getListToBooleanFunction()); } + private List> getListenableFutures(List> relations, Cache cache, boolean isRemove) { + List> results = new ArrayList<>(); + for (List relationList : relations) { + relationList.stream().forEach(relation -> { + checkFromDeleteAsync(cache, results, relation, isRemove); + }); + } + return results; + } + + private void checkFromDeleteAsync(Cache cache, List> results, EntityRelation relation, boolean isRemove) { + if (isRemove) { + results.add(relationDao.deleteRelationAsync(relation)); + } + cacheEviction(relation, relation.getTo(), cache); + } + + private void cacheEviction(EntityRelation relation, EntityId entityId, Cache cache) { + cache.evict(entityId); + + List toAndType = new ArrayList<>(); + toAndType.add(entityId); + toAndType.add(relation.getType()); + cache.evict(toAndType); + + List fromToAndType = new ArrayList<>(); + fromToAndType.add(relation.getFrom()); + fromToAndType.add(relation.getTo()); + fromToAndType.add(relation.getType()); + cache.evict(fromToAndType); + } + + @Cacheable(cacheNames = RELATIONS_CACHE, key = "#from") @Override public ListenableFuture> findByFrom(EntityId from, RelationTypeGroup typeGroup) { log.trace("Executing findByFrom [{}][{}]", from, typeGroup); @@ -176,17 +285,18 @@ public class BaseRelationService implements RelationService { ListenableFuture> relations = relationDao.findAllByFrom(from, typeGroup); ListenableFuture> relationsInfo = Futures.transform(relations, (AsyncFunction, List>) relations1 -> { - List> futures = new ArrayList<>(); + List> futures = new ArrayList<>(); relations1.stream().forEach(relation -> futures.add(fetchRelationInfoAsync(relation, relation2 -> relation2.getTo(), (EntityRelationInfo relationInfo, String entityName) -> relationInfo.setToName(entityName))) ); return Futures.successfulAsList(futures); - }); + }); return relationsInfo; } + @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #relationType}") @Override public ListenableFuture> findByFromAndType(EntityId from, String relationType, RelationTypeGroup typeGroup) { log.trace("Executing findByFromAndType [{}][{}][{}]", from, relationType, typeGroup); @@ -196,6 +306,7 @@ public class BaseRelationService implements RelationService { return relationDao.findAllByFromAndType(from, relationType, typeGroup); } + @Cacheable(cacheNames = RELATIONS_CACHE, key = "#to") @Override public ListenableFuture> findByTo(EntityId to, RelationTypeGroup typeGroup) { log.trace("Executing findByTo [{}][{}]", to, typeGroup); @@ -214,9 +325,9 @@ public class BaseRelationService implements RelationService { (AsyncFunction, List>) relations1 -> { List> futures = new ArrayList<>(); relations1.stream().forEach(relation -> - futures.add(fetchRelationInfoAsync(relation, - relation2 -> relation2.getFrom(), - (EntityRelationInfo relationInfo, String entityName) -> relationInfo.setFromName(entityName))) + futures.add(fetchRelationInfoAsync(relation, + relation2 -> relation2.getFrom(), + (EntityRelationInfo relationInfo, String entityName) -> relationInfo.setFromName(entityName))) ); return Futures.successfulAsList(futures); }); @@ -236,6 +347,7 @@ public class BaseRelationService implements RelationService { return entityRelationInfo; } + @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#to, #relationType}") @Override public ListenableFuture> findByToAndType(EntityId to, String relationType, RelationTypeGroup typeGroup) { log.trace("Executing findByToAndType [{}][{}][{}]", to, relationType, typeGroup); @@ -417,5 +529,4 @@ public class BaseRelationService implements RelationService { } return relations; } - } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java index 31b599a154..9ff8b2a5cb 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java @@ -15,16 +15,14 @@ */ package org.thingsboard.server.dao.service; -import com.hazelcast.core.HazelcastInstance; import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.aop.framework.Advised; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.test.context.TestPropertySource; +import org.springframework.cache.CacheManager; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.Device; @@ -40,7 +38,6 @@ import java.util.UUID; import static org.mockito.Mockito.*; -@TestPropertySource(properties = {"cache.enabled = true"}) public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest { private static final String CREDENTIALS_ID_1 = RandomStringUtils.randomAlphanumeric(20); @@ -53,7 +50,7 @@ public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest private DeviceService deviceService; @Autowired - private HazelcastInstance hazelcastInstance; + private CacheManager cacheManager; private UUID deviceId = UUID.randomUUID(); @@ -67,7 +64,7 @@ public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest @After public void cleanup() { - hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).evictAll(); + cacheManager.getCache(CacheConstants.DEVICE_CREDENTIALS_CACHE).clear(); } @Test @@ -77,7 +74,6 @@ public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1); deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1); - Assert.assertEquals(1, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size()); verify(deviceCredentialsDao, times(1)).findByCredentialsId(CREDENTIALS_ID_1); } @@ -88,17 +84,13 @@ public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1); deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1); - Assert.assertEquals(1, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size()); verify(deviceCredentialsDao, times(1)).findByCredentialsId(CREDENTIALS_ID_1); deviceCredentialsService.deleteDeviceCredentials(createDummyDeviceCredentials(CREDENTIALS_ID_1, deviceId)); - Assert.assertEquals(0, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size()); - deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1); deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1); - Assert.assertEquals(1, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size()); verify(deviceCredentialsDao, times(2)).findByCredentialsId(CREDENTIALS_ID_1); } @@ -109,7 +101,6 @@ public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1); deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1); - Assert.assertEquals(1, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size()); verify(deviceCredentialsDao, times(1)).findByCredentialsId(CREDENTIALS_ID_1); when(deviceCredentialsDao.findByDeviceId(deviceId)).thenReturn(createDummyDeviceCredentialsEntity(CREDENTIALS_ID_1)); @@ -119,13 +110,11 @@ public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest when(deviceService.findDeviceById(new DeviceId(deviceId))).thenReturn(new Device()); deviceCredentialsService.updateDeviceCredentials(createDummyDeviceCredentials(deviceCredentialsId, CREDENTIALS_ID_2, deviceId)); - Assert.assertEquals(0, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size()); when(deviceCredentialsDao.findByCredentialsId(CREDENTIALS_ID_1)).thenReturn(null); deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1); deviceCredentialsService.findDeviceCredentialsByCredentialsId(CREDENTIALS_ID_1); - Assert.assertEquals(0, hazelcastInstance.getMap(CacheConstants.DEVICE_CREDENTIALS_CACHE).size()); verify(deviceCredentialsDao, times(3)).findByCredentialsId(CREDENTIALS_ID_1); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java new file mode 100644 index 0000000000..5798825377 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationCacheTest.java @@ -0,0 +1,101 @@ +/** + * Copyright © 2016-2017 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.service; + +import com.google.common.util.concurrent.Futures; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.aop.framework.Advised; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.dao.relation.RelationDao; +import org.thingsboard.server.dao.relation.RelationService; + +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +import static org.mockito.Mockito.*; +import static org.thingsboard.server.common.data.CacheConstants.RELATIONS_CACHE; + +public abstract class BaseRelationCacheTest extends AbstractServiceTest { + + private static final EntityId ENTITY_ID_FROM = new DeviceId(UUID.randomUUID()); + private static final EntityId ENTITY_ID_TO = new DeviceId(UUID.randomUUID()); + private static final String RELATION_TYPE = "Contains"; + + @Autowired + private RelationService relationService; + @Autowired + private CacheManager cacheManager; + + private RelationDao relationDao; + + @Before + public void setup() throws Exception { + relationDao = mock(RelationDao.class); + ReflectionTestUtils.setField(unwrapRelationService(), "relationDao", relationDao); + } + + @After + public void cleanup() { + cacheManager.getCache(RELATIONS_CACHE).clear(); + } + + private RelationService unwrapRelationService() throws Exception { + if (AopUtils.isAopProxy(relationService) && relationService instanceof Advised) { + Object target = ((Advised) relationService).getTargetSource().getTarget(); + return (RelationService) target; + } + return null; + } + + @Test + public void testFindRelationByFrom_Cached() throws ExecutionException, InterruptedException { + when(relationDao.getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON)) + .thenReturn(Futures.immediateFuture(new EntityRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE))); + + relationService.getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); + relationService.getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); + + verify(relationDao, times(1)).getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); + } + + @Test + public void testDeleteRelations_EvictsCache() { + when(relationDao.getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON)) + .thenReturn(Futures.immediateFuture(new EntityRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE))); + + relationService.getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); + relationService.getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); + + verify(relationDao, times(1)).getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); + + relationService.deleteRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); + + relationService.getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); + relationService.getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); + + verify(relationDao, times(2)).getRelation(ENTITY_ID_FROM, ENTITY_ID_TO, RELATION_TYPE, RelationTypeGroup.COMMON); + + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationCacheNoSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationCacheNoSqlTest.java new file mode 100644 index 0000000000..e71a9bb697 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/nosql/RelationCacheNoSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2017 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.service.nosql; + +import org.thingsboard.server.dao.service.BaseRelationCacheTest; +import org.thingsboard.server.dao.service.DaoNoSqlTest; + +@DaoNoSqlTest +public class RelationCacheNoSqlTest extends BaseRelationCacheTest { +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/sql/RelationCacheSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/sql/RelationCacheSqlTest.java new file mode 100644 index 0000000000..566d026831 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/sql/RelationCacheSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2017 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.service.sql; + +import org.thingsboard.server.dao.service.BaseRelationCacheTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class RelationCacheSqlTest extends BaseRelationCacheTest { +} diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index d87a181d0a..c615df58d2 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -7,4 +7,13 @@ zk.enabled=false zk.url=localhost:2181 zk.zk_dir=/thingsboard -updates.enabled=false \ No newline at end of file +updates.enabled=false + +caching.specs.relations.timeToLiveInMinutes=1440 +caching.specs.relations.maxSize=100000 + +caching.specs.deviceCredentials.timeToLiveInMinutes=1440 +caching.specs.deviceCredentials.maxSize=100000 + +caching.specs.devices.timeToLiveInMinutes=1440 +caching.specs.devices.maxSize=100000 \ No newline at end of file diff --git a/pom.xml b/pom.xml index 4b77abbd98..dd3cad9a6c 100755 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,7 @@ 3.0.0.1 1.2.7 18.0 + 2.6.1 3.4 1.5.0 2.5 @@ -644,6 +645,11 @@ guava ${guava.version} + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + com.google.protobuf protobuf-java diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java index a78c718d4f..02a2706776 100644 --- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java +++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java @@ -26,17 +26,17 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; import org.eclipse.californium.core.CoapServer; import org.eclipse.californium.core.network.CoapEndpoint; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.thingsboard.server.common.transport.SessionMsgProcessor; import org.thingsboard.server.common.transport.auth.DeviceAuthService; import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; @Service("CoapTransportService") +@ConditionalOnProperty(prefix = "coap", value = "enabled", havingValue = "true", matchIfMissing = true) @Slf4j public class CoapTransportService { diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java index 179dad5721..850519573b 100644 --- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java +++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java @@ -24,6 +24,7 @@ import io.netty.util.ResourceLeakDetector; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; import org.thingsboard.server.common.transport.SessionMsgProcessor; @@ -39,6 +40,7 @@ import javax.annotation.PreDestroy; * @author Andrew Shvayka */ @Service("MqttTransportService") +@ConditionalOnProperty(prefix = "mqtt", value = "enabled", havingValue = "true", matchIfMissing = false) @Slf4j public class MqttTransportService { diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java index d69341c999..485965011f 100644 --- a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java +++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java @@ -84,16 +84,15 @@ public class GatewaySessionCtx { private void onDeviceConnect(String deviceName, String deviceType) { if (!devices.containsKey(deviceName)) { - Optional deviceOpt = deviceService.findDeviceByTenantIdAndName(gateway.getTenantId(), deviceName); - Device device = deviceOpt.orElseGet(() -> { - Device newDevice = new Device(); - newDevice.setTenantId(gateway.getTenantId()); - newDevice.setName(deviceName); - newDevice.setType(deviceType); - newDevice = deviceService.saveDevice(newDevice); - relationService.saveRelationAsync(new EntityRelation(gateway.getId(), newDevice.getId(), "Created")); - return newDevice; - }); + Device device = deviceService.findDeviceByTenantIdAndName(gateway.getTenantId(), deviceName); + if (device == null) { + device = new Device(); + device.setTenantId(gateway.getTenantId()); + device.setName(deviceName); + device.setType(deviceType); + device = deviceService.saveDevice(device); + relationService.saveRelationAsync(new EntityRelation(gateway.getId(), device.getId(), "Created")); + } GatewayDeviceSessionCtx ctx = new GatewayDeviceSessionCtx(this, device); devices.put(deviceName, ctx); log.debug("[{}] Added device [{}] to the gateway session", gatewaySessionId, deviceName); From 9cae286c9a2495dea160ed10fb3ab9bf2d3c6480 Mon Sep 17 00:00:00 2001 From: Dima Landiak Date: Thu, 8 Feb 2018 13:10:48 +0200 Subject: [PATCH 17/57] fix in relations caching --- .../dao/relation/BaseRelationService.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index f89d0eb059..6e3e75b7e9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -200,8 +200,10 @@ public class BaseRelationService implements RelationService { private void checkFromDeleteSync(Cache cache, List results, EntityRelation relation, boolean isRemove) { if (isRemove) { results.add(relationDao.deleteRelation(relation)); + cacheEviction(relation, relation.getTo(), cache); + } else { + cacheEviction(relation, relation.getFrom(), cache); } - cacheEviction(relation, relation.getTo(), cache); } @Override @@ -209,12 +211,12 @@ public class BaseRelationService implements RelationService { Cache cache = cacheManager.getCache(RELATIONS_CACHE); log.trace("Executing deleteEntityRelationsAsync [{}]", entity); validate(entity); - List>> inboundRelationsList = new ArrayList<>(); + List>> inboundRelationsListTo = new ArrayList<>(); for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { - inboundRelationsList.add(relationDao.findAllByTo(entity, typeGroup)); + inboundRelationsListTo.add(relationDao.findAllByTo(entity, typeGroup)); } - ListenableFuture>> inboundRelations = Futures.allAsList(inboundRelationsList); - ListenableFuture> inboundDeletions = Futures.transform(inboundRelations, + ListenableFuture>> inboundRelationsTo = Futures.allAsList(inboundRelationsListTo); + ListenableFuture> inboundDeletions = Futures.transform(inboundRelationsTo, (AsyncFunction>, List>) relations -> { List> results = getListenableFutures(relations, cache, true); return Futures.allAsList(results); @@ -222,12 +224,12 @@ public class BaseRelationService implements RelationService { ListenableFuture inboundFuture = Futures.transform(inboundDeletions, getListToBooleanFunction()); - List>> inboundRelationsList1 = new ArrayList<>(); + List>> inboundRelationsListFrom = new ArrayList<>(); for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { - inboundRelationsList1.add(relationDao.findAllByTo(entity, typeGroup)); + inboundRelationsListFrom.add(relationDao.findAllByTo(entity, typeGroup)); } - ListenableFuture>> inboundRelations1 = Futures.allAsList(inboundRelationsList1); - Futures.transform(inboundRelations1, (AsyncFunction>, List>) relations -> { + ListenableFuture>> inboundRelationsFrom = Futures.allAsList(inboundRelationsListFrom); + Futures.transform(inboundRelationsFrom, (AsyncFunction>, List>) relations -> { List> results = getListenableFutures(relations, cache, false); return Futures.allAsList(results); }); @@ -249,8 +251,10 @@ public class BaseRelationService implements RelationService { private void checkFromDeleteAsync(Cache cache, List> results, EntityRelation relation, boolean isRemove) { if (isRemove) { results.add(relationDao.deleteRelationAsync(relation)); + cacheEviction(relation, relation.getTo(), cache); + } else { + cacheEviction(relation, relation.getFrom(), cache); } - cacheEviction(relation, relation.getTo(), cache); } private void cacheEviction(EntityRelation relation, EntityId entityId, Cache cache) { From 4ec03f7db1576a101d6e719b6a3d9b6cf2d1c1a4 Mon Sep 17 00:00:00 2001 From: affoltep Date: Thu, 8 Feb 2018 15:49:55 +0100 Subject: [PATCH 18/57] Update MailPlugin.java Allows to add multiple e-mail recipients in TO, CC, BCC seperated by "," see https://groups.google.com/forum/#!searchin/thingsboard/attribute%7Csort:date/thingsboard/03V4Out1bgc/XVm9IswTEAAJ --- .../server/extensions/core/plugin/mail/MailPlugin.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java index d0c3490434..540235fd7d 100644 --- a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java +++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java @@ -108,12 +108,12 @@ public class MailPlugin extends AbstractPlugin implemen MimeMessage mailMsg = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(mailMsg, "UTF-8"); helper.setFrom(msg.getFrom()); - helper.setTo(msg.getTo()); + helper.setTo(msg.getTo().split("\\s*,\\s*")); if (!StringUtils.isEmpty(msg.getCc())) { - helper.setCc(msg.getCc()); + helper.setCc(msg.getCc().split("\\s*,\\s*")); } if (!StringUtils.isEmpty(msg.getBcc())) { - helper.setBcc(msg.getBcc()); + helper.setBcc(msg.getBcc().split("\\s*,\\s*")); } helper.setSubject(msg.getSubject()); helper.setText(msg.getBody()); From 3898e70cb261ca13ef3627b26db0d641d0daa51a Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Fri, 9 Feb 2018 17:17:44 +0200 Subject: [PATCH 19/57] Added audit log base impl --- .../server/controller/AuditLogController.java | 70 +++++ .../server/controller/BaseController.java | 4 + .../server/controller/DeviceController.java | 35 ++- .../controller/AbstractControllerTest.java | 30 +++ .../BaseAuditLogControllerTest.java | 118 +++++++++ .../nosql/AuditLogControllerNoSqlTest.java | 23 ++ .../sql/AuditLogControllerSqlTest.java | 23 ++ .../common/data/audit/ActionStatus.java | 20 ++ .../server/common/data/audit/ActionType.java | 20 ++ .../server/common/data/audit/AuditLog.java | 60 +++++ .../server/common/data/id/AuditLogId.java | 35 +++ .../server/dao/audit/AuditLogDao.java | 37 +++ .../server/dao/audit/AuditLogService.java | 50 ++++ .../server/dao/audit/AuditLogServiceImpl.java | 130 ++++++++++ .../dao/audit/CassandraAuditLogDao.java | 242 ++++++++++++++++++ .../server/dao/model/ModelConstants.java | 37 ++- .../dao/model/nosql/AuditLogEntity.java | 137 ++++++++++ .../server/dao/model/sql/AuditLogEntity.java | 132 ++++++++++ .../dao/model/type/ActionStatusCodec.java | 26 ++ .../dao/model/type/ActionTypeCodec.java | 26 ++ .../dao/sql/audit/AuditLogRepository.java | 44 ++++ .../server/dao/sql/audit/JpaAuditLogDao.java | 103 ++++++++ dao/src/main/resources/cassandra/schema.cql | 41 +++ dao/src/main/resources/sql/schema.sql | 16 ++ .../server/dao/JpaDaoTestSuite.java | 2 +- .../dao/sql/audit/JpaAuditLogDaoTest.java | 21 ++ .../test/resources/sql/drop-all-tables.sql | 1 + 27 files changed, 1472 insertions(+), 11 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/controller/AuditLogController.java create mode 100644 application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java create mode 100644 application/src/test/java/org/thingsboard/server/controller/nosql/AuditLogControllerNoSqlTest.java create mode 100644 application/src/test/java/org/thingsboard/server/controller/sql/AuditLogControllerSqlTest.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionStatus.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/id/AuditLogId.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/type/ActionStatusCodec.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/type/ActionTypeCodec.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java create mode 100644 dao/src/test/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDaoTest.java diff --git a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java new file mode 100644 index 0000000000..b79359b44f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2017 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.controller; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.exception.ThingsboardException; + +@RestController +@RequestMapping("/api") +public class AuditLogController extends BaseController { + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/audit/logs/{entityType}/{entityId}", params = {"limit"}, method = RequestMethod.GET) + @ResponseBody + public TimePageData getAuditLogs( + @PathVariable("entityType") String strEntityType, + @PathVariable("entityId") String strEntityId, + @RequestParam int limit, + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime, + @RequestParam(required = false, defaultValue = "false") boolean ascOrder, + @RequestParam(required = false) String offset) throws ThingsboardException { + try { + checkParameter("EntityId", strEntityId); + checkParameter("EntityType", strEntityType); + TenantId tenantId = getCurrentUser().getTenantId(); + TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); + return checkNotNull(auditLogService.findAuditLogsByTenantIdAndEntityId(tenantId, EntityIdFactory.getByTypeAndId(strEntityType, strEntityId), pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/audit/logs", params = {"limit"}, method = RequestMethod.GET) + @ResponseBody + public TimePageData getAuditLogs( + @RequestParam int limit, + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime, + @RequestParam(required = false, defaultValue = "false") boolean ascOrder, + @RequestParam(required = false) String offset) throws ThingsboardException { + try { + TenantId tenantId = getCurrentUser().getTenantId(); + TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); + return checkNotNull(auditLogService.findAuditLogsByTenantId(tenantId, pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 16ba93eca8..ed5975a218 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.asset.AssetService; +import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceCredentialsService; @@ -117,6 +118,9 @@ public abstract class BaseController { @Autowired protected RelationService relationService; + @Autowired + protected AuditLogService auditLogService; + @ExceptionHandler(ThingsboardException.class) public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) { diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index eeb10c8c83..8d08706993 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -22,6 +22,9 @@ import org.springframework.web.bind.annotation.*; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.audit.ActionStatus; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; @@ -29,7 +32,6 @@ import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.DeviceCredentials; -import org.thingsboard.server.common.data.device.DeviceSearchQuery; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.exception.ThingsboardErrorCode; @@ -75,12 +77,21 @@ public class DeviceController extends BaseController { } } Device savedDevice = checkNotNull(deviceService.saveDevice(device)); + actorService .onDeviceNameOrTypeUpdate( savedDevice.getTenantId(), savedDevice.getId(), savedDevice.getName(), savedDevice.getType()); + + // TODO: refactor to ANNOTATION usage + if (device.getId() == null) { + logDeviceAction(savedDevice, ActionType.ADDED); + } else { + logDeviceAction(savedDevice, ActionType.UPDATED); + } + return savedDevice; } catch (Exception e) { throw handleException(e); @@ -94,8 +105,10 @@ public class DeviceController extends BaseController { checkParameter(DEVICE_ID, strDeviceId); try { DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); - checkDeviceId(deviceId); + Device device = checkDeviceId(deviceId); deviceService.deleteDevice(deviceId); + // TODO: refactor to ANNOTATION usage + logDeviceAction(device, ActionType.DELETED); } catch (Exception e) { throw handleException(e); } @@ -173,9 +186,11 @@ public class DeviceController extends BaseController { public DeviceCredentials saveDeviceCredentials(@RequestBody DeviceCredentials deviceCredentials) throws ThingsboardException { checkNotNull(deviceCredentials); try { - checkDeviceId(deviceCredentials.getDeviceId()); + Device device = checkDeviceId(deviceCredentials.getDeviceId()); DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials)); actorService.onCredentialsUpdate(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId()); + // TODO: refactor to ANNOTATION usage + logDeviceAction(device, ActionType.CREDENTIALS_UPDATED); return result; } catch (Exception e) { throw handleException(e); @@ -307,4 +322,18 @@ public class DeviceController extends BaseController { } } + // TODO: refactor to ANNOTATION usage + private void logDeviceAction(Device device, ActionType actionType) throws ThingsboardException { + auditLogService.logAction( + getCurrentUser().getTenantId(), + device.getId(), + device.getName(), + device.getCustomerId(), + getCurrentUser().getId(), + getCurrentUser().getName(), + actionType, + null, + ActionStatus.SUCCESS, + null); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java index 19e4329c33..36d736d6e7 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java @@ -66,6 +66,7 @@ import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.config.ThingsboardSecurityConfiguration; import org.thingsboard.server.service.mail.TestMailService; @@ -331,6 +332,35 @@ public abstract class AbstractControllerTest { return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType); } + protected T doGetTypedWithTimePageLink(String urlTemplate, TypeReference responseType, + TimePageLink pageLink, + Object... urlVariables) throws Exception { + List pageLinkVariables = new ArrayList<>(); + urlTemplate += "limit={limit}"; + pageLinkVariables.add(pageLink.getLimit()); + if (pageLink.getStartTime() != null) { + urlTemplate += "&startTime={startTime}"; + pageLinkVariables.add(pageLink.getStartTime()); + } + if (pageLink.getEndTime() != null) { + urlTemplate += "&endTime={endTime}"; + pageLinkVariables.add(pageLink.getEndTime()); + } + if (pageLink.getIdOffset() != null) { + urlTemplate += "&offset={offset}"; + pageLinkVariables.add(pageLink.getIdOffset().toString()); + } + if (pageLink.isAscOrder()) { + urlTemplate += "&ascOrder={ascOrder}"; + pageLinkVariables.add(pageLink.isAscOrder()); + } + Object[] vars = new Object[urlVariables.length + pageLinkVariables.size()]; + System.arraycopy(urlVariables, 0, vars, 0, urlVariables.length); + System.arraycopy(pageLinkVariables.toArray(), 0, vars, urlVariables.length, pageLinkVariables.size()); + + return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType); + } + protected T doPost(String urlTemplate, Class responseClass, String... params) throws Exception { return readResponse(doPost(urlTemplate, params).andExpect(status().isOk()), responseClass); } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java new file mode 100644 index 0000000000..5e74416868 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java @@ -0,0 +1,118 @@ +/** + * Copyright © 2016-2017 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.controller; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.security.Authority; + +import java.util.ArrayList; +import java.util.List; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +public abstract class BaseAuditLogControllerTest extends AbstractControllerTest { + + private Tenant savedTenant; + + @Before + public void beforeTest() throws Exception { + loginSysAdmin(); + + Tenant tenant = new Tenant(); + tenant.setTitle("My tenant"); + savedTenant = doPost("/api/tenant", tenant, Tenant.class); + Assert.assertNotNull(savedTenant); + + User tenantAdmin = new User(); + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); + tenantAdmin.setTenantId(savedTenant.getId()); + tenantAdmin.setEmail("tenant2@thingsboard.org"); + tenantAdmin.setFirstName("Joe"); + tenantAdmin.setLastName("Downs"); + + createUserAndLogin(tenantAdmin, "testPassword1"); + } + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testSaveDeviceAuditLogs() throws Exception { + for (int i = 0; i < 178; i++) { + Device device = new Device(); + device.setName("Device" + i); + device.setType("default"); + doPost("/api/device", device, Device.class); + } + + List loadedAuditLogs = new ArrayList<>(); + TimePageLink pageLink = new TimePageLink(23); + TimePageData pageData; + do { + pageData = doGetTypedWithTimePageLink("/api/audit/logs?", + new TypeReference>() { + }, pageLink); + loadedAuditLogs.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageData.getNextPageLink(); + } + } while (pageData.hasNext()); + + Assert.assertEquals(178, loadedAuditLogs.size()); + } + + @Test + public void testUpdateDeviceAuditLogs() throws Exception { + Device device = new Device(); + device.setName("Device name"); + device.setType("default"); + Device savedDevice = doPost("/api/device", device, Device.class); + for (int i = 0; i < 178; i++) { + savedDevice.setName("Device name" + i); + doPost("/api/device", savedDevice, Device.class); + } + + List loadedAuditLogs = new ArrayList<>(); + TimePageLink pageLink = new TimePageLink(23); + TimePageData pageData; + do { + pageData = doGetTypedWithTimePageLink("/api/audit/logs/DEVICE/" + savedDevice.getId().getId() + "?", + new TypeReference>() { + }, pageLink); + loadedAuditLogs.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageData.getNextPageLink(); + } + } while (pageData.hasNext()); + + Assert.assertEquals(179, loadedAuditLogs.size()); + } +} diff --git a/application/src/test/java/org/thingsboard/server/controller/nosql/AuditLogControllerNoSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/nosql/AuditLogControllerNoSqlTest.java new file mode 100644 index 0000000000..4692afe303 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/nosql/AuditLogControllerNoSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2017 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.controller.nosql; + +import org.thingsboard.server.controller.BaseAuditLogControllerTest; +import org.thingsboard.server.dao.service.DaoNoSqlTest; + +@DaoNoSqlTest +public class AuditLogControllerNoSqlTest extends BaseAuditLogControllerTest { +} diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/AuditLogControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/AuditLogControllerSqlTest.java new file mode 100644 index 0000000000..df6804eea4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/controller/sql/AuditLogControllerSqlTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2017 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.controller.sql; + +import org.thingsboard.server.controller.BaseAuditLogControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +public class AuditLogControllerSqlTest extends BaseAuditLogControllerTest { +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionStatus.java new file mode 100644 index 0000000000..5ee8beb398 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionStatus.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2017 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.audit; + +public enum ActionStatus { + SUCCESS, FAILURE +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java new file mode 100644 index 0000000000..495f80dc6f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2017 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.audit; + +public enum ActionType { + ADDED, DELETED, UPDATED, ATTRIBUTE_UPDATED, ATTRIBUTE_DELETED, ATTRIBUTE_ADDED, RPC_CALL, CREDENTIALS_UPDATED +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java new file mode 100644 index 0000000000..62d4bfac04 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/AuditLog.java @@ -0,0 +1,60 @@ +/** + * Copyright © 2016-2017 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.audit; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.id.*; + +@EqualsAndHashCode(callSuper = true) +@Data +public class AuditLog extends BaseData { + + private TenantId tenantId; + private CustomerId customerId; + private EntityId entityId; + private String entityName; + private UserId userId; + private String userName; + private ActionType actionType; + private JsonNode actionData; + private ActionStatus actionStatus; + private String actionFailureDetails; + + public AuditLog() { + super(); + } + + public AuditLog(AuditLogId id) { + super(id); + } + + public AuditLog(AuditLog auditLog) { + super(auditLog); + this.tenantId = auditLog.getTenantId(); + this.customerId = auditLog.getCustomerId(); + this.entityId = auditLog.getEntityId(); + this.entityName = auditLog.getEntityName(); + this.userId = auditLog.getUserId(); + this.userName = auditLog.getUserName(); + this.actionType = auditLog.getActionType(); + this.actionData = auditLog.getActionData(); + this.actionStatus = auditLog.getActionStatus(); + this.actionFailureDetails = auditLog.getActionFailureDetails(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AuditLogId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AuditLogId.java new file mode 100644 index 0000000000..5327212739 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AuditLogId.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2017 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.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.UUID; + +public class AuditLogId extends UUIDBased { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public AuditLogId(@JsonProperty("id") UUID id) { + super(id); + } + + public static AuditLogId fromString(String auditLogId) { + return new AuditLogId(UUID.fromString(auditLogId)); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java new file mode 100644 index 0000000000..f401ad1ac6 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2017 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.audit; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.TimePageLink; + +import java.util.List; +import java.util.UUID; + +public interface AuditLogDao { + + ListenableFuture saveByTenantId(AuditLog auditLog); + + ListenableFuture saveByTenantIdAndEntityId(AuditLog auditLog); + + ListenableFuture savePartitionsByTenantId(AuditLog auditLog); + + List findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink); + + List findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java new file mode 100644 index 0000000000..5593edee9d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2017 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.audit; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.audit.ActionStatus; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.TimePageLink; + +import java.util.List; + +public interface AuditLogService { + + TimePageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink); + + TimePageData findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink); + + ListenableFuture> logAction(TenantId tenantId, + EntityId entityId, + String entityName, + CustomerId customerId, + UserId userId, + String userName, + ActionType actionType, + JsonNode actionData, + ActionStatus actionStatus, + String actionFailureDetails); + + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java new file mode 100644 index 0000000000..1baf573155 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -0,0 +1,130 @@ +/** + * Copyright © 2016-2017 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.audit; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.audit.ActionStatus; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; + +import java.util.List; + +import static org.thingsboard.server.dao.service.Validator.validateEntityId; +import static org.thingsboard.server.dao.service.Validator.validateId; + +@Slf4j +@Service +public class AuditLogServiceImpl implements AuditLogService { + + private static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; + private static final int INSERTS_PER_ENTRY = 3; + + @Autowired + private AuditLogDao auditLogDao; + + @Override + public TimePageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink) { + log.trace("Executing findAuditLogsByTenantIdAndEntityId [{}], [{}], [{}]", tenantId, entityId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateEntityId(entityId, INCORRECT_TENANT_ID + entityId); + List auditLogs = auditLogDao.findAuditLogsByTenantIdAndEntityId(tenantId.getId(), entityId, pageLink); + return new TimePageData<>(auditLogs, pageLink); + } + + @Override + public TimePageData findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink) { + log.trace("Executing findAuditLogs [{}]", pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + List auditLogs = auditLogDao.findAuditLogsByTenantId(tenantId.getId(), pageLink); + return new TimePageData<>(auditLogs, pageLink); + } + + private AuditLog createAuditLogEntry(TenantId tenantId, + EntityId entityId, + String entityName, + CustomerId customerId, + UserId userId, + String userName, + ActionType actionType, + JsonNode actionData, + ActionStatus actionStatus, + String actionFailureDetails) { + AuditLog result = new AuditLog(); + result.setTenantId(tenantId); + result.setEntityId(entityId); + result.setEntityName(entityName); + result.setCustomerId(customerId); + result.setUserId(userId); + result.setUserName(userName); + result.setActionType(actionType); + result.setActionData(actionData); + result.setActionStatus(actionStatus); + result.setActionFailureDetails(actionFailureDetails); + return result; + } + + @Override + public ListenableFuture> logAction(TenantId tenantId, + EntityId entityId, + String entityName, + CustomerId customerId, + UserId userId, + String userName, + ActionType actionType, + JsonNode actionData, + ActionStatus actionStatus, + String actionFailureDetails) { + AuditLog auditLogEntry = createAuditLogEntry(tenantId, entityId, entityName, customerId, userId, userName, + actionType, actionData, actionStatus, actionFailureDetails); + log.trace("Executing logAction [{}]", auditLogEntry); + auditLogValidator.validate(auditLogEntry); + List> futures = Lists.newArrayListWithExpectedSize(INSERTS_PER_ENTRY); + futures.add(auditLogDao.savePartitionsByTenantId(auditLogEntry)); + futures.add(auditLogDao.saveByTenantId(auditLogEntry)); + futures.add(auditLogDao.saveByTenantIdAndEntityId(auditLogEntry)); + return Futures.allAsList(futures); + } + + private DataValidator auditLogValidator = + new DataValidator() { + @Override + protected void validateDataImpl(AuditLog auditLog) { + if (auditLog.getEntityId() == null) { + throw new DataValidationException("Entity Id should be specified!"); + } + if (auditLog.getTenantId() == null) { + throw new DataValidationException("Tenant Id should be specified!"); + } + if (auditLog.getUserId() == null) { + throw new DataValidationException("User Id should be specified!"); + } + } + }; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java new file mode 100644 index 0000000000..2fcb73281d --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java @@ -0,0 +1,242 @@ +/** + * Copyright © 2016-2017 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.audit; + +import com.datastax.driver.core.BoundStatement; +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.ResultSetFuture; +import com.datastax.driver.core.utils.UUIDs; +import com.google.common.base.Function; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.id.AuditLogId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.model.nosql.AuditLogEntity; +import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTimeDao; +import org.thingsboard.server.dao.timeseries.TsPartitionDate; +import org.thingsboard.server.dao.util.NoSqlDao; + +import javax.annotation.Nullable; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; +import static org.thingsboard.server.dao.model.ModelConstants.*; + +@Component +@Slf4j +@NoSqlDao +public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao implements AuditLogDao { + + private static final String INSERT_INTO = "INSERT INTO "; + + @Autowired + private Environment environment; + + @Override + protected Class getColumnFamilyClass() { + return AuditLogEntity.class; + } + + @Override + protected String getColumnFamilyName() { + return AUDIT_LOG_COLUMN_FAMILY_NAME; + } + + protected ExecutorService readResultsProcessingExecutor; + + @Value("${cassandra.query.ts_key_value_partitioning}") + private String partitioning; + private TsPartitionDate tsFormat; + + private PreparedStatement[] saveStmts; + + private boolean isInstall() { + return environment.acceptsProfiles("install"); + } + + @PostConstruct + public void init() { + if (!isInstall()) { + Optional partition = TsPartitionDate.parse(partitioning); + if (partition.isPresent()) { + tsFormat = partition.get(); + } else { + log.warn("Incorrect configuration of partitioning {}", partitioning); + throw new RuntimeException("Failed to parse partitioning property: " + partitioning + "!"); + } + } + readResultsProcessingExecutor = Executors.newCachedThreadPool(); + } + + @PreDestroy + public void stopExecutor() { + if (readResultsProcessingExecutor != null) { + readResultsProcessingExecutor.shutdownNow(); + } + } + + private ListenableFuture getFuture(ResultSetFuture future, java.util.function.Function transformer) { + return Futures.transform(future, new Function() { + @Nullable + @Override + public T apply(@Nullable ResultSet input) { + return transformer.apply(input); + } + }, readResultsProcessingExecutor); + } + + @Override + public ListenableFuture saveByTenantId(AuditLog auditLog) { + log.debug("Save saveByTenantId [{}] ", auditLog); + + AuditLogId auditLogId = new AuditLogId(UUIDs.timeBased()); + + long partition = toPartitionTs(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli()); + BoundStatement stmt = getSaveByTenantStmt().bind(); + stmt.setUUID(0, auditLogId.getId()) + .setUUID(1, auditLog.getTenantId().getId()) + .setUUID(2, auditLog.getEntityId().getId()) + .setString(3, auditLog.getEntityId().getEntityType().name()) + .setString(4, auditLog.getActionType().name()) + .setLong(5, partition); + return getFuture(executeAsyncWrite(stmt), rs -> null); + } + + @Override + public ListenableFuture saveByTenantIdAndEntityId(AuditLog auditLog) { + log.debug("Save saveByTenantIdAndEntityId [{}] ", auditLog); + + AuditLogId auditLogId = new AuditLogId(UUIDs.timeBased()); + + BoundStatement stmt = getSaveByTenantIdAndEntityIdStmt().bind(); + stmt.setUUID(0, auditLogId.getId()) + .setUUID(1, auditLog.getTenantId().getId()) + .setUUID(2, auditLog.getEntityId().getId()) + .setString(3, auditLog.getEntityId().getEntityType().name()) + .setString(4, auditLog.getActionType().name()); + return getFuture(executeAsyncWrite(stmt), rs -> null); + } + + @Override + public ListenableFuture savePartitionsByTenantId(AuditLog auditLog) { + log.debug("Save savePartitionsByTenantId [{}] ", auditLog); + + long partition = toPartitionTs(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli()); + + BoundStatement stmt = getPartitionInsertStmt().bind(); + stmt = stmt.setUUID(0, auditLog.getTenantId().getId()) + .setLong(1, partition); + return getFuture(executeAsyncWrite(stmt), rs -> null); + } + + private PreparedStatement getPartitionInsertStmt() { + // TODO: ADD CACHE LOGIC + return getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF + + "(" + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY + + "," + ModelConstants.AUDIT_LOG_PARTITION_PROPERTY + ")" + + " VALUES(?, ?)"); + } + + private PreparedStatement getSaveByTenantIdAndEntityIdStmt() { + // TODO: ADD CACHE LOGIC + return getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_ENTITY_ID_CF + + "(" + ModelConstants.AUDIT_LOG_ID_PROPERTY + + "," + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY + + "," + ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY + + "," + ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY + + "," + ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY + ")" + + " VALUES(?, ?, ?, ?, ?)"); + } + + private PreparedStatement getSaveByTenantStmt() { + // TODO: ADD CACHE LOGIC + return getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_CF + + "(" + ModelConstants.AUDIT_LOG_ID_PROPERTY + + "," + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY + + "," + ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY + + "," + ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY + + "," + ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY + + "," + ModelConstants.AUDIT_LOG_PARTITION_PROPERTY + ")" + + " VALUES(?, ?, ?, ?, ?, ?)"); + } + + private long toPartitionTs(long ts) { + LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneOffset.UTC); + return tsFormat.truncatedTo(time).toInstant(ZoneOffset.UTC).toEpochMilli(); + } + + + @Override + public List findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink) { + log.trace("Try to find audit logs by tenant [{}], entity [{}] and pageLink [{}]", tenantId, entityId, pageLink); + List entities = findPageWithTimeSearch(AUDIT_LOG_BY_ENTITY_ID_CF, + Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId), + eq(ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY, entityId.getEntityType()), + eq(ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY, entityId.getId())), + pageLink); + log.trace("Found audit logs by tenant [{}], entity [{}] and pageLink [{}]", tenantId, entityId, pageLink); + return DaoUtil.convertDataList(entities); + } + + @Override + public List findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) { + log.trace("Try to find audit logs by tenant [{}] and pageLink [{}]", tenantId, pageLink); + + // TODO: ADD AUDIT LOG PARTITION CURSOR LOGIC + + long minPartition; + long maxPartition; + + if (pageLink.getStartTime() != null && pageLink.getStartTime() != 0) { + minPartition = toPartitionTs(pageLink.getStartTime()); + } else { + minPartition = toPartitionTs(LocalDate.now().minusMonths(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli()); + } + + if (pageLink.getEndTime() != null && pageLink.getEndTime() != 0) { + maxPartition = toPartitionTs(pageLink.getEndTime()); + } else { + maxPartition = toPartitionTs(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli()); + } + List entities = findPageWithTimeSearch(AUDIT_LOG_BY_TENANT_ID_CF, + Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId), + eq(ModelConstants.AUDIT_LOG_PARTITION_PROPERTY, maxPartition)), + pageLink); + log.trace("Found audit logs by tenant [{}] and pageLink [{}]", tenantId, pageLink); + return DaoUtil.convertDataList(entities); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 9b596fc4ca..58c8d2f1f7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -44,6 +44,13 @@ public class ModelConstants { public static final String ADDITIONAL_INFO_PROPERTY = "additional_info"; public static final String ENTITY_TYPE_PROPERTY = "entity_type"; + public static final String ENTITY_TYPE_COLUMN = ENTITY_TYPE_PROPERTY; + public static final String ENTITY_ID_COLUMN = "entity_id"; + public static final String ATTRIBUTE_TYPE_COLUMN = "attribute_type"; + public static final String ATTRIBUTE_KEY_COLUMN = "attribute_key"; + public static final String LAST_UPDATE_TS_COLUMN = "last_update_ts"; + + /** * Cassandra user constants. */ @@ -134,6 +141,29 @@ public class ModelConstants { public static final String DEVICE_BY_TENANT_AND_NAME_VIEW_NAME = "device_by_tenant_and_name"; public static final String DEVICE_TYPES_BY_TENANT_VIEW_NAME = "device_types_by_tenant"; + /** + * Cassandra audit log constants. + */ + public static final String AUDIT_LOG_COLUMN_FAMILY_NAME = "audit_log"; + + public static final String AUDIT_LOG_BY_ENTITY_ID_CF = "audit_log_by_entity_id"; + public static final String AUDIT_LOG_BY_TENANT_ID_CF = "audit_log_by_tenant_id"; + public static final String AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF = "audit_log_by_tenant_id_partitions"; + + public static final String AUDIT_LOG_ID_PROPERTY = ID_PROPERTY; + public static final String AUDIT_LOG_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + public static final String AUDIT_LOG_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY; + public static final String AUDIT_LOG_ENTITY_TYPE_PROPERTY = ENTITY_TYPE_PROPERTY; + public static final String AUDIT_LOG_ENTITY_ID_PROPERTY = ENTITY_ID_COLUMN; + public static final String AUDIT_LOG_ENTITY_NAME_PROPERTY = "entity_name"; + public static final String AUDIT_LOG_USER_ID_PROPERTY = USER_ID_PROPERTY; + public static final String AUDIT_LOG_PARTITION_PROPERTY = "partition"; + public static final String AUDIT_LOG_USER_NAME_PROPERTY = "user_name"; + public static final String AUDIT_LOG_ACTION_TYPE_PROPERTY = "action_type"; + public static final String AUDIT_LOG_ACTION_DATA_PROPERTY = "action_data"; + public static final String AUDIT_LOG_ACTION_STATUS_PROPERTY = "action_status"; + public static final String AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY = "action_failure_details"; + /** * Cassandra asset constants. */ @@ -310,13 +340,6 @@ public class ModelConstants { public static final String TS_KV_PARTITIONS_CF = "ts_kv_partitions_cf"; public static final String TS_KV_LATEST_CF = "ts_kv_latest_cf"; - - public static final String ENTITY_TYPE_COLUMN = ENTITY_TYPE_PROPERTY; - public static final String ENTITY_ID_COLUMN = "entity_id"; - public static final String ATTRIBUTE_TYPE_COLUMN = "attribute_type"; - public static final String ATTRIBUTE_KEY_COLUMN = "attribute_key"; - public static final String LAST_UPDATE_TS_COLUMN = "last_update_ts"; - public static final String PARTITION_COLUMN = "partition"; public static final String KEY_COLUMN = "key"; public static final String TS_COLUMN = "ts"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java new file mode 100644 index 0000000000..25b6a21727 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/AuditLogEntity.java @@ -0,0 +1,137 @@ +/** + * Copyright © 2016-2017 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.model.nosql; + +import com.datastax.driver.mapping.annotations.Column; +import com.datastax.driver.mapping.annotations.Table; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.audit.ActionStatus; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.id.*; +import org.thingsboard.server.dao.model.BaseEntity; +import org.thingsboard.server.dao.model.type.ActionStatusCodec; +import org.thingsboard.server.dao.model.type.ActionTypeCodec; +import org.thingsboard.server.dao.model.type.EntityTypeCodec; +import org.thingsboard.server.dao.model.type.JsonCodec; + +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.*; + +@Table(name = AUDIT_LOG_COLUMN_FAMILY_NAME) +@Data +@NoArgsConstructor +public class AuditLogEntity implements BaseEntity { + + @Column(name = ID_PROPERTY) + private UUID id; + + @Column(name = AUDIT_LOG_TENANT_ID_PROPERTY) + private UUID tenantId; + + @Column(name = AUDIT_LOG_CUSTOMER_ID_PROPERTY) + private UUID customerId; + + @Column(name = AUDIT_LOG_ENTITY_TYPE_PROPERTY, codec = EntityTypeCodec.class) + private EntityType entityType; + + @Column(name = AUDIT_LOG_ENTITY_ID_PROPERTY) + private UUID entityId; + + @Column(name = AUDIT_LOG_ENTITY_NAME_PROPERTY) + private String entityName; + + @Column(name = AUDIT_LOG_USER_ID_PROPERTY) + private UUID userId; + + @Column(name = AUDIT_LOG_USER_NAME_PROPERTY) + private String userName; + + @Column(name = AUDIT_LOG_ACTION_TYPE_PROPERTY, codec = ActionTypeCodec.class) + private ActionType actionType; + + @Column(name = AUDIT_LOG_ACTION_DATA_PROPERTY, codec = JsonCodec.class) + private JsonNode actionData; + + @Column(name = AUDIT_LOG_ACTION_STATUS_PROPERTY, codec = ActionStatusCodec.class) + private ActionStatus actionStatus; + + @Column(name = AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY) + private String actionFailureDetails; + + @Override + public UUID getId() { + return id; + } + + @Override + public void setId(UUID id) { + this.id = id; + } + + public AuditLogEntity(AuditLog auditLog) { + if (auditLog.getId() != null) { + this.id = auditLog.getId().getId(); + } + if (auditLog.getTenantId() != null) { + this.tenantId = auditLog.getTenantId().getId(); + } + if (auditLog.getEntityId() != null) { + this.entityType = auditLog.getEntityId().getEntityType(); + this.entityId = auditLog.getEntityId().getId(); + } + if (auditLog.getCustomerId() != null) { + this.customerId = auditLog.getCustomerId().getId(); + } + if (auditLog.getUserId() != null) { + this.userId = auditLog.getUserId().getId(); + } + this.entityName = auditLog.getEntityName(); + this.userName = auditLog.getUserName(); + this.actionType = auditLog.getActionType(); + this.actionData = auditLog.getActionData(); + this.actionStatus = auditLog.getActionStatus(); + this.actionFailureDetails = auditLog.getActionFailureDetails(); + } + + @Override + public AuditLog toData() { + AuditLog auditLog = new AuditLog(new AuditLogId(id)); + if (tenantId != null) { + auditLog.setTenantId(new TenantId(tenantId)); + } + if (entityId != null & entityType != null) { + auditLog.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); + } + if (customerId != null) { + auditLog.setCustomerId(new CustomerId(customerId)); + } + if (userId != null) { + auditLog.setUserId(new UserId(userId)); + } + auditLog.setEntityName(this.entityName); + auditLog.setUserName(this.userName); + auditLog.setActionType(this.actionType); + auditLog.setActionData(this.actionData); + auditLog.setActionStatus(this.actionStatus); + auditLog.setActionFailureDetails(this.actionFailureDetails); + return auditLog; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java new file mode 100644 index 0000000000..0f7109106b --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java @@ -0,0 +1,132 @@ +/** + * Copyright © 2016-2017 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.model.sql; + +import com.datastax.driver.core.utils.UUIDs; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.audit.ActionStatus; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.id.AuditLogId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.model.BaseEntity; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.*; + +import static org.thingsboard.server.dao.model.ModelConstants.*; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = ModelConstants.AUDIT_LOG_COLUMN_FAMILY_NAME) +public class AuditLogEntity extends BaseSqlEntity implements BaseEntity { + + @Column(name = AUDIT_LOG_TENANT_ID_PROPERTY) + private String tenantId; + + @Column(name = AUDIT_LOG_CUSTOMER_ID_PROPERTY) + private String customerId; + + @Enumerated(EnumType.STRING) + @Column(name = AUDIT_LOG_ENTITY_TYPE_PROPERTY) + private EntityType entityType; + + @Column(name = AUDIT_LOG_ENTITY_ID_PROPERTY) + private String entityId; + + @Column(name = AUDIT_LOG_ENTITY_NAME_PROPERTY) + private String entityName; + + @Column(name = AUDIT_LOG_USER_ID_PROPERTY) + private String userId; + + @Column(name = AUDIT_LOG_USER_NAME_PROPERTY) + private String userName; + + @Enumerated(EnumType.STRING) + @Column(name = AUDIT_LOG_ACTION_TYPE_PROPERTY) + private ActionType actionType; + + @Type(type = "json") + @Column(name = AUDIT_LOG_ACTION_DATA_PROPERTY) + private JsonNode actionData; + + @Enumerated(EnumType.STRING) + @Column(name = AUDIT_LOG_ACTION_STATUS_PROPERTY) + private ActionStatus actionStatus; + + @Column(name = AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY) + private String actionFailureDetails; + + public AuditLogEntity() { + super(); + } + + public AuditLogEntity(AuditLog auditLog) { + if (auditLog.getId() != null) { + this.setId(auditLog.getId().getId()); + } + if (auditLog.getTenantId() != null) { + this.tenantId = toString(auditLog.getTenantId().getId()); + } + if (auditLog.getCustomerId() != null) { + this.customerId = toString(auditLog.getCustomerId().getId()); + } + if (auditLog.getEntityId() != null) { + this.entityId = toString(auditLog.getEntityId().getId()); + this.entityType = auditLog.getEntityId().getEntityType(); + } + this.entityName = auditLog.getEntityName(); + this.userName = auditLog.getUserName(); + this.actionType = auditLog.getActionType(); + this.actionData = auditLog.getActionData(); + this.actionStatus = auditLog.getActionStatus(); + this.actionFailureDetails = auditLog.getActionFailureDetails(); + } + + @Override + public AuditLog toData() { + AuditLog auditLog = new AuditLog(new AuditLogId(getId())); + auditLog.setCreatedTime(UUIDs.unixTimestamp(getId())); + if (tenantId != null) { + auditLog.setTenantId(new TenantId(toUUID(tenantId))); + } + if (customerId != null) { + auditLog.setCustomerId(new CustomerId(toUUID(customerId))); + } + if (entityId != null) { + auditLog.setEntityId(EntityIdFactory.getByTypeAndId(entityType.name(), toUUID(entityId).toString())); + } + auditLog.setEntityName(this.entityName); + auditLog.setUserName(this.userName); + auditLog.setActionType(this.actionType); + auditLog.setActionData(this.actionData); + auditLog.setActionStatus(this.actionStatus); + auditLog.setActionFailureDetails(this.actionFailureDetails); + return auditLog; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionStatusCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionStatusCodec.java new file mode 100644 index 0000000000..a207c40c7a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionStatusCodec.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2017 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.model.type; + +import com.datastax.driver.extras.codecs.enums.EnumNameCodec; +import org.thingsboard.server.common.data.audit.ActionStatus; + +public class ActionStatusCodec extends EnumNameCodec { + + public ActionStatusCodec() { + super(ActionStatus.class); + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionTypeCodec.java b/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionTypeCodec.java new file mode 100644 index 0000000000..9f22d5f7d0 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/type/ActionTypeCodec.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2017 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.model.type; + +import com.datastax.driver.extras.codecs.enums.EnumNameCodec; +import org.thingsboard.server.common.data.audit.ActionType; + +public class ActionTypeCodec extends EnumNameCodec { + + public ActionTypeCodec() { + super(ActionType.class); + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java new file mode 100644 index 0000000000..7e4fdc1ce2 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2017 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.sql.audit; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.dao.model.sql.AuditLogEntity; + +import java.util.List; + +public interface AuditLogRepository extends CrudRepository { + + @Query("SELECT al FROM AuditLogEntity al WHERE al.tenantId = :tenantId " + + "AND al.id > :idOffset ORDER BY al.id") + List findByTenantId(@Param("tenantId") String tenantId, + @Param("idOffset") String idOffset, + Pageable pageable); + + @Query("SELECT al FROM AuditLogEntity al WHERE al.tenantId = :tenantId " + + "AND al.entityType = :entityType " + + "AND al.entityId = :entityId " + + "AND al.id > :idOffset ORDER BY al.id") + List findByTenantIdAndEntityId(@Param("tenantId") String tenantId, + @Param("entityId") String entityId, + @Param("entityType") EntityType entityType, + @Param("idOffset") String idOffset, + Pageable pageable); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java new file mode 100644 index 0000000000..fe7edf8d95 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java @@ -0,0 +1,103 @@ +/** + * Copyright © 2016-2017 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.sql.audit; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.audit.AuditLogDao; +import org.thingsboard.server.dao.model.sql.AuditLogEntity; +import org.thingsboard.server.dao.sql.JpaAbstractDao; +import org.thingsboard.server.dao.util.SqlDao; + +import javax.annotation.PreDestroy; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.Executors; + +import static org.thingsboard.server.common.data.UUIDConverter.fromTimeUUID; +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR; + +@Component +@SqlDao +public class JpaAuditLogDao extends JpaAbstractDao implements AuditLogDao { + + private ListeningExecutorService insertService = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()); + + @Autowired + private AuditLogRepository auditLogRepository; + + @Override + protected Class getEntityClass() { + return AuditLogEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return auditLogRepository; + } + + @PreDestroy + void onDestroy() { + insertService.shutdown(); + } + + @Override + public ListenableFuture saveByTenantId(AuditLog auditLog) { + return insertService.submit(() -> { + save(auditLog); + return null; + }); + } + + @Override + public ListenableFuture saveByTenantIdAndEntityId(AuditLog auditLog) { + return insertService.submit(() -> null); + } + + @Override + public ListenableFuture savePartitionsByTenantId(AuditLog auditLog) { + return insertService.submit(() -> null); + } + + @Override + public List findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink) { + return DaoUtil.convertDataList( + auditLogRepository.findByTenantIdAndEntityId( + fromTimeUUID(tenantId), + fromTimeUUID(entityId.getId()), + entityId.getEntityType(), + pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), + new PageRequest(0, pageLink.getLimit()))); + } + + @Override + public List findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) { + return DaoUtil.convertDataList( + auditLogRepository.findByTenantId( + fromTimeUUID(tenantId), + pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), + new PageRequest(0, pageLink.getLimit()))); + } +} diff --git a/dao/src/main/resources/cassandra/schema.cql b/dao/src/main/resources/cassandra/schema.cql index dda80676e2..6dc2becca9 100644 --- a/dao/src/main/resources/cassandra/schema.cql +++ b/dao/src/main/resources/cassandra/schema.cql @@ -548,3 +548,44 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.event_by_id AS AND event_type IS NOT NULL AND event_uid IS NOT NULL PRIMARY KEY ((tenant_id, entity_type, entity_id), id, event_type, event_uid) WITH CLUSTERING ORDER BY (id ASC, event_type ASC, event_uid ASC); + + +CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_entity_id ( + tenant_id timeuuid, + id timeuuid, + customer_id timeuuid, + entity_id timeuuid, + entity_type text, + entity_name text, + user_id timeuuid, + user_name text, + action_type text, + action_data text, + action_status text, + action_failure_details text, + PRIMARY KEY ((tenant_id, entity_id, entity_type), id) +); + +CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_tenant_id ( + tenant_id timeuuid, + id timeuuid, + partition bigint, + customer_id timeuuid, + entity_id timeuuid, + entity_type text, + entity_name text, + user_id timeuuid, + user_name text, + action_type text, + action_data text, + action_status text, + action_failure_details text, + PRIMARY KEY ((tenant_id, partition), id) +); + +CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_tenant_id_partitions ( + tenant_id timeuuid, + partition bigint, + PRIMARY KEY (( tenant_id ), partition) +) WITH CLUSTERING ORDER BY ( partition ASC ) +AND compaction = { 'class' : 'LeveledCompactionStrategy' }; \ No newline at end of file diff --git a/dao/src/main/resources/sql/schema.sql b/dao/src/main/resources/sql/schema.sql index 26b314c05d..7c0f172432 100644 --- a/dao/src/main/resources/sql/schema.sql +++ b/dao/src/main/resources/sql/schema.sql @@ -47,6 +47,22 @@ CREATE TABLE IF NOT EXISTS asset ( type varchar(255) ); +CREATE TABLE IF NOT EXISTS audit_log ( + id varchar(31) NOT NULL CONSTRAINT audit_log_pkey PRIMARY KEY, + tenant_id varchar(31), + customer_id varchar(31), + entity_id varchar(31), + entity_type varchar(255), + entity_name varchar(255), + user_id varchar(31), + user_name varchar(255), + action_type varchar(255), + action_data varchar(255), + action_status varchar(255), + search_text varchar(255), + action_failure_details varchar +); + CREATE TABLE IF NOT EXISTS attribute_kv ( entity_type varchar(255), entity_id varchar(31), diff --git a/dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java index d936a38fa0..817f6ee5c7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java +++ b/dao/src/test/java/org/thingsboard/server/dao/JpaDaoTestSuite.java @@ -24,7 +24,7 @@ import java.util.Arrays; @RunWith(ClasspathSuite.class) @ClassnameFilters({ - "org.thingsboard.server.dao.sql.*AAATest" + "org.thingsboard.server.dao.sql.*THIS_MUST_BE_FIXED_Test" }) public class JpaDaoTestSuite { diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDaoTest.java new file mode 100644 index 0000000000..ed174a746b --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDaoTest.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2017 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.sql.audit; + +import org.thingsboard.server.dao.AbstractJpaDaoTest; + +public class JpaAuditLogDaoTest extends AbstractJpaDaoTest { +} diff --git a/dao/src/test/resources/sql/drop-all-tables.sql b/dao/src/test/resources/sql/drop-all-tables.sql index 49a3774e51..dfdc90fc35 100644 --- a/dao/src/test/resources/sql/drop-all-tables.sql +++ b/dao/src/test/resources/sql/drop-all-tables.sql @@ -1,6 +1,7 @@ DROP TABLE IF EXISTS admin_settings; DROP TABLE IF EXISTS alarm; DROP TABLE IF EXISTS asset; +DROP TABLE IF EXISTS audit_log; DROP TABLE IF EXISTS attribute_kv; DROP TABLE IF EXISTS component_descriptor; DROP TABLE IF EXISTS customer; From ead30d700bd84ceffbbb5764ec5f39bf78681265 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Mon, 12 Feb 2018 19:12:08 +0200 Subject: [PATCH 20/57] Fixed user ID for JPA impl --- .../server/dao/model/sql/AuditLogEntity.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java index 0f7109106b..6dd70b23f4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AuditLogEntity.java @@ -25,10 +25,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.audit.ActionStatus; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; -import org.thingsboard.server.common.data.id.AuditLogId; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.EntityIdFactory; -import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.*; import org.thingsboard.server.dao.model.BaseEntity; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; @@ -100,6 +97,9 @@ public class AuditLogEntity extends BaseSqlEntity implements BaseEntit this.entityId = toString(auditLog.getEntityId().getId()); this.entityType = auditLog.getEntityId().getEntityType(); } + if (auditLog.getUserId() != null) { + this.userId = toString(auditLog.getUserId().getId()); + } this.entityName = auditLog.getEntityName(); this.userName = auditLog.getUserName(); this.actionType = auditLog.getActionType(); @@ -121,6 +121,9 @@ public class AuditLogEntity extends BaseSqlEntity implements BaseEntit if (entityId != null) { auditLog.setEntityId(EntityIdFactory.getByTypeAndId(entityType.name(), toUUID(entityId).toString())); } + if (userId != null) { + auditLog.setUserId(new UserId(toUUID(entityId))); + } auditLog.setEntityName(this.entityName); auditLog.setUserName(this.userName); auditLog.setActionType(this.actionType); From 19e32dd645a3e21858095755603439620c6cec78 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 13 Feb 2018 15:31:44 +0200 Subject: [PATCH 21/57] Fix cassandra database upgrade service. --- .../server/service/install/CassandraDatabaseUpgradeService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java index 7f009f4382..5b32374f60 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java @@ -66,6 +66,8 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService { //Dump devices, assets and relations + cluster.getSession(); + KeyspaceMetadata ks = cluster.getCluster().getMetadata().getKeyspace(cluster.getKeyspaceName()); log.info("Dumping devices ..."); From 23b21b29128230673bc8cc89a1e32410d022174f Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 14 Feb 2018 19:51:23 +0200 Subject: [PATCH 22/57] Added get by user id and customer id Improved partitions query --- .../server/controller/AuditLogController.java | 48 ++++- .../server/controller/DeviceController.java | 53 ++--- .../src/main/resources/thingsboard.yml | 6 +- .../BaseAuditLogControllerTest.java | 40 +++- .../server/dao/audit/AuditLogDao.java | 10 + .../server/dao/audit/AuditLogQueryCursor.java | 70 +++++++ .../server/dao/audit/AuditLogService.java | 24 ++- .../server/dao/audit/AuditLogServiceImpl.java | 73 +++++-- .../dao/audit/CassandraAuditLogDao.java | 191 ++++++++++++++---- .../dao/audit/DummyAuditLogServiceImpl.java | 61 ++++++ .../server/dao/model/ModelConstants.java | 2 + .../dao/sql/audit/AuditLogRepository.java | 16 ++ .../server/dao/sql/audit/JpaAuditLogDao.java | 32 +++ dao/src/main/resources/cassandra/schema.cql | 34 ++++ .../resources/application-test.properties | 4 +- 15 files changed, 562 insertions(+), 102 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogQueryCursor.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java diff --git a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java index b79359b44f..4d1d50ecab 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java @@ -18,20 +18,64 @@ package org.thingsboard.server.controller; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.TimePageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.exception.ThingsboardException; +import java.util.UUID; + @RestController @RequestMapping("/api") public class AuditLogController extends BaseController { @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/audit/logs/{entityType}/{entityId}", params = {"limit"}, method = RequestMethod.GET) + @RequestMapping(value = "/audit/logs/customer/{customerId}", params = {"limit"}, method = RequestMethod.GET) @ResponseBody - public TimePageData getAuditLogs( + public TimePageData getAuditLogsByCustomerId( + @PathVariable("customerId") String strCustomerId, + @RequestParam int limit, + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime, + @RequestParam(required = false, defaultValue = "false") boolean ascOrder, + @RequestParam(required = false) String offset) throws ThingsboardException { + try { + checkParameter("CustomerId", strCustomerId); + TenantId tenantId = getCurrentUser().getTenantId(); + TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); + return checkNotNull(auditLogService.findAuditLogsByTenantIdAndCustomerId(tenantId, new CustomerId(UUID.fromString(strCustomerId)), pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/audit/logs/user/{userId}", params = {"limit"}, method = RequestMethod.GET) + @ResponseBody + public TimePageData getAuditLogsByUserId( + @PathVariable("userId") String strUserId, + @RequestParam int limit, + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime, + @RequestParam(required = false, defaultValue = "false") boolean ascOrder, + @RequestParam(required = false) String offset) throws ThingsboardException { + try { + checkParameter("UserId", strUserId); + TenantId tenantId = getCurrentUser().getTenantId(); + TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); + return checkNotNull(auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, new UserId(UUID.fromString(strUserId)), pageLink)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/audit/logs/entity/{entityType}/{entityId}", params = {"limit"}, method = RequestMethod.GET) + @ResponseBody + public TimePageData getAuditLogsByEntityId( @PathVariable("entityType") String strEntityType, @PathVariable("entityId") String strEntityId, @RequestParam int limit, diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 8d08706993..0844ce0bf4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -85,12 +85,16 @@ public class DeviceController extends BaseController { savedDevice.getName(), savedDevice.getType()); - // TODO: refactor to ANNOTATION usage - if (device.getId() == null) { - logDeviceAction(savedDevice, ActionType.ADDED); - } else { - logDeviceAction(savedDevice, ActionType.UPDATED); - } + auditLogService.logEntityAction( + getCurrentUser(), + savedDevice.getId(), + savedDevice.getName(), + savedDevice.getCustomerId(), + device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, + null, + ActionStatus.SUCCESS, + null); + return savedDevice; } catch (Exception e) { @@ -107,8 +111,15 @@ public class DeviceController extends BaseController { DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); Device device = checkDeviceId(deviceId); deviceService.deleteDevice(deviceId); - // TODO: refactor to ANNOTATION usage - logDeviceAction(device, ActionType.DELETED); + auditLogService.logEntityAction( + getCurrentUser(), + device.getId(), + device.getName(), + device.getCustomerId(), + ActionType.DELETED, + null, + ActionStatus.SUCCESS, + null); } catch (Exception e) { throw handleException(e); } @@ -189,8 +200,15 @@ public class DeviceController extends BaseController { Device device = checkDeviceId(deviceCredentials.getDeviceId()); DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials)); actorService.onCredentialsUpdate(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId()); - // TODO: refactor to ANNOTATION usage - logDeviceAction(device, ActionType.CREDENTIALS_UPDATED); + auditLogService.logEntityAction( + getCurrentUser(), + device.getId(), + device.getName(), + device.getCustomerId(), + ActionType.CREDENTIALS_UPDATED, + null, + ActionStatus.SUCCESS, + null); return result; } catch (Exception e) { throw handleException(e); @@ -321,19 +339,4 @@ public class DeviceController extends BaseController { throw handleException(e); } } - - // TODO: refactor to ANNOTATION usage - private void logDeviceAction(Device device, ActionType actionType) throws ThingsboardException { - auditLogService.logAction( - getCurrentUser().getTenantId(), - device.getId(), - device.getName(), - device.getCustomerId(), - getCurrentUser().getId(), - getCurrentUser().getName(), - actionType, - null, - ActionStatus.SUCCESS, - null); - } } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 83630ce56a..3d8501fd0a 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -244,7 +244,6 @@ spring: username: "${SPRING_DATASOURCE_USERNAME:sa}" password: "${SPRING_DATASOURCE_PASSWORD:}" - # PostgreSQL DAO Configuration #spring: # data: @@ -260,3 +259,8 @@ spring: # url: "${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/thingsboard}" # username: "${SPRING_DATASOURCE_USERNAME:postgres}" # password: "${SPRING_DATASOURCE_PASSWORD:postgres}" + +# Audit log parameters +audit_log: + # Enable/disable audit log functionality. + enabled: "${AUDIT_LOG_ENABLED:true}" \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java index 5e74416868..a826ee85d1 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.page.TimePageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.model.ModelConstants; import java.util.ArrayList; import java.util.List; @@ -36,6 +37,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. public abstract class BaseAuditLogControllerTest extends AbstractControllerTest { private Tenant savedTenant; + private User tenantAdmin; @Before public void beforeTest() throws Exception { @@ -46,14 +48,14 @@ public abstract class BaseAuditLogControllerTest extends AbstractControllerTest savedTenant = doPost("/api/tenant", tenant, Tenant.class); Assert.assertNotNull(savedTenant); - User tenantAdmin = new User(); + tenantAdmin = new User(); tenantAdmin.setAuthority(Authority.TENANT_ADMIN); tenantAdmin.setTenantId(savedTenant.getId()); tenantAdmin.setEmail("tenant2@thingsboard.org"); tenantAdmin.setFirstName("Joe"); tenantAdmin.setLastName("Downs"); - createUserAndLogin(tenantAdmin, "testPassword1"); + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); } @After @@ -65,7 +67,7 @@ public abstract class BaseAuditLogControllerTest extends AbstractControllerTest } @Test - public void testSaveDeviceAuditLogs() throws Exception { + public void testAuditLogs() throws Exception { for (int i = 0; i < 178; i++) { Device device = new Device(); device.setName("Device" + i); @@ -87,10 +89,38 @@ public abstract class BaseAuditLogControllerTest extends AbstractControllerTest } while (pageData.hasNext()); Assert.assertEquals(178, loadedAuditLogs.size()); + + loadedAuditLogs = new ArrayList<>(); + pageLink = new TimePageLink(23); + do { + pageData = doGetTypedWithTimePageLink("/api/audit/logs/customer/" + ModelConstants.NULL_UUID + "?", + new TypeReference>() { + }, pageLink); + loadedAuditLogs.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageData.getNextPageLink(); + } + } while (pageData.hasNext()); + + Assert.assertEquals(178, loadedAuditLogs.size()); + + loadedAuditLogs = new ArrayList<>(); + pageLink = new TimePageLink(23); + do { + pageData = doGetTypedWithTimePageLink("/api/audit/logs/user/" + tenantAdmin.getId().getId().toString() + "?", + new TypeReference>() { + }, pageLink); + loadedAuditLogs.addAll(pageData.getData()); + if (pageData.hasNext()) { + pageLink = pageData.getNextPageLink(); + } + } while (pageData.hasNext()); + + Assert.assertEquals(178, loadedAuditLogs.size()); } @Test - public void testUpdateDeviceAuditLogs() throws Exception { + public void testAuditLogs_byTenantIdAndEntityId() throws Exception { Device device = new Device(); device.setName("Device name"); device.setType("default"); @@ -104,7 +134,7 @@ public abstract class BaseAuditLogControllerTest extends AbstractControllerTest TimePageLink pageLink = new TimePageLink(23); TimePageData pageData; do { - pageData = doGetTypedWithTimePageLink("/api/audit/logs/DEVICE/" + savedDevice.getId().getId() + "?", + pageData = doGetTypedWithTimePageLink("/api/audit/logs/entity/DEVICE/" + savedDevice.getId().getId() + "?", new TypeReference>() { }, pageLink); loadedAuditLogs.addAll(pageData.getData()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java index f401ad1ac6..8562b6a5ab 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogDao.java @@ -17,7 +17,9 @@ package org.thingsboard.server.dao.audit; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.TimePageLink; import java.util.List; @@ -29,9 +31,17 @@ public interface AuditLogDao { ListenableFuture saveByTenantIdAndEntityId(AuditLog auditLog); + ListenableFuture saveByTenantIdAndCustomerId(AuditLog auditLog); + + ListenableFuture saveByTenantIdAndUserId(AuditLog auditLog); + ListenableFuture savePartitionsByTenantId(AuditLog auditLog); List findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink); + List findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink); + + List findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink); + List findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogQueryCursor.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogQueryCursor.java new file mode 100644 index 0000000000..78b043b727 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogQueryCursor.java @@ -0,0 +1,70 @@ +/** + * Copyright © 2016-2017 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.audit; + +import lombok.Getter; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.dao.model.nosql.AuditLogEntity; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class AuditLogQueryCursor { + @Getter + private final UUID tenantId; + @Getter + private final List data; + @Getter + private final TimePageLink pageLink; + + private final List partitions; + + private int partitionIndex; + private int currentLimit; + + public AuditLogQueryCursor(UUID tenantId, TimePageLink pageLink, List partitions) { + this.tenantId = tenantId; + this.partitions = partitions; + this.partitionIndex = partitions.size() - 1; + this.data = new ArrayList<>(); + this.currentLimit = pageLink.getLimit(); + this.pageLink = pageLink; + } + + public boolean hasNextPartition() { + return partitionIndex >= 0; + } + + public boolean isFull() { + return currentLimit <= 0; + } + + public long getNextPartition() { + long partition = partitions.get(partitionIndex); + partitionIndex--; + return partition; + } + + public int getCurrentLimit() { + return currentLimit; + } + + public void addData(List newData) { + currentLimit -= newData.size(); + data.addAll(newData); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java index 5593edee9d..d6e61abcc2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.audit; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionStatus; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; @@ -31,20 +32,21 @@ import java.util.List; public interface AuditLogService { + TimePageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink); + + TimePageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink); + TimePageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink); TimePageData findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink); - ListenableFuture> logAction(TenantId tenantId, - EntityId entityId, - String entityName, - CustomerId customerId, - UserId userId, - String userName, - ActionType actionType, - JsonNode actionData, - ActionStatus actionStatus, - String actionFailureDetails); - + ListenableFuture> logEntityAction(User user, + EntityId entityId, + String entityName, + CustomerId customerId, + ActionType actionType, + JsonNode actionData, + ActionStatus actionStatus, + String actionFailureDetails); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index 1baf573155..1ee26e5dd8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -15,20 +15,20 @@ */ package org.thingsboard.server.dao.audit; +import com.datastax.driver.core.utils.UUIDs; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionStatus; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; -import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.id.*; import org.thingsboard.server.common.data.page.TimePageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.exception.DataValidationException; @@ -41,6 +41,7 @@ import static org.thingsboard.server.dao.service.Validator.validateId; @Slf4j @Service +@ConditionalOnProperty(prefix = "audit_log", value = "enabled", havingValue = "true") public class AuditLogServiceImpl implements AuditLogService { private static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; @@ -49,6 +50,24 @@ public class AuditLogServiceImpl implements AuditLogService { @Autowired private AuditLogDao auditLogDao; + @Override + public TimePageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) { + log.trace("Executing findAuditLogsByTenantIdAndCustomerId [{}], [{}], [{}]", tenantId, customerId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(customerId, "Incorrect customerId " + customerId); + List auditLogs = auditLogDao.findAuditLogsByTenantIdAndCustomerId(tenantId.getId(), customerId, pageLink); + return new TimePageData<>(auditLogs, pageLink); + } + + @Override + public TimePageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink) { + log.trace("Executing findAuditLogsByTenantIdAndUserId [{}], [{}], [{}]", tenantId, userId, pageLink); + validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + validateId(userId, "Incorrect userId" + userId); + List auditLogs = auditLogDao.findAuditLogsByTenantIdAndUserId(tenantId.getId(), userId, pageLink); + return new TimePageData<>(auditLogs, pageLink); + } + @Override public TimePageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink) { log.trace("Executing findAuditLogsByTenantIdAndEntityId [{}], [{}], [{}]", tenantId, entityId, pageLink); @@ -66,6 +85,28 @@ public class AuditLogServiceImpl implements AuditLogService { return new TimePageData<>(auditLogs, pageLink); } + @Override + public ListenableFuture> logEntityAction(User user, + EntityId entityId, + String entityName, + CustomerId customerId, + ActionType actionType, + JsonNode actionData, + ActionStatus actionStatus, + String actionFailureDetails) { + return logAction( + user.getTenantId(), + entityId, + entityName, + customerId, + user.getId(), + user.getName(), + actionType, + actionData, + actionStatus, + actionFailureDetails); + } + private AuditLog createAuditLogEntry(TenantId tenantId, EntityId entityId, String entityName, @@ -77,6 +118,7 @@ public class AuditLogServiceImpl implements AuditLogService { ActionStatus actionStatus, String actionFailureDetails) { AuditLog result = new AuditLog(); + result.setId(new AuditLogId(UUIDs.timeBased())); result.setTenantId(tenantId); result.setEntityId(entityId); result.setEntityName(entityName); @@ -90,17 +132,16 @@ public class AuditLogServiceImpl implements AuditLogService { return result; } - @Override - public ListenableFuture> logAction(TenantId tenantId, - EntityId entityId, - String entityName, - CustomerId customerId, - UserId userId, - String userName, - ActionType actionType, - JsonNode actionData, - ActionStatus actionStatus, - String actionFailureDetails) { + private ListenableFuture> logAction(TenantId tenantId, + EntityId entityId, + String entityName, + CustomerId customerId, + UserId userId, + String userName, + ActionType actionType, + JsonNode actionData, + ActionStatus actionStatus, + String actionFailureDetails) { AuditLog auditLogEntry = createAuditLogEntry(tenantId, entityId, entityName, customerId, userId, userName, actionType, actionData, actionStatus, actionFailureDetails); log.trace("Executing logAction [{}]", auditLogEntry); @@ -109,6 +150,8 @@ public class AuditLogServiceImpl implements AuditLogService { futures.add(auditLogDao.savePartitionsByTenantId(auditLogEntry)); futures.add(auditLogDao.saveByTenantId(auditLogEntry)); futures.add(auditLogDao.saveByTenantIdAndEntityId(auditLogEntry)); + futures.add(auditLogDao.saveByTenantIdAndCustomerId(auditLogEntry)); + futures.add(auditLogDao.saveByTenantIdAndUserId(auditLogEntry)); return Futures.allAsList(futures); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java index 2fcb73281d..8262933711 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java @@ -19,7 +19,8 @@ import com.datastax.driver.core.BoundStatement; import com.datastax.driver.core.PreparedStatement; import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.ResultSetFuture; -import com.datastax.driver.core.utils.UUIDs; +import com.datastax.driver.core.querybuilder.QueryBuilder; +import com.datastax.driver.core.querybuilder.Select; import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -29,8 +30,9 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.audit.AuditLog; -import org.thingsboard.server.common.data.id.AuditLogId; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.ModelConstants; @@ -52,6 +54,7 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.stream.Collectors; import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; import static org.thingsboard.server.dao.model.ModelConstants.*; @@ -82,7 +85,11 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao saveByTenantId(AuditLog auditLog) { log.debug("Save saveByTenantId [{}] ", auditLog); - AuditLogId auditLogId = new AuditLogId(UUIDs.timeBased()); - long partition = toPartitionTs(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli()); BoundStatement stmt = getSaveByTenantStmt().bind(); - stmt.setUUID(0, auditLogId.getId()) + stmt = stmt.setUUID(0, auditLog.getId().getId()) .setUUID(1, auditLog.getTenantId().getId()) .setUUID(2, auditLog.getEntityId().getId()) .setString(3, auditLog.getEntityId().getEntityType().name()) @@ -140,17 +145,44 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao saveByTenantIdAndEntityId(AuditLog auditLog) { log.debug("Save saveByTenantIdAndEntityId [{}] ", auditLog); - AuditLogId auditLogId = new AuditLogId(UUIDs.timeBased()); - BoundStatement stmt = getSaveByTenantIdAndEntityIdStmt().bind(); - stmt.setUUID(0, auditLogId.getId()) - .setUUID(1, auditLog.getTenantId().getId()) - .setUUID(2, auditLog.getEntityId().getId()) - .setString(3, auditLog.getEntityId().getEntityType().name()) - .setString(4, auditLog.getActionType().name()); + stmt = setSaveStmtVariables(stmt, auditLog); return getFuture(executeAsyncWrite(stmt), rs -> null); } + @Override + public ListenableFuture saveByTenantIdAndCustomerId(AuditLog auditLog) { + log.debug("Save saveByTenantIdAndCustomerId [{}] ", auditLog); + + BoundStatement stmt = getSaveByTenantIdAndCustomerIdStmt().bind(); + stmt = setSaveStmtVariables(stmt, auditLog); + return getFuture(executeAsyncWrite(stmt), rs -> null); + } + + @Override + public ListenableFuture saveByTenantIdAndUserId(AuditLog auditLog) { + log.debug("Save saveByTenantIdAndUserId [{}] ", auditLog); + + BoundStatement stmt = getSaveByTenantIdAndUserIdStmt().bind(); + stmt = setSaveStmtVariables(stmt, auditLog); + return getFuture(executeAsyncWrite(stmt), rs -> null); + } + + private BoundStatement setSaveStmtVariables(BoundStatement stmt, AuditLog auditLog) { + return stmt.setUUID(0, auditLog.getId().getId()) + .setUUID(1, auditLog.getTenantId().getId()) + .setUUID(2, auditLog.getCustomerId().getId()) + .setUUID(3, auditLog.getEntityId().getId()) + .setString(4, auditLog.getEntityId().getEntityType().name()) + .setString(5, auditLog.getEntityName()) + .setUUID(6, auditLog.getUserId().getId()) + .setString(7, auditLog.getUserName()) + .setString(8, auditLog.getActionType().name()) + .setString(9, auditLog.getActionData() != null ? auditLog.getActionData().toString() : null) + .setString(10, auditLog.getActionStatus().name()) + .setString(11, auditLog.getActionFailureDetails()); + } + @Override public ListenableFuture savePartitionsByTenantId(AuditLog auditLog) { log.debug("Save savePartitionsByTenantId [{}] ", auditLog); @@ -163,35 +195,66 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao null); } - private PreparedStatement getPartitionInsertStmt() { - // TODO: ADD CACHE LOGIC - return getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF + - "(" + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY + - "," + ModelConstants.AUDIT_LOG_PARTITION_PROPERTY + ")" + - " VALUES(?, ?)"); + private PreparedStatement getSaveByTenantIdAndEntityIdStmt() { + if (saveByTenantIdAndEntityIdStmt == null) { + saveByTenantIdAndEntityIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_ENTITY_ID_CF); + } + return saveByTenantIdAndEntityIdStmt; } - private PreparedStatement getSaveByTenantIdAndEntityIdStmt() { - // TODO: ADD CACHE LOGIC - return getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_ENTITY_ID_CF + - "(" + ModelConstants.AUDIT_LOG_ID_PROPERTY + - "," + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY + - "," + ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY + - "," + ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY + - "," + ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY + ")" + - " VALUES(?, ?, ?, ?, ?)"); + private PreparedStatement getSaveByTenantIdAndCustomerIdStmt() { + if (saveByTenantIdAndCustomerIdStmt == null) { + saveByTenantIdAndCustomerIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_CUSTOMER_ID_CF); + } + return saveByTenantIdAndCustomerIdStmt; } - private PreparedStatement getSaveByTenantStmt() { - // TODO: ADD CACHE LOGIC - return getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_CF + + private PreparedStatement getSaveByTenantIdAndUserIdStmt() { + if (saveByTenantIdAndUserIdStmt == null) { + saveByTenantIdAndUserIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_USER_ID_CF); + } + return saveByTenantIdAndUserIdStmt; + } + + private PreparedStatement getSaveByTenantIdAndCFName(String cfName) { + return getSession().prepare(INSERT_INTO + cfName + "(" + ModelConstants.AUDIT_LOG_ID_PROPERTY + "," + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY + + "," + ModelConstants.AUDIT_LOG_CUSTOMER_ID_PROPERTY + "," + ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY + "," + ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY + + "," + ModelConstants.AUDIT_LOG_ENTITY_NAME_PROPERTY + + "," + ModelConstants.AUDIT_LOG_USER_ID_PROPERTY + + "," + ModelConstants.AUDIT_LOG_USER_NAME_PROPERTY + "," + ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY + - "," + ModelConstants.AUDIT_LOG_PARTITION_PROPERTY + ")" + - " VALUES(?, ?, ?, ?, ?, ?)"); + "," + ModelConstants.AUDIT_LOG_ACTION_DATA_PROPERTY + + "," + ModelConstants.AUDIT_LOG_ACTION_STATUS_PROPERTY + + "," + ModelConstants.AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY + ")" + + " VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + } + + private PreparedStatement getPartitionInsertStmt() { + if (partitionInsertStmt == null) { + partitionInsertStmt = getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF + + "(" + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY + + "," + ModelConstants.AUDIT_LOG_PARTITION_PROPERTY + ")" + + " VALUES(?, ?)"); + } + return partitionInsertStmt; + } + + private PreparedStatement getSaveByTenantStmt() { + if (saveByTenantStmt == null) { + saveByTenantStmt = getSession().prepare(INSERT_INTO + ModelConstants.AUDIT_LOG_BY_TENANT_ID_CF + + "(" + ModelConstants.AUDIT_LOG_ID_PROPERTY + + "," + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY + + "," + ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY + + "," + ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY + + "," + ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY + + "," + ModelConstants.AUDIT_LOG_PARTITION_PROPERTY + ")" + + " VALUES(?, ?, ?, ?, ?, ?)"); + } + return saveByTenantStmt; } private long toPartitionTs(long ts) { @@ -199,7 +262,6 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao findAuditLogsByTenantIdAndEntityId(UUID tenantId, EntityId entityId, TimePageLink pageLink) { log.trace("Try to find audit logs by tenant [{}], entity [{}] and pageLink [{}]", tenantId, entityId, pageLink); @@ -212,31 +274,76 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink) { + log.trace("Try to find audit logs by tenant [{}], customer [{}] and pageLink [{}]", tenantId, customerId, pageLink); + List entities = findPageWithTimeSearch(AUDIT_LOG_BY_CUSTOMER_ID_CF, + Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId), + eq(ModelConstants.AUDIT_LOG_CUSTOMER_ID_PROPERTY, customerId.getId())), + pageLink); + log.trace("Found audit logs by tenant [{}], customer [{}] and pageLink [{}]", tenantId, customerId, pageLink); + return DaoUtil.convertDataList(entities); + } + + @Override + public List findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink) { + log.trace("Try to find audit logs by tenant [{}], user [{}] and pageLink [{}]", tenantId, userId, pageLink); + List entities = findPageWithTimeSearch(AUDIT_LOG_BY_USER_ID_CF, + Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId), + eq(ModelConstants.AUDIT_LOG_USER_ID_PROPERTY, userId.getId())), + pageLink); + log.trace("Found audit logs by tenant [{}], user [{}] and pageLink [{}]", tenantId, userId, pageLink); + return DaoUtil.convertDataList(entities); + } + @Override public List findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) { log.trace("Try to find audit logs by tenant [{}] and pageLink [{}]", tenantId, pageLink); - // TODO: ADD AUDIT LOG PARTITION CURSOR LOGIC - long minPartition; - long maxPartition; - if (pageLink.getStartTime() != null && pageLink.getStartTime() != 0) { minPartition = toPartitionTs(pageLink.getStartTime()); } else { minPartition = toPartitionTs(LocalDate.now().minusMonths(1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli()); } + long maxPartition; if (pageLink.getEndTime() != null && pageLink.getEndTime() != 0) { maxPartition = toPartitionTs(pageLink.getEndTime()); } else { maxPartition = toPartitionTs(LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli()); } - List entities = findPageWithTimeSearch(AUDIT_LOG_BY_TENANT_ID_CF, - Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId), - eq(ModelConstants.AUDIT_LOG_PARTITION_PROPERTY, maxPartition)), - pageLink); + + List partitions = fetchPartitions(tenantId, minPartition, maxPartition) + .all() + .stream() + .map(row -> row.getLong(ModelConstants.PARTITION_COLUMN)) + .collect(Collectors.toList()); + + AuditLogQueryCursor cursor = new AuditLogQueryCursor(tenantId, pageLink, partitions); + List entities = fetchSequentiallyWithLimit(cursor); log.trace("Found audit logs by tenant [{}] and pageLink [{}]", tenantId, pageLink); return DaoUtil.convertDataList(entities); } + + private List fetchSequentiallyWithLimit(AuditLogQueryCursor cursor) { + if (cursor.isFull() || !cursor.hasNextPartition()) { + return cursor.getData(); + } else { + cursor.addData(findPageWithTimeSearch(AUDIT_LOG_BY_TENANT_ID_CF, + Arrays.asList(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, cursor.getTenantId()), + eq(ModelConstants.AUDIT_LOG_PARTITION_PROPERTY, cursor.getNextPartition())), + cursor.getPageLink())); + return fetchSequentiallyWithLimit(cursor); + } + } + + private ResultSet fetchPartitions(UUID tenantId, long minPartition, long maxPartition) { + Select.Where select = QueryBuilder.select(ModelConstants.AUDIT_LOG_PARTITION_PROPERTY).from(ModelConstants.AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF) + .where(eq(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY, tenantId)); + select.and(QueryBuilder.gte(ModelConstants.PARTITION_COLUMN, minPartition)); + select.and(QueryBuilder.lte(ModelConstants.PARTITION_COLUMN, maxPartition)); + return getSession().execute(select); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java new file mode 100644 index 0000000000..c6107694e1 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2017 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.audit; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.ListenableFuture; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.audit.ActionStatus; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.TimePageLink; + +import java.util.List; + +@ConditionalOnProperty(prefix = "audit_log", value = "enabled", havingValue = "false") +public class DummyAuditLogServiceImpl implements AuditLogService { + + @Override + public TimePageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) { + return null; + } + + @Override + public TimePageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink) { + return null; + } + + @Override + public TimePageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink) { + return null; + } + + @Override + public TimePageData findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink) { + return null; + } + + @Override + public ListenableFuture> logEntityAction(User user, EntityId entityId, String entityName, CustomerId customerId, ActionType actionType, JsonNode actionData, ActionStatus actionStatus, String actionFailureDetails) { + return null; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 58c8d2f1f7..66e03b0629 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -147,6 +147,8 @@ public class ModelConstants { public static final String AUDIT_LOG_COLUMN_FAMILY_NAME = "audit_log"; public static final String AUDIT_LOG_BY_ENTITY_ID_CF = "audit_log_by_entity_id"; + public static final String AUDIT_LOG_BY_CUSTOMER_ID_CF = "audit_log_by_customer_id"; + public static final String AUDIT_LOG_BY_USER_ID_CF = "audit_log_by_user_id"; public static final String AUDIT_LOG_BY_TENANT_ID_CF = "audit_log_by_tenant_id"; public static final String AUDIT_LOG_BY_TENANT_ID_PARTITIONS_CF = "audit_log_by_tenant_id_partitions"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java index 7e4fdc1ce2..bac44c5121 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/AuditLogRepository.java @@ -41,4 +41,20 @@ public interface AuditLogRepository extends CrudRepository :idOffset ORDER BY al.id") + List findByTenantIdAndCustomerId(@Param("tenantId") String tenantId, + @Param("customerId") String customerId, + @Param("idOffset") String idOffset, + Pageable pageable); + + @Query("SELECT al FROM AuditLogEntity al WHERE al.tenantId = :tenantId " + + "AND al.userId = :userId " + + "AND al.id > :idOffset ORDER BY al.id") + List findByTenantIdAndUserId(@Param("tenantId") String tenantId, + @Param("userId") String userId, + @Param("idOffset") String idOffset, + Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java index fe7edf8d95..b4abf07416 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/audit/JpaAuditLogDao.java @@ -23,7 +23,9 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.audit.AuditLogDao; @@ -76,6 +78,16 @@ public class JpaAuditLogDao extends JpaAbstractDao imp return insertService.submit(() -> null); } + @Override + public ListenableFuture saveByTenantIdAndCustomerId(AuditLog auditLog) { + return insertService.submit(() -> null); + } + + @Override + public ListenableFuture saveByTenantIdAndUserId(AuditLog auditLog) { + return insertService.submit(() -> null); + } + @Override public ListenableFuture savePartitionsByTenantId(AuditLog auditLog) { return insertService.submit(() -> null); @@ -92,6 +104,26 @@ public class JpaAuditLogDao extends JpaAbstractDao imp new PageRequest(0, pageLink.getLimit()))); } + @Override + public List findAuditLogsByTenantIdAndCustomerId(UUID tenantId, CustomerId customerId, TimePageLink pageLink) { + return DaoUtil.convertDataList( + auditLogRepository.findByTenantIdAndCustomerId( + fromTimeUUID(tenantId), + fromTimeUUID(customerId.getId()), + pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), + new PageRequest(0, pageLink.getLimit()))); + } + + @Override + public List findAuditLogsByTenantIdAndUserId(UUID tenantId, UserId userId, TimePageLink pageLink) { + return DaoUtil.convertDataList( + auditLogRepository.findByTenantIdAndUserId( + fromTimeUUID(tenantId), + fromTimeUUID(userId.getId()), + pageLink.getIdOffset() == null ? NULL_UUID_STR : fromTimeUUID(pageLink.getIdOffset()), + new PageRequest(0, pageLink.getLimit()))); + } + @Override public List findAuditLogsByTenantId(UUID tenantId, TimePageLink pageLink) { return DaoUtil.convertDataList( diff --git a/dao/src/main/resources/cassandra/schema.cql b/dao/src/main/resources/cassandra/schema.cql index 6dc2becca9..bc7884dfbe 100644 --- a/dao/src/main/resources/cassandra/schema.cql +++ b/dao/src/main/resources/cassandra/schema.cql @@ -566,6 +566,40 @@ CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_entity_id ( PRIMARY KEY ((tenant_id, entity_id, entity_type), id) ); +CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_customer_id ( + tenant_id timeuuid, + id timeuuid, + customer_id timeuuid, + entity_id timeuuid, + entity_type text, + entity_name text, + user_id timeuuid, + user_name text, + action_type text, + action_data text, + action_status text, + action_failure_details text, + PRIMARY KEY ((tenant_id, customer_id), id) +); + +CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_user_id ( + tenant_id timeuuid, + id timeuuid, + customer_id timeuuid, + entity_id timeuuid, + entity_type text, + entity_name text, + user_id timeuuid, + user_name text, + action_type text, + action_data text, + action_status text, + action_failure_details text, + PRIMARY KEY ((tenant_id, user_id), id) +); + + + CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_tenant_id ( tenant_id timeuuid, id timeuuid, diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index d87a181d0a..cbca287d66 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -7,4 +7,6 @@ zk.enabled=false zk.url=localhost:2181 zk.zk_dir=/thingsboard -updates.enabled=false \ No newline at end of file +updates.enabled=false + +audit_log.enabled=true \ No newline at end of file From 12951115f922312c2c49e8399657a9748345ae6b Mon Sep 17 00:00:00 2001 From: vparomskiy Date: Fri, 16 Feb 2018 14:22:55 +0200 Subject: [PATCH 23/57] Add white & black list. Fix after review --- .../src/main/resources/thingsboard.yml | 9 ++++++ .../inmemory/HostRequestIntervalRegistry.java | 25 ++++++++++++---- .../inmemory/IntervalRegistryLogger.java | 2 +- .../quota/HostRequestsQuotaServiceTest.java | 16 ++++++++-- .../HostRequestIntervalRegistryTest.java | 30 ++++++++++++++++++- .../transport/coap/CoapTransportResource.java | 1 - 6 files changed, 72 insertions(+), 11 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index da35c6c993..a2ae7e2a43 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -110,11 +110,20 @@ coap: #Quota parameters quota: host: + # Max allowed number of API requests in interval for single host limit: "${QUOTA_HOST_LIMIT:10000}" + # Interval duration intervalMs: "${QUOTA_HOST_INTERVAL_MS:60000}" + # Maximum silence duration for host after which Host removed from QuotaService. Must be bigger than intervalMs ttlMs: "${QUOTA_HOST_TTL_MS:60000}" + # Interval for scheduled task that cleans expired records. TTL is used for expiring cleanPeriodMs: "${QUOTA_HOST_CLEAN_PERIOD_MS:300000}" + # Enable Host API Limits enabled: "${QUOTA_HOST_ENABLED:false}" + # Array of whitelist hosts + whitelist: "${QUOTA_HOST_WHITELIST:localhost,127.0.0.1}" + # Array of blacklist hosts + blacklist: "${QUOTA_HOST_BLACKLIST:}" log: topSize: 10 intervalMin: 2 diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java index 9f8f9465c2..4db6ff01b9 100644 --- a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java +++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java @@ -1,12 +1,12 @@ /** * Copyright © 2016-2017 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 - * + *

+ * 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. @@ -15,12 +15,15 @@ */ package org.thingsboard.server.common.transport.quota.inmemory; +import com.google.common.collect.Sets; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @@ -35,11 +38,17 @@ public class HostRequestIntervalRegistry { private final Map hostCounts = new ConcurrentHashMap<>(); private final long intervalDurationMs; private final long ttlMs; + private final Set whiteList; + private final Set blackList; public HostRequestIntervalRegistry(@Value("${quota.host.intervalMs}") long intervalDurationMs, - @Value("${quota.host.ttlMs}") long ttlMs) { + @Value("${quota.host.ttlMs}") long ttlMs, + @Value("${quota.host.whitelist}") String whiteList, + @Value("${quota.host.blacklist}") String blackList) { this.intervalDurationMs = intervalDurationMs; this.ttlMs = ttlMs; + this.whiteList = Sets.newHashSet(StringUtils.split(whiteList, ',')); + this.blackList = Sets.newHashSet(StringUtils.split(blackList, ',')); } @PostConstruct @@ -47,11 +56,15 @@ public class HostRequestIntervalRegistry { if (ttlMs < intervalDurationMs) { log.warn("TTL for IntervalRegistry [{}] smaller than interval duration [{}]", ttlMs, intervalDurationMs); } + log.info("Start Host Quota Service with whitelist {}", whiteList); + log.info("Start Host Quota Service with blacklist {}", blackList); } public long tick(String clientHostId) { - if ("localhost".equals(clientHostId) || "127.0.0.1".equals(clientHostId)) { + if (whiteList.contains(clientHostId)) { return 0; + } else if (blackList.contains(clientHostId)) { + return Long.MAX_VALUE; } IntervalCount intervalCount = hostCounts.computeIfAbsent(clientHostId, s -> new IntervalCount(intervalDurationMs)); return intervalCount.resetIfExpiredAndTick(); diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java index 91797d4422..3f660fccb3 100644 --- a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java +++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java @@ -80,7 +80,7 @@ public class IntervalRegistryLogger { private void log(Map top) { StringBuilder builder = new StringBuilder("Quota Statistic : "); for (Map.Entry host : top.entrySet()) { - builder.append(host.getKey()).append(" : ").append(host.getValue()); + builder.append(host.getKey()).append(" : ").append(host.getValue()).append(" ; "); } log.info(builder.toString()); diff --git a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaServiceTest.java b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaServiceTest.java index 475fcccead..b8c9284fda 100644 --- a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaServiceTest.java +++ b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/HostRequestsQuotaServiceTest.java @@ -44,9 +44,9 @@ public class HostRequestsQuotaServiceTest { } @Test - public void hostQuotaValidated() { + public void quotaExceededIfRequestCountBiggerThanAllowed() { when(requestRegistry.tick("key")).thenReturn(10L); - when(requestsPolicy.isValid(10L)).thenReturn(true); + when(requestsPolicy.isValid(10L)).thenReturn(false); assertTrue(quotaService.isQuotaExceeded("key")); @@ -55,6 +55,18 @@ public class HostRequestsQuotaServiceTest { verifyNoMoreInteractions(requestRegistry, requestsPolicy); } + @Test + public void quotaNotExceededIfRequestCountLessThanAllowed() { + when(requestRegistry.tick("key")).thenReturn(10L); + when(requestsPolicy.isValid(10L)).thenReturn(true); + + assertFalse(quotaService.isQuotaExceeded("key")); + + verify(requestRegistry).tick("key"); + verify(requestsPolicy).isValid(10L); + verifyNoMoreInteractions(requestRegistry, requestsPolicy); + } + @Test public void serviceCanBeDisabled() { quotaService = new HostRequestsQuotaService(requestRegistry, requestsPolicy, registryCleaner, registryLogger, false); diff --git a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistryTest.java b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistryTest.java index cb21ccebd5..ff1e525f91 100644 --- a/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistryTest.java +++ b/common/transport/src/test/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistryTest.java @@ -15,9 +15,12 @@ */ package org.thingsboard.server.common.transport.quota.inmemory; +import com.google.common.collect.Sets; import org.junit.Before; import org.junit.Test; +import java.util.Collections; + import static org.junit.Assert.assertEquals; /** @@ -30,7 +33,7 @@ public class HostRequestIntervalRegistryTest { @Before public void init() { - registry = new HostRequestIntervalRegistry(10000L, 100L); + registry = new HostRequestIntervalRegistry(10000L, 100L,"g1,g2", "b1"); } @Test @@ -54,4 +57,29 @@ public class HostRequestIntervalRegistryTest { assertEquals(1L, registry.tick("aaa")); assertEquals(2L, registry.tick("bbb")); } + + @Test + public void domainFromWhitelistNotCounted(){ + assertEquals(0L, registry.tick("g1")); + assertEquals(0L, registry.tick("g1")); + assertEquals(0L, registry.tick("g2")); + } + + @Test + public void domainFromBlackListReturnMaxValue(){ + assertEquals(Long.MAX_VALUE, registry.tick("b1")); + assertEquals(Long.MAX_VALUE, registry.tick("b1")); + } + + @Test + public void emptyWhitelistParsedOk(){ + registry = new HostRequestIntervalRegistry(10000L, 100L,"", "b1"); + assertEquals(1L, registry.tick("aaa")); + } + + @Test + public void emptyBlacklistParsedOk(){ + registry = new HostRequestIntervalRegistry(10000L, 100L,"", ""); + assertEquals(1L, registry.tick("aaa")); + } } \ No newline at end of file diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java index 2ffbf05f6e..958c4a79c8 100644 --- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java +++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java @@ -75,7 +75,6 @@ public class CoapTransportResource extends CoapResource { @Override public void handleGET(CoapExchange exchange) { if(quotaService.isQuotaExceeded(exchange.getSourceAddress().getHostAddress())) { - log.warn("Missing feature type parameter"); log.warn("COAP Quota exceeded for [{}:{}] . Disconnect", exchange.getSourceAddress().getHostAddress(), exchange.getSourcePort()); exchange.respond(ResponseCode.BAD_REQUEST); return; From 3b4b95633de03979db384c918c27eaeac103b1b7 Mon Sep 17 00:00:00 2001 From: vparomskiy Date: Fri, 16 Feb 2018 14:28:48 +0200 Subject: [PATCH 24/57] Fix license header --- .../quota/inmemory/HostRequestIntervalRegistry.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java index 4db6ff01b9..b35ce47515 100644 --- a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java +++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/HostRequestIntervalRegistry.java @@ -1,12 +1,12 @@ /** * Copyright © 2016-2017 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 - *

+ * + * 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. From f7c0f6e7b396477c7b4e53f5586591b2bdb3e5a8 Mon Sep 17 00:00:00 2001 From: vparomskiy Date: Fri, 16 Feb 2018 14:56:58 +0200 Subject: [PATCH 25/57] Add RPS logging for all requests --- .../quota/inmemory/IntervalRegistryLogger.java | 17 ++++++++++++----- .../transport/coap/CoapTransportService.java | 5 +---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java index 3f660fccb3..8afc7b7f10 100644 --- a/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java +++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/quota/inmemory/IntervalRegistryLogger.java @@ -20,7 +20,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import javax.annotation.PostConstruct; import java.util.Comparator; import java.util.Map; import java.util.concurrent.Executors; @@ -64,8 +63,11 @@ public class IntervalRegistryLogger { } public void logStatistic() { - Map top = getTopElements(intervalRegistry.getContent()); - log(top); + Map registryContent = intervalRegistry.getContent(); + int uniqHosts = registryContent.size(); + long requestsCount = registryContent.values().stream().mapToLong(i -> i).sum(); + Map top = getTopElements(registryContent); + log(top, uniqHosts, requestsCount); } protected Map getTopElements(Map countMap) { @@ -77,10 +79,15 @@ public class IntervalRegistryLogger { return topQueue.stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } - private void log(Map top) { + private void log(Map top, int uniqHosts, long requestsCount) { + long rps = requestsCount / TimeUnit.MINUTES.toSeconds(logIntervalMin); StringBuilder builder = new StringBuilder("Quota Statistic : "); + builder.append("uniqHosts : ").append(uniqHosts).append("; "); + builder.append("requestsCount : ").append(requestsCount).append("; "); + builder.append("RPS : ").append(rps).append(" "); + builder.append("top -> "); for (Map.Entry host : top.entrySet()) { - builder.append(host.getKey()).append(" : ").append(host.getValue()).append(" ; "); + builder.append(host.getKey()).append(" : ").append(host.getValue()).append("; "); } log.info(builder.toString()); diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java index 8946af5c00..df8725aa87 100644 --- a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java +++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java @@ -19,12 +19,9 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapResource; import org.eclipse.californium.core.CoapServer; import org.eclipse.californium.core.network.CoapEndpoint; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.thingsboard.server.common.transport.SessionMsgProcessor; -import org.thingsboard.server.common.transport.auth.DeviceAuthService; -import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; import org.thingsboard.server.common.transport.SessionMsgProcessor; From 92fdbbf89c6e411a6fa14dc4badcd63a35f4e116 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Fri, 16 Feb 2018 16:45:59 +0200 Subject: [PATCH 26/57] Code review fixes --- .../server/controller/BaseController.java | 64 +++++++++++++++++++ .../server/controller/DeviceController.java | 37 ++--------- .../src/main/resources/thingsboard.yml | 9 ++- .../dao/audit/CassandraAuditLogDao.java | 7 +- .../dao/audit/DummyAuditLogServiceImpl.java | 12 ++-- .../resources/application-test.properties | 3 + 6 files changed, 93 insertions(+), 39 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index ed5975a218..8ae04357a4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -15,9 +15,12 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -27,6 +30,8 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmId; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.audit.ActionStatus; +import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.*; import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TimePageLink; @@ -73,6 +78,10 @@ public abstract class BaseController { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!"; + + @Value("${audit_log.exceptions.enabled}") + private boolean auditLogExceptionsEnabled; + @Autowired private ThingsboardErrorResponseHandler errorResponseHandler; @@ -131,6 +140,11 @@ public abstract class BaseController { return handleException(exception, true); } + ThingsboardException handleException(Exception exception, ActionType actionType, String actionData) { + logExceptionToAuditLog(exception, actionType, actionData); + return handleException(exception, true); + } + private ThingsboardException handleException(Exception exception, boolean logException) { if (logException) { log.error("Error [{}]", exception.getMessage()); @@ -153,6 +167,36 @@ public abstract class BaseController { } } + private void logExceptionToAuditLog(Exception exception, ActionType actionType, String actionData) { + try { + if (auditLogExceptionsEnabled) { + SecurityUser currentUser = getCurrentUser(); + EntityId entityId; + CustomerId customerId; + if (!currentUser.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + entityId = currentUser.getCustomerId(); + customerId = currentUser.getCustomerId(); + } else { + entityId = currentUser.getTenantId(); + customerId = new CustomerId(ModelConstants.NULL_UUID); + } + + JsonNode actionDataNode = new ObjectMapper().createObjectNode().put("actionData", actionData); + + auditLogService.logEntityAction(currentUser, + entityId, + null, + customerId, + actionType, + actionDataNode, + ActionStatus.FAILURE, + exception.getMessage()); + } + } catch (Exception e) { + log.error("Exception happend during saving to audit log", e); + } + } + T checkNotNull(T reference) throws ThingsboardException { if (reference == null) { throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND); @@ -545,4 +589,24 @@ public abstract class BaseController { serverPort); return baseUrl; } + + protected void logEntityDeleted(EntityId entityId, String entityName, CustomerId customerId) throws ThingsboardException { + logEntitySuccess(entityId, entityName, customerId, ActionType.DELETED); + } + + protected void logEntityAddedOrUpdated(EntityId entityId, String entityName, CustomerId customerId, boolean isAddAction) throws ThingsboardException { + logEntitySuccess(entityId, entityName, customerId, isAddAction ? ActionType.ADDED : ActionType.UPDATED); + } + + protected void logEntitySuccess(EntityId entityId, String entityName, CustomerId customerId, ActionType actionType) throws ThingsboardException { + auditLogService.logEntityAction( + getCurrentUser(), + entityId, + entityName, + customerId, + actionType, + null, + ActionStatus.SUCCESS, + null); + } } diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java index 0844ce0bf4..65fcaeaa88 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java @@ -85,20 +85,11 @@ public class DeviceController extends BaseController { savedDevice.getName(), savedDevice.getType()); - auditLogService.logEntityAction( - getCurrentUser(), - savedDevice.getId(), - savedDevice.getName(), - savedDevice.getCustomerId(), - device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, - null, - ActionStatus.SUCCESS, - null); - + logEntityAddedOrUpdated(savedDevice.getId(), savedDevice.getName(), savedDevice.getCustomerId(), device.getId() == null); return savedDevice; } catch (Exception e) { - throw handleException(e); + throw handleException(e, device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, "addDevice(" + device + ")"); } } @@ -111,17 +102,9 @@ public class DeviceController extends BaseController { DeviceId deviceId = new DeviceId(toUUID(strDeviceId)); Device device = checkDeviceId(deviceId); deviceService.deleteDevice(deviceId); - auditLogService.logEntityAction( - getCurrentUser(), - device.getId(), - device.getName(), - device.getCustomerId(), - ActionType.DELETED, - null, - ActionStatus.SUCCESS, - null); + logEntityDeleted(device.getId(), device.getName(), device.getCustomerId()); } catch (Exception e) { - throw handleException(e); + throw handleException(e, ActionType.DELETED, "deleteDevice(" + strDeviceId + ")"); } } @@ -200,18 +183,10 @@ public class DeviceController extends BaseController { Device device = checkDeviceId(deviceCredentials.getDeviceId()); DeviceCredentials result = checkNotNull(deviceCredentialsService.updateDeviceCredentials(deviceCredentials)); actorService.onCredentialsUpdate(getCurrentUser().getTenantId(), deviceCredentials.getDeviceId()); - auditLogService.logEntityAction( - getCurrentUser(), - device.getId(), - device.getName(), - device.getCustomerId(), - ActionType.CREDENTIALS_UPDATED, - null, - ActionStatus.SUCCESS, - null); + logEntitySuccess(device.getId(), device.getName(), device.getCustomerId(), ActionType.CREDENTIALS_UPDATED); return result; } catch (Exception e) { - throw handleException(e); + throw handleException(e, ActionType.CREDENTIALS_UPDATED, "saveDeviceCredentials(" + deviceCredentials + ")"); } } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 876a1d7964..83ff3a9848 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -286,4 +286,11 @@ spring: # Audit log parameters audit_log: # Enable/disable audit log functionality. - enabled: "${AUDIT_LOG_ENABLED:true}" \ No newline at end of file + enabled: "${AUDIT_LOG_ENABLED:true}" + # Specify partitioning size for audit log by tenant id storage. Example MINUTES, HOURS, DAYS, MONTHS + by_tenant_partitioning: "${AUDIT_LOG_BY_TENANT_PARTITIONING:MONTHS}" + # Number of days as history period if startTime and endTime are not specified + default_query_period: "${AUDIT_LOG_DEFAULT_QUERY_PERIOD:30}" + exceptions: + # Enable/disable audit log functionality for exceptions. + enabled: "${AUDIT_LOG_EXCEPTIONS_ENABLED:true}" \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java index 8262933711..085d798ea5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java @@ -81,10 +81,13 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) { - return null; + return new TimePageData<>(null, pageLink); } @Override public TimePageData findAuditLogsByTenantIdAndUserId(TenantId tenantId, UserId userId, TimePageLink pageLink) { - return null; + return new TimePageData<>(null, pageLink); } @Override public TimePageData findAuditLogsByTenantIdAndEntityId(TenantId tenantId, EntityId entityId, TimePageLink pageLink) { - return null; + return new TimePageData<>(null, pageLink); } @Override public TimePageData findAuditLogsByTenantId(TenantId tenantId, TimePageLink pageLink) { - return null; + return new TimePageData<>(null, pageLink); } @Override public ListenableFuture> logEntityAction(User user, EntityId entityId, String entityName, CustomerId customerId, ActionType actionType, JsonNode actionData, ActionStatus actionStatus, String actionFailureDetails) { - return null; + return Futures.immediateFuture(Collections.emptyList()); } } diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index a09a5a74f9..4a73d37f08 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -10,6 +10,9 @@ zk.zk_dir=/thingsboard updates.enabled=false audit_log.enabled=true +audit_log.exceptions.enabled=false +audit_log.by_tenant_partitioning=MONTHS +audit_log.default_query_period=30 caching.specs.relations.timeToLiveInMinutes=1440 caching.specs.relations.maxSize=100000 From e8df3ccfdd07bedf77af878fa90cc978b5445a0f Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 19 Feb 2018 15:52:26 +0200 Subject: [PATCH 27/57] Add logo to login page. --- ui/src/app/login/login.controller.js | 9 +++++++++ ui/src/app/login/login.scss | 8 ++++++++ ui/src/app/login/login.tpl.html | 6 ++---- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/ui/src/app/login/login.controller.js b/ui/src/app/login/login.controller.js index 3dfb2d8c18..ab95b47d3e 100644 --- a/ui/src/app/login/login.controller.js +++ b/ui/src/app/login/login.controller.js @@ -13,10 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +/* eslint-disable import/no-unresolved, import/default */ + +import logoSvg from '../../svg/logo_title_white.svg'; + +/* eslint-enable import/no-unresolved, import/default */ + /*@ngInject*/ export default function LoginController(toast, loginService, userService/*, $rootScope, $log, $translate*/) { var vm = this; + vm.logoSvg = logoSvg; + vm.user = { name: '', password: '' diff --git a/ui/src/app/login/login.scss b/ui/src/app/login/login.scss index 0dbf8ad419..093b0afa95 100644 --- a/ui/src/app/login/login.scss +++ b/ui/src/app/login/login.scss @@ -20,4 +20,12 @@ md-card.tb-login-card { @media (min-width: $layout-breakpoint-sm) { width: 450px !important; } + md-card-title { + img.tb-login-logo { + height: 50px; + } + } + md-card-content { + margin-top: -50px; + } } diff --git a/ui/src/app/login/login.tpl.html b/ui/src/app/login/login.tpl.html index 05f9c8936f..213879adb4 100644 --- a/ui/src/app/login/login.tpl.html +++ b/ui/src/app/login/login.tpl.html @@ -18,11 +18,9 @@

- + @@ -39,7 +39,7 @@ class="tb-prompt" ng-show="noData()">alarm.no-alarms-prompt - + diff --git a/ui/src/app/api/audit-log.service.js b/ui/src/app/api/audit-log.service.js new file mode 100644 index 0000000000..b8f73b9cab --- /dev/null +++ b/ui/src/app/api/audit-log.service.js @@ -0,0 +1,116 @@ +/* + * Copyright © 2016-2017 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. + */ +export default angular.module('thingsboard.api.auditLog', []) + .factory('auditLogService', AuditLogService) + .name; + +/*@ngInject*/ +function AuditLogService($http, $q) { + + var service = { + getAuditLogsByEntityId: getAuditLogsByEntityId, + getAuditLogsByUserId: getAuditLogsByUserId, + getAuditLogsByCustomerId: getAuditLogsByCustomerId, + getAuditLogs: getAuditLogs + } + + return service; + + function getAuditLogsByEntityId (entityType, entityId, pageLink) { + var deferred = $q.defer(); + var url = `/api/audit/logs/entity/${entityType}/${entityId}?limit=${pageLink.limit}`; + + if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) { + url += '&startTime=' + pageLink.startTime; + } + if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) { + url += '&endTime=' + pageLink.endTime; + } + if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) { + url += '&offset=' + pageLink.idOffset; + } + $http.get(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function getAuditLogsByUserId (userId, pageLink) { + var deferred = $q.defer(); + var url = `/api/audit/logs/user/${userId}?limit=${pageLink.limit}`; + + if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) { + url += '&startTime=' + pageLink.startTime; + } + if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) { + url += '&endTime=' + pageLink.endTime; + } + if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) { + url += '&offset=' + pageLink.idOffset; + } + $http.get(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function getAuditLogsByCustomerId (customerId, pageLink) { + var deferred = $q.defer(); + var url = `/api/audit/logs/customer/${customerId}?limit=${pageLink.limit}`; + + if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) { + url += '&startTime=' + pageLink.startTime; + } + if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) { + url += '&endTime=' + pageLink.endTime; + } + if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) { + url += '&offset=' + pageLink.idOffset; + } + $http.get(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function getAuditLogs (pageLink) { + var deferred = $q.defer(); + var url = `/api/audit/logs?limit=${pageLink.limit}`; + + if (angular.isDefined(pageLink.startTime) && pageLink.startTime != null) { + url += '&startTime=' + pageLink.startTime; + } + if (angular.isDefined(pageLink.endTime) && pageLink.endTime != null) { + url += '&endTime=' + pageLink.endTime; + } + if (angular.isDefined(pageLink.idOffset) && pageLink.idOffset != null) { + url += '&offset=' + pageLink.idOffset; + } + $http.get(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + +} diff --git a/ui/src/app/api/device.service.js b/ui/src/app/api/device.service.js index bb4249e190..f2ae8cd30c 100644 --- a/ui/src/app/api/device.service.js +++ b/ui/src/app/api/device.service.js @@ -20,7 +20,7 @@ export default angular.module('thingsboard.api.device', [thingsboardTypes]) .name; /*@ngInject*/ -function DeviceService($http, $q, attributeService, customerService, types) { +function DeviceService($http, $q, $window, userService, attributeService, customerService, types) { var service = { assignDeviceToCustomer: assignDeviceToCustomer, @@ -181,14 +181,27 @@ function DeviceService($http, $q, attributeService, customerService, types) { return deferred.promise; } - function getDeviceCredentials(deviceId) { + function getDeviceCredentials(deviceId, sync) { var deferred = $q.defer(); var url = '/api/device/' + deviceId + '/credentials'; - $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); - }, function fail() { - deferred.reject(); - }); + if (sync) { + var request = new $window.XMLHttpRequest(); + request.open('GET', url, false); + request.setRequestHeader("Accept", "application/json, text/plain, */*"); + userService.setAuthorizationRequestHeader(request); + request.send(null); + if (request.status === 200) { + deferred.resolve(angular.fromJson(request.responseText)); + } else { + deferred.reject(); + } + } else { + $http.get(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + } return deferred.promise; } diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js index 1a146b4e00..ce5e673f18 100644 --- a/ui/src/app/api/user.service.js +++ b/ui/src/app/api/user.service.js @@ -54,6 +54,7 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi refreshJwtToken: refreshJwtToken, refreshTokenPending: refreshTokenPending, updateAuthorizationHeader: updateAuthorizationHeader, + setAuthorizationRequestHeader: setAuthorizationRequestHeader, gotoDefaultPlace: gotoDefaultPlace, forceDefaultPlace: forceDefaultPlace, updateLastPublicDashboardId: updateLastPublicDashboardId, @@ -367,6 +368,14 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi return jwtToken; } + function setAuthorizationRequestHeader(request) { + var jwtToken = store.get('jwt_token'); + if (jwtToken) { + request.setRequestHeader('X-Authorization', 'Bearer ' + jwtToken); + } + return jwtToken; + } + function getTenantAdmins(tenantId, pageLink) { var deferred = $q.defer(); var url = '/api/tenant/' + tenantId + '/users?limit=' + pageLink.limit; diff --git a/ui/src/app/app.js b/ui/src/app/app.js index 5b1908928e..23f69330ba 100644 --- a/ui/src/app/app.js +++ b/ui/src/app/app.js @@ -63,6 +63,7 @@ import thingsboardApiTime from './api/time.service'; import thingsboardKeyboardShortcut from './components/keyboard-shortcut.filter'; import thingsboardHelp from './help/help.directive'; import thingsboardToast from './services/toast'; +import thingsboardClipboard from './services/clipboard.service'; import thingsboardHome from './layout'; import thingsboardApiLogin from './api/login.service'; import thingsboardApiDevice from './api/device.service'; @@ -72,6 +73,7 @@ import thingsboardApiAsset from './api/asset.service'; import thingsboardApiAttribute from './api/attribute.service'; import thingsboardApiEntity from './api/entity.service'; import thingsboardApiAlarm from './api/alarm.service'; +import thingsboardApiAuditLog from './api/audit-log.service'; import 'typeface-roboto'; import 'font-awesome/css/font-awesome.min.css'; @@ -123,6 +125,7 @@ angular.module('thingsboard', [ thingsboardKeyboardShortcut, thingsboardHelp, thingsboardToast, + thingsboardClipboard, thingsboardHome, thingsboardApiLogin, thingsboardApiDevice, @@ -132,6 +135,7 @@ angular.module('thingsboard', [ thingsboardApiAttribute, thingsboardApiEntity, thingsboardApiAlarm, + thingsboardApiAuditLog, uiRouter]) .config(AppConfig) .factory('globalInterceptor', GlobalInterceptor) diff --git a/ui/src/app/asset/assets.tpl.html b/ui/src/app/asset/assets.tpl.html index c4c0267993..f16e1cdd73 100644 --- a/ui/src/app/asset/assets.tpl.html +++ b/ui/src/app/asset/assets.tpl.html @@ -66,4 +66,10 @@ entity-type="{{vm.types.entityType.asset}}"> + + + + diff --git a/ui/src/app/audit/audit-log-details-dialog.controller.js b/ui/src/app/audit/audit-log-details-dialog.controller.js new file mode 100644 index 0000000000..88cb10dc78 --- /dev/null +++ b/ui/src/app/audit/audit-log-details-dialog.controller.js @@ -0,0 +1,103 @@ +/* + * Copyright © 2016-2017 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 $ from 'jquery'; +import 'brace/ext/language_tools'; +import 'brace/mode/java'; +import 'brace/theme/github'; + +/* eslint-disable angular/angularelement */ + +import './audit-log-details-dialog.scss'; + +/*@ngInject*/ +export default function AuditLogDetailsDialogController($mdDialog, types, auditLog, showingCallback) { + + var vm = this; + + showingCallback.onShowing = function(scope, element) { + updateEditorSize(element, vm.actionData, 'tb-audit-log-action-data'); + vm.actionDataEditor.resize(); + if (vm.displayFailureDetails) { + updateEditorSize(element, vm.actionFailureDetails, 'tb-audit-log-failure-details'); + vm.failureDetailsEditor.resize(); + } + }; + + vm.types = types; + vm.auditLog = auditLog; + vm.displayFailureDetails = auditLog.actionStatus == types.auditLogActionStatus.FAILURE.value; + vm.actionData = auditLog.actionDataText; + vm.actionFailureDetails = auditLog.actionFailureDetails; + + vm.actionDataContentOptions = { + useWrapMode: false, + mode: 'java', + showGutter: false, + showPrintMargin: false, + theme: 'github', + advanced: { + enableSnippets: false, + enableBasicAutocompletion: false, + enableLiveAutocompletion: false + }, + onLoad: function (_ace) { + vm.actionDataEditor = _ace; + } + }; + + vm.failureDetailsContentOptions = { + useWrapMode: false, + mode: 'java', + showGutter: false, + showPrintMargin: false, + theme: 'github', + advanced: { + enableSnippets: false, + enableBasicAutocompletion: false, + enableLiveAutocompletion: false + }, + onLoad: function (_ace) { + vm.failureDetailsEditor = _ace; + } + }; + + function updateEditorSize(element, content, editorId) { + var newHeight = 200; + var newWidth = 600; + if (content && content.length > 0) { + var lines = content.split('\n'); + newHeight = 16 * lines.length + 16; + var maxLineLength = 0; + for (var i in lines) { + var line = lines[i].replace(/\t/g, ' ').replace(/\n/g, ''); + var lineLength = line.length; + maxLineLength = Math.max(maxLineLength, lineLength); + } + newWidth = 8 * maxLineLength + 16; + } + $('#'+editorId, element).height(newHeight.toString() + "px").css('min-height', newHeight.toString() + "px") + .width(newWidth.toString() + "px"); + } + + vm.close = close; + + function close () { + $mdDialog.hide(); + } + +} + +/* eslint-enable angular/angularelement */ diff --git a/ui/src/app/audit/audit-log-details-dialog.scss b/ui/src/app/audit/audit-log-details-dialog.scss new file mode 100644 index 0000000000..83aaf40f61 --- /dev/null +++ b/ui/src/app/audit/audit-log-details-dialog.scss @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2017 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. + */ + +#tb-audit-log-action-data, #tb-audit-log-failure-details { + min-width: 400px; + min-height: 50px; + width: 100%; + height: 100%; + border: 1px solid #C0C0C0; +} \ No newline at end of file diff --git a/ui/src/app/audit/audit-log-details-dialog.tpl.html b/ui/src/app/audit/audit-log-details-dialog.tpl.html new file mode 100644 index 0000000000..b1e1e63864 --- /dev/null +++ b/ui/src/app/audit/audit-log-details-dialog.tpl.html @@ -0,0 +1,49 @@ + + + +
+

audit-log.audit-log-details

+ + + + +
+
+ +
+ +
+
+ + +
+
+
+
+ + + {{ 'action.close' | + translate }} + + +
diff --git a/ui/src/app/audit/audit-log-header.directive.js b/ui/src/app/audit/audit-log-header.directive.js new file mode 100644 index 0000000000..504e96c37f --- /dev/null +++ b/ui/src/app/audit/audit-log-header.directive.js @@ -0,0 +1,41 @@ +/* + * Copyright © 2016-2017 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. + */ +/* eslint-disable import/no-unresolved, import/default */ + +import auditLogHeaderTemplate from './audit-log-header.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function AuditLogHeaderDirective($compile, $templateCache, types) { + + var linker = function (scope, element, attrs) { + + var template = $templateCache.get(auditLogHeaderTemplate); + element.html(template); + scope.auditLogMode = attrs.auditLogMode; + scope.types = types; + $compile(element.contents())(scope); + + }; + + return { + restrict: "A", + replace: false, + link: linker, + scope: false + }; +} diff --git a/ui/src/app/audit/audit-log-header.tpl.html b/ui/src/app/audit/audit-log-header.tpl.html new file mode 100644 index 0000000000..6927ad78c9 --- /dev/null +++ b/ui/src/app/audit/audit-log-header.tpl.html @@ -0,0 +1,24 @@ + +
audit-log.timestamp
+
audit-log.entity-type
+
audit-log.entity-name
+
audit-log.user
+
audit-log.type
+
audit-log.status
+
audit-log.details
diff --git a/ui/src/app/audit/audit-log-row.directive.js b/ui/src/app/audit/audit-log-row.directive.js new file mode 100644 index 0000000000..1cb6d5f5d7 --- /dev/null +++ b/ui/src/app/audit/audit-log-row.directive.js @@ -0,0 +1,67 @@ +/* + * Copyright © 2016-2017 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. + */ +/* eslint-disable import/no-unresolved, import/default */ + +import auditLogDetailsDialogTemplate from './audit-log-details-dialog.tpl.html'; + +import auditLogRowTemplate from './audit-log-row.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function AuditLogRowDirective($compile, $templateCache, types, $mdDialog, $document) { + + var linker = function (scope, element, attrs) { + + var template = $templateCache.get(auditLogRowTemplate); + element.html(template); + + scope.auditLog = attrs.auditLog; + scope.auditLogMode = attrs.auditLogMode; + scope.types = types; + + scope.showAuditLogDetails = function($event) { + var onShowingCallback = { + onShowing: function(){} + } + $mdDialog.show({ + controller: 'AuditLogDetailsDialogController', + controllerAs: 'vm', + templateUrl: auditLogDetailsDialogTemplate, + locals: { + auditLog: scope.auditLog, + showingCallback: onShowingCallback + }, + parent: angular.element($document[0].body), + targetEvent: $event, + fullscreen: true, + skipHide: true, + onShowing: function(scope, element) { + onShowingCallback.onShowing(scope, element); + } + }); + } + + $compile(element.contents())(scope); + } + + return { + restrict: "A", + replace: false, + link: linker, + scope: false + }; +} diff --git a/ui/src/app/audit/audit-log-row.tpl.html b/ui/src/app/audit/audit-log-row.tpl.html new file mode 100644 index 0000000000..fd0af2d15f --- /dev/null +++ b/ui/src/app/audit/audit-log-row.tpl.html @@ -0,0 +1,36 @@ + +
{{ auditLog.createdTime | date : 'yyyy-MM-dd HH:mm:ss' }}
+
{{ auditLog.entityTypeText }}
+
{{ auditLog.entityName }}
+
{{ auditLog.userName }}
+
{{ auditLog.actionTypeText }}
+
{{ auditLog.actionStatusText }}
+
+ + + {{ 'audit-log.details' | translate }} + + + more_horiz + + +
diff --git a/ui/src/app/audit/audit-log-table.directive.js b/ui/src/app/audit/audit-log-table.directive.js new file mode 100644 index 0000000000..ba15249253 --- /dev/null +++ b/ui/src/app/audit/audit-log-table.directive.js @@ -0,0 +1,262 @@ +/* + * Copyright © 2016-2017 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 './audit-log.scss'; + +/* eslint-disable import/no-unresolved, import/default */ + +import auditLogTableTemplate from './audit-log-table.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function AuditLogTableDirective($compile, $templateCache, $rootScope, $filter, $translate, types, auditLogService) { + + var linker = function (scope, element) { + + var template = $templateCache.get(auditLogTableTemplate); + + element.html(template); + + scope.types = types; + + var pageSize = 20; + var startTime = 0; + var endTime = 0; + + scope.timewindow = { + history: { + timewindowMs: 24 * 60 * 60 * 1000 // 1 day + } + } + + scope.topIndex = 0; + scope.searchText = ''; + + scope.theAuditLogs = { + getItemAtIndex: function (index) { + if (index > scope.auditLogs.filtered.length) { + scope.theAuditLogs.fetchMoreItems_(index); + return null; + } + return scope.auditLogs.filtered[index]; + }, + + getLength: function () { + if (scope.auditLogs.hasNext) { + return scope.auditLogs.filtered.length + scope.auditLogs.nextPageLink.limit; + } else { + return scope.auditLogs.filtered.length; + } + }, + + fetchMoreItems_: function () { + if (scope.auditLogs.hasNext && !scope.auditLogs.pending) { + var promise = getAuditLogsPromise(scope.auditLogs.nextPageLink); + if (promise) { + scope.auditLogs.pending = true; + promise.then( + function success(auditLogs) { + scope.auditLogs.data = scope.auditLogs.data.concat(prepareAuditLogsData(auditLogs.data)); + scope.auditLogs.filtered = $filter('filter')(scope.auditLogs.data, {$: scope.searchText}); + scope.auditLogs.nextPageLink = auditLogs.nextPageLink; + scope.auditLogs.hasNext = auditLogs.hasNext; + if (scope.auditLogs.hasNext) { + scope.auditLogs.nextPageLink.limit = pageSize; + } + scope.auditLogs.pending = false; + }, + function fail() { + scope.auditLogs.hasNext = false; + scope.auditLogs.pending = false; + }); + } else { + scope.auditLogs.hasNext = false; + } + } + } + }; + + function prepareAuditLogsData(data) { + data.forEach( + auditLog => { + auditLog.entityTypeText = $translate.instant(types.entityTypeTranslations[auditLog.entityId.entityType].type); + auditLog.actionTypeText = $translate.instant(types.auditLogActionType[auditLog.actionType].name); + auditLog.actionStatusText = $translate.instant(types.auditLogActionStatus[auditLog.actionStatus].name); + auditLog.actionDataText = auditLog.actionData ? angular.toJson(auditLog.actionData, true) : ''; + } + ); + return data; + } + + scope.$watch("entityId", function(newVal, prevVal) { + if (newVal && !angular.equals(newVal, prevVal)) { + resetFilter(); + scope.reload(); + } + }); + + scope.$watch("userId", function(newVal, prevVal) { + if (newVal && !angular.equals(newVal, prevVal)) { + resetFilter(); + scope.reload(); + } + }); + + scope.$watch("customerId", function(newVal, prevVal) { + if (newVal && !angular.equals(newVal, prevVal)) { + resetFilter(); + scope.reload(); + } + }); + + function getAuditLogsPromise(pageLink) { + switch(scope.auditLogMode) { + case types.auditLogMode.tenant: + return auditLogService.getAuditLogs(pageLink); + case types.auditLogMode.entity: + if (scope.entityType && scope.entityId) { + return auditLogService.getAuditLogsByEntityId(scope.entityType, scope.entityId, + pageLink); + } else { + return null; + } + case types.auditLogMode.user: + if (scope.userId) { + return auditLogService.getAuditLogsByUserId(scope.userId, pageLink); + } else { + return null; + } + case types.auditLogMode.customer: + if (scope.customerId) { + return auditLogService.getAuditLogsByCustomerId(scope.customerId, pageLink); + } else { + return null; + } + } + } + + function destroyWatchers() { + if (scope.timewindowWatchHandle) { + scope.timewindowWatchHandle(); + scope.timewindowWatchHandle = null; + } + if (scope.searchTextWatchHandle) { + scope.searchTextWatchHandle(); + scope.searchTextWatchHandle = null; + } + } + + function initWatchers() { + scope.timewindowWatchHandle = scope.$watch("timewindow", function(newVal, prevVal) { + if (newVal && !angular.equals(newVal, prevVal)) { + scope.reload(); + } + }, true); + + scope.searchTextWatchHandle = scope.$watch("searchText", function(newVal, prevVal) { + if (!angular.equals(newVal, prevVal)) { + scope.searchTextUpdated(); + } + }, true); + } + + function resetFilter() { + destroyWatchers(); + scope.timewindow = { + history: { + timewindowMs: 24 * 60 * 60 * 1000 // 1 day + } + }; + scope.searchText = ''; + initWatchers(); + } + + function updateTimeWindowRange () { + if (scope.timewindow.history.timewindowMs) { + var currentTime = (new Date).getTime(); + startTime = currentTime - scope.timewindow.history.timewindowMs; + endTime = currentTime; + } else { + startTime = scope.timewindow.history.fixedTimewindow.startTimeMs; + endTime = scope.timewindow.history.fixedTimewindow.endTimeMs; + } + } + + scope.reload = function() { + scope.topIndex = 0; + updateTimeWindowRange(); + scope.auditLogs = { + data: [], + filtered: [], + nextPageLink: { + limit: pageSize, + startTime: startTime, + endTime: endTime + }, + hasNext: true, + pending: false + }; + scope.theAuditLogs.getItemAtIndex(pageSize); + } + + scope.searchTextUpdated = function() { + scope.auditLogs.filtered = $filter('filter')(scope.auditLogs.data, {$: scope.searchText}); + scope.theAuditLogs.getItemAtIndex(pageSize); + } + + scope.noData = function() { + return scope.auditLogs.data.length == 0 && !scope.auditLogs.hasNext; + } + + scope.hasData = function() { + return scope.auditLogs.data.length > 0; + } + + scope.loading = function() { + return $rootScope.loading; + } + + scope.hasScroll = function() { + var repeatContainer = scope.repeatContainer[0]; + if (repeatContainer) { + var scrollElement = repeatContainer.children[0]; + if (scrollElement) { + return scrollElement.scrollHeight > scrollElement.clientHeight; + } + } + return false; + } + + scope.reload(); + + initWatchers(); + + $compile(element.contents())(scope); + } + + return { + restrict: "E", + link: linker, + scope: { + entityType: '=?', + entityId: '=?', + userId: '=?', + customerId: '=?', + auditLogMode: '@', + pageMode: '@?' + } + }; +} diff --git a/ui/src/app/audit/audit-log-table.tpl.html b/ui/src/app/audit/audit-log-table.tpl.html new file mode 100644 index 0000000000..88a1d9376e --- /dev/null +++ b/ui/src/app/audit/audit-log-table.tpl.html @@ -0,0 +1,68 @@ + + +
+
+ +
+ + search + + {{'audit-log.search' | translate}} + + + + + + + + close + + {{ 'audit-log.clear-search' | translate }} + + + + refresh + + {{ 'action.refresh' | translate }} + + +
+
+
+ + + + + + audit-log.no-audit-logs-prompt + + + + + + + + +
+
+
diff --git a/ui/src/app/audit/audit-log.routes.js b/ui/src/app/audit/audit-log.routes.js new file mode 100644 index 0000000000..be26b9c457 --- /dev/null +++ b/ui/src/app/audit/audit-log.routes.js @@ -0,0 +1,44 @@ +/* + * Copyright © 2016-2017 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. + */ +/* eslint-disable import/no-unresolved, import/default */ + +import auditLogsTemplate from './audit-logs.tpl.html'; + +/* eslint-enable import/no-unresolved, import/default */ + +/*@ngInject*/ +export default function AuditLogRoutes($stateProvider) { + $stateProvider + .state('home.auditLogs', { + url: '/auditLogs', + module: 'private', + auth: ['TENANT_ADMIN'], + views: { + "content@home": { + templateUrl: auditLogsTemplate, + controller: 'AuditLogsController', + controllerAs: 'vm' + } + }, + data: { + searchEnabled: false, + pageTitle: 'audit-log.audit-logs' + }, + ncyBreadcrumb: { + label: '{"icon": "track_changes", "label": "audit-log.audit-logs"}' + } + }); +} diff --git a/ui/src/app/audit/audit-log.scss b/ui/src/app/audit/audit-log.scss new file mode 100644 index 0000000000..1642cbe7e4 --- /dev/null +++ b/ui/src/app/audit/audit-log.scss @@ -0,0 +1,91 @@ +/** + * Copyright © 2016-2017 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. + */ + +.tb-audit-logs { + background-color: #fff; + .tb-audit-log-margin-18px { + margin-bottom: 18px; + } + .tb-audit-log-toolbar { + font-size: 20px; + } + md-input-container.tb-audit-log-search-input { + .md-errors-spacer { + min-height: 0px; + } + } +} + +.tb-audit-log-container { + overflow-x: auto; +} + + + +md-list.tb-audit-log-table { + padding: 0px; + min-width: 700px; + &.tb-audit-log-table-full { + min-width: 900px; + } + + md-list-item { + padding: 0px; + } + + .tb-row { + height: 48px; + padding: 0px; + overflow: hidden; + } + + .tb-row:hover { + background-color: #EEEEEE; + } + + .tb-header:hover { + background: none; + } + + .tb-header { + .tb-cell { + color: rgba(0,0,0,.54); + font-size: 12px; + font-weight: 700; + white-space: nowrap; + background: none; + } + } + + .tb-cell { + padding: 0 24px; + margin: auto 0; + color: rgba(0,0,0,.87); + font-size: 13px; + vertical-align: middle; + text-align: left; + overflow: hidden; + .md-button { + padding: 0; + margin: 0; + } + } + + .tb-cell.tb-number { + text-align: right; + } + +} diff --git a/ui/src/app/audit/audit-logs.controller.js b/ui/src/app/audit/audit-logs.controller.js new file mode 100644 index 0000000000..d2cb1e3cd4 --- /dev/null +++ b/ui/src/app/audit/audit-logs.controller.js @@ -0,0 +1,24 @@ +/* + * Copyright © 2016-2017 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. + */ + +/*@ngInject*/ +export default function AuditLogsController(types) { + + var vm = this; + + vm.types = types; + +} \ No newline at end of file diff --git a/ui/src/app/audit/audit-logs.tpl.html b/ui/src/app/audit/audit-logs.tpl.html new file mode 100644 index 0000000000..088c339498 --- /dev/null +++ b/ui/src/app/audit/audit-logs.tpl.html @@ -0,0 +1,23 @@ + + + + diff --git a/ui/src/app/audit/index.js b/ui/src/app/audit/index.js new file mode 100644 index 0000000000..aebcd33e73 --- /dev/null +++ b/ui/src/app/audit/index.js @@ -0,0 +1,31 @@ +/* + * Copyright © 2016-2017 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 AuditLogRoutes from './audit-log.routes'; +import AuditLogsController from './audit-logs.controller'; +import AuditLogDetailsDialogController from './audit-log-details-dialog.controller'; +import AuditLogHeaderDirective from './audit-log-header.directive'; +import AuditLogRowDirective from './audit-log-row.directive'; +import AuditLogTableDirective from './audit-log-table.directive'; + +export default angular.module('thingsboard.auditLog', []) + .config(AuditLogRoutes) + .controller('AuditLogsController', AuditLogsController) + .controller('AuditLogDetailsDialogController', AuditLogDetailsDialogController) + .directive('tbAuditLogHeader', AuditLogHeaderDirective) + .directive('tbAuditLogRow', AuditLogRowDirective) + .directive('tbAuditLogTable', AuditLogTableDirective) + .name; diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index 5b3c9e4ac4..d82add14c0 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -156,6 +156,63 @@ export default angular.module('thingsboard.types', []) color: "green" } }, + auditLogActionType: { + "ADDED": { + name: "audit-log.type-added" + }, + "DELETED": { + name: "audit-log.type-deleted" + }, + "UPDATED": { + name: "audit-log.type-updated" + }, + "ATTRIBUTES_UPDATED": { + name: "audit-log.type-attributes-updated" + }, + "ATTRIBUTES_DELETED": { + name: "audit-log.type-attributes-deleted" + }, + "RPC_CALL": { + name: "audit-log.type-rpc-call" + }, + "CREDENTIALS_UPDATED": { + name: "audit-log.type-credentials-updated" + }, + "ASSIGNED_TO_CUSTOMER": { + name: "audit-log.type-assigned-to-customer" + }, + "UNASSIGNED_FROM_CUSTOMER": { + name: "audit-log.type-unassigned-from-customer" + }, + "ACTIVATED": { + name: "audit-log.type-activated" + }, + "SUSPENDED": { + name: "audit-log.type-suspended" + }, + "CREDENTIALS_READ": { + name: "audit-log.type-credentials-read" + }, + "ATTRIBUTES_READ": { + name: "audit-log.type-attributes-read" + } + }, + auditLogActionStatus: { + "SUCCESS": { + value: "SUCCESS", + name: "audit-log.status-success" + }, + "FAILURE": { + value: "FAILURE", + name: "audit-log.status-failure" + } + }, + auditLogMode: { + tenant: "tenant", + entity: "entity", + user: "user", + customer: "customer" + }, aliasFilterType: { singleEntity: { value: 'singleEntity', diff --git a/ui/src/app/components/grid.directive.js b/ui/src/app/components/grid.directive.js index 727b999d75..290431ce9b 100644 --- a/ui/src/app/components/grid.directive.js +++ b/ui/src/app/components/grid.directive.js @@ -125,7 +125,7 @@ function Grid() { } /*@ngInject*/ -function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $timeout, $translate, $mdMedia, $templateCache, $window) { +function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $timeout, $translate, $mdMedia, $templateCache, $window, userService) { var vm = this; @@ -157,6 +157,7 @@ function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $time vm.saveItem = saveItem; vm.toggleItemSelection = toggleItemSelection; vm.triggerResize = triggerResize; + vm.isTenantAdmin = isTenantAdmin; $scope.$watch(function () { return $mdMedia('xs') || $mdMedia('sm'); @@ -634,6 +635,10 @@ function GridController($scope, $state, $mdDialog, $document, $q, $mdUtil, $time w.triggerHandler('resize'); } + function isTenantAdmin() { + return userService.getAuthority() == 'TENANT_ADMIN'; + } + function moveToTop() { moveToIndex(0, true); } diff --git a/ui/src/app/customer/customers.tpl.html b/ui/src/app/customer/customers.tpl.html index da0a2e950b..0a0ca84b21 100644 --- a/ui/src/app/customer/customers.tpl.html +++ b/ui/src/app/customer/customers.tpl.html @@ -66,5 +66,10 @@ entity-type="{{vm.types.entityType.customer}}"> + + + + diff --git a/ui/src/app/dashboard/dashboards.tpl.html b/ui/src/app/dashboard/dashboards.tpl.html index dde2f864b7..8149821454 100644 --- a/ui/src/app/dashboard/dashboards.tpl.html +++ b/ui/src/app/dashboard/dashboards.tpl.html @@ -19,13 +19,24 @@
- + + + + + + + + + diff --git a/ui/src/app/device/device-fieldset.tpl.html b/ui/src/app/device/device-fieldset.tpl.html index a4d15f33eb..758c77f867 100644 --- a/ui/src/app/device/device-fieldset.tpl.html +++ b/ui/src/app/device/device-fieldset.tpl.html @@ -39,10 +39,8 @@ device.copyId - + device.copyAccessToken diff --git a/ui/src/app/device/device.directive.js b/ui/src/app/device/device.directive.js index eda4fb2ce9..a40e4da01d 100644 --- a/ui/src/app/device/device.directive.js +++ b/ui/src/app/device/device.directive.js @@ -20,7 +20,7 @@ import deviceFieldsetTemplate from './device-fieldset.tpl.html'; /* eslint-enable import/no-unresolved, import/default */ /*@ngInject*/ -export default function DeviceDirective($compile, $templateCache, toast, $translate, types, deviceService, customerService) { +export default function DeviceDirective($compile, $templateCache, toast, $translate, types, clipboardService, deviceService, customerService) { var linker = function (scope, element) { var template = $templateCache.get(deviceFieldsetTemplate); element.html(template); @@ -30,17 +30,8 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl scope.isPublic = false; scope.assignedCustomer = null; - scope.deviceCredentials = null; - scope.$watch('device', function(newVal) { if (newVal) { - if (scope.device.id) { - deviceService.getDeviceCredentials(scope.device.id.id).then( - function success(credentials) { - scope.deviceCredentials = credentials; - } - ); - } if (scope.device.customerId && scope.device.customerId.id !== types.id.nullUid) { scope.isAssignedToCustomer = true; customerService.getShortCustomerInfo(scope.device.customerId.id).then( @@ -61,8 +52,20 @@ export default function DeviceDirective($compile, $templateCache, toast, $transl toast.showSuccess($translate.instant('device.idCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); }; - scope.onAccessTokenCopied = function() { - toast.showSuccess($translate.instant('device.accessTokenCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); + scope.copyAccessToken = function(e) { + const trigger = e.delegateTarget || e.currentTarget; + if (scope.device.id) { + deviceService.getDeviceCredentials(scope.device.id.id, true).then( + function success(credentials) { + var credentialsId = credentials.credentialsId; + clipboardService.copyToClipboard(trigger, credentialsId).then( + () => { + toast.showSuccess($translate.instant('device.accessTokenCopiedMessage'), 750, angular.element(element).parent().parent(), 'bottom left'); + } + ); + } + ); + } }; $compile(element.contents())(scope); diff --git a/ui/src/app/device/devices.tpl.html b/ui/src/app/device/devices.tpl.html index 1ec0134444..513465da3b 100644 --- a/ui/src/app/device/devices.tpl.html +++ b/ui/src/app/device/devices.tpl.html @@ -74,4 +74,10 @@ entity-type="{{vm.types.entityType.device}}"> + + + + diff --git a/ui/src/app/event/event-table.tpl.html b/ui/src/app/event/event-table.tpl.html index 760b55bfff..e056671720 100644 --- a/ui/src/app/event/event-table.tpl.html +++ b/ui/src/app/event/event-table.tpl.html @@ -26,9 +26,16 @@ + + refresh + + {{ 'action.refresh' | translate }} + + - + @@ -38,7 +45,7 @@ class="tb-prompt" ng-show="noData()">event.no-events-prompt - + diff --git a/ui/src/app/layout/index.js b/ui/src/app/layout/index.js index c23b0086a6..9102928226 100644 --- a/ui/src/app/layout/index.js +++ b/ui/src/app/layout/index.js @@ -35,6 +35,7 @@ import thingsboardUserMenu from './user-menu.directive'; import thingsboardEntity from '../entity'; import thingsboardEvent from '../event'; import thingsboardAlarm from '../alarm'; +import thingsboardAuditLog from '../audit'; import thingsboardExtension from '../extension'; import thingsboardTenant from '../tenant'; import thingsboardCustomer from '../customer'; @@ -67,6 +68,7 @@ export default angular.module('thingsboard.home', [ thingsboardEntity, thingsboardEvent, thingsboardAlarm, + thingsboardAuditLog, thingsboardExtension, thingsboardTenant, thingsboardCustomer, diff --git a/ui/src/app/locale/locale.constant.js b/ui/src/app/locale/locale.constant.js index b9977f6f9e..96dc1523ec 100644 --- a/ui/src/app/locale/locale.constant.js +++ b/ui/src/app/locale/locale.constant.js @@ -286,6 +286,38 @@ export default angular.module('thingsboard.locale', []) "selected-attributes": "{ count, select, 1 {1 attribute} other {# attributes} } selected", "selected-telemetry": "{ count, select, 1 {1 telemetry unit} other {# telemetry units} } selected" }, + "audit-log": { + "audit": "Audit", + "audit-logs": "Audit Logs", + "timestamp": "Timestamp", + "entity-type": "Entity Type", + "entity-name": "Entity Name", + "user": "User", + "type": "Type", + "status": "Status", + "details": "Details", + "type-added": "Added", + "type-deleted": "Deleted", + "type-updated": "Updated", + "type-attributes-updated": "Attributes updated", + "type-attributes-deleted": "Attributes deleted", + "type-rpc-call": "RPC call", + "type-credentials-updated": "Credentials updated", + "type-assigned-to-customer": "Assigned to Customer", + "type-unassigned-from-customer": "Unassigned from Customer", + "type-activated": "Activated", + "type-suspended": "Suspended", + "type-credentials-read": "Credentials read", + "type-attributes-read": "Attributes read", + "status-success": "Success", + "status-failure": "Failure", + "audit-log-details": "Audit log details", + "no-audit-logs-prompt": "No logs found", + "action-data": "Action data", + "failure-details": "Failure details", + "search": "Search audit logs", + "clear-search": "Clear search" + }, "confirm-on-exit": { "message": "You have unsaved changes. Are you sure you want to leave this page?", "html-message": "You have unsaved changes.
Are you sure you want to leave this page?", @@ -1183,7 +1215,8 @@ export default angular.module('thingsboard.locale', []) "activation-link": "User activation link", "activation-link-text": "In order to activate user use the following activation link :", "copy-activation-link": "Copy activation link", - "activation-link-copied-message": "User activation link has been copied to clipboard" + "activation-link-copied-message": "User activation link has been copied to clipboard", + "details": "Details" }, "value": { "type": "Value type", diff --git a/ui/src/app/plugin/plugins.tpl.html b/ui/src/app/plugin/plugins.tpl.html index b3563e9945..eafb9f7aa7 100644 --- a/ui/src/app/plugin/plugins.tpl.html +++ b/ui/src/app/plugin/plugins.tpl.html @@ -66,5 +66,12 @@ entity-type="{{vm.types.entityType.plugin}}"> + + + + diff --git a/ui/src/app/rule/rules.tpl.html b/ui/src/app/rule/rules.tpl.html index 064728baa9..f46d1248a8 100644 --- a/ui/src/app/rule/rules.tpl.html +++ b/ui/src/app/rule/rules.tpl.html @@ -66,5 +66,12 @@ entity-type="{{vm.types.entityType.rule}}"> + + + + diff --git a/ui/src/app/services/clipboard.service.js b/ui/src/app/services/clipboard.service.js new file mode 100644 index 0000000000..7c6d960959 --- /dev/null +++ b/ui/src/app/services/clipboard.service.js @@ -0,0 +1,128 @@ +/* + * Copyright © 2016-2017 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. + */ +export default angular.module('thingsboard.clipboard', []) + .factory('clipboardService', ClipboardService) + .name; + +/*@ngInject*/ +function ClipboardService($q) { + + var fakeHandler, fakeHandlerCallback, fakeElem; + + var service = { + copyToClipboard: copyToClipboard + }; + + return service; + + /* eslint-disable */ + function copyToClipboard(trigger, text) { + var deferred = $q.defer(); + const isRTL = document.documentElement.getAttribute('dir') == 'rtl'; + removeFake(); + fakeHandlerCallback = () => removeFake(); + fakeHandler = document.body.addEventListener('click', fakeHandlerCallback) || true; + fakeElem = document.createElement('textarea'); + fakeElem.style.fontSize = '12pt'; + fakeElem.style.border = '0'; + fakeElem.style.padding = '0'; + fakeElem.style.margin = '0'; + fakeElem.style.position = 'absolute'; + fakeElem.style[ isRTL ? 'right' : 'left' ] = '-9999px'; + let yPosition = window.pageYOffset || document.documentElement.scrollTop; + fakeElem.style.top = `${yPosition}px`; + fakeElem.setAttribute('readonly', ''); + fakeElem.value = text; + document.body.appendChild(fakeElem); + var selectedText = select(fakeElem); + + let succeeded; + try { + succeeded = document.execCommand('copy'); + } + catch (err) { + succeeded = false; + } + if (trigger) { + trigger.focus(); + } + window.getSelection().removeAllRanges(); + removeFake(); + if (succeeded) { + deferred.resolve(selectedText); + } else { + deferred.reject(); + } + return deferred.promise; + } + + function removeFake() { + if (fakeHandler) { + document.body.removeEventListener('click', fakeHandlerCallback); + fakeHandler = null; + fakeHandlerCallback = null; + } + if (fakeElem) { + document.body.removeChild(fakeElem); + fakeElem = null; + } + } + + function select(element) { + var selectedText; + + if (element.nodeName === 'SELECT') { + element.focus(); + + selectedText = element.value; + } + else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') { + var isReadOnly = element.hasAttribute('readonly'); + + if (!isReadOnly) { + element.setAttribute('readonly', ''); + } + + element.select(); + element.setSelectionRange(0, element.value.length); + + if (!isReadOnly) { + element.removeAttribute('readonly'); + } + + selectedText = element.value; + } + else { + if (element.hasAttribute('contenteditable')) { + element.focus(); + } + + var selection = window.getSelection(); + var range = document.createRange(); + + range.selectNodeContents(element); + selection.removeAllRanges(); + selection.addRange(range); + + selectedText = selection.toString(); + } + + return selectedText; + } + + /* eslint-enable */ + +} \ No newline at end of file diff --git a/ui/src/app/services/menu.service.js b/ui/src/app/services/menu.service.js index 24db768ac5..a492585cf1 100644 --- a/ui/src/app/services/menu.service.js +++ b/ui/src/app/services/menu.service.js @@ -211,6 +211,12 @@ function Menu(userService, $state, $rootScope) { type: 'link', state: 'home.dashboards', icon: 'dashboards' + }, + { + name: 'audit-log.audit-logs', + type: 'link', + state: 'home.auditLogs', + icon: 'track_changes' }]; homeSections = @@ -273,6 +279,16 @@ function Menu(userService, $state, $rootScope) { state: 'home.dashboards' } ] + }, + { + name: 'audit-log.audit', + places: [ + { + name: 'audit-log.audit-logs', + icon: 'track_changes', + state: 'home.auditLogs' + } + ] }]; } else if (authority === 'CUSTOMER_USER') { diff --git a/ui/src/app/user/user.controller.js b/ui/src/app/user/user.controller.js index a9f4ce3b84..6a9fdde060 100644 --- a/ui/src/app/user/user.controller.js +++ b/ui/src/app/user/user.controller.js @@ -42,6 +42,8 @@ export default function UserController(userService, toast, $scope, $mdDialog, $d var vm = this; + vm.types = types; + vm.userGridConfig = { deleteItemTitleFunc: deleteUserTitle, deleteItemContentFunc: deleteUserText, diff --git a/ui/src/app/user/users.tpl.html b/ui/src/app/user/users.tpl.html index 72f67d2853..03a5da24ca 100644 --- a/ui/src/app/user/users.tpl.html +++ b/ui/src/app/user/users.tpl.html @@ -19,10 +19,20 @@
- + + + + + + + + + diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss index 93418b27fe..a961ab528a 100644 --- a/ui/src/scss/main.scss +++ b/ui/src/scss/main.scss @@ -203,6 +203,19 @@ md-sidenav { * THINGSBOARD SPECIFIC ***********************/ +label { + &.tb-title { + pointer-events: none; + color: #666; + font-size: 13px; + font-weight: 400; + padding-bottom: 15px; + &.no-padding { + padding-bottom: 0px; + } + } +} + .tb-noselect { -webkit-touch-callout: none; /* iOS Safari */ -webkit-user-select: none; /* Safari */ From e639dd1fe8f2556ef9e0ec6aa786a18f1ed77073 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 22 Feb 2018 11:28:24 +0200 Subject: [PATCH 31/57] Fix cassandra audit log dao. --- .../ThingsboardInstallConfiguration.java | 34 +++++++ .../src/main/resources/thingsboard.yml | 14 +-- .../dao/audit/CassandraAuditLogDao.java | 91 +++++++++---------- .../cassandra/AbstractCassandraCluster.java | 10 +- .../dao/model/nosql/AuditLogEntity.java | 4 +- 5 files changed, 93 insertions(+), 60 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/install/ThingsboardInstallConfiguration.java diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallConfiguration.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallConfiguration.java new file mode 100644 index 0000000000..d0132f8461 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallConfiguration.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2017 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.install; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.thingsboard.server.dao.audit.AuditLogLevelFilter; + +import java.util.HashMap; + +@Configuration +@Profile("install") +public class ThingsboardInstallConfiguration { + + @Bean + public AuditLogLevelFilter emptyAuditLogLevelFilter() { + return new AuditLogLevelFilter(new HashMap<>()); + } +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 7ea68c73d3..f7a3a80738 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -310,10 +310,10 @@ audit_log: # Allowed values: OFF (disable), W (log write operations), RW (log read and write operations) logging_level: mask: - "device": "W" - "asset": "W" - "dashboard": "W" - "customer": "W" - "user": "W" - "rule": "W" - "plugin": "W" + "device": "${AUDIT_LOG_MASK_DEVICE:W}" + "asset": "${AUDIT_LOG_MASK_ASSET:W}" + "dashboard": "${AUDIT_LOG_MASK_DASHBOARD:W}" + "customer": "${AUDIT_LOG_MASK_CUSTOMER:W}" + "user": "${AUDIT_LOG_MASK_USER:W}" + "rule": "${AUDIT_LOG_MASK_RULE:W}" + "plugin": "${AUDIT_LOG_MASK_PLUGIN:W}" diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java index 085d798ea5..fa32b5de0e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/CassandraAuditLogDao.java @@ -48,10 +48,7 @@ import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneOffset; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.stream.Collectors; @@ -135,12 +132,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao null); } @@ -149,7 +141,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao null); } @@ -158,7 +150,7 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao null); } @@ -167,12 +159,12 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao null); } - private BoundStatement setSaveStmtVariables(BoundStatement stmt, AuditLog auditLog) { - return stmt.setUUID(0, auditLog.getId().getId()) + private BoundStatement setSaveStmtVariables(BoundStatement stmt, AuditLog auditLog, long partition) { + stmt.setUUID(0, auditLog.getId().getId()) .setUUID(1, auditLog.getTenantId().getId()) .setUUID(2, auditLog.getCustomerId().getId()) .setUUID(3, auditLog.getEntityId().getId()) @@ -184,6 +176,10 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao -1) { + stmt.setLong(12, partition); + } + return stmt; } @Override @@ -198,42 +194,57 @@ public class CassandraAuditLogDao extends CassandraAbstractSearchTimeDao null); } + private PreparedStatement getSaveByTenantStmt() { + if (saveByTenantStmt == null) { + saveByTenantStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_TENANT_ID_CF, true); + } + return saveByTenantStmt; + } + private PreparedStatement getSaveByTenantIdAndEntityIdStmt() { if (saveByTenantIdAndEntityIdStmt == null) { - saveByTenantIdAndEntityIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_ENTITY_ID_CF); + saveByTenantIdAndEntityIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_ENTITY_ID_CF, false); } return saveByTenantIdAndEntityIdStmt; } private PreparedStatement getSaveByTenantIdAndCustomerIdStmt() { if (saveByTenantIdAndCustomerIdStmt == null) { - saveByTenantIdAndCustomerIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_CUSTOMER_ID_CF); + saveByTenantIdAndCustomerIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_CUSTOMER_ID_CF, false); } return saveByTenantIdAndCustomerIdStmt; } private PreparedStatement getSaveByTenantIdAndUserIdStmt() { if (saveByTenantIdAndUserIdStmt == null) { - saveByTenantIdAndUserIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_USER_ID_CF); + saveByTenantIdAndUserIdStmt = getSaveByTenantIdAndCFName(ModelConstants.AUDIT_LOG_BY_USER_ID_CF, false); } return saveByTenantIdAndUserIdStmt; } - private PreparedStatement getSaveByTenantIdAndCFName(String cfName) { - return getSession().prepare(INSERT_INTO + cfName + - "(" + ModelConstants.AUDIT_LOG_ID_PROPERTY + - "," + ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY + - "," + ModelConstants.AUDIT_LOG_CUSTOMER_ID_PROPERTY + - "," + ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY + - "," + ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY + - "," + ModelConstants.AUDIT_LOG_ENTITY_NAME_PROPERTY + - "," + ModelConstants.AUDIT_LOG_USER_ID_PROPERTY + - "," + ModelConstants.AUDIT_LOG_USER_NAME_PROPERTY + - "," + ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY + - "," + ModelConstants.AUDIT_LOG_ACTION_DATA_PROPERTY + - "," + ModelConstants.AUDIT_LOG_ACTION_STATUS_PROPERTY + - "," + ModelConstants.AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY + ")" + - " VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + private PreparedStatement getSaveByTenantIdAndCFName(String cfName, boolean hasPartition) { + List columnsList = new ArrayList(); + columnsList.add(ModelConstants.AUDIT_LOG_ID_PROPERTY); + columnsList.add(ModelConstants.AUDIT_LOG_TENANT_ID_PROPERTY); + columnsList.add(ModelConstants.AUDIT_LOG_CUSTOMER_ID_PROPERTY); + columnsList.add(ModelConstants.AUDIT_LOG_ENTITY_ID_PROPERTY); + columnsList.add(ModelConstants.AUDIT_LOG_ENTITY_TYPE_PROPERTY); + columnsList.add(ModelConstants.AUDIT_LOG_ENTITY_NAME_PROPERTY); + columnsList.add(ModelConstants.AUDIT_LOG_USER_ID_PROPERTY); + columnsList.add(ModelConstants.AUDIT_LOG_USER_NAME_PROPERTY); + columnsList.add(ModelConstants.AUDIT_LOG_ACTION_TYPE_PROPERTY); + columnsList.add(ModelConstants.AUDIT_LOG_ACTION_DATA_PROPERTY); + columnsList.add(ModelConstants.AUDIT_LOG_ACTION_STATUS_PROPERTY); + columnsList.add(ModelConstants.AUDIT_LOG_ACTION_FAILURE_DETAILS_PROPERTY); + if (hasPartition) { + columnsList.add(ModelConstants.AUDIT_LOG_PARTITION_PROPERTY); + } + StringJoiner values = new StringJoiner(","); + for (int i=0;i { @Override public AuditLog toData() { AuditLog auditLog = new AuditLog(new AuditLogId(id)); + auditLog.setCreatedTime(UUIDs.unixTimestamp(id)); if (tenantId != null) { auditLog.setTenantId(new TenantId(tenantId)); } - if (entityId != null & entityType != null) { + if (entityId != null && entityType != null) { auditLog.setEntityId(EntityIdFactory.getByTypeAndUuid(entityType, entityId)); } if (customerId != null) { From d072f86455798eb445a31cc9ac7a3eeb284780ef Mon Sep 17 00:00:00 2001 From: Sergey Tarnavskiy Date: Fri, 23 Feb 2018 13:40:23 +0200 Subject: [PATCH 32/57] changed company logo in html-value-card widget --- .../src/main/data/json/system/widget_bundles/cards.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index ddb90dbaae..a024ce193a 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -66,7 +66,7 @@ "controllerScript": "self.onInit = function() {\n self.ctx.varsRegex = /\\$\\{([^\\}]*)\\}/g;\n self.ctx.htmlSet = false;\n \n var cssParser = new cssjs();\n cssParser.testMode = false;\n var namespace = 'html-value-card-' + hashCode(self.ctx.settings.cardCss);\n cssParser.cssPreviewNamespace = namespace;\n cssParser.createStyleElement(namespace, self.ctx.settings.cardCss);\n self.ctx.$container.addClass(namespace);\n self.ctx.html = self.ctx.settings.cardHtml;\n self.ctx.replaceInfo = processHtmlPattern(self.ctx.html, self.ctx.data);\n \n updateHtml();\n \n function hashCode(str) {\n var hash = 0;\n var i, char;\n if (str.length === 0) return hash;\n for (i = 0; i < str.length; i++) {\n char = str.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n }\n return hash;\n }\n \n function processHtmlPattern(pattern, data) {\n var match = self.ctx.varsRegex.exec(pattern);\n var replaceInfo = {};\n replaceInfo.variables = [];\n while (match !== null) {\n var variableInfo = {};\n variableInfo.dataKeyIndex = -1;\n var variable = match[0];\n var label = match[1];\n var valDec = 2;\n var splitVals = label.split(':');\n if (splitVals.length > 1) {\n label = splitVals[0];\n valDec = parseFloat(splitVals[1]);\n }\n variableInfo.variable = variable;\n variableInfo.valDec = valDec;\n if (label == 'entityName') {\n variableInfo.isEntityName = true;\n } else if (label.startsWith('#')) {\n var keyIndexStr = label.substring(1);\n var n = Math.floor(Number(keyIndexStr));\n if (String(n) === keyIndexStr && n >= 0) {\n variableInfo.dataKeyIndex = n;\n }\n }\n if (!variableInfo.isEntityName && variableInfo.dataKeyIndex === -1) {\n for (var i = 0; i < data.length; i++) {\n var datasourceData = data[i];\n var dataKey = datasourceData.dataKey;\n if (dataKey.label === label) {\n variableInfo.dataKeyIndex = i;\n break;\n }\n }\n }\n replaceInfo.variables.push(variableInfo);\n match = self.ctx.varsRegex.exec(pattern);\n }\n return replaceInfo;\n } \n}\n\nself.onDataUpdated = function() {\n updateHtml();\n}\n\nself.onDestroy = function() {\n}\n\nfunction isNumber(n) {\n return !isNaN(parseFloat(n)) && isFinite(n);\n}\n\nfunction padValue(val, dec, int) {\n var i = 0;\n var s, strVal, n;\n\n val = parseFloat(val);\n n = (val < 0);\n val = Math.abs(val);\n\n if (dec > 0) {\n strVal = val.toFixed(dec).toString().split('.');\n s = int - strVal[0].length;\n\n for (; i < s; ++i) {\n strVal[0] = '0' + strVal[0];\n }\n\n strVal = (n ? '-' : '') + strVal[0] + '.' + strVal[1];\n }\n\n else {\n strVal = Math.round(val).toString();\n s = int - strVal.length;\n\n for (; i < s; ++i) {\n strVal = '0' + strVal;\n }\n\n strVal = (n ? '-' : '') + strVal;\n }\n\n return strVal;\n}\n\nfunction updateHtml() {\n var text = self.ctx.html;\n var updated = false;\n for (var v in self.ctx.replaceInfo.variables) {\n var variableInfo = self.ctx.replaceInfo.variables[v];\n var txtVal = '';\n if (variableInfo.dataKeyIndex > -1) {\n var varData = self.ctx.data[variableInfo.dataKeyIndex].data;\n if (varData.length > 0) {\n var val = varData[varData.length-1][1];\n if (isNumber(val)) {\n txtVal = padValue(val, variableInfo.valDec, 0);\n } else {\n txtVal = val;\n }\n }\n } else if (variableInfo.isEntityName) {\n if (self.ctx.defaultSubscription.datasources.length) {\n txtVal = self.ctx.defaultSubscription.datasources[0].entityName;\n } else {\n txtVal = 'Unknown';\n }\n }\n if (typeof variableInfo.lastVal === undefined ||\n variableInfo.lastVal !== txtVal) {\n updated = true;\n variableInfo.lastVal = txtVal;\n }\n text = text.split(variableInfo.variable).join(txtVal);\n }\n if (updated || !self.ctx.htmlSet) {\n self.ctx.$container.html(text);\n if (!self.ctx.htmlSet) {\n self.ctx.htmlSet = true;\n }\n }\n}\n\n", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"Settings\",\n \"required\": [\"cardHtml\"],\n \"properties\": {\n \"cardCss\": {\n \"title\": \"CSS\",\n \"type\": \"string\",\n \"default\": \".card {\\n font-weight: bold; \\n}\"\n },\n \"cardHtml\": {\n \"title\": \"HTML\",\n \"type\": \"string\",\n \"default\": \"
HTML code here
\"\n }\n }\n },\n \"form\": [\n {\n \"key\": \"cardCss\",\n \"type\": \"css\"\n }, \n {\n \"key\": \"cardHtml\",\n \"type\": \"html\"\n } \n ]\n}", "dataKeySettingsSchema": "{}\n", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"My value\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.random() * 5.45;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"cardCss\":\".card {\\n width: 100%;\\n height: 100%;\\n border: 2px solid #ccc;\\n box-sizing: border-box;\\n}\\n\\n.card .content {\\n padding: 20px;\\n display: flex;\\n flex-direction: row;\\n align-items: center;\\n justify-content: space-around;\\n height: 100%;\\n box-sizing: border-box;\\n}\\n\\n.card .content .column {\\n display: flex;\\n flex-direction: column; \\n justify-content: space-around;\\n height: 100%;\\n}\\n\\n.card h1 {\\n text-transform: uppercase;\\n color: #999;\\n font-size: 20px;\\n font-weight: bold;\\n margin: 0;\\n padding-bottom: 10px;\\n line-height: 32px;\\n}\\n\\n.card .value {\\n font-size: 38px;\\n font-weight: 200;\\n}\\n\\n.card .description {\\n font-size: 20px;\\n color: #999;\\n}\\n\",\"cardHtml\":\"
\\n
\\n
\\n

Value title

\\n
\\n ${My value:2} units.\\n
\\n
\\n Value description text\\n
\\n
\\n \\n
\\n
\"},\"title\":\"HTML Value Card\",\"dropShadow\":false,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"My value\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"return Math.random() * 5.45;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"cardCss\":\".card {\\n width: 100%;\\n height: 100%;\\n border: 2px solid #ccc;\\n box-sizing: border-box;\\n}\\n\\n.card .content {\\n padding: 20px;\\n display: flex;\\n flex-direction: row;\\n align-items: center;\\n justify-content: space-around;\\n height: 100%;\\n box-sizing: border-box;\\n}\\n\\n.card .content .column {\\n display: flex;\\n flex-direction: column; \\n justify-content: space-around;\\n height: 100%;\\n}\\n\\n.card h1 {\\n text-transform: uppercase;\\n color: #999;\\n font-size: 20px;\\n font-weight: bold;\\n margin: 0;\\n padding-bottom: 10px;\\n line-height: 32px;\\n}\\n\\n.card .value {\\n font-size: 38px;\\n font-weight: 200;\\n}\\n\\n.card .description {\\n font-size: 20px;\\n color: #999;\\n}\\n\",\"cardHtml\":\"
\\n
\\n
\\n

Value title

\\n
\\n ${My value:2} units.\\n
\\n
\\n Value description text\\n
\\n
\\n \\n
\\n
\"},\"title\":\"HTML Value Card\",\"dropShadow\":false,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":true,\"showLegend\":false,\"actions\":{}}" } }, { @@ -112,9 +112,9 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('timeseries-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.onDestroy = function() {\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\",\n \"displayPagination\",\n \"defaultPageSize\"\n ]\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\"\n ]\n}", "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false}" } } ] From b08d3a5a2545097e1e168833375ab4cc9d0633b1 Mon Sep 17 00:00:00 2001 From: Sergey Tarnavskiy Date: Fri, 23 Feb 2018 15:55:13 +0200 Subject: [PATCH 33/57] small fix --- .../src/main/data/json/system/widget_bundles/cards.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index a024ce193a..803f1d3953 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -112,9 +112,9 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('timeseries-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.onDestroy = function() {\n}", - "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\"\n ]\n}", + "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\",\n \"displayPagination\",\n \"defaultPageSize\"\n ]\n}", "dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}" } } ] From 52578afd09047903f049f2fce02181425b6684b2 Mon Sep 17 00:00:00 2001 From: oleg Date: Fri, 23 Feb 2018 17:42:56 +0200 Subject: [PATCH 34/57] UI:Fix. "Widget custom CSS editor" it was impossible to use base64 --- ui/src/vendor/css.js/css.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/vendor/css.js/css.js b/ui/src/vendor/css.js/css.js index 8c4160acc5..254e6b5e0e 100644 --- a/ui/src/vendor/css.js/css.js +++ b/ui/src/vendor/css.js/css.js @@ -147,7 +147,8 @@ fi.prototype.parseRules = function (rules) { rules = rules.split('\r\n').join('\n'); var ret = []; - rules = rules.split(';'); + // Split all rules but keep semicolon for base64 url data + rules = rules.split(/;(?!base64)/); //proccess rules line by line for (var i = 0; i < rules.length; i++) { From 4f13461e895a0c1b677b3dcdf07a7be327cde0d9 Mon Sep 17 00:00:00 2001 From: Andrew Shvayka Date: Tue, 27 Feb 2018 16:34:51 +0200 Subject: [PATCH 35/57] Updated Netty Version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 80e846d56f..f37fe81dee 100755 --- a/pom.xml +++ b/pom.xml @@ -64,7 +64,7 @@ 1.0.0 1.16.18 1.1.0 - 4.1.3.Final + 4.1.22.Final 1.5.0 3.6.5 0.9.0.0 From 063a1093e0179ae7840d0f8e3bce3afdeeb08dad Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 27 Feb 2018 18:30:23 +0200 Subject: [PATCH 36/57] Ability to assign dashboards to multiple customers - DAO Layer. --- .../main/data/upgrade/1.4.0/schema_update.cql | 23 +++ .../main/data/upgrade/1.4.0/schema_update.sql | 10 ++ .../server/controller/BaseController.java | 12 +- .../controller/DashboardController.java | 67 +++++--- .../CassandraDatabaseUpgradeService.java | 34 +++- .../service/install/DatabaseHelper.java | 93 ++++++++++ .../DefaultSystemDataLoaderService.java | 6 +- .../install/SqlDatabaseUpgradeService.java | 34 +++- .../install/cql/CassandraDbHelper.java | 5 +- .../service/install/sql/SqlDbHelper.java | 146 ++++++++++++++++ .../BaseDashboardControllerTest.java | 118 ++----------- .../server/common/data/Dashboard.java | 2 - .../server/common/data/DashboardInfo.java | 63 +++++-- .../data/relation/RelationTypeGroup.java | 3 +- .../dao/customer/CustomerServiceImpl.java | 4 +- .../dashboard/CassandraDashboardInfoDao.java | 32 +++- .../dao/dashboard/DashboardInfoDao.java | 4 +- .../dao/dashboard/DashboardService.java | 10 +- .../dao/dashboard/DashboardServiceImpl.java | 159 ++++++++++++++---- .../server/dao/model/ModelConstants.java | 4 +- .../dao/model/nosql/DashboardEntity.java | 51 +++--- .../dao/model/nosql/DashboardInfoEntity.java | 49 ++++-- .../server/dao/model/sql/DashboardEntity.java | 32 +++- .../dao/model/sql/DashboardInfoEntity.java | 33 +++- .../dao/service/TimePaginatedRemover.java | 50 ++++++ .../server/dao/service/Validator.java | 3 +- .../dashboard/DashboardInfoRepository.java | 8 - .../sql/dashboard/JpaDashboardInfoDao.java | 36 +++- dao/src/main/resources/cassandra/schema.cql | 17 +- dao/src/main/resources/sql/schema.sql | 2 +- .../dao/service/BaseDashboardServiceTest.java | 102 ++--------- .../dashboard/JpaDashboardInfoDaoTest.java | 38 +---- 32 files changed, 830 insertions(+), 420 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java create mode 100644 application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java diff --git a/application/src/main/data/upgrade/1.4.0/schema_update.cql b/application/src/main/data/upgrade/1.4.0/schema_update.cql index c5b656feef..7a63bb3fba 100644 --- a/application/src/main/data/upgrade/1.4.0/schema_update.cql +++ b/application/src/main/data/upgrade/1.4.0/schema_update.cql @@ -87,3 +87,26 @@ CREATE TABLE IF NOT EXISTS thingsboard.audit_log_by_tenant_id_partitions ( PRIMARY KEY (( tenant_id ), partition) ) WITH CLUSTERING ORDER BY ( partition ASC ) AND compaction = { 'class' : 'LeveledCompactionStrategy' }; + +DROP MATERIALIZED VIEW IF EXISTS thingsboard.dashboard_by_tenant_and_search_text; +DROP MATERIALIZED VIEW IF EXISTS thingsboard.dashboard_by_customer_and_search_text; + +DROP TABLE IF EXISTS thingsboard.dashboard; + +CREATE TABLE IF NOT EXISTS thingsboard.dashboard ( + id timeuuid, + tenant_id timeuuid, + title text, + search_text text, + assigned_customers text, + configuration text, + PRIMARY KEY (id, tenant_id) +); + +CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.dashboard_by_tenant_and_search_text AS + SELECT * + from thingsboard.dashboard + WHERE tenant_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL + PRIMARY KEY ( tenant_id, search_text, id ) + WITH CLUSTERING ORDER BY ( search_text ASC, id DESC ); + diff --git a/application/src/main/data/upgrade/1.4.0/schema_update.sql b/application/src/main/data/upgrade/1.4.0/schema_update.sql index b2e3a9720f..c635414e52 100644 --- a/application/src/main/data/upgrade/1.4.0/schema_update.sql +++ b/application/src/main/data/upgrade/1.4.0/schema_update.sql @@ -29,3 +29,13 @@ CREATE TABLE IF NOT EXISTS audit_log ( action_failure_details varchar(1000000) ); +DROP TABLE IF EXISTS dashboard; + +CREATE TABLE IF NOT EXISTS dashboard ( + id varchar(31) NOT NULL CONSTRAINT dashboard_pkey PRIMARY KEY, + configuration varchar(10000000), + assigned_customers varchar(1000000), + search_text varchar(255), + tenant_id varchar(31), + title varchar(255) +); diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 28489ca427..a72f2f384b 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -423,7 +423,7 @@ public abstract class BaseController { try { validateId(dashboardId, "Incorrect dashboardId " + dashboardId); Dashboard dashboard = dashboardService.findDashboardById(dashboardId); - checkDashboard(dashboard, true); + checkDashboard(dashboard); return dashboard; } catch (Exception e) { throw handleException(e, false); @@ -435,27 +435,23 @@ public abstract class BaseController { validateId(dashboardId, "Incorrect dashboardId " + dashboardId); DashboardInfo dashboardInfo = dashboardService.findDashboardInfoById(dashboardId); SecurityUser authUser = getCurrentUser(); - checkDashboard(dashboardInfo, authUser.getAuthority() != Authority.SYS_ADMIN); + checkDashboard(dashboardInfo); return dashboardInfo; } catch (Exception e) { throw handleException(e, false); } } - private void checkDashboard(DashboardInfo dashboard, boolean checkCustomerId) throws ThingsboardException { + private void checkDashboard(DashboardInfo dashboard) throws ThingsboardException { checkNotNull(dashboard); checkTenantId(dashboard.getTenantId()); SecurityUser authUser = getCurrentUser(); if (authUser.getAuthority() == Authority.CUSTOMER_USER) { - if (dashboard.getCustomerId() == null || dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { + if (dashboard.getAssignedCustomers() == null || !dashboard.getAssignedCustomers().containsKey(authUser.getCustomerId().toString())) { throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION, ThingsboardErrorCode.PERMISSION_DENIED); } } - if (checkCustomerId && - dashboard.getCustomerId() != null && !dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { - checkCustomerId(dashboard.getCustomerId()); - } } ComponentDescriptor checkComponentDescriptorByClazz(String clazz) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index 34320b12df..30d57f3f4d 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -28,6 +28,8 @@ import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.exception.ThingsboardException; @@ -80,7 +82,7 @@ public class DashboardController extends BaseController { Dashboard savedDashboard = checkNotNull(dashboardService.saveDashboard(dashboard)); logEntityAction(savedDashboard.getId(), savedDashboard, - savedDashboard.getCustomerId(), + null, dashboard.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null); return savedDashboard; @@ -103,7 +105,7 @@ public class DashboardController extends BaseController { dashboardService.deleteDashboard(dashboardId); logEntityAction(dashboardId, dashboard, - dashboard.getCustomerId(), + null, ActionType.DELETED, null, strDashboardId); } catch (Exception e) { @@ -134,7 +136,7 @@ public class DashboardController extends BaseController { Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, customerId)); logEntityAction(dashboardId, savedDashboard, - savedDashboard.getCustomerId(), + customerId, ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, strCustomerId, customer.getName()); @@ -150,23 +152,22 @@ public class DashboardController extends BaseController { } @PreAuthorize("hasAuthority('TENANT_ADMIN')") - @RequestMapping(value = "/customer/dashboard/{dashboardId}", method = RequestMethod.DELETE) + @RequestMapping(value = "/customer/{customerId}/dashboard/{dashboardId}", method = RequestMethod.DELETE) @ResponseBody - public Dashboard unassignDashboardFromCustomer(@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { + public Dashboard unassignDashboardFromCustomer(@PathVariable("customerId") String strCustomerId, + @PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { + checkParameter("customerId", strCustomerId); checkParameter(DASHBOARD_ID, strDashboardId); try { + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + Customer customer = checkCustomerId(customerId); DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); Dashboard dashboard = checkDashboardId(dashboardId); - if (dashboard.getCustomerId() == null || dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { - throw new IncorrectParameterException("Dashboard isn't assigned to any customer!"); - } - - Customer customer = checkCustomerId(dashboard.getCustomerId()); - Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(dashboardId)); + Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(dashboardId, customerId)); logEntityAction(dashboardId, dashboard, - dashboard.getCustomerId(), + customerId, ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDashboardId, customer.getId().toString(), customer.getName()); return savedDashboard; @@ -192,7 +193,7 @@ public class DashboardController extends BaseController { Dashboard savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, publicCustomer.getId())); logEntityAction(dashboardId, savedDashboard, - savedDashboard.getCustomerId(), + publicCustomer.getId(), ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, publicCustomer.getId().toString(), publicCustomer.getName()); return savedDashboard; @@ -206,6 +207,33 @@ public class DashboardController extends BaseController { } } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/customer/public/dashboard/{dashboardId}", method = RequestMethod.DELETE) + @ResponseBody + public Dashboard unassignDashboardFromPublicCustomer(@PathVariable(DASHBOARD_ID) String strDashboardId) throws ThingsboardException { + checkParameter(DASHBOARD_ID, strDashboardId); + try { + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + Dashboard dashboard = checkDashboardId(dashboardId); + Customer publicCustomer = customerService.findOrCreatePublicCustomer(dashboard.getTenantId()); + + Dashboard savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(dashboardId, publicCustomer.getId())); + + logEntityAction(dashboardId, dashboard, + publicCustomer.getId(), + ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDashboardId, publicCustomer.getId().toString(), publicCustomer.getName()); + + return savedDashboard; + } catch (Exception e) { + + logEntityAction(emptyId(EntityType.DASHBOARD), null, + null, + ActionType.UNASSIGNED_FROM_CUSTOMER, e, strDashboardId); + + throw handleException(e); + } + } + @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/tenant/{tenantId}/dashboards", params = { "limit" }, method = RequestMethod.GET) @ResponseBody @@ -245,19 +273,20 @@ public class DashboardController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/customer/{customerId}/dashboards", params = { "limit" }, method = RequestMethod.GET) @ResponseBody - public TextPageData getCustomerDashboards( + public TimePageData getCustomerDashboards( @PathVariable("customerId") String strCustomerId, @RequestParam int limit, - @RequestParam(required = false) String textSearch, - @RequestParam(required = false) String idOffset, - @RequestParam(required = false) String textOffset) throws ThingsboardException { + @RequestParam(required = false) Long startTime, + @RequestParam(required = false) Long endTime, + @RequestParam(required = false, defaultValue = "false") boolean ascOrder, + @RequestParam(required = false) String offset) throws ThingsboardException { checkParameter("customerId", strCustomerId); try { TenantId tenantId = getCurrentUser().getTenantId(); CustomerId customerId = new CustomerId(toUUID(strCustomerId)); checkCustomerId(customerId); - TextPageLink pageLink = createPageLink(limit, textSearch, idOffset, textOffset); - return checkNotNull(dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink)); + TimePageLink pageLink = createPageLink(limit, startTime, endTime, ascOrder, offset); + return checkNotNull(dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink).get()); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java index dacf453605..e3691535bc 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java @@ -24,6 +24,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import org.thingsboard.server.dao.cassandra.CassandraCluster; import org.thingsboard.server.dao.cassandra.CassandraInstallCluster; +import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.util.NoSqlDao; import org.thingsboard.server.service.install.cql.CQLStatementsParser; import org.thingsboard.server.service.install.cql.CassandraDbHelper; @@ -33,6 +34,8 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; +import static org.thingsboard.server.service.install.DatabaseHelper.*; + @Service @NoSqlDao @Profile("install") @@ -40,12 +43,6 @@ import java.util.List; public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService { private static final String SCHEMA_UPDATE_CQL = "schema_update.cql"; - public static final String DEVICE = "device"; - public static final String TENANT_ID = "tenant_id"; - public static final String CUSTOMER_ID = "customer_id"; - public static final String SEARCH_TEXT = "search_text"; - public static final String ADDITIONAL_INFO = "additional_info"; - public static final String ASSET = "asset"; @Value("${install.data_dir}") private String dataDir; @@ -56,6 +53,9 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService { @Autowired private CassandraInstallCluster installCluster; + @Autowired + private DashboardService dashboardService; + @Override public void upgradeDatabase(String fromVersion) throws Exception { @@ -160,10 +160,32 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService { case "1.3.0": break; case "1.3.1": + + cluster.getSession(); + + ks = cluster.getCluster().getMetadata().getKeyspace(cluster.getKeyspaceName()); + + log.info("Dumping dashboards ..."); + Path dashboardsDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), DASHBOARD, + new String[]{ID, TENANT_ID, CUSTOMER_ID, TITLE, SEARCH_TEXT, ASSIGNED_CUSTOMERS, CONFIGURATION}, + new String[]{"", "", "", "", "", "", ""}, + "tb-dashboards"); + log.info("Dashboards dumped."); + + log.info("Updating schema ..."); schemaUpdateFile = Paths.get(this.dataDir, "upgrade", "1.4.0", SCHEMA_UPDATE_CQL); loadCql(schemaUpdateFile); log.info("Schema updated."); + + log.info("Restoring dashboards ..."); + if (dashboardsDump != null) { + CassandraDbHelper.loadCf(ks, cluster.getSession(), DASHBOARD, + new String[]{ID, TENANT_ID, TITLE, SEARCH_TEXT, CONFIGURATION}, dashboardsDump); + DatabaseHelper.upgradeTo40_assignDashboards(dashboardsDump, dashboardService, false); + Files.deleteIfExists(dashboardsDump); + } + log.info("Dashboards restored."); break; default: throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion); diff --git a/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java new file mode 100644 index 0000000000..44f42dd570 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java @@ -0,0 +1,93 @@ +/** + * Copyright © 2016-2017 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.install; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.lang3.StringUtils; +import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.UUIDConverter; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.dao.dashboard.DashboardService; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +/** + * Created by igor on 2/27/18. + */ +@Slf4j +public class DatabaseHelper { + + public static final CSVFormat CSV_DUMP_FORMAT = CSVFormat.DEFAULT.withNullString("\\N"); + + public static final String DEVICE = "device"; + public static final String TENANT_ID = "tenant_id"; + public static final String CUSTOMER_ID = "customer_id"; + public static final String SEARCH_TEXT = "search_text"; + public static final String ADDITIONAL_INFO = "additional_info"; + public static final String ASSET = "asset"; + public static final String DASHBOARD = "dashboard"; + public static final String ID = "id"; + public static final String TITLE = "title"; + public static final String ASSIGNED_CUSTOMERS = "assigned_customers"; + public static final String CONFIGURATION = "configuration"; + + public static final ObjectMapper objectMapper = new ObjectMapper(); + + public static void upgradeTo40_assignDashboards(Path dashboardsDump, DashboardService dashboardService, boolean sql) throws Exception { + String[] columns = new String[]{ID, TENANT_ID, CUSTOMER_ID, TITLE, SEARCH_TEXT, ASSIGNED_CUSTOMERS, CONFIGURATION}; + try (CSVParser csvParser = new CSVParser(Files.newBufferedReader(dashboardsDump), CSV_DUMP_FORMAT.withHeader(columns))) { + csvParser.forEach(record -> { + String customerIdString = record.get(CUSTOMER_ID); + String assignedCustomersString = record.get(ASSIGNED_CUSTOMERS); + DashboardId dashboardId = new DashboardId(toUUID(record.get(ID), sql)); + List customerIds = new ArrayList<>(); + if (!StringUtils.isEmpty(assignedCustomersString)) { + try { + JsonNode assignedCustomersJson = objectMapper.readTree(assignedCustomersString); + Map assignedCustomers = objectMapper.treeToValue(assignedCustomersJson, HashMap.class); + assignedCustomers.forEach((strCustomerId, title) -> { + customerIds.add(new CustomerId(UUID.fromString(strCustomerId))); + }); + } catch (IOException e) { + log.error("Unable to parse assigned customers field", e); + } + } + if (!StringUtils.isEmpty(customerIdString)) { + customerIds.add(new CustomerId(toUUID(customerIdString, sql))); + } + for (CustomerId customerId : customerIds) { + dashboardService.assignDashboardToCustomer(dashboardId, customerId); + } + }); + } + } + + private static UUID toUUID(String src, boolean sql) { + if (sql) { + return UUIDConverter.fromString(src); + } else { + return UUID.fromString(src); + } + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index 4e81f94419..a102dcbc18 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -339,8 +339,10 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { JsonNode dashboardJson = objectMapper.readTree(path.toFile()); Dashboard dashboard = objectMapper.treeToValue(dashboardJson, Dashboard.class); dashboard.setTenantId(tenantId); - dashboard.setCustomerId(customerId); - dashboardService.saveDashboard(dashboard); + Dashboard savedDashboard = dashboardService.saveDashboard(dashboard); + if (customerId != null && !customerId.isNullUid()) { + dashboardService.assignDashboardToCustomer(savedDashboard.getId(), customerId); + } } catch (Exception e) { log.error("Unable to load dashboard from json: [{}]", path.toString()); throw new RuntimeException("Unable to load dashboard from json", e); diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index 098b4fe970..e1e1e5f945 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -17,18 +17,26 @@ package org.thingsboard.server.service.install; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.util.SqlDao; +import org.thingsboard.server.service.install.cql.CassandraDbHelper; +import org.thingsboard.server.service.install.sql.SqlDbHelper; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.sql.Connection; +import java.sql.DatabaseMetaData; import java.sql.DriverManager; +import static org.thingsboard.server.service.install.DatabaseHelper.*; +import static org.thingsboard.server.service.install.DatabaseHelper.CONFIGURATION; + @Service @Profile("install") @Slf4j @@ -49,6 +57,9 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService { @Value("${spring.datasource.password}") private String dbPassword; + @Autowired + private DashboardService dashboardService; + @Override public void upgradeDatabase(String fromVersion) throws Exception { switch (fromVersion) { @@ -62,13 +73,30 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService { log.info("Schema updated."); break; case "1.3.1": - log.info("Updating schema ..."); - schemaUpdateFile = Paths.get(this.dataDir, "upgrade", "1.4.0", SCHEMA_UPDATE_SQL); try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) { + + log.info("Dumping dashboards ..."); + Path dashboardsDump = SqlDbHelper.dumpTableIfExists(conn, DASHBOARD, + new String[]{ID, TENANT_ID, CUSTOMER_ID, TITLE, SEARCH_TEXT, ASSIGNED_CUSTOMERS, CONFIGURATION}, + new String[]{"", "", "", "", "", "", ""}, + "tb-dashboards"); + log.info("Dashboards dumped."); + + log.info("Updating schema ..."); + schemaUpdateFile = Paths.get(this.dataDir, "upgrade", "1.4.0", SCHEMA_UPDATE_SQL); String sql = new String(Files.readAllBytes(schemaUpdateFile), Charset.forName("UTF-8")); conn.createStatement().execute(sql); //NOSONAR, ignoring because method used to execute thingsboard database upgrade script + log.info("Schema updated."); + + log.info("Restoring dashboards ..."); + if (dashboardsDump != null) { + SqlDbHelper.loadTable(conn, DASHBOARD, + new String[]{ID, TENANT_ID, TITLE, SEARCH_TEXT, CONFIGURATION}, dashboardsDump); + DatabaseHelper.upgradeTo40_assignDashboards(dashboardsDump, dashboardService, true); + Files.deleteIfExists(dashboardsDump); + } + log.info("Dashboards restored."); } - log.info("Schema updated."); break; default: throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion); diff --git a/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java index 0a411f65a9..ef4610eaf1 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java +++ b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java @@ -17,7 +17,6 @@ package org.thingsboard.server.service.install.cql; import com.datastax.driver.core.*; -import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVPrinter; import org.apache.commons.csv.CSVRecord; @@ -28,9 +27,9 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.*; -public class CassandraDbHelper { +import static org.thingsboard.server.service.install.DatabaseHelper.CSV_DUMP_FORMAT; - private static final CSVFormat CSV_DUMP_FORMAT = CSVFormat.DEFAULT.withNullString("\\N"); +public class CassandraDbHelper { public static Path dumpCfIfExists(KeyspaceMetadata ks, Session session, String cfName, String[] columns, String[] defaultValues, String dumpPrefix) throws Exception { diff --git a/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java b/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java new file mode 100644 index 0000000000..fa5175fe89 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java @@ -0,0 +1,146 @@ +/** + * Copyright © 2016-2017 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.install.sql; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.csv.CSVRecord; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.thingsboard.server.service.install.DatabaseHelper.CSV_DUMP_FORMAT; + +/** + * Created by igor on 2/27/18. + */ +@Slf4j +public class SqlDbHelper { + + public static Path dumpTableIfExists(Connection conn, String tableName, + String[] columns, String[] defaultValues, String dumpPrefix) throws Exception { + + DatabaseMetaData metaData = conn.getMetaData(); + ResultSet res = metaData.getTables(null, null, tableName, + new String[] {"TABLE"}); + if (res.next()) { + res.close(); + Path dumpFile = Files.createTempFile(dumpPrefix, null); + Files.deleteIfExists(dumpFile); + try (CSVPrinter csvPrinter = new CSVPrinter(Files.newBufferedWriter(dumpFile), CSV_DUMP_FORMAT)) { + try (PreparedStatement stmt = conn.prepareStatement("SELECT * FROM " + tableName)) { + try (ResultSet tableRes = stmt.executeQuery()) { + ResultSetMetaData resMetaData = tableRes.getMetaData(); + Map columnIndexMap = new HashMap<>(); + for (int i = 0; i < resMetaData.getColumnCount(); i++) { + String columnName = resMetaData.getColumnName(i); + columnIndexMap.put(columnName, i); + } + while(tableRes.next()) { + dumpRow(tableRes, columnIndexMap, columns, defaultValues, csvPrinter); + } + } + } + } + return dumpFile; + } else { + return null; + } + } + + public static void loadTable(Connection conn, String tableName, String[] columns, Path sourceFile) throws Exception { + PreparedStatement prepared = conn.prepareStatement(createInsertStatement(tableName, columns)); + prepared.getParameterMetaData(); + try (CSVParser csvParser = new CSVParser(Files.newBufferedReader(sourceFile), CSV_DUMP_FORMAT.withHeader(columns))) { + csvParser.forEach(record -> { + try { + for (int i=0;i columnIndexMap, String[] columns, + String[] defaultValues, CSVPrinter csvPrinter) throws Exception { + List record = new ArrayList<>(); + for (int i=0;i columnIndexMap, ResultSet res) { + int index = columnIndexMap.containsKey(column) ? columnIndexMap.get(column) : -1; + if (index > -1) { + String str; + try { + Object obj = res.getObject(index); + if (obj == null) { + str = ""; + } else { + str = obj.toString(); + } + } catch (Exception e) { + str = ""; + } + return str; + } else { + return defaultValue; + } + } + + private static void setColumnValue(int index, String column, + CSVRecord record, PreparedStatement preparedStatement) throws SQLException { + String value = record.get(column); + int type = preparedStatement.getParameterMetaData().getParameterType(index + 1); + preparedStatement.setObject(index + 1, value, type); + } + + private static String createInsertStatement(String tableName, String[] columns) { + StringBuilder insertStatementBuilder = new StringBuilder(); + insertStatementBuilder.append("INSERT INTO ").append(tableName).append(" ("); + for (String column : columns) { + insertStatementBuilder.append(column).append(","); + } + insertStatementBuilder.deleteCharAt(insertStatementBuilder.length() - 1); + insertStatementBuilder.append(") VALUES ("); + for (String column : columns) { + insertStatementBuilder.append("?").append(","); + } + insertStatementBuilder.deleteCharAt(insertStatementBuilder.length() - 1); + insertStatementBuilder.append(")"); + return insertStatementBuilder.toString(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java index bb064116f1..434862bd9d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java @@ -19,6 +19,7 @@ import static org.hamcrest.Matchers.containsString; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.sql.Time; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -29,6 +30,8 @@ import org.thingsboard.server.common.data.*; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.model.ModelConstants; import org.junit.After; @@ -82,8 +85,6 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest Assert.assertNotNull(savedDashboard.getId()); Assert.assertTrue(savedDashboard.getCreatedTime() > 0); Assert.assertEquals(savedTenant.getId(), savedDashboard.getTenantId()); - Assert.assertNotNull(savedDashboard.getCustomerId()); - Assert.assertEquals(NULL_UUID, savedDashboard.getCustomerId().getId()); Assert.assertEquals(dashboard.getTitle(), savedDashboard.getTitle()); savedDashboard.setTitle("My new dashboard"); @@ -136,17 +137,20 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest Dashboard assignedDashboard = doPost("/api/customer/" + savedCustomer.getId().getId().toString() + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); - Assert.assertEquals(savedCustomer.getId(), assignedDashboard.getCustomerId()); - + + Assert.assertTrue(assignedDashboard.getAssignedCustomers().containsKey(savedCustomer.getId().toString())); + Dashboard foundDashboard = doGet("/api/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); - Assert.assertEquals(savedCustomer.getId(), foundDashboard.getCustomerId()); + Assert.assertTrue(foundDashboard.getAssignedCustomers().containsKey(savedCustomer.getId().toString())); Dashboard unassignedDashboard = - doDelete("/api/customer/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); - Assert.assertEquals(ModelConstants.NULL_UUID, unassignedDashboard.getCustomerId().getId()); - + doDelete("/api/customer/"+savedCustomer.getId().getId().toString()+"/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); + + Assert.assertTrue(unassignedDashboard.getAssignedCustomers() == null || unassignedDashboard.getAssignedCustomers().isEmpty()); + foundDashboard = doGet("/api/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); - Assert.assertEquals(ModelConstants.NULL_UUID, foundDashboard.getCustomerId().getId()); + + Assert.assertTrue(foundDashboard.getAssignedCustomers() == null || foundDashboard.getAssignedCustomers().isEmpty()); } @Test @@ -320,11 +324,11 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest } List loadedDashboards = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(21); - TextPageData pageData = null; + TimePageLink pageLink = new TimePageLink(21); + TimePageData pageData = null; do { - pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?", - new TypeReference>(){}, pageLink); + pageData = doGetTypedWithTimePageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?", + new TypeReference>(){}, pageLink); loadedDashboards.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageData.getNextPageLink(); @@ -336,93 +340,5 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest Assert.assertEquals(dashboards, loadedDashboards); } - - @Test - public void testFindCustomerDashboardsByTitle() throws Exception { - Customer customer = new Customer(); - customer.setTitle("Test customer"); - customer = doPost("/api/customer", customer, Customer.class); - CustomerId customerId = customer.getId(); - - String title1 = "Dashboard title 1"; - List dashboardsTitle1 = new ArrayList<>(); - for (int i=0;i<125;i++) { - Dashboard dashboard = new Dashboard(); - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); - String title = title1+suffix; - title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); - dashboard.setTitle(title); - dashboard = doPost("/api/dashboard", dashboard, Dashboard.class); - dashboardsTitle1.add(new DashboardInfo(doPost("/api/customer/" + customerId.getId().toString() - + "/dashboard/" + dashboard.getId().getId().toString(), Dashboard.class))); - } - String title2 = "Dashboard title 2"; - List dashboardsTitle2 = new ArrayList<>(); - for (int i=0;i<143;i++) { - Dashboard dashboard = new Dashboard(); - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); - String title = title2+suffix; - title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); - dashboard.setTitle(title); - dashboard = doPost("/api/dashboard", dashboard, Dashboard.class); - dashboardsTitle2.add(new DashboardInfo(doPost("/api/customer/" + customerId.getId().toString() - + "/dashboard/" + dashboard.getId().getId().toString(), Dashboard.class))); - } - - List loadedDashboardsTitle1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(18, title1); - TextPageData pageData = null; - do { - pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?", - new TypeReference>(){}, pageLink); - loadedDashboardsTitle1.addAll(pageData.getData()); - if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); - } - } while (pageData.hasNext()); - - Collections.sort(dashboardsTitle1, idComparator); - Collections.sort(loadedDashboardsTitle1, idComparator); - - Assert.assertEquals(dashboardsTitle1, loadedDashboardsTitle1); - - List loadedDashboardsTitle2 = new ArrayList<>(); - pageLink = new TextPageLink(7, title2); - do { - pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?", - new TypeReference>(){}, pageLink); - loadedDashboardsTitle2.addAll(pageData.getData()); - if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); - } - } while (pageData.hasNext()); - - Collections.sort(dashboardsTitle2, idComparator); - Collections.sort(loadedDashboardsTitle2, idComparator); - - Assert.assertEquals(dashboardsTitle2, loadedDashboardsTitle2); - - for (DashboardInfo dashboard : loadedDashboardsTitle1) { - doDelete("/api/customer/dashboard/" + dashboard.getId().getId().toString()) - .andExpect(status().isOk()); - } - - pageLink = new TextPageLink(5, title1); - pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?", - new TypeReference>(){}, pageLink); - Assert.assertFalse(pageData.hasNext()); - Assert.assertEquals(0, pageData.getData().size()); - - for (DashboardInfo dashboard : loadedDashboardsTitle2) { - doDelete("/api/customer/dashboard/" + dashboard.getId().getId().toString()) - .andExpect(status().isOk()); - } - - pageLink = new TextPageLink(9, title2); - pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?", - new TypeReference>(){}, pageLink); - Assert.assertFalse(pageData.hasNext()); - Assert.assertEquals(0, pageData.getData().size()); - } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java b/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java index 71f2b1308b..fa9f048e2a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java @@ -79,8 +79,6 @@ public class Dashboard extends DashboardInfo { StringBuilder builder = new StringBuilder(); builder.append("Dashboard [tenantId="); builder.append(getTenantId()); - builder.append(", customerId="); - builder.append(getCustomerId()); builder.append(", title="); builder.append(getTitle()); builder.append(", configuration="); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java index 15898c6c63..7e18dc6b80 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java @@ -20,11 +20,16 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + public class DashboardInfo extends SearchTextBased implements HasName { private TenantId tenantId; - private CustomerId customerId; private String title; + private Map assignedCustomers; public DashboardInfo() { super(); @@ -37,8 +42,8 @@ public class DashboardInfo extends SearchTextBased implements HasNa public DashboardInfo(DashboardInfo dashboardInfo) { super(dashboardInfo); this.tenantId = dashboardInfo.getTenantId(); - this.customerId = dashboardInfo.getCustomerId(); this.title = dashboardInfo.getTitle(); + this.assignedCustomers = dashboardInfo.getAssignedCustomers(); } public TenantId getTenantId() { @@ -49,14 +54,6 @@ public class DashboardInfo extends SearchTextBased implements HasNa this.tenantId = tenantId; } - public CustomerId getCustomerId() { - return customerId; - } - - public void setCustomerId(CustomerId customerId) { - this.customerId = customerId; - } - public String getTitle() { return title; } @@ -65,6 +62,44 @@ public class DashboardInfo extends SearchTextBased implements HasNa this.title = title; } + public Map getAssignedCustomers() { + return assignedCustomers; + } + + public void setAssignedCustomers(Map assignedCustomers) { + this.assignedCustomers = assignedCustomers; + } + + public boolean addAssignedCustomer(CustomerId customerId, String title) { + if (this.assignedCustomers != null && this.assignedCustomers.containsKey(customerId.toString())) { + return false; + } else { + if (this.assignedCustomers == null) { + this.assignedCustomers = new HashMap<>(); + } + this.assignedCustomers.put(customerId.toString(), title); + return true; + } + } + + public boolean updateAssignedCustomer(CustomerId customerId, String title) { + if (this.assignedCustomers != null && this.assignedCustomers.containsKey(customerId.toString())) { + this.assignedCustomers.put(customerId.toString(), title); + return true; + } else { + return false; + } + } + + public boolean removeAssignedCustomer(CustomerId customerId) { + if (this.assignedCustomers != null && this.assignedCustomers.containsKey(customerId.toString())) { + this.assignedCustomers.remove(customerId.toString()); + return true; + } else { + return false; + } + } + @Override @JsonProperty(access = JsonProperty.Access.READ_ONLY) public String getName() { @@ -80,7 +115,6 @@ public class DashboardInfo extends SearchTextBased implements HasNa public int hashCode() { final int prime = 31; int result = super.hashCode(); - result = prime * result + ((customerId == null) ? 0 : customerId.hashCode()); result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode()); result = prime * result + ((title == null) ? 0 : title.hashCode()); return result; @@ -95,11 +129,6 @@ public class DashboardInfo extends SearchTextBased implements HasNa if (getClass() != obj.getClass()) return false; DashboardInfo other = (DashboardInfo) obj; - if (customerId == null) { - if (other.customerId != null) - return false; - } else if (!customerId.equals(other.customerId)) - return false; if (tenantId == null) { if (other.tenantId != null) return false; @@ -118,8 +147,6 @@ public class DashboardInfo extends SearchTextBased implements HasNa StringBuilder builder = new StringBuilder(); builder.append("DashboardInfo [tenantId="); builder.append(tenantId); - builder.append(", customerId="); - builder.append(customerId); builder.append(", title="); builder.append(title); builder.append("]"); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java index 82798ab792..7c9c5e62fd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationTypeGroup.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.relation; public enum RelationTypeGroup { COMMON, - ALARM + ALARM, + DASHBOARD } diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index f76d654bf9..f118aff7a2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -97,7 +97,9 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom public Customer saveCustomer(Customer customer) { log.trace("Executing saveCustomer [{}]", customer); customerValidator.validate(customer); - return customerDao.save(customer); + Customer savedCustomer = customerDao.save(customer); + dashboardService.updateCustomerDashboards(savedCustomer.getTenantId(), savedCustomer.getId(), savedCustomer.getTitle()); + return savedCustomer; } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardInfoDao.java index d0651a47c3..5f35913088 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/CassandraDashboardInfoDao.java @@ -15,16 +15,26 @@ */ package org.thingsboard.server.dao.dashboard; +import com.google.common.util.concurrent.AsyncFunction; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.nosql.DashboardInfoEntity; import org.thingsboard.server.dao.nosql.CassandraAbstractSearchTextDao; +import org.thingsboard.server.dao.relation.RelationDao; import org.thingsboard.server.dao.util.NoSqlDao; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.UUID; @@ -37,6 +47,9 @@ import static org.thingsboard.server.dao.model.ModelConstants.*; @NoSqlDao public class CassandraDashboardInfoDao extends CassandraAbstractSearchTextDao implements DashboardInfoDao { + @Autowired + private RelationDao relationDao; + @Override protected Class getColumnFamilyClass() { return DashboardInfoEntity.class; @@ -59,15 +72,18 @@ public class CassandraDashboardInfoDao extends CassandraAbstractSearchTextDao findDashboardsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) { + public ListenableFuture> findDashboardsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TimePageLink pageLink) { log.debug("Try to find dashboards by tenantId [{}], customerId[{}] and pageLink [{}]", tenantId, customerId, pageLink); - List dashboardEntities = findPageWithTextSearch(DASHBOARD_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME, - Arrays.asList(eq(DASHBOARD_CUSTOMER_ID_PROPERTY, customerId), - eq(DASHBOARD_TENANT_ID_PROPERTY, tenantId)), - pageLink); - log.trace("Found dashboards [{}] by tenantId [{}], customerId [{}] and pageLink [{}]", dashboardEntities, tenantId, customerId, pageLink); - return DaoUtil.convertDataList(dashboardEntities); + ListenableFuture> relations = relationDao.findRelations(new CustomerId(customerId), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.DASHBOARD, EntityType.DASHBOARD, pageLink); + + return Futures.transform(relations, (AsyncFunction, List>) input -> { + List> dashboardFutures = new ArrayList<>(input.size()); + for (EntityRelation relation : input) { + dashboardFutures.add(findByIdAsync(relation.getTo().getId())); + } + return Futures.successfulAsList(dashboardFutures); + }); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java index a26bd1410f..baa020f95f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardInfoDao.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.dao.dashboard; +import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.Dao; import java.util.List; @@ -44,6 +46,6 @@ public interface DashboardInfoDao extends Dao { * @param pageLink the page link * @return the list of dashboard objects */ - List findDashboardsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink); + ListenableFuture> findDashboardsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TimePageLink pageLink); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java index 74d85445f8..f4af29c88d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java @@ -23,6 +23,10 @@ import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.TimePageLink; + +import java.sql.Time; public interface DashboardService { @@ -38,7 +42,7 @@ public interface DashboardService { Dashboard assignDashboardToCustomer(DashboardId dashboardId, CustomerId customerId); - Dashboard unassignDashboardFromCustomer(DashboardId dashboardId); + Dashboard unassignDashboardFromCustomer(DashboardId dashboardId, CustomerId customerId); void deleteDashboard(DashboardId dashboardId); @@ -46,8 +50,10 @@ public interface DashboardService { void deleteDashboardsByTenantId(TenantId tenantId); - TextPageData findDashboardsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink); + ListenableFuture> findDashboardsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink); void unassignCustomerDashboards(TenantId tenantId, CustomerId customerId); + void updateCustomerDashboards(TenantId tenantId, CustomerId customerId, String customerTitle); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 53fdb731dc..b9fa3abdc9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -15,30 +15,42 @@ */ package org.thingsboard.server.dao.dashboard; +import com.google.common.base.Function; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.DashboardInfo; -import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.*; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.relation.RelationDao; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; +import org.thingsboard.server.dao.service.TimePaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.tenant.TenantDao; +import javax.annotation.Nullable; +import java.sql.Time; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -59,7 +71,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Autowired private CustomerDao customerDao; - + @Override public Dashboard findDashboardById(DashboardId dashboardId) { log.trace("Executing findDashboardById [{}]", dashboardId); @@ -98,15 +110,59 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Override public Dashboard assignDashboardToCustomer(DashboardId dashboardId, CustomerId customerId) { Dashboard dashboard = findDashboardById(dashboardId); - dashboard.setCustomerId(customerId); - return saveDashboard(dashboard); + Customer customer = customerDao.findById(customerId.getId()); + if (customer == null) { + throw new DataValidationException("Can't assign dashboard to non-existent customer!"); + } + if (!customer.getTenantId().getId().equals(dashboard.getTenantId().getId())) { + throw new DataValidationException("Can't assign dashboard to customer from different tenant!"); + } + if (dashboard.addAssignedCustomer(customerId, customer.getTitle())) { + try { + createRelation(new EntityRelation(customerId, dashboardId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.DASHBOARD)); + } catch (ExecutionException | InterruptedException e) { + log.warn("[{}] Failed to create dashboard relation. Customer Id: [{}]", dashboardId, customerId); + throw new RuntimeException(e); + } + return saveDashboard(dashboard); + } else { + return dashboard; + } } @Override - public Dashboard unassignDashboardFromCustomer(DashboardId dashboardId) { + public Dashboard unassignDashboardFromCustomer(DashboardId dashboardId, CustomerId customerId) { Dashboard dashboard = findDashboardById(dashboardId); - dashboard.setCustomerId(null); - return saveDashboard(dashboard); + if (dashboard.removeAssignedCustomer(customerId)) { + try { + deleteRelation(new EntityRelation(customerId, dashboardId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.DASHBOARD)); + } catch (ExecutionException | InterruptedException e) { + log.warn("[{}] Failed to delete dashboard relation. Customer Id: [{}]", dashboardId, customerId); + throw new RuntimeException(e); + } + return saveDashboard(dashboard); + } else { + return dashboard; + } + } + + private Dashboard updateAssignedCustomerTitle(DashboardId dashboardId, CustomerId customerId, String customerTitle) { + Dashboard dashboard = findDashboardById(dashboardId); + if (dashboard.updateAssignedCustomer(customerId, customerTitle)) { + return saveDashboard(dashboard); + } else { + return dashboard; + } + } + + private void deleteRelation(EntityRelation dashboardRelation) throws ExecutionException, InterruptedException { + log.debug("Deleting Dashboard relation: {}", dashboardRelation); + relationService.deleteRelationAsync(dashboardRelation).get(); + } + + private void createRelation(EntityRelation dashboardRelation) throws ExecutionException, InterruptedException { + log.debug("Creating Dashboard relation: {}", dashboardRelation); + relationService.saveRelationAsync(dashboardRelation).get(); } @Override @@ -134,13 +190,20 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb } @Override - public TextPageData findDashboardsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TextPageLink pageLink) { + public ListenableFuture> findDashboardsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) { log.trace("Executing findDashboardsByTenantIdAndCustomerId, tenantId [{}], customerId [{}], pageLink [{}]", tenantId, customerId, pageLink); Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); Validator.validateId(customerId, "Incorrect customerId " + customerId); Validator.validatePageLink(pageLink, "Incorrect page link " + pageLink); - List dashboards = dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); - return new TextPageData<>(dashboards, pageLink); + ListenableFuture> dashboards = dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(tenantId.getId(), customerId.getId(), pageLink); + + return Futures.transform(dashboards, new Function, TimePageData>() { + @Nullable + @Override + public TimePageData apply(@Nullable List dashboards) { + return new TimePageData<>(dashboards, pageLink); + } + }); } @Override @@ -148,9 +211,18 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb log.trace("Executing unassignCustomerDashboards, tenantId [{}], customerId [{}]", tenantId, customerId); Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); Validator.validateId(customerId, "Incorrect customerId " + customerId); - new CustomerDashboardsUnassigner(tenantId).removeEntities(customerId); + new CustomerDashboardsUnassigner(tenantId, customerId).removeEntities(customerId); } - + + @Override + public void updateCustomerDashboards(TenantId tenantId, CustomerId customerId, String customerTitle) { + log.trace("Executing updateCustomerDashboards, tenantId [{}], customerId [{}], customerTitle [{}]", tenantId, customerId, customerTitle); + Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + Validator.validateId(customerId, "Incorrect customerId " + customerId); + Validator.validateString(customerTitle, "Incorrect customerTitle " + customerTitle); + new CustomerDashboardsUpdater(tenantId, customerId, customerTitle).removeEntities(customerId); + } + private DataValidator dashboardValidator = new DataValidator() { @Override @@ -166,17 +238,6 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb throw new DataValidationException("Dashboard is referencing to non-existent tenant!"); } } - if (dashboard.getCustomerId() == null) { - dashboard.setCustomerId(new CustomerId(ModelConstants.NULL_UUID)); - } else if (!dashboard.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { - Customer customer = customerDao.findById(dashboard.getCustomerId().getId()); - if (customer == null) { - throw new DataValidationException("Can't assign dashboard to non-existent customer!"); - } - if (!customer.getTenantId().getId().equals(dashboard.getTenantId().getId())) { - throw new DataValidationException("Can't assign dashboard to customer from different tenant!"); - } - } } }; @@ -194,24 +255,60 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb } }; - private class CustomerDashboardsUnassigner extends PaginatedRemover { + private class CustomerDashboardsUnassigner extends TimePaginatedRemover { private TenantId tenantId; + private CustomerId customerId; - CustomerDashboardsUnassigner(TenantId tenantId) { + CustomerDashboardsUnassigner(TenantId tenantId, CustomerId customerId) { this.tenantId = tenantId; + this.customerId = customerId; } @Override - protected List findEntities(CustomerId id, TextPageLink pageLink) { - return dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(tenantId.getId(), id.getId(), pageLink); + protected List findEntities(CustomerId id, TimePageLink pageLink) { + try { + return dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(tenantId.getId(), id.getId(), pageLink).get(); + } catch (InterruptedException | ExecutionException e) { + log.warn("Failed to get dashboards by tenantId [{}] and customerId [{}].", tenantId, id); + throw new RuntimeException(e); + } } @Override protected void removeEntity(DashboardInfo entity) { - unassignDashboardFromCustomer(new DashboardId(entity.getUuidId())); + unassignDashboardFromCustomer(new DashboardId(entity.getUuidId()), this.customerId); } } + private class CustomerDashboardsUpdater extends TimePaginatedRemover { + + private TenantId tenantId; + private CustomerId customerId; + private String customerTitle; + + CustomerDashboardsUpdater(TenantId tenantId, CustomerId customerId, String customerTitle) { + this.tenantId = tenantId; + this.customerId = customerId; + this.customerTitle = customerTitle; + } + + @Override + protected List findEntities(CustomerId id, TimePageLink pageLink) { + try { + return dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(tenantId.getId(), id.getId(), pageLink).get(); + } catch (InterruptedException | ExecutionException e) { + log.warn("Failed to get dashboards by tenantId [{}] and customerId [{}].", tenantId, id); + throw new RuntimeException(e); + } + } + + @Override + protected void removeEntity(DashboardInfo entity) { + updateAssignedCustomerTitle(new DashboardId(entity.getUuidId()), this.customerId, this.customerTitle); + } + + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 66e03b0629..c275ad0f6c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -266,13 +266,11 @@ public class ModelConstants { */ public static final String DASHBOARD_COLUMN_FAMILY_NAME = "dashboard"; public static final String DASHBOARD_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; - public static final String DASHBOARD_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY; public static final String DASHBOARD_TITLE_PROPERTY = TITLE_PROPERTY; public static final String DASHBOARD_CONFIGURATION_PROPERTY = "configuration"; + public static final String DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY = "assigned_customers"; public static final String DASHBOARD_BY_TENANT_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "dashboard_by_tenant_and_search_text"; - public static final String DASHBOARD_BY_CUSTOMER_AND_SEARCH_TEXT_COLUMN_FAMILY_NAME = "dashboard_by_customer_and_search_text"; - /** * Cassandra plugin metadata constants. diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardEntity.java index 047a8f269e..8590c2a36f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardEntity.java @@ -19,16 +19,19 @@ import com.datastax.driver.core.utils.UUIDs; import com.datastax.driver.mapping.annotations.Column; import com.datastax.driver.mapping.annotations.PartitionKey; import com.datastax.driver.mapping.annotations.Table; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.EqualsAndHashCode; import lombok.ToString; +import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.model.type.JsonCodec; +import java.util.HashMap; import java.util.UUID; import static org.thingsboard.server.dao.model.ModelConstants.*; @@ -36,8 +39,11 @@ import static org.thingsboard.server.dao.model.ModelConstants.*; @Table(name = DASHBOARD_COLUMN_FAMILY_NAME) @EqualsAndHashCode @ToString +@Slf4j public final class DashboardEntity implements SearchTextEntity { - + + private static final ObjectMapper objectMapper = new ObjectMapper(); + @PartitionKey(value = 0) @Column(name = ID_PROPERTY) private UUID id; @@ -46,16 +52,15 @@ public final class DashboardEntity implements SearchTextEntity { @Column(name = DASHBOARD_TENANT_ID_PROPERTY) private UUID tenantId; - @PartitionKey(value = 2) - @Column(name = DASHBOARD_CUSTOMER_ID_PROPERTY) - private UUID customerId; - @Column(name = DASHBOARD_TITLE_PROPERTY) private String title; @Column(name = SEARCH_TEXT_PROPERTY) private String searchText; - + + @Column(name = DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY, codec = JsonCodec.class) + private JsonNode assignedCustomers; + @Column(name = DASHBOARD_CONFIGURATION_PROPERTY, codec = JsonCodec.class) private JsonNode configuration; @@ -70,10 +75,10 @@ public final class DashboardEntity implements SearchTextEntity { if (dashboard.getTenantId() != null) { this.tenantId = dashboard.getTenantId().getId(); } - if (dashboard.getCustomerId() != null) { - this.customerId = dashboard.getCustomerId().getId(); - } this.title = dashboard.getTitle(); + if (dashboard.getAssignedCustomers() != null) { + this.assignedCustomers = objectMapper.valueToTree(dashboard.getAssignedCustomers()); + } this.configuration = dashboard.getConfiguration(); } @@ -93,14 +98,6 @@ public final class DashboardEntity implements SearchTextEntity { this.tenantId = tenantId; } - public UUID getCustomerId() { - return customerId; - } - - public void setCustomerId(UUID customerId) { - this.customerId = customerId; - } - public String getTitle() { return title; } @@ -109,6 +106,14 @@ public final class DashboardEntity implements SearchTextEntity { this.title = title; } + public JsonNode getAssignedCustomers() { + return assignedCustomers; + } + + public void setAssignedCustomers(JsonNode assignedCustomers) { + this.assignedCustomers = assignedCustomers; + } + public JsonNode getConfiguration() { return configuration; } @@ -138,10 +143,14 @@ public final class DashboardEntity implements SearchTextEntity { if (tenantId != null) { dashboard.setTenantId(new TenantId(tenantId)); } - if (customerId != null) { - dashboard.setCustomerId(new CustomerId(customerId)); - } dashboard.setTitle(title); + if (assignedCustomers != null) { + try { + dashboard.setAssignedCustomers(objectMapper.treeToValue(assignedCustomers, HashMap.class)); + } catch (JsonProcessingException e) { + log.warn("Unable to parse assigned customers!", e); + } + } dashboard.setConfiguration(configuration); return dashboard; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardInfoEntity.java index a2a5280a30..609f3bb598 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardInfoEntity.java @@ -19,14 +19,21 @@ import com.datastax.driver.core.utils.UUIDs; import com.datastax.driver.mapping.annotations.Column; import com.datastax.driver.mapping.annotations.PartitionKey; import com.datastax.driver.mapping.annotations.Table; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.EqualsAndHashCode; import lombok.ToString; +import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.SearchTextEntity; +import org.thingsboard.server.dao.model.type.JsonCodec; +import java.util.HashMap; +import java.util.Set; import java.util.UUID; import static org.thingsboard.server.dao.model.ModelConstants.*; @@ -34,8 +41,11 @@ import static org.thingsboard.server.dao.model.ModelConstants.*; @Table(name = DASHBOARD_COLUMN_FAMILY_NAME) @EqualsAndHashCode @ToString +@Slf4j public class DashboardInfoEntity implements SearchTextEntity { + private static final ObjectMapper objectMapper = new ObjectMapper(); + @PartitionKey(value = 0) @Column(name = ID_PROPERTY) private UUID id; @@ -44,16 +54,15 @@ public class DashboardInfoEntity implements SearchTextEntity { @Column(name = DASHBOARD_TENANT_ID_PROPERTY) private UUID tenantId; - @PartitionKey(value = 2) - @Column(name = DASHBOARD_CUSTOMER_ID_PROPERTY) - private UUID customerId; - @Column(name = DASHBOARD_TITLE_PROPERTY) private String title; @Column(name = SEARCH_TEXT_PROPERTY) private String searchText; + @Column(name = DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY, codec = JsonCodec.class) + private JsonNode assignedCustomers; + public DashboardInfoEntity() { super(); } @@ -65,10 +74,10 @@ public class DashboardInfoEntity implements SearchTextEntity { if (dashboardInfo.getTenantId() != null) { this.tenantId = dashboardInfo.getTenantId().getId(); } - if (dashboardInfo.getCustomerId() != null) { - this.customerId = dashboardInfo.getCustomerId().getId(); - } this.title = dashboardInfo.getTitle(); + if (dashboardInfo.getAssignedCustomers() != null) { + this.assignedCustomers = objectMapper.valueToTree(dashboardInfo.getAssignedCustomers()); + } } public UUID getId() { @@ -87,14 +96,6 @@ public class DashboardInfoEntity implements SearchTextEntity { this.tenantId = tenantId; } - public UUID getCustomerId() { - return customerId; - } - - public void setCustomerId(UUID customerId) { - this.customerId = customerId; - } - public String getTitle() { return title; } @@ -103,6 +104,14 @@ public class DashboardInfoEntity implements SearchTextEntity { this.title = title; } + public JsonNode getAssignedCustomers() { + return assignedCustomers; + } + + public void setAssignedCustomers(JsonNode assignedCustomers) { + this.assignedCustomers = assignedCustomers; + } + @Override public String getSearchTextSource() { return getTitle(); @@ -124,10 +133,14 @@ public class DashboardInfoEntity implements SearchTextEntity { if (tenantId != null) { dashboardInfo.setTenantId(new TenantId(tenantId)); } - if (customerId != null) { - dashboardInfo.setCustomerId(new CustomerId(customerId)); - } dashboardInfo.setTitle(title); + if (assignedCustomers != null) { + try { + dashboardInfo.setAssignedCustomers(objectMapper.treeToValue(assignedCustomers, HashMap.class)); + } catch (JsonProcessingException e) { + log.warn("Unable to parse assigned customers!", e); + } + } return dashboardInfo; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java index bd0e0dc39f..6f7810bf55 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java @@ -16,9 +16,12 @@ package org.thingsboard.server.dao.model.sql; import com.datastax.driver.core.utils.UUIDs; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; import org.thingsboard.server.common.data.Dashboard; @@ -33,26 +36,33 @@ import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; +import java.util.HashMap; +import java.util.List; +import java.util.Set; @Data +@Slf4j @EqualsAndHashCode(callSuper = true) @Entity @TypeDef(name = "json", typeClass = JsonStringType.class) @Table(name = ModelConstants.DASHBOARD_COLUMN_FAMILY_NAME) public final class DashboardEntity extends BaseSqlEntity implements SearchTextEntity { + private static final ObjectMapper objectMapper = new ObjectMapper(); + @Column(name = ModelConstants.DASHBOARD_TENANT_ID_PROPERTY) private String tenantId; - @Column(name = ModelConstants.DASHBOARD_CUSTOMER_ID_PROPERTY) - private String customerId; - @Column(name = ModelConstants.DASHBOARD_TITLE_PROPERTY) private String title; @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) private String searchText; + @Type(type = "json") + @Column(name = ModelConstants.DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY) + private JsonNode assignedCustomers; + @Type(type = "json") @Column(name = ModelConstants.DASHBOARD_CONFIGURATION_PROPERTY) private JsonNode configuration; @@ -68,10 +78,10 @@ public final class DashboardEntity extends BaseSqlEntity implements S if (dashboard.getTenantId() != null) { this.tenantId = toString(dashboard.getTenantId().getId()); } - if (dashboard.getCustomerId() != null) { - this.customerId = toString(dashboard.getCustomerId().getId()); - } this.title = dashboard.getTitle(); + if (dashboard.getAssignedCustomers() != null) { + this.assignedCustomers = objectMapper.valueToTree(dashboard.getAssignedCustomers()); + } this.configuration = dashboard.getConfiguration(); } @@ -92,10 +102,14 @@ public final class DashboardEntity extends BaseSqlEntity implements S if (tenantId != null) { dashboard.setTenantId(new TenantId(toUUID(tenantId))); } - if (customerId != null) { - dashboard.setCustomerId(new CustomerId(toUUID(customerId))); - } dashboard.setTitle(title); + if (assignedCustomers != null) { + try { + dashboard.setAssignedCustomers(objectMapper.treeToValue(assignedCustomers, HashMap.class)); + } catch (JsonProcessingException e) { + log.warn("Unable to parse assigned customers!", e); + } + } dashboard.setConfiguration(configuration); return dashboard; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java index c87e97d2e3..d9b8efe8f4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java @@ -16,8 +16,13 @@ package org.thingsboard.server.dao.model.sql; import com.datastax.driver.core.utils.UUIDs; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.annotations.Type; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; @@ -29,25 +34,31 @@ import org.thingsboard.server.dao.model.SearchTextEntity; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; +import java.util.HashMap; +import java.util.Set; @Data +@Slf4j @EqualsAndHashCode(callSuper = true) @Entity @Table(name = ModelConstants.DASHBOARD_COLUMN_FAMILY_NAME) public class DashboardInfoEntity extends BaseSqlEntity implements SearchTextEntity { + private static final ObjectMapper objectMapper = new ObjectMapper(); + @Column(name = ModelConstants.DASHBOARD_TENANT_ID_PROPERTY) private String tenantId; - @Column(name = ModelConstants.DASHBOARD_CUSTOMER_ID_PROPERTY) - private String customerId; - @Column(name = ModelConstants.DASHBOARD_TITLE_PROPERTY) private String title; @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) private String searchText; + @Type(type = "json") + @Column(name = ModelConstants.DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY) + private JsonNode assignedCustomers; + public DashboardInfoEntity() { super(); } @@ -59,10 +70,10 @@ public class DashboardInfoEntity extends BaseSqlEntity implements if (dashboardInfo.getTenantId() != null) { this.tenantId = toString(dashboardInfo.getTenantId().getId()); } - if (dashboardInfo.getCustomerId() != null) { - this.customerId = toString(dashboardInfo.getCustomerId().getId()); - } this.title = dashboardInfo.getTitle(); + if (dashboardInfo.getAssignedCustomers() != null) { + this.assignedCustomers = objectMapper.valueToTree(dashboardInfo.getAssignedCustomers()); + } } @Override @@ -86,10 +97,14 @@ public class DashboardInfoEntity extends BaseSqlEntity implements if (tenantId != null) { dashboardInfo.setTenantId(new TenantId(toUUID(tenantId))); } - if (customerId != null) { - dashboardInfo.setCustomerId(new CustomerId(toUUID(customerId))); - } dashboardInfo.setTitle(title); + if (assignedCustomers != null) { + try { + dashboardInfo.setAssignedCustomers(objectMapper.treeToValue(assignedCustomers, HashMap.class)); + } catch (JsonProcessingException e) { + log.warn("Unable to parse assigned customers!", e); + } + } return dashboardInfo; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java b/dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java new file mode 100644 index 0000000000..b52f7475b2 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/service/TimePaginatedRemover.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2017 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.service; + +import org.thingsboard.server.common.data.id.IdBased; +import org.thingsboard.server.common.data.page.TimePageLink; + +import java.sql.Time; +import java.util.List; +import java.util.UUID; + +public abstract class TimePaginatedRemover> { + + private static final int DEFAULT_LIMIT = 100; + + public void removeEntities(I id) { + TimePageLink pageLink = new TimePageLink(DEFAULT_LIMIT); + boolean hasNext = true; + while (hasNext) { + List entities = findEntities(id, pageLink); + for (D entity : entities) { + removeEntity(entity); + } + hasNext = entities.size() == pageLink.getLimit(); + if (hasNext) { + int index = entities.size() - 1; + UUID idOffset = entities.get(index).getUuidId(); + pageLink.setIdOffset(idOffset); + } + } + } + + protected abstract List findEntities(I id, TimePageLink pageLink); + + protected abstract void removeEntity(D entity); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java index d8a511d9c7..d962977d98 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/Validator.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.service; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UUIDBased; +import org.thingsboard.server.common.data.page.BasePageLink; import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.dao.exception.IncorrectParameterException; @@ -116,7 +117,7 @@ public class Validator { * @param pageLink the page link * @param errorMessage the error message for exception */ - public static void validatePageLink(TextPageLink pageLink, String errorMessage) { + public static void validatePageLink(BasePageLink pageLink, String errorMessage) { if (pageLink == null || pageLink.getLimit() < 1 || (pageLink.getIdOffset() != null && pageLink.getIdOffset().version() != 1)) { throw new IncorrectParameterException(errorMessage); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java index 7f0ec7abf6..ef7d0a09b3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java @@ -39,12 +39,4 @@ public interface DashboardInfoRepository extends CrudRepository :idOffset ORDER BY di.id") - List findByTenantIdAndCustomerId(@Param("tenantId") String tenantId, - @Param("customerId") String customerId, - @Param("searchText") String searchText, - @Param("idOffset") String idOffset, - Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java index 204189af11..1e343a48f8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java @@ -15,19 +15,31 @@ */ package org.thingsboard.server.dao.sql.dashboard; +import com.google.common.util.concurrent.AsyncFunction; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.UUIDConverter; +import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.dashboard.DashboardInfoDao; import org.thingsboard.server.dao.model.sql.DashboardInfoEntity; +import org.thingsboard.server.dao.relation.RelationDao; import org.thingsboard.server.dao.sql.JpaAbstractSearchTextDao; import org.thingsboard.server.dao.util.SqlDao; +import java.sql.Time; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.UUID; @@ -37,10 +49,14 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID_STR; /** * Created by Valerii Sosliuk on 5/6/2017. */ +@Slf4j @Component @SqlDao public class JpaDashboardInfoDao extends JpaAbstractSearchTextDao implements DashboardInfoDao { + @Autowired + private RelationDao relationDao; + @Autowired private DashboardInfoRepository dashboardInfoRepository; @@ -65,13 +81,17 @@ public class JpaDashboardInfoDao extends JpaAbstractSearchTextDao findDashboardsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TextPageLink pageLink) { - return DaoUtil.convertDataList(dashboardInfoRepository - .findByTenantIdAndCustomerId( - UUIDConverter.fromTimeUUID(tenantId), - UUIDConverter.fromTimeUUID(customerId), - Objects.toString(pageLink.getTextSearch(), ""), - pageLink.getIdOffset() == null ? NULL_UUID_STR : UUIDConverter.fromTimeUUID(pageLink.getIdOffset()), - new PageRequest(0, pageLink.getLimit()))); + public ListenableFuture> findDashboardsByTenantIdAndCustomerId(UUID tenantId, UUID customerId, TimePageLink pageLink) { + log.debug("Try to find dashboards by tenantId [{}], customerId[{}] and pageLink [{}]", tenantId, customerId, pageLink); + + ListenableFuture> relations = relationDao.findRelations(new CustomerId(customerId), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.DASHBOARD, EntityType.DASHBOARD, pageLink); + + return Futures.transform(relations, (AsyncFunction, List>) input -> { + List> dashboardFutures = new ArrayList<>(input.size()); + for (EntityRelation relation : input) { + dashboardFutures.add(findByIdAsync(relation.getTo().getId())); + } + return Futures.successfulAsList(dashboardFutures); + }); } } diff --git a/dao/src/main/resources/cassandra/schema.cql b/dao/src/main/resources/cassandra/schema.cql index bc7884dfbe..dc83045eb4 100644 --- a/dao/src/main/resources/cassandra/schema.cql +++ b/dao/src/main/resources/cassandra/schema.cql @@ -364,26 +364,19 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.widget_type_by_tenant_and_ali CREATE TABLE IF NOT EXISTS thingsboard.dashboard ( id timeuuid, tenant_id timeuuid, - customer_id timeuuid, title text, search_text text, + assigned_customers text, configuration text, - PRIMARY KEY (id, tenant_id, customer_id) + PRIMARY KEY (id, tenant_id) ); CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.dashboard_by_tenant_and_search_text AS SELECT * from thingsboard.dashboard - WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( tenant_id, search_text, id, customer_id ) - WITH CLUSTERING ORDER BY ( search_text ASC, id DESC, customer_id DESC ); - -CREATE MATERIALIZED VIEW IF NOT EXISTS thingsboard.dashboard_by_customer_and_search_text AS - SELECT * - from thingsboard.dashboard - WHERE tenant_id IS NOT NULL AND customer_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL - PRIMARY KEY ( customer_id, tenant_id, search_text, id ) - WITH CLUSTERING ORDER BY ( tenant_id DESC, search_text ASC, id DESC ); + WHERE tenant_id IS NOT NULL AND search_text IS NOT NULL AND id IS NOT NULL + PRIMARY KEY ( tenant_id, search_text, id ) + WITH CLUSTERING ORDER BY ( search_text ASC, id DESC ); CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_cf ( entity_type text, // (DEVICE, CUSTOMER, TENANT) diff --git a/dao/src/main/resources/sql/schema.sql b/dao/src/main/resources/sql/schema.sql index 1f739f8ea3..53ec2235e2 100644 --- a/dao/src/main/resources/sql/schema.sql +++ b/dao/src/main/resources/sql/schema.sql @@ -105,7 +105,7 @@ CREATE TABLE IF NOT EXISTS customer ( CREATE TABLE IF NOT EXISTS dashboard ( id varchar(31) NOT NULL CONSTRAINT dashboard_pkey PRIMARY KEY, configuration varchar(10000000), - customer_id varchar(31), + assigned_customers varchar(1000000), search_text varchar(255), tenant_id varchar(31), title varchar(255) diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java index f0ed0b3474..0427fb44f7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java @@ -29,13 +29,17 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.TextPageData; import org.thingsboard.server.common.data.page.TextPageLink; +import org.thingsboard.server.common.data.page.TimePageData; +import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import java.io.IOException; +import java.sql.Time; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.ExecutionException; public abstract class BaseDashboardServiceTest extends AbstractServiceTest { @@ -68,8 +72,6 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { Assert.assertNotNull(savedDashboard.getId()); Assert.assertTrue(savedDashboard.getCreatedTime() > 0); Assert.assertEquals(dashboard.getTenantId(), savedDashboard.getTenantId()); - Assert.assertNotNull(savedDashboard.getCustomerId()); - Assert.assertEquals(ModelConstants.NULL_UUID, savedDashboard.getCustomerId().getId()); Assert.assertEquals(dashboard.getTitle(), savedDashboard.getTitle()); savedDashboard.setTitle("My new dashboard"); @@ -280,7 +282,7 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { } @Test - public void testFindDashboardsByTenantIdAndCustomerId() { + public void testFindDashboardsByTenantIdAndCustomerId() throws ExecutionException, InterruptedException { Tenant tenant = new Tenant(); tenant.setTitle("Test tenant"); tenant = tenantService.saveTenant(tenant); @@ -303,10 +305,10 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { } List loadedDashboards = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(23); - TextPageData pageData = null; + TimePageLink pageLink = new TimePageLink(23); + TimePageData pageData = null; do { - pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink); + pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink).get(); loadedDashboards.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageData.getNextPageLink(); @@ -320,96 +322,12 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { dashboardService.unassignCustomerDashboards(tenantId, customerId); - pageLink = new TextPageLink(42); - pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink); + pageLink = new TimePageLink(42); + pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink).get(); Assert.assertFalse(pageData.hasNext()); Assert.assertTrue(pageData.getData().isEmpty()); tenantService.deleteTenant(tenantId); } - - @Test - public void testFindDashboardsByTenantIdCustomerIdAndTitle() { - - Customer customer = new Customer(); - customer.setTitle("Test customer"); - customer.setTenantId(tenantId); - customer = customerService.saveCustomer(customer); - CustomerId customerId = customer.getId(); - - String title1 = "Dashboard title 1"; - List dashboardsTitle1 = new ArrayList<>(); - for (int i=0;i<124;i++) { - Dashboard dashboard = new Dashboard(); - dashboard.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); - String title = title1+suffix; - title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); - dashboard.setTitle(title); - dashboard = dashboardService.saveDashboard(dashboard); - dashboardsTitle1.add(new DashboardInfo(dashboardService.assignDashboardToCustomer(dashboard.getId(), customerId))); - } - String title2 = "Dashboard title 2"; - List dashboardsTitle2 = new ArrayList<>(); - for (int i=0;i<151;i++) { - Dashboard dashboard = new Dashboard(); - dashboard.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); - String title = title2+suffix; - title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); - dashboard.setTitle(title); - dashboard = dashboardService.saveDashboard(dashboard); - dashboardsTitle2.add(new DashboardInfo(dashboardService.assignDashboardToCustomer(dashboard.getId(), customerId))); - } - - List loadedDashboardsTitle1 = new ArrayList<>(); - TextPageLink pageLink = new TextPageLink(24, title1); - TextPageData pageData = null; - do { - pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink); - loadedDashboardsTitle1.addAll(pageData.getData()); - if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); - } - } while (pageData.hasNext()); - - Collections.sort(dashboardsTitle1, idComparator); - Collections.sort(loadedDashboardsTitle1, idComparator); - - Assert.assertEquals(dashboardsTitle1, loadedDashboardsTitle1); - - List loadedDashboardsTitle2 = new ArrayList<>(); - pageLink = new TextPageLink(4, title2); - do { - pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink); - loadedDashboardsTitle2.addAll(pageData.getData()); - if (pageData.hasNext()) { - pageLink = pageData.getNextPageLink(); - } - } while (pageData.hasNext()); - Collections.sort(dashboardsTitle2, idComparator); - Collections.sort(loadedDashboardsTitle2, idComparator); - - Assert.assertEquals(dashboardsTitle2, loadedDashboardsTitle2); - - for (DashboardInfo dashboard : loadedDashboardsTitle1) { - dashboardService.deleteDashboard(dashboard.getId()); - } - - pageLink = new TextPageLink(4, title1); - pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink); - Assert.assertFalse(pageData.hasNext()); - Assert.assertEquals(0, pageData.getData().size()); - - for (DashboardInfo dashboard : loadedDashboardsTitle2) { - dashboardService.deleteDashboard(dashboard.getId()); - } - - pageLink = new TextPageLink(4, title2); - pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink); - Assert.assertFalse(pageData.hasNext()); - Assert.assertEquals(0, pageData.getData().size()); - customerService.deleteCustomer(customerId); - } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java index 39bb084c33..3648cceefc 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDaoTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.sql.dashboard; import com.datastax.driver.core.utils.UUIDs; +import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.DashboardInfo; @@ -40,53 +41,26 @@ public class JpaDashboardInfoDaoTest extends AbstractJpaDaoTest { @Test public void testFindDashboardsByTenantId() { UUID tenantId1 = UUIDs.timeBased(); - UUID customerId1 = UUIDs.timeBased(); UUID tenantId2 = UUIDs.timeBased(); - UUID customerId2 = UUIDs.timeBased(); for (int i = 0; i < 20; i++) { - createDashboard(tenantId1, customerId1, i); - createDashboard(tenantId2, customerId2, i * 2); + createDashboard(tenantId1, i); + createDashboard(tenantId2, i * 2); } TextPageLink pageLink1 = new TextPageLink(15, "DASHBOARD"); List dashboardInfos1 = dashboardInfoDao.findDashboardsByTenantId(tenantId1, pageLink1); - assertEquals(15, dashboardInfos1.size()); + Assert.assertEquals(15, dashboardInfos1.size()); TextPageLink pageLink2 = new TextPageLink(15, "DASHBOARD", dashboardInfos1.get(14).getId().getId(), null); List dashboardInfos2 = dashboardInfoDao.findDashboardsByTenantId(tenantId1, pageLink2); - assertEquals(5, dashboardInfos2.size()); + Assert.assertEquals(5, dashboardInfos2.size()); } - @Test - public void testFindDashboardsByTenantAndCustomerId() { - UUID tenantId1 = UUIDs.timeBased(); - UUID customerId1 = UUIDs.timeBased(); - UUID tenantId2 = UUIDs.timeBased(); - UUID customerId2 = UUIDs.timeBased(); - - for (int i = 0; i < 20; i++) { - createDashboard(tenantId1, customerId1, i); - createDashboard(tenantId2, customerId2, i * 2); - } - - TextPageLink pageLink1 = new TextPageLink(15, "DASHBOARD"); - List dashboardInfos1 = dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink1); - assertEquals(15, dashboardInfos1.size()); - - TextPageLink pageLink2 = new TextPageLink(15, "DASHBOARD", dashboardInfos1.get(14).getId().getId(), null); - List dashboardInfos2 = dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(tenantId1, customerId1, pageLink2); - assertEquals(5, dashboardInfos2.size()); - } - - private void assertEquals(int i, int size) { - } - - private void createDashboard(UUID tenantId, UUID customerId, int index) { + private void createDashboard(UUID tenantId, int index) { DashboardInfo dashboardInfo = new DashboardInfo(); dashboardInfo.setId(new DashboardId(UUIDs.timeBased())); dashboardInfo.setTenantId(new TenantId(tenantId)); - dashboardInfo.setCustomerId(new CustomerId(customerId)); dashboardInfo.setTitle("DASHBOARD_" + index); dashboardInfoDao.save(dashboardInfo); } From 99444395393f0f2966e94de9a67017ee6be742f9 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 27 Feb 2018 19:15:20 +0200 Subject: [PATCH 37/57] Improve CSV data dump. --- .../CassandraDatabaseUpgradeService.java | 4 +- .../install/SqlDatabaseUpgradeService.java | 4 +- .../install/cql/CassandraDbHelper.java | 24 +++++++++- .../service/install/sql/SqlDbHelper.java | 47 ++++++++++++++----- 4 files changed, 60 insertions(+), 19 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java index e3691535bc..0b2c808c4f 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraDatabaseUpgradeService.java @@ -169,7 +169,7 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService { Path dashboardsDump = CassandraDbHelper.dumpCfIfExists(ks, cluster.getSession(), DASHBOARD, new String[]{ID, TENANT_ID, CUSTOMER_ID, TITLE, SEARCH_TEXT, ASSIGNED_CUSTOMERS, CONFIGURATION}, new String[]{"", "", "", "", "", "", ""}, - "tb-dashboards"); + "tb-dashboards", true); log.info("Dashboards dumped."); @@ -181,7 +181,7 @@ public class CassandraDatabaseUpgradeService implements DatabaseUpgradeService { log.info("Restoring dashboards ..."); if (dashboardsDump != null) { CassandraDbHelper.loadCf(ks, cluster.getSession(), DASHBOARD, - new String[]{ID, TENANT_ID, TITLE, SEARCH_TEXT, CONFIGURATION}, dashboardsDump); + new String[]{ID, TENANT_ID, TITLE, SEARCH_TEXT, CONFIGURATION}, dashboardsDump, true); DatabaseHelper.upgradeTo40_assignDashboards(dashboardsDump, dashboardService, false); Files.deleteIfExists(dashboardsDump); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java index e1e1e5f945..3d73ffce47 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java @@ -79,7 +79,7 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService { Path dashboardsDump = SqlDbHelper.dumpTableIfExists(conn, DASHBOARD, new String[]{ID, TENANT_ID, CUSTOMER_ID, TITLE, SEARCH_TEXT, ASSIGNED_CUSTOMERS, CONFIGURATION}, new String[]{"", "", "", "", "", "", ""}, - "tb-dashboards"); + "tb-dashboards", true); log.info("Dashboards dumped."); log.info("Updating schema ..."); @@ -91,7 +91,7 @@ public class SqlDatabaseUpgradeService implements DatabaseUpgradeService { log.info("Restoring dashboards ..."); if (dashboardsDump != null) { SqlDbHelper.loadTable(conn, DASHBOARD, - new String[]{ID, TENANT_ID, TITLE, SEARCH_TEXT, CONFIGURATION}, dashboardsDump); + new String[]{ID, TENANT_ID, TITLE, SEARCH_TEXT, CONFIGURATION}, dashboardsDump, true); DatabaseHelper.upgradeTo40_assignDashboards(dashboardsDump, dashboardService, true); Files.deleteIfExists(dashboardsDump); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java index ef4610eaf1..dae9bb6126 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java +++ b/application/src/main/java/org/thingsboard/server/service/install/cql/CassandraDbHelper.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.install.cql; import com.datastax.driver.core.*; +import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVPrinter; import org.apache.commons.csv.CSVRecord; @@ -33,10 +34,19 @@ public class CassandraDbHelper { public static Path dumpCfIfExists(KeyspaceMetadata ks, Session session, String cfName, String[] columns, String[] defaultValues, String dumpPrefix) throws Exception { + return dumpCfIfExists(ks, session, cfName, columns, defaultValues, dumpPrefix, false); + } + + public static Path dumpCfIfExists(KeyspaceMetadata ks, Session session, String cfName, + String[] columns, String[] defaultValues, String dumpPrefix, boolean printHeader) throws Exception { if (ks.getTable(cfName) != null) { Path dumpFile = Files.createTempFile(dumpPrefix, null); Files.deleteIfExists(dumpFile); - try (CSVPrinter csvPrinter = new CSVPrinter(Files.newBufferedWriter(dumpFile), CSV_DUMP_FORMAT)) { + CSVFormat csvFormat = CSV_DUMP_FORMAT; + if (printHeader) { + csvFormat = csvFormat.withHeader(columns); + } + try (CSVPrinter csvPrinter = new CSVPrinter(Files.newBufferedWriter(dumpFile), csvFormat)) { Statement stmt = new SimpleStatement("SELECT * FROM " + cfName); stmt.setFetchSize(1000); ResultSet rs = session.execute(stmt); @@ -74,9 +84,19 @@ public class CassandraDbHelper { } public static void loadCf(KeyspaceMetadata ks, Session session, String cfName, String[] columns, Path sourceFile) throws Exception { + loadCf(ks, session, cfName, columns, sourceFile, false); + } + + public static void loadCf(KeyspaceMetadata ks, Session session, String cfName, String[] columns, Path sourceFile, boolean parseHeader) throws Exception { TableMetadata tableMetadata = ks.getTable(cfName); PreparedStatement prepared = session.prepare(createInsertStatement(cfName, columns)); - try (CSVParser csvParser = new CSVParser(Files.newBufferedReader(sourceFile), CSV_DUMP_FORMAT.withHeader(columns))) { + CSVFormat csvFormat = CSV_DUMP_FORMAT; + if (parseHeader) { + csvFormat = csvFormat.withFirstRecordAsHeader(); + } else { + csvFormat = CSV_DUMP_FORMAT.withHeader(columns); + } + try (CSVParser csvParser = new CSVParser(Files.newBufferedReader(sourceFile), csvFormat)) { csvParser.forEach(record -> { BoundStatement boundStatement = prepared.bind(); for (String column : columns) { diff --git a/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java b/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java index fa5175fe89..c78ceda02d 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java +++ b/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.install.sql; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVPrinter; import org.apache.commons.csv.CSVRecord; @@ -38,6 +39,11 @@ public class SqlDbHelper { public static Path dumpTableIfExists(Connection conn, String tableName, String[] columns, String[] defaultValues, String dumpPrefix) throws Exception { + return dumpTableIfExists(conn, tableName, columns, defaultValues, dumpPrefix, false); + } + + public static Path dumpTableIfExists(Connection conn, String tableName, + String[] columns, String[] defaultValues, String dumpPrefix, boolean printHeader) throws Exception { DatabaseMetaData metaData = conn.getMetaData(); ResultSet res = metaData.getTables(null, null, tableName, @@ -46,7 +52,11 @@ public class SqlDbHelper { res.close(); Path dumpFile = Files.createTempFile(dumpPrefix, null); Files.deleteIfExists(dumpFile); - try (CSVPrinter csvPrinter = new CSVPrinter(Files.newBufferedWriter(dumpFile), CSV_DUMP_FORMAT)) { + CSVFormat csvFormat = CSV_DUMP_FORMAT; + if (printHeader) { + csvFormat = csvFormat.withHeader(columns); + } + try (CSVPrinter csvPrinter = new CSVPrinter(Files.newBufferedWriter(dumpFile), csvFormat)) { try (PreparedStatement stmt = conn.prepareStatement("SELECT * FROM " + tableName)) { try (ResultSet tableRes = stmt.executeQuery()) { ResultSetMetaData resMetaData = tableRes.getMetaData(); @@ -68,19 +78,30 @@ public class SqlDbHelper { } public static void loadTable(Connection conn, String tableName, String[] columns, Path sourceFile) throws Exception { - PreparedStatement prepared = conn.prepareStatement(createInsertStatement(tableName, columns)); - prepared.getParameterMetaData(); - try (CSVParser csvParser = new CSVParser(Files.newBufferedReader(sourceFile), CSV_DUMP_FORMAT.withHeader(columns))) { - csvParser.forEach(record -> { - try { - for (int i=0;i { + try { + for (int i = 0; i < columns.length; i++) { + setColumnValue(i, columns[i], record, prepared); + } + prepared.execute(); + } catch (SQLException e) { + log.error("Unable to load table record!", e); } - prepared.execute(); - } catch (SQLException e) { - log.error("Unable to load table record!", e); - } - }); + }); + } } } From 7645e6f26ecc8e341194fc155c05026970abdcaa Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Tue, 27 Feb 2018 19:32:36 +0200 Subject: [PATCH 38/57] Fix Dashboards dump parsing --- .../server/service/install/DatabaseHelper.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java index 44f42dd570..d26565f0b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java @@ -55,7 +55,7 @@ public class DatabaseHelper { public static void upgradeTo40_assignDashboards(Path dashboardsDump, DashboardService dashboardService, boolean sql) throws Exception { String[] columns = new String[]{ID, TENANT_ID, CUSTOMER_ID, TITLE, SEARCH_TEXT, ASSIGNED_CUSTOMERS, CONFIGURATION}; - try (CSVParser csvParser = new CSVParser(Files.newBufferedReader(dashboardsDump), CSV_DUMP_FORMAT.withHeader(columns))) { + try (CSVParser csvParser = new CSVParser(Files.newBufferedReader(dashboardsDump), CSV_DUMP_FORMAT.withFirstRecordAsHeader())) { csvParser.forEach(record -> { String customerIdString = record.get(CUSTOMER_ID); String assignedCustomersString = record.get(ASSIGNED_CUSTOMERS); @@ -66,14 +66,20 @@ public class DatabaseHelper { JsonNode assignedCustomersJson = objectMapper.readTree(assignedCustomersString); Map assignedCustomers = objectMapper.treeToValue(assignedCustomersJson, HashMap.class); assignedCustomers.forEach((strCustomerId, title) -> { - customerIds.add(new CustomerId(UUID.fromString(strCustomerId))); + CustomerId customerId = new CustomerId(UUID.fromString(strCustomerId)); + if (!customerId.isNullUid()) { + customerIds.add(new CustomerId(UUID.fromString(strCustomerId))); + } }); } catch (IOException e) { log.error("Unable to parse assigned customers field", e); } } if (!StringUtils.isEmpty(customerIdString)) { - customerIds.add(new CustomerId(toUUID(customerIdString, sql))); + CustomerId customerId = new CustomerId(toUUID(customerIdString, sql)); + if (!customerId.isNullUid()) { + customerIds.add(new CustomerId(toUUID(customerIdString, sql))); + } } for (CustomerId customerId : customerIds) { dashboardService.assignDashboardToCustomer(dashboardId, customerId); From 8d1f7040b60fc6ba21102ec5f755cd6efb24b540 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 28 Feb 2018 09:03:06 +0200 Subject: [PATCH 39/57] Added elastic sink. audit log service --- .../src/main/resources/thingsboard.yml | 13 ++ dao/pom.xml | 11 +- .../server/dao/audit/AuditLogServiceImpl.java | 8 + .../dao/audit/DummyAuditLogServiceImpl.java | 7 +- .../server/dao/audit/sink/AuditLogSink.java | 23 +++ .../dao/audit/sink/DummyAuditLogSink.java | 29 ++++ .../audit/sink/ElasticsearchAuditLogSink.java | 160 ++++++++++++++++++ .../resources/application-test.properties | 1 + pom.xml | 6 + 9 files changed, 252 insertions(+), 6 deletions(-) create mode 100644 dao/src/main/java/org/thingsboard/server/dao/audit/sink/AuditLogSink.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/audit/sink/DummyAuditLogSink.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index f7a3a80738..2f6ba6696e 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -317,3 +317,16 @@ audit_log: "user": "${AUDIT_LOG_MASK_USER:W}" "rule": "${AUDIT_LOG_MASK_RULE:W}" "plugin": "${AUDIT_LOG_MASK_PLUGIN:W}" + sink: + # type of external sink. possible options: none, elasticsearch + type: "${AUDIT_LOG_SINK_TYPE:none}" + # name of the index where audit logs stored + # Index name could contain next placeholders (not mandatory): + # @{TENANT} - substituted by tenant ID + # @{DATE} - substituted by current date in YYYY.MM.DD format + index_pattern: "${AUDIT_LOG_SINK_INDEX_PATTERN:@{TENANT}_AUDIT_LOG_@{DATE}}" + scheme_name: "${AUDIT_LOG_SINK_SCHEME_NAME:http}" # http or https + host: "${AUDIT_LOG_SINK_HOST:localhost}" + port: "${AUDIT_LOG_SINK_POST:9200}" + user_name: "${AUDIT_LOG_SINK_USER_NAME:}" + password: "${AUDIT_LOG_SINK_PASSWORD:}" \ No newline at end of file diff --git a/dao/pom.xml b/dao/pom.xml index 0e72a6c7d6..86cb0b99fc 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -103,7 +103,12 @@ org.springframework spring-tx - + + + org.springframework + spring-web + provided + com.datastax.cassandra cassandra-driver-core @@ -190,6 +195,10 @@ redis.clients jedis + + org.elasticsearch.client + rest + diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index ab1c313fe2..2d93c16955 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.page.TimePageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.dao.audit.sink.AuditLogSink; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @@ -69,6 +70,9 @@ public class AuditLogServiceImpl implements AuditLogService { @Autowired private EntityService entityService; + @Autowired + private AuditLogSink auditLogSink; + @Override public TimePageData findAuditLogsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink) { log.trace("Executing findAuditLogsByTenantIdAndCustomerId [{}], [{}], [{}]", tenantId, customerId, pageLink); @@ -295,6 +299,10 @@ public class AuditLogServiceImpl implements AuditLogService { futures.add(auditLogDao.saveByTenantIdAndEntityId(auditLogEntry)); futures.add(auditLogDao.saveByTenantIdAndCustomerId(auditLogEntry)); futures.add(auditLogDao.saveByTenantIdAndUserId(auditLogEntry)); + + // TODO: is this correct place to log action into sink? + auditLogSink.logAction(auditLogEntry); + return Futures.allAsList(futures); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java index 885cd2f30b..29976179c8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/DummyAuditLogServiceImpl.java @@ -15,23 +15,20 @@ */ package org.thingsboard.server.dao.audit; -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.HasName; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.audit.ActionStatus; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.id.*; import org.thingsboard.server.common.data.page.TimePageData; import org.thingsboard.server.common.data.page.TimePageLink; -import java.util.Collections; import java.util.List; +@Service @ConditionalOnProperty(prefix = "audit_log", value = "enabled", havingValue = "false") public class DummyAuditLogServiceImpl implements AuditLogService { diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/sink/AuditLogSink.java b/dao/src/main/java/org/thingsboard/server/dao/audit/sink/AuditLogSink.java new file mode 100644 index 0000000000..1e8358981a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/sink/AuditLogSink.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2017 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.audit.sink; + +import org.thingsboard.server.common.data.audit.AuditLog; + +public interface AuditLogSink { + + void logAction(AuditLog auditLogEntry); +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/sink/DummyAuditLogSink.java b/dao/src/main/java/org/thingsboard/server/dao/audit/sink/DummyAuditLogSink.java new file mode 100644 index 0000000000..300cdeee15 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/sink/DummyAuditLogSink.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2017 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.audit.sink; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.audit.AuditLog; + +@Component +@ConditionalOnProperty(prefix = "audit_log.sink", value = "type", havingValue = "none") +public class DummyAuditLogSink implements AuditLogSink { + + @Override + public void logAction(AuditLog auditLogEntry) { + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java b/dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java new file mode 100644 index 0000000000..436af2aaf5 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java @@ -0,0 +1,160 @@ +/** + * Copyright © 2016-2017 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.audit.sink; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.entity.ContentType; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.nio.entity.NStringEntity; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseListener; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.audit.AuditLog; +import org.thingsboard.server.common.data.id.TenantId; + +import javax.annotation.PostConstruct; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collections; + +@Component +@ConditionalOnProperty(prefix = "audit_log.sink", value = "type", havingValue = "elasticsearch") +@Slf4j +public class ElasticsearchAuditLogSink implements AuditLogSink { + + private static final String TENANT_PLACEHOLDER = "@{TENANT}"; + private static final String DATE_PLACEHOLDER = "@{DATE}"; + private static final String DATE_FORMAT = "YYYY.MM.dd"; + + private static final String INDEX_TYPE = "audit_log"; + + private final ObjectMapper mapper = new ObjectMapper(); + + @Value("${audit_log.sink.index_pattern}") + private String indexPattern; + @Value("${audit_log.sink.scheme_name}") + private String schemeName; + @Value("${audit_log.sink.host}") + private String host; + @Value("${audit_log.sink.port}") + private int port; + @Value("${audit_log.sink.user_name}") + private String userName; + @Value("${audit_log.sink.epassword}") + private String password; + + private RestClient restClient; + + @PostConstruct + public void init() { + try { + log.trace("Adding elastic rest endpoint... host [{}], port [{}], scheme name [{}]", + host, port, schemeName); + RestClientBuilder builder = RestClient.builder( + new HttpHost(host, port, schemeName)); + + if (StringUtils.isNotEmpty(userName) && + StringUtils.isNotEmpty(password)) { + log.trace("...using username [{}] and password ***", userName); + final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(AuthScope.ANY, + new UsernamePasswordCredentials(userName, password)); + builder.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)); + } + + this.restClient = builder.build(); + } catch (Exception e) { + log.error("Sink init failed!", e); + throw new RuntimeException(e.getMessage(), e); + } + } + + @Override + public void logAction(AuditLog auditLogEntry) { + String jsonContent = createElasticJsonRecord(auditLogEntry); + + HttpEntity entity = new NStringEntity( + jsonContent, + ContentType.APPLICATION_JSON); + + restClient.performRequestAsync( + HttpMethod.POST.name(), + String.format("/%s/%s", getIndexName(auditLogEntry.getTenantId()), INDEX_TYPE), + Collections.emptyMap(), + entity, + responseListener); + } + + private String createElasticJsonRecord(AuditLog auditLog) { + ObjectNode auditLogNode = mapper.createObjectNode(); + auditLogNode.put("postDate", LocalDateTime.now().toString()); + auditLogNode.put("id", auditLog.getId().getId().toString()); + auditLogNode.put("entityName", auditLog.getEntityName()); + auditLogNode.put("tenantId", auditLog.getTenantId().getId().toString()); + if (auditLog.getCustomerId() != null) { + auditLogNode.put("customerId", auditLog.getCustomerId().getId().toString()); + } + auditLogNode.put("entityId", auditLog.getEntityId().getId().toString()); + auditLogNode.put("entityType", auditLog.getEntityId().getEntityType().name()); + auditLogNode.put("userId", auditLog.getUserId().getId().toString()); + auditLogNode.put("userName", auditLog.getUserName()); + auditLogNode.put("actionType", auditLog.getActionType().name()); + if (auditLog.getActionData() != null) { + auditLogNode.put("actionData", auditLog.getActionData().toString()); + } + auditLogNode.put("actionStatus", auditLog.getActionStatus().name()); + auditLogNode.put("actionFailureDetails", auditLog.getActionFailureDetails()); + return auditLogNode.toString(); + } + + private ResponseListener responseListener = new ResponseListener() { + @Override + public void onSuccess(Response response) { + log.trace("Elasticsearch sink log action method succeeded. Response result [{}]!", response); + } + + @Override + public void onFailure(Exception exception) { + log.warn("Elasticsearch sink log action method failed!", exception); + } + }; + + private String getIndexName(TenantId tenantId) { + String indexName = indexPattern; + if (indexName.contains(TENANT_PLACEHOLDER) && tenantId != null) { + indexName = indexName.replace(TENANT_PLACEHOLDER, tenantId.getId().toString()); + } + if (indexName.contains(DATE_PLACEHOLDER)) { + LocalDateTime now = LocalDateTime.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT); + indexName = indexName.replace(DATE_PLACEHOLDER, now.format(formatter)); + } + return indexName.toLowerCase(); + } +} diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index 21f1794232..42b71f84e3 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -7,6 +7,7 @@ updates.enabled=false audit_log.enabled=true audit_log.by_tenant_partitioning=MONTHS audit_log.default_query_period=30 +audit_log.sink.type=none cache.type=caffeine #cache.type=redis diff --git a/pom.xml b/pom.xml index f37fe81dee..e7d4369e6d 100755 --- a/pom.xml +++ b/pom.xml @@ -80,6 +80,7 @@ 1.2.1 9.4.1211 org/thingsboard/server/gen/**/*, org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/* + 5.0.2 @@ -803,6 +804,11 @@ exe provided + + org.elasticsearch.client + rest + ${elasticsearch.version} + From 4ef97a40b883ac57a42df6cb99255dfaf34d4428 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 28 Feb 2018 09:21:58 +0200 Subject: [PATCH 40/57] Added elastic sink. audit log service --- .../server/dao/audit/sink/ElasticsearchAuditLogSink.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java b/dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java index 436af2aaf5..c6a7808fc5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java @@ -66,7 +66,7 @@ public class ElasticsearchAuditLogSink implements AuditLogSink { private int port; @Value("${audit_log.sink.user_name}") private String userName; - @Value("${audit_log.sink.epassword}") + @Value("${audit_log.sink.password}") private String password; private RestClient restClient; From 12b8f77e2d4a68f2aa63af5e46db804c033126e3 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 28 Feb 2018 09:27:12 +0200 Subject: [PATCH 41/57] Added elastic sink. audit log service --- application/src/main/resources/thingsboard.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 2f6ba6696e..b421ab124f 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -318,9 +318,9 @@ audit_log: "rule": "${AUDIT_LOG_MASK_RULE:W}" "plugin": "${AUDIT_LOG_MASK_PLUGIN:W}" sink: - # type of external sink. possible options: none, elasticsearch + # Type of external sink. possible options: none, elasticsearch type: "${AUDIT_LOG_SINK_TYPE:none}" - # name of the index where audit logs stored + # Name of the index where audit logs stored # Index name could contain next placeholders (not mandatory): # @{TENANT} - substituted by tenant ID # @{DATE} - substituted by current date in YYYY.MM.DD format From 7f1651c807c26cec5bc8cef6c8f01d9d663e6ec2 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Wed, 28 Feb 2018 12:32:25 +0200 Subject: [PATCH 42/57] Code review fixes --- application/src/main/resources/thingsboard.yml | 5 ++++- .../thingsboard/server/dao/audit/AuditLogServiceImpl.java | 1 - .../server/dao/audit/sink/ElasticsearchAuditLogSink.java | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index b421ab124f..97da989004 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -323,8 +323,11 @@ audit_log: # Name of the index where audit logs stored # Index name could contain next placeholders (not mandatory): # @{TENANT} - substituted by tenant ID - # @{DATE} - substituted by current date in YYYY.MM.DD format + # @{DATE} - substituted by current date in format provided in audit_log.sink.date_format index_pattern: "${AUDIT_LOG_SINK_INDEX_PATTERN:@{TENANT}_AUDIT_LOG_@{DATE}}" + # Date format. Details of the pattern could be found here: + # https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html + date_format: "${AUDIT_LOG_SINK_DATE_FORMAT:YYYY.MM.DD}" scheme_name: "${AUDIT_LOG_SINK_SCHEME_NAME:http}" # http or https host: "${AUDIT_LOG_SINK_HOST:localhost}" port: "${AUDIT_LOG_SINK_POST:9200}" diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index 2d93c16955..184e75b12c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -300,7 +300,6 @@ public class AuditLogServiceImpl implements AuditLogService { futures.add(auditLogDao.saveByTenantIdAndCustomerId(auditLogEntry)); futures.add(auditLogDao.saveByTenantIdAndUserId(auditLogEntry)); - // TODO: is this correct place to log action into sink? auditLogSink.logAction(auditLogEntry); return Futures.allAsList(futures); diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java b/dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java index c6a7808fc5..4d3b6b8d98 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/sink/ElasticsearchAuditLogSink.java @@ -50,8 +50,6 @@ public class ElasticsearchAuditLogSink implements AuditLogSink { private static final String TENANT_PLACEHOLDER = "@{TENANT}"; private static final String DATE_PLACEHOLDER = "@{DATE}"; - private static final String DATE_FORMAT = "YYYY.MM.dd"; - private static final String INDEX_TYPE = "audit_log"; private final ObjectMapper mapper = new ObjectMapper(); @@ -68,6 +66,8 @@ public class ElasticsearchAuditLogSink implements AuditLogSink { private String userName; @Value("${audit_log.sink.password}") private String password; + @Value("${audit_log.sink.date_format}") + private String dateFormat; private RestClient restClient; @@ -152,7 +152,7 @@ public class ElasticsearchAuditLogSink implements AuditLogSink { } if (indexName.contains(DATE_PLACEHOLDER)) { LocalDateTime now = LocalDateTime.now(); - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat); indexName = indexName.replace(DATE_PLACEHOLDER, now.format(formatter)); } return indexName.toLowerCase(); From 24a4f71eda11edd46883b6cd066919458019f397 Mon Sep 17 00:00:00 2001 From: oleg Date: Wed, 28 Feb 2018 12:39:10 +0200 Subject: [PATCH 43/57] UI:Fix. "Take account if there is encoding defined inside base64" --- ui/src/vendor/css.js/css.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/vendor/css.js/css.js b/ui/src/vendor/css.js/css.js index 254e6b5e0e..d8ee7160ba 100644 --- a/ui/src/vendor/css.js/css.js +++ b/ui/src/vendor/css.js/css.js @@ -148,7 +148,7 @@ fi.prototype.parseRules = function (rules) { var ret = []; // Split all rules but keep semicolon for base64 url data - rules = rules.split(/;(?!base64)/); + rules = rules.split(/;(?![^\(]*\))/); //proccess rules line by line for (var i = 0; i < rules.length; i++) { From 0021f007f1de05bba8e202ebc7c3c3ed0ee4aaa7 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 1 Mar 2018 11:52:52 +0200 Subject: [PATCH 44/57] Implement multiple customer assigned dashboards UI. Add Current Customer entity type for dashboard aliases. --- .../server/controller/BaseController.java | 3 +- .../controller/DashboardController.java | 160 ++++++++++++- .../service/install/DatabaseHelper.java | 17 +- .../BaseDashboardControllerTest.java | 4 +- .../server/common/data/Customer.java | 5 + .../server/common/data/DashboardInfo.java | 49 ++-- .../server/common/data/ShortCustomerInfo.java | 54 +++++ .../dao/customer/CustomerServiceImpl.java | 4 +- .../dao/dashboard/DashboardService.java | 6 +- .../dao/dashboard/DashboardServiceImpl.java | 75 +++--- .../dao/model/nosql/DashboardEntity.java | 28 ++- .../dao/model/nosql/DashboardInfoEntity.java | 32 ++- .../server/dao/model/sql/DashboardEntity.java | 26 +- .../dao/model/sql/DashboardInfoEntity.java | 27 ++- .../dao/service/BaseDashboardServiceTest.java | 2 +- ui/src/app/api/dashboard.service.js | 150 ++++++++---- ui/src/app/api/entity.service.js | 43 +++- ui/src/app/api/user.service.js | 4 +- ui/src/app/common/types.constant.js | 7 + .../dashboard-autocomplete.directive.js | 4 +- .../components/dashboard-select.directive.js | 4 +- .../add-dashboards-to-customer.controller.js | 2 +- .../assign-to-customer.controller.js | 123 ---------- .../app/dashboard/assign-to-customer.tpl.html | 76 ------ ui/src/app/dashboard/dashboard-card.scss | 27 +++ ui/src/app/dashboard/dashboard-card.tpl.html | 4 +- .../app/dashboard/dashboard-fieldset.tpl.html | 27 ++- ui/src/app/dashboard/dashboard.directive.js | 29 +-- ui/src/app/dashboard/dashboards.controller.js | 226 ++++++++++-------- ui/src/app/dashboard/dashboards.tpl.html | 6 +- ui/src/app/dashboard/index.js | 4 +- .../manage-assigned-customers.controller.js | 69 ++++++ .../manage-assigned-customers.tpl.html | 51 ++++ .../entity/entity-autocomplete.directive.js | 37 ++- ui/src/app/entity/entity-filter.tpl.html | 8 + ui/src/app/entity/entity-select.directive.js | 3 +- ui/src/app/entity/entity-select.tpl.html | 1 + .../entity/entity-type-select.directive.js | 5 +- .../import-export/import-export.service.js | 11 +- ui/src/app/locale/locale.constant.js | 16 +- 40 files changed, 890 insertions(+), 539 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java delete mode 100644 ui/src/app/dashboard/assign-to-customer.controller.js delete mode 100644 ui/src/app/dashboard/assign-to-customer.tpl.html create mode 100644 ui/src/app/dashboard/dashboard-card.scss create mode 100644 ui/src/app/dashboard/manage-assigned-customers.controller.js create mode 100644 ui/src/app/dashboard/manage-assigned-customers.tpl.html diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index a72f2f384b..c78ad18ded 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -434,7 +434,6 @@ public abstract class BaseController { try { validateId(dashboardId, "Incorrect dashboardId " + dashboardId); DashboardInfo dashboardInfo = dashboardService.findDashboardInfoById(dashboardId); - SecurityUser authUser = getCurrentUser(); checkDashboard(dashboardInfo); return dashboardInfo; } catch (Exception e) { @@ -447,7 +446,7 @@ public abstract class BaseController { checkTenantId(dashboard.getTenantId()); SecurityUser authUser = getCurrentUser(); if (authUser.getAuthority() == Authority.CUSTOMER_USER) { - if (dashboard.getAssignedCustomers() == null || !dashboard.getAssignedCustomers().containsKey(authUser.getCustomerId().toString())) { + if (!dashboard.isAssignedToCustomer(authUser.getCustomerId())) { throw new ThingsboardException(YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION, ThingsboardErrorCode.PERMISSION_DENIED); } diff --git a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java index 30d57f3f4d..a4664a5a14 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DashboardController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DashboardController.java @@ -18,10 +18,7 @@ package org.thingsboard.server.controller; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; -import org.thingsboard.server.common.data.Customer; -import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.DashboardInfo; -import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.*; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; @@ -34,6 +31,9 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.exception.ThingsboardException; +import java.util.HashSet; +import java.util.Set; + @RestController @RequestMapping("/api") public class DashboardController extends BaseController { @@ -181,6 +181,158 @@ public class DashboardController extends BaseController { } } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/dashboard/{dashboardId}/customers", method = RequestMethod.POST) + @ResponseBody + public Dashboard updateDashboardCustomers(@PathVariable(DASHBOARD_ID) String strDashboardId, + @RequestBody String[] strCustomerIds) throws ThingsboardException { + checkParameter(DASHBOARD_ID, strDashboardId); + try { + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + Dashboard dashboard = checkDashboardId(dashboardId); + + Set customerIds = new HashSet<>(); + if (strCustomerIds != null) { + for (String strCustomerId : strCustomerIds) { + customerIds.add(new CustomerId(toUUID(strCustomerId))); + } + } + + Set addedCustomerIds = new HashSet<>(); + Set removedCustomerIds = new HashSet<>(); + for (CustomerId customerId : customerIds) { + if (!dashboard.isAssignedToCustomer(customerId)) { + addedCustomerIds.add(customerId); + } + } + + Set assignedCustomers = dashboard.getAssignedCustomers(); + if (assignedCustomers != null) { + for (ShortCustomerInfo customerInfo : assignedCustomers) { + if (!customerIds.contains(customerInfo.getCustomerId())) { + removedCustomerIds.add(customerInfo.getCustomerId()); + } + } + } + + if (addedCustomerIds.isEmpty() && removedCustomerIds.isEmpty()) { + return dashboard; + } else { + Dashboard savedDashboard = null; + for (CustomerId customerId : addedCustomerIds) { + savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, customerId)); + ShortCustomerInfo customerInfo = savedDashboard.getAssignedCustomerInfo(customerId); + logEntityAction(dashboardId, savedDashboard, + customerId, + ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, customerId.toString(), customerInfo.getTitle()); + } + for (CustomerId customerId : removedCustomerIds) { + ShortCustomerInfo customerInfo = dashboard.getAssignedCustomerInfo(customerId); + savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(dashboardId, customerId)); + logEntityAction(dashboardId, dashboard, + customerId, + ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDashboardId, customerId.toString(), customerInfo.getTitle()); + + } + return savedDashboard; + } + } catch (Exception e) { + + logEntityAction(emptyId(EntityType.DASHBOARD), null, + null, + ActionType.ASSIGNED_TO_CUSTOMER, e, strDashboardId); + + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/dashboard/{dashboardId}/customers/add", method = RequestMethod.POST) + @ResponseBody + public Dashboard addDashboardCustomers(@PathVariable(DASHBOARD_ID) String strDashboardId, + @RequestBody String[] strCustomerIds) throws ThingsboardException { + checkParameter(DASHBOARD_ID, strDashboardId); + try { + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + Dashboard dashboard = checkDashboardId(dashboardId); + + Set customerIds = new HashSet<>(); + if (strCustomerIds != null) { + for (String strCustomerId : strCustomerIds) { + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + if (!dashboard.isAssignedToCustomer(customerId)) { + customerIds.add(customerId); + } + } + } + + if (customerIds.isEmpty()) { + return dashboard; + } else { + Dashboard savedDashboard = null; + for (CustomerId customerId : customerIds) { + savedDashboard = checkNotNull(dashboardService.assignDashboardToCustomer(dashboardId, customerId)); + ShortCustomerInfo customerInfo = savedDashboard.getAssignedCustomerInfo(customerId); + logEntityAction(dashboardId, savedDashboard, + customerId, + ActionType.ASSIGNED_TO_CUSTOMER, null, strDashboardId, customerId.toString(), customerInfo.getTitle()); + } + return savedDashboard; + } + } catch (Exception e) { + + logEntityAction(emptyId(EntityType.DASHBOARD), null, + null, + ActionType.ASSIGNED_TO_CUSTOMER, e, strDashboardId); + + throw handleException(e); + } + } + + @PreAuthorize("hasAuthority('TENANT_ADMIN')") + @RequestMapping(value = "/dashboard/{dashboardId}/customers/remove", method = RequestMethod.POST) + @ResponseBody + public Dashboard removeDashboardCustomers(@PathVariable(DASHBOARD_ID) String strDashboardId, + @RequestBody String[] strCustomerIds) throws ThingsboardException { + checkParameter(DASHBOARD_ID, strDashboardId); + try { + DashboardId dashboardId = new DashboardId(toUUID(strDashboardId)); + Dashboard dashboard = checkDashboardId(dashboardId); + + Set customerIds = new HashSet<>(); + if (strCustomerIds != null) { + for (String strCustomerId : strCustomerIds) { + CustomerId customerId = new CustomerId(toUUID(strCustomerId)); + if (dashboard.isAssignedToCustomer(customerId)) { + customerIds.add(customerId); + } + } + } + + if (customerIds.isEmpty()) { + return dashboard; + } else { + Dashboard savedDashboard = null; + for (CustomerId customerId : customerIds) { + ShortCustomerInfo customerInfo = dashboard.getAssignedCustomerInfo(customerId); + savedDashboard = checkNotNull(dashboardService.unassignDashboardFromCustomer(dashboardId, customerId)); + logEntityAction(dashboardId, dashboard, + customerId, + ActionType.UNASSIGNED_FROM_CUSTOMER, null, strDashboardId, customerId.toString(), customerInfo.getTitle()); + + } + return savedDashboard; + } + } catch (Exception e) { + + logEntityAction(emptyId(EntityType.DASHBOARD), null, + null, + ActionType.UNASSIGNED_FROM_CUSTOMER, e, strDashboardId); + + throw handleException(e); + } + } + @PreAuthorize("hasAuthority('TENANT_ADMIN')") @RequestMapping(value = "/customer/public/dashboard/{dashboardId}", method = RequestMethod.POST) @ResponseBody diff --git a/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java index d26565f0b5..fa0dd2f4f1 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java @@ -15,12 +15,13 @@ */ package org.thingsboard.server.service.install; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; import org.apache.commons.lang3.StringUtils; -import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; @@ -54,7 +55,8 @@ public class DatabaseHelper { public static final ObjectMapper objectMapper = new ObjectMapper(); public static void upgradeTo40_assignDashboards(Path dashboardsDump, DashboardService dashboardService, boolean sql) throws Exception { - String[] columns = new String[]{ID, TENANT_ID, CUSTOMER_ID, TITLE, SEARCH_TEXT, ASSIGNED_CUSTOMERS, CONFIGURATION}; + JavaType assignedCustomersType = + objectMapper.getTypeFactory().constructCollectionType(HashSet.class, ShortCustomerInfo.class); try (CSVParser csvParser = new CSVParser(Files.newBufferedReader(dashboardsDump), CSV_DUMP_FORMAT.withFirstRecordAsHeader())) { csvParser.forEach(record -> { String customerIdString = record.get(CUSTOMER_ID); @@ -63,12 +65,11 @@ public class DatabaseHelper { List customerIds = new ArrayList<>(); if (!StringUtils.isEmpty(assignedCustomersString)) { try { - JsonNode assignedCustomersJson = objectMapper.readTree(assignedCustomersString); - Map assignedCustomers = objectMapper.treeToValue(assignedCustomersJson, HashMap.class); - assignedCustomers.forEach((strCustomerId, title) -> { - CustomerId customerId = new CustomerId(UUID.fromString(strCustomerId)); + Set assignedCustomers = objectMapper.readValue(assignedCustomersString, assignedCustomersType); + assignedCustomers.forEach((customerInfo) -> { + CustomerId customerId = customerInfo.getCustomerId(); if (!customerId.isNullUid()) { - customerIds.add(new CustomerId(UUID.fromString(strCustomerId))); + customerIds.add(customerId); } }); } catch (IOException e) { @@ -78,7 +79,7 @@ public class DatabaseHelper { if (!StringUtils.isEmpty(customerIdString)) { CustomerId customerId = new CustomerId(toUUID(customerIdString, sql)); if (!customerId.isNullUid()) { - customerIds.add(new CustomerId(toUUID(customerIdString, sql))); + customerIds.add(customerId); } } for (CustomerId customerId : customerIds) { diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java index 434862bd9d..d73e7bab0c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java @@ -138,10 +138,10 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest Dashboard assignedDashboard = doPost("/api/customer/" + savedCustomer.getId().getId().toString() + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); - Assert.assertTrue(assignedDashboard.getAssignedCustomers().containsKey(savedCustomer.getId().toString())); + Assert.assertTrue(assignedDashboard.getAssignedCustomers().contains(savedCustomer.toShortCustomerInfo())); Dashboard foundDashboard = doGet("/api/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); - Assert.assertTrue(foundDashboard.getAssignedCustomers().containsKey(savedCustomer.getId().toString())); + Assert.assertTrue(foundDashboard.getAssignedCustomers().contains(savedCustomer.toShortCustomerInfo())); Dashboard unassignedDashboard = doDelete("/api/customer/"+savedCustomer.getId().getId().toString()+"/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java index 1ee9cab68e..59431f3381 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java @@ -69,6 +69,11 @@ public class Customer extends ContactBased implements HasName { return false; } + @JsonIgnore + public ShortCustomerInfo toShortCustomerInfo() { + return new ShortCustomerInfo(id, title, isPublic()); + } + @Override @JsonProperty(access = Access.READ_ONLY) public String getName() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java index 7e18dc6b80..65a467dbfd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java @@ -20,16 +20,13 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; public class DashboardInfo extends SearchTextBased implements HasName { private TenantId tenantId; private String title; - private Map assignedCustomers; + private Set assignedCustomers; public DashboardInfo() { super(); @@ -62,38 +59,56 @@ public class DashboardInfo extends SearchTextBased implements HasNa this.title = title; } - public Map getAssignedCustomers() { + public Set getAssignedCustomers() { return assignedCustomers; } - public void setAssignedCustomers(Map assignedCustomers) { + public void setAssignedCustomers(Set assignedCustomers) { this.assignedCustomers = assignedCustomers; } - public boolean addAssignedCustomer(CustomerId customerId, String title) { - if (this.assignedCustomers != null && this.assignedCustomers.containsKey(customerId.toString())) { + public boolean isAssignedToCustomer(CustomerId customerId) { + return this.assignedCustomers != null && this.assignedCustomers.contains(new ShortCustomerInfo(customerId, null, false)); + } + + public ShortCustomerInfo getAssignedCustomerInfo(CustomerId customerId) { + if (this.assignedCustomers != null) { + for (ShortCustomerInfo customerInfo : this.assignedCustomers) { + if (customerInfo.getCustomerId().equals(customerId)) { + return customerInfo; + } + } + } + return null; + } + + public boolean addAssignedCustomer(Customer customer) { + ShortCustomerInfo customerInfo = customer.toShortCustomerInfo(); + if (this.assignedCustomers != null && this.assignedCustomers.contains(customerInfo)) { return false; } else { if (this.assignedCustomers == null) { - this.assignedCustomers = new HashMap<>(); + this.assignedCustomers = new HashSet<>(); } - this.assignedCustomers.put(customerId.toString(), title); + this.assignedCustomers.add(customerInfo); return true; } } - public boolean updateAssignedCustomer(CustomerId customerId, String title) { - if (this.assignedCustomers != null && this.assignedCustomers.containsKey(customerId.toString())) { - this.assignedCustomers.put(customerId.toString(), title); + public boolean updateAssignedCustomer(Customer customer) { + ShortCustomerInfo customerInfo = customer.toShortCustomerInfo(); + if (this.assignedCustomers != null && this.assignedCustomers.contains(customerInfo)) { + this.assignedCustomers.add(customerInfo); return true; } else { return false; } } - public boolean removeAssignedCustomer(CustomerId customerId) { - if (this.assignedCustomers != null && this.assignedCustomers.containsKey(customerId.toString())) { - this.assignedCustomers.remove(customerId.toString()); + public boolean removeAssignedCustomer(Customer customer) { + ShortCustomerInfo customerInfo = customer.toShortCustomerInfo(); + if (this.assignedCustomers != null && this.assignedCustomers.contains(customerInfo)) { + this.assignedCustomers.remove(customerInfo); return true; } else { return false; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java new file mode 100644 index 0000000000..68ca46ab99 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ShortCustomerInfo.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2017 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; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import org.thingsboard.server.common.data.id.CustomerId; + +/** + * Created by igor on 2/27/18. + */ + +@AllArgsConstructor +public class ShortCustomerInfo { + + @Getter @Setter + private CustomerId customerId; + + @Getter @Setter + private String title; + + @Getter @Setter + private boolean isPublic; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ShortCustomerInfo that = (ShortCustomerInfo) o; + + return customerId.equals(that.customerId); + + } + + @Override + public int hashCode() { + return customerId.hashCode(); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index f118aff7a2..657bfba3d1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -98,7 +98,7 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom log.trace("Executing saveCustomer [{}]", customer); customerValidator.validate(customer); Customer savedCustomer = customerDao.save(customer); - dashboardService.updateCustomerDashboards(savedCustomer.getTenantId(), savedCustomer.getId(), savedCustomer.getTitle()); + dashboardService.updateCustomerDashboards(savedCustomer.getId()); return savedCustomer; } @@ -110,7 +110,7 @@ public class CustomerServiceImpl extends AbstractEntityService implements Custom if (customer == null) { throw new IncorrectParameterException("Unable to delete non-existent customer."); } - dashboardService.unassignCustomerDashboards(customer.getTenantId(), customerId); + dashboardService.unassignCustomerDashboards(customerId); assetService.unassignCustomerAssets(customer.getTenantId(), customerId); deviceService.unassignCustomerDevices(customer.getTenantId(), customerId); userService.deleteCustomerUsers(customer.getTenantId(), customerId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java index f4af29c88d..06426e71a3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardService.java @@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TimePageData; import org.thingsboard.server.common.data.page.TimePageLink; -import java.sql.Time; +import java.util.Set; public interface DashboardService { @@ -52,8 +52,8 @@ public interface DashboardService { ListenableFuture> findDashboardsByTenantIdAndCustomerId(TenantId tenantId, CustomerId customerId, TimePageLink pageLink); - void unassignCustomerDashboards(TenantId tenantId, CustomerId customerId); + void unassignCustomerDashboards(CustomerId customerId); - void updateCustomerDashboards(TenantId tenantId, CustomerId customerId, String customerTitle); + void updateCustomerDashboards(CustomerId customerId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index b9fa3abdc9..741d313940 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -117,7 +117,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb if (!customer.getTenantId().getId().equals(dashboard.getTenantId().getId())) { throw new DataValidationException("Can't assign dashboard to customer from different tenant!"); } - if (dashboard.addAssignedCustomer(customerId, customer.getTitle())) { + if (dashboard.addAssignedCustomer(customer)) { try { createRelation(new EntityRelation(customerId, dashboardId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.DASHBOARD)); } catch (ExecutionException | InterruptedException e) { @@ -133,7 +133,11 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Override public Dashboard unassignDashboardFromCustomer(DashboardId dashboardId, CustomerId customerId) { Dashboard dashboard = findDashboardById(dashboardId); - if (dashboard.removeAssignedCustomer(customerId)) { + Customer customer = customerDao.findById(customerId.getId()); + if (customer == null) { + throw new DataValidationException("Can't unassign dashboard from non-existent customer!"); + } + if (dashboard.removeAssignedCustomer(customer)) { try { deleteRelation(new EntityRelation(customerId, dashboardId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.DASHBOARD)); } catch (ExecutionException | InterruptedException e) { @@ -146,9 +150,9 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb } } - private Dashboard updateAssignedCustomerTitle(DashboardId dashboardId, CustomerId customerId, String customerTitle) { + private Dashboard updateAssignedCustomer(DashboardId dashboardId, Customer customer) { Dashboard dashboard = findDashboardById(dashboardId); - if (dashboard.updateAssignedCustomer(customerId, customerTitle)) { + if (dashboard.updateAssignedCustomer(customer)) { return saveDashboard(dashboard); } else { return dashboard; @@ -207,20 +211,25 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb } @Override - public void unassignCustomerDashboards(TenantId tenantId, CustomerId customerId) { - log.trace("Executing unassignCustomerDashboards, tenantId [{}], customerId [{}]", tenantId, customerId); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + public void unassignCustomerDashboards(CustomerId customerId) { + log.trace("Executing unassignCustomerDashboards, customerId [{}]", customerId); Validator.validateId(customerId, "Incorrect customerId " + customerId); - new CustomerDashboardsUnassigner(tenantId, customerId).removeEntities(customerId); + Customer customer = customerDao.findById(customerId.getId()); + if (customer == null) { + throw new DataValidationException("Can't unassign dashboards from non-existent customer!"); + } + new CustomerDashboardsUnassigner(customer).removeEntities(customer); } @Override - public void updateCustomerDashboards(TenantId tenantId, CustomerId customerId, String customerTitle) { - log.trace("Executing updateCustomerDashboards, tenantId [{}], customerId [{}], customerTitle [{}]", tenantId, customerId, customerTitle); - Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId); + public void updateCustomerDashboards(CustomerId customerId) { + log.trace("Executing updateCustomerDashboards, customerId [{}]", customerId); Validator.validateId(customerId, "Incorrect customerId " + customerId); - Validator.validateString(customerTitle, "Incorrect customerTitle " + customerTitle); - new CustomerDashboardsUpdater(tenantId, customerId, customerTitle).removeEntities(customerId); + Customer customer = customerDao.findById(customerId.getId()); + if (customer == null) { + throw new DataValidationException("Can't update dashboards for non-existent customer!"); + } + new CustomerDashboardsUpdater(customer).removeEntities(customer); } private DataValidator dashboardValidator = @@ -255,58 +264,52 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb } }; - private class CustomerDashboardsUnassigner extends TimePaginatedRemover { - - private TenantId tenantId; - private CustomerId customerId; + private class CustomerDashboardsUnassigner extends TimePaginatedRemover { - CustomerDashboardsUnassigner(TenantId tenantId, CustomerId customerId) { - this.tenantId = tenantId; - this.customerId = customerId; + private Customer customer; + + CustomerDashboardsUnassigner(Customer customer) { + this.customer = customer; } @Override - protected List findEntities(CustomerId id, TimePageLink pageLink) { + protected List findEntities(Customer customer, TimePageLink pageLink) { try { - return dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(tenantId.getId(), id.getId(), pageLink).get(); + return dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(customer.getTenantId().getId(), customer.getId().getId(), pageLink).get(); } catch (InterruptedException | ExecutionException e) { - log.warn("Failed to get dashboards by tenantId [{}] and customerId [{}].", tenantId, id); + log.warn("Failed to get dashboards by tenantId [{}] and customerId [{}].", customer.getTenantId().getId(), customer.getId().getId()); throw new RuntimeException(e); } } @Override protected void removeEntity(DashboardInfo entity) { - unassignDashboardFromCustomer(new DashboardId(entity.getUuidId()), this.customerId); + unassignDashboardFromCustomer(new DashboardId(entity.getUuidId()), this.customer.getId()); } } - private class CustomerDashboardsUpdater extends TimePaginatedRemover { + private class CustomerDashboardsUpdater extends TimePaginatedRemover { - private TenantId tenantId; - private CustomerId customerId; - private String customerTitle; + private Customer customer; - CustomerDashboardsUpdater(TenantId tenantId, CustomerId customerId, String customerTitle) { - this.tenantId = tenantId; - this.customerId = customerId; - this.customerTitle = customerTitle; + CustomerDashboardsUpdater(Customer customer) { + this.customer = customer; } @Override - protected List findEntities(CustomerId id, TimePageLink pageLink) { + protected List findEntities(Customer customer, TimePageLink pageLink) { try { - return dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(tenantId.getId(), id.getId(), pageLink).get(); + return dashboardInfoDao.findDashboardsByTenantIdAndCustomerId(customer.getTenantId().getId(), customer.getId().getId(), pageLink).get(); } catch (InterruptedException | ExecutionException e) { - log.warn("Failed to get dashboards by tenantId [{}] and customerId [{}].", tenantId, id); + log.warn("Failed to get dashboards by tenantId [{}] and customerId [{}].", customer.getTenantId().getId(), customer.getId().getId()); throw new RuntimeException(e); } } @Override protected void removeEntity(DashboardInfo entity) { - updateAssignedCustomerTitle(new DashboardId(entity.getUuidId()), this.customerId, this.customerTitle); + updateAssignedCustomer(new DashboardId(entity.getUuidId()), this.customer); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardEntity.java index 8590c2a36f..0327232f37 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardEntity.java @@ -20,18 +20,22 @@ import com.datastax.driver.mapping.annotations.Column; import com.datastax.driver.mapping.annotations.PartitionKey; import com.datastax.driver.mapping.annotations.Table; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.SearchTextEntity; import org.thingsboard.server.dao.model.type.JsonCodec; -import java.util.HashMap; +import java.io.IOException; +import java.util.HashSet; import java.util.UUID; import static org.thingsboard.server.dao.model.ModelConstants.*; @@ -43,6 +47,8 @@ import static org.thingsboard.server.dao.model.ModelConstants.*; public final class DashboardEntity implements SearchTextEntity { private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final JavaType assignedCustomersType = + objectMapper.getTypeFactory().constructCollectionType(HashSet.class, ShortCustomerInfo.class); @PartitionKey(value = 0) @Column(name = ID_PROPERTY) @@ -58,8 +64,8 @@ public final class DashboardEntity implements SearchTextEntity { @Column(name = SEARCH_TEXT_PROPERTY) private String searchText; - @Column(name = DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY, codec = JsonCodec.class) - private JsonNode assignedCustomers; + @Column(name = DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY) + private String assignedCustomers; @Column(name = DASHBOARD_CONFIGURATION_PROPERTY, codec = JsonCodec.class) private JsonNode configuration; @@ -77,7 +83,11 @@ public final class DashboardEntity implements SearchTextEntity { } this.title = dashboard.getTitle(); if (dashboard.getAssignedCustomers() != null) { - this.assignedCustomers = objectMapper.valueToTree(dashboard.getAssignedCustomers()); + try { + this.assignedCustomers = objectMapper.writeValueAsString(dashboard.getAssignedCustomers()); + } catch (JsonProcessingException e) { + log.error("Unable to serialize assigned customers to string!", e); + } } this.configuration = dashboard.getConfiguration(); } @@ -106,11 +116,11 @@ public final class DashboardEntity implements SearchTextEntity { this.title = title; } - public JsonNode getAssignedCustomers() { + public String getAssignedCustomers() { return assignedCustomers; } - public void setAssignedCustomers(JsonNode assignedCustomers) { + public void setAssignedCustomers(String assignedCustomers) { this.assignedCustomers = assignedCustomers; } @@ -144,10 +154,10 @@ public final class DashboardEntity implements SearchTextEntity { dashboard.setTenantId(new TenantId(tenantId)); } dashboard.setTitle(title); - if (assignedCustomers != null) { + if (!StringUtils.isEmpty(assignedCustomers)) { try { - dashboard.setAssignedCustomers(objectMapper.treeToValue(assignedCustomers, HashMap.class)); - } catch (JsonProcessingException e) { + dashboard.setAssignedCustomers(objectMapper.readValue(assignedCustomers, assignedCustomersType)); + } catch (IOException e) { log.warn("Unable to parse assigned customers!", e); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardInfoEntity.java index 609f3bb598..f64bc44dc1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/nosql/DashboardInfoEntity.java @@ -20,20 +20,20 @@ import com.datastax.driver.mapping.annotations.Column; import com.datastax.driver.mapping.annotations.PartitionKey; import com.datastax.driver.mapping.annotations.Table; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.EqualsAndHashCode; import lombok.ToString; import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.DashboardInfo; -import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.SearchTextEntity; -import org.thingsboard.server.dao.model.type.JsonCodec; -import java.util.HashMap; -import java.util.Set; +import java.io.IOException; +import java.util.HashSet; import java.util.UUID; import static org.thingsboard.server.dao.model.ModelConstants.*; @@ -45,6 +45,8 @@ import static org.thingsboard.server.dao.model.ModelConstants.*; public class DashboardInfoEntity implements SearchTextEntity { private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final JavaType assignedCustomersType = + objectMapper.getTypeFactory().constructCollectionType(HashSet.class, ShortCustomerInfo.class); @PartitionKey(value = 0) @Column(name = ID_PROPERTY) @@ -60,8 +62,8 @@ public class DashboardInfoEntity implements SearchTextEntity { @Column(name = SEARCH_TEXT_PROPERTY) private String searchText; - @Column(name = DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY, codec = JsonCodec.class) - private JsonNode assignedCustomers; + @Column(name = DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY) + private String assignedCustomers; public DashboardInfoEntity() { super(); @@ -76,7 +78,11 @@ public class DashboardInfoEntity implements SearchTextEntity { } this.title = dashboardInfo.getTitle(); if (dashboardInfo.getAssignedCustomers() != null) { - this.assignedCustomers = objectMapper.valueToTree(dashboardInfo.getAssignedCustomers()); + try { + this.assignedCustomers = objectMapper.writeValueAsString(dashboardInfo.getAssignedCustomers()); + } catch (JsonProcessingException e) { + log.error("Unable to serialize assigned customers to string!", e); + } } } @@ -104,11 +110,11 @@ public class DashboardInfoEntity implements SearchTextEntity { this.title = title; } - public JsonNode getAssignedCustomers() { + public String getAssignedCustomers() { return assignedCustomers; } - public void setAssignedCustomers(JsonNode assignedCustomers) { + public void setAssignedCustomers(String assignedCustomers) { this.assignedCustomers = assignedCustomers; } @@ -134,10 +140,10 @@ public class DashboardInfoEntity implements SearchTextEntity { dashboardInfo.setTenantId(new TenantId(tenantId)); } dashboardInfo.setTitle(title); - if (assignedCustomers != null) { + if (!StringUtils.isEmpty(assignedCustomers)) { try { - dashboardInfo.setAssignedCustomers(objectMapper.treeToValue(assignedCustomers, HashMap.class)); - } catch (JsonProcessingException e) { + dashboardInfo.setAssignedCustomers(objectMapper.readValue(assignedCustomers, assignedCustomersType)); + } catch (IOException e) { log.warn("Unable to parse assigned customers!", e); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java index 6f7810bf55..99f65f0a32 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.model.sql; import com.datastax.driver.core.utils.UUIDs; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Data; @@ -24,8 +25,9 @@ import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; +import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseSqlEntity; @@ -36,9 +38,8 @@ import org.thingsboard.server.dao.util.mapping.JsonStringType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; -import java.util.HashMap; -import java.util.List; -import java.util.Set; +import java.io.IOException; +import java.util.HashSet; @Data @Slf4j @@ -49,6 +50,8 @@ import java.util.Set; public final class DashboardEntity extends BaseSqlEntity implements SearchTextEntity { private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final JavaType assignedCustomersType = + objectMapper.getTypeFactory().constructCollectionType(HashSet.class, ShortCustomerInfo.class); @Column(name = ModelConstants.DASHBOARD_TENANT_ID_PROPERTY) private String tenantId; @@ -59,9 +62,8 @@ public final class DashboardEntity extends BaseSqlEntity implements S @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) private String searchText; - @Type(type = "json") @Column(name = ModelConstants.DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY) - private JsonNode assignedCustomers; + private String assignedCustomers; @Type(type = "json") @Column(name = ModelConstants.DASHBOARD_CONFIGURATION_PROPERTY) @@ -80,7 +82,11 @@ public final class DashboardEntity extends BaseSqlEntity implements S } this.title = dashboard.getTitle(); if (dashboard.getAssignedCustomers() != null) { - this.assignedCustomers = objectMapper.valueToTree(dashboard.getAssignedCustomers()); + try { + this.assignedCustomers = objectMapper.writeValueAsString(dashboard.getAssignedCustomers()); + } catch (JsonProcessingException e) { + log.error("Unable to serialize assigned customers to string!", e); + } } this.configuration = dashboard.getConfiguration(); } @@ -103,10 +109,10 @@ public final class DashboardEntity extends BaseSqlEntity implements S dashboard.setTenantId(new TenantId(toUUID(tenantId))); } dashboard.setTitle(title); - if (assignedCustomers != null) { + if (!StringUtils.isEmpty(assignedCustomers)) { try { - dashboard.setAssignedCustomers(objectMapper.treeToValue(assignedCustomers, HashMap.class)); - } catch (JsonProcessingException e) { + dashboard.setAssignedCustomers(objectMapper.readValue(assignedCustomers, assignedCustomersType)); + } catch (IOException e) { log.warn("Unable to parse assigned customers!", e); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java index d9b8efe8f4..7c295d1bab 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java @@ -17,14 +17,14 @@ package org.thingsboard.server.dao.model.sql; import com.datastax.driver.core.utils.UUIDs; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; -import org.hibernate.annotations.Type; +import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.DashboardInfo; -import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseSqlEntity; @@ -34,8 +34,8 @@ import org.thingsboard.server.dao.model.SearchTextEntity; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Table; -import java.util.HashMap; -import java.util.Set; +import java.io.IOException; +import java.util.HashSet; @Data @Slf4j @@ -45,6 +45,8 @@ import java.util.Set; public class DashboardInfoEntity extends BaseSqlEntity implements SearchTextEntity { private static final ObjectMapper objectMapper = new ObjectMapper(); + private static final JavaType assignedCustomersType = + objectMapper.getTypeFactory().constructCollectionType(HashSet.class, ShortCustomerInfo.class); @Column(name = ModelConstants.DASHBOARD_TENANT_ID_PROPERTY) private String tenantId; @@ -55,9 +57,8 @@ public class DashboardInfoEntity extends BaseSqlEntity implements @Column(name = ModelConstants.SEARCH_TEXT_PROPERTY) private String searchText; - @Type(type = "json") @Column(name = ModelConstants.DASHBOARD_ASSIGNED_CUSTOMERS_PROPERTY) - private JsonNode assignedCustomers; + private String assignedCustomers; public DashboardInfoEntity() { super(); @@ -72,7 +73,11 @@ public class DashboardInfoEntity extends BaseSqlEntity implements } this.title = dashboardInfo.getTitle(); if (dashboardInfo.getAssignedCustomers() != null) { - this.assignedCustomers = objectMapper.valueToTree(dashboardInfo.getAssignedCustomers()); + try { + this.assignedCustomers = objectMapper.writeValueAsString(dashboardInfo.getAssignedCustomers()); + } catch (JsonProcessingException e) { + log.error("Unable to serialize assigned customers to string!", e); + } } } @@ -98,10 +103,10 @@ public class DashboardInfoEntity extends BaseSqlEntity implements dashboardInfo.setTenantId(new TenantId(toUUID(tenantId))); } dashboardInfo.setTitle(title); - if (assignedCustomers != null) { + if (!StringUtils.isEmpty(assignedCustomers)) { try { - dashboardInfo.setAssignedCustomers(objectMapper.treeToValue(assignedCustomers, HashMap.class)); - } catch (JsonProcessingException e) { + dashboardInfo.setAssignedCustomers(objectMapper.readValue(assignedCustomers, assignedCustomersType)); + } catch (IOException e) { log.warn("Unable to parse assigned customers!", e); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java index 0427fb44f7..380c65e8f7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java @@ -320,7 +320,7 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { Assert.assertEquals(dashboards, loadedDashboards); - dashboardService.unassignCustomerDashboards(tenantId, customerId); + dashboardService.unassignCustomerDashboards(customerId); pageLink = new TimePageLink(42); pageData = dashboardService.findDashboardsByTenantIdAndCustomerId(tenantId, customerId, pageLink).get(); diff --git a/ui/src/app/api/dashboard.service.js b/ui/src/app/api/dashboard.service.js index 3082bd3811..ae11f84e8b 100644 --- a/ui/src/app/api/dashboard.service.js +++ b/ui/src/app/api/dashboard.service.js @@ -17,7 +17,7 @@ export default angular.module('thingsboard.api.dashboard', []) .factory('dashboardService', DashboardService).name; /*@ngInject*/ -function DashboardService($rootScope, $http, $q, $location, customerService) { +function DashboardService($rootScope, $http, $q, $location, $filter) { var stDiffPromise; @@ -37,7 +37,11 @@ function DashboardService($rootScope, $http, $q, $location, customerService) { deleteDashboard: deleteDashboard, saveDashboard: saveDashboard, unassignDashboardFromCustomer: unassignDashboardFromCustomer, + updateDashboardCustomers: updateDashboardCustomers, + addDashboardCustomers: addDashboardCustomers, + removeDashboardCustomers: removeDashboardCustomers, makeDashboardPublic: makeDashboardPublic, + makeDashboardPrivate: makeDashboardPrivate, getPublicDashboardLink: getPublicDashboardLink } @@ -56,14 +60,14 @@ function DashboardService($rootScope, $http, $q, $location, customerService) { url += '&textOffset=' + pageLink.textOffset; } $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); + deferred.resolve(prepareDashboards(response.data)); }, function fail() { deferred.reject(); }); return deferred.promise; } - function getTenantDashboards(pageLink, applyCustomersInfo, config) { + function getTenantDashboards(pageLink, config) { var deferred = $q.defer(); var url = '/api/tenant/dashboards?limit=' + pageLink.limit; if (angular.isDefined(pageLink.textSearch)) { @@ -76,51 +80,25 @@ function DashboardService($rootScope, $http, $q, $location, customerService) { url += '&textOffset=' + pageLink.textOffset; } $http.get(url, config).then(function success(response) { - if (applyCustomersInfo) { - customerService.applyAssignedCustomersInfo(response.data.data).then( - function success(data) { - response.data.data = data; - deferred.resolve(response.data); - }, - function fail() { - deferred.reject(); - } - ); - } else { - deferred.resolve(response.data); - } + deferred.resolve(prepareDashboards(response.data)); }, function fail() { deferred.reject(); }); return deferred.promise; } - function getCustomerDashboards(customerId, pageLink, applyCustomersInfo, config) { + function getCustomerDashboards(customerId, pageLink, config) { var deferred = $q.defer(); var url = '/api/customer/' + customerId + '/dashboards?limit=' + pageLink.limit; - if (angular.isDefined(pageLink.textSearch)) { - url += '&textSearch=' + pageLink.textSearch; - } if (angular.isDefined(pageLink.idOffset)) { - url += '&idOffset=' + pageLink.idOffset; - } - if (angular.isDefined(pageLink.textOffset)) { - url += '&textOffset=' + pageLink.textOffset; + url += '&offset=' + pageLink.idOffset; } $http.get(url, config).then(function success(response) { - if (applyCustomersInfo) { - customerService.applyAssignedCustomerInfo(response.data.data, customerId).then( - function success(data) { - response.data.data = data; - deferred.resolve(response.data); - }, - function fail() { - deferred.reject(); - } - ); - } else { - deferred.resolve(response.data); + response.data = prepareDashboards(response.data); + if (pageLink.textSearch) { + response.data.data = $filter('filter')(response.data.data, {title: pageLink.textSearch}); } + deferred.resolve(response.data); }, function fail() { deferred.reject(); }); @@ -151,7 +129,7 @@ function DashboardService($rootScope, $http, $q, $location, customerService) { var deferred = $q.defer(); var url = '/api/dashboard/' + dashboardId; $http.get(url, null).then(function success(response) { - deferred.resolve(response.data); + deferred.resolve(prepareDashboard(response.data)); }, function fail() { deferred.reject(); }); @@ -162,7 +140,7 @@ function DashboardService($rootScope, $http, $q, $location, customerService) { var deferred = $q.defer(); var url = '/api/dashboard/info/' + dashboardId; $http.get(url, config).then(function success(response) { - deferred.resolve(response.data); + deferred.resolve(prepareDashboard(response.data)); }, function fail() { deferred.reject(); }); @@ -172,8 +150,8 @@ function DashboardService($rootScope, $http, $q, $location, customerService) { function saveDashboard(dashboard) { var deferred = $q.defer(); var url = '/api/dashboard'; - $http.post(url, dashboard).then(function success(response) { - deferred.resolve(response.data); + $http.post(url, cleanDashboard(dashboard)).then(function success(response) { + deferred.resolve(prepareDashboard(response.data)); }, function fail() { deferred.reject(); }); @@ -195,18 +173,51 @@ function DashboardService($rootScope, $http, $q, $location, customerService) { var deferred = $q.defer(); var url = '/api/customer/' + customerId + '/dashboard/' + dashboardId; $http.post(url, null).then(function success(response) { - deferred.resolve(response.data); + deferred.resolve(prepareDashboard(response.data)); }, function fail() { deferred.reject(); }); return deferred.promise; } - function unassignDashboardFromCustomer(dashboardId) { + function unassignDashboardFromCustomer(customerId, dashboardId) { var deferred = $q.defer(); - var url = '/api/customer/dashboard/' + dashboardId; + var url = '/api/customer/' + customerId + '/dashboard/' + dashboardId; $http.delete(url).then(function success(response) { - deferred.resolve(response.data); + deferred.resolve(prepareDashboard(response.data)); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function updateDashboardCustomers(dashboardId, customerIds) { + var deferred = $q.defer(); + var url = '/api/dashboard/' + dashboardId + '/customers'; + $http.post(url, customerIds).then(function success(response) { + deferred.resolve(prepareDashboard(response.data)); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function addDashboardCustomers(dashboardId, customerIds) { + var deferred = $q.defer(); + var url = '/api/dashboard/' + dashboardId + '/customers/add'; + $http.post(url, customerIds).then(function success(response) { + deferred.resolve(prepareDashboard(response.data)); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function removeDashboardCustomers(dashboardId, customerIds) { + var deferred = $q.defer(); + var url = '/api/dashboard/' + dashboardId + '/customers/remove'; + $http.post(url, customerIds).then(function success(response) { + deferred.resolve(prepareDashboard(response.data)); }, function fail() { deferred.reject(); }); @@ -217,7 +228,18 @@ function DashboardService($rootScope, $http, $q, $location, customerService) { var deferred = $q.defer(); var url = '/api/customer/public/dashboard/' + dashboardId; $http.post(url, null).then(function success(response) { - deferred.resolve(response.data); + deferred.resolve(prepareDashboard(response.data)); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } + + function makeDashboardPrivate(dashboardId) { + var deferred = $q.defer(); + var url = '/api/customer/public/dashboard/' + dashboardId; + $http.delete(url).then(function success(response) { + deferred.resolve(prepareDashboard(response.data)); }, function fail() { deferred.reject(); }); @@ -230,8 +252,44 @@ function DashboardService($rootScope, $http, $q, $location, customerService) { if (port != 80 && port != 443) { url += ":" + port; } - url += "/dashboards/" + dashboard.id.id + "?publicId=" + dashboard.customerId.id; + url += "/dashboards/" + dashboard.id.id + "?publicId=" + dashboard.publicCustomerId; return url; } + function prepareDashboards(dashboardsData) { + if (dashboardsData.data) { + for (var i = 0; i < dashboardsData.data.length; i++) { + dashboardsData.data[i] = prepareDashboard(dashboardsData.data[i]); + } + } + return dashboardsData; + } + + function prepareDashboard(dashboard) { + dashboard.publicCustomerId = null; + dashboard.assignedCustomersText = ""; + dashboard.assignedCustomersIds = []; + if (dashboard.assignedCustomers && dashboard.assignedCustomers.length) { + var assignedCustomersTitles = []; + for (var i = 0; i < dashboard.assignedCustomers.length; i++) { + var assignedCustomer = dashboard.assignedCustomers[i]; + dashboard.assignedCustomersIds.push(assignedCustomer.customerId.id); + if (assignedCustomer.public) { + dashboard.publicCustomerId = assignedCustomer.customerId.id; + } else { + assignedCustomersTitles.push(assignedCustomer.title); + } + } + dashboard.assignedCustomersText = assignedCustomersTitles.join(', '); + } + return dashboard; + } + + function cleanDashboard(dashboard) { + delete dashboard.publicCustomerId; + delete dashboard.assignedCustomersText; + delete dashboard.assignedCustomersIds; + return dashboard; + } + } diff --git a/ui/src/app/api/entity.service.js b/ui/src/app/api/entity.service.js index df1c3e0bc9..d30c10cc37 100644 --- a/ui/src/app/api/entity.service.js +++ b/ui/src/app/api/entity.service.js @@ -273,9 +273,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device break; case types.entityType.dashboard: if (user.authority === 'CUSTOMER_USER') { - promise = dashboardService.getCustomerDashboards(customerId, pageLink, false, config); + promise = dashboardService.getCustomerDashboards(customerId, pageLink, config); } else { - promise = dashboardService.getTenantDashboards(pageLink, false, config); + promise = dashboardService.getTenantDashboards(pageLink, config); } break; case types.entityType.user: @@ -403,6 +403,21 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device return deferred.promise; } + function resolveAliasEntityId(entityType, id) { + var entityId = { + entityType: entityType, + id: id + }; + if (entityType == types.aliasEntityType.current_customer) { + var user = userService.getCurrentUser(); + entityId.entityType = types.entityType.customer; + if (user.authority === 'CUSTOMER_USER') { + entityId.id = user.customerId; + } + } + return entityId; + } + function getStateEntityId(filter, stateParams) { var entityId = null; if (stateParams) { @@ -417,6 +432,9 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device if (!entityId) { entityId = filter.defaultStateEntity; } + if (entityId) { + entityId = resolveAliasEntityId(entityId.entityType, entityId.id); + } return entityId; } @@ -432,7 +450,8 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device var stateEntityId = getStateEntityId(filter, stateParams); switch (filter.type) { case types.aliasFilterType.singleEntity.value: - getEntity(filter.singleEntity.entityType, filter.singleEntity.id, {ignoreLoading: true}).then( + var aliasEntityId = resolveAliasEntityId(filter.singleEntity.entityType, filter.singleEntity.id); + getEntity(aliasEntityId.entityType, aliasEntityId.id, {ignoreLoading: true}).then( function success(entity) { result.entities = entitiesToEntitiesInfo([entity]); deferred.resolve(result); @@ -530,10 +549,11 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device rootEntityId = filter.rootEntity.id; } if (rootEntityType && rootEntityId) { + var relationQueryRootEntityId = resolveAliasEntityId(rootEntityType, rootEntityId); var searchQuery = { parameters: { - rootId: rootEntityId, - rootType: rootEntityType, + rootId: relationQueryRootEntityId.id, + rootType: relationQueryRootEntityId.entityType, direction: filter.direction }, filters: filter.filters @@ -571,10 +591,11 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device rootEntityId = filter.rootEntity.id; } if (rootEntityType && rootEntityId) { + var searchQueryRootEntityId = resolveAliasEntityId(rootEntityType, rootEntityId); searchQuery = { parameters: { - rootId: rootEntityId, - rootType: rootEntityType, + rootId: searchQueryRootEntityId.id, + rootType: searchQueryRootEntityId.entityType, direction: filter.direction }, relationType: filter.relationType @@ -709,7 +730,7 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device return result; } - function prepareAllowedEntityTypesList(allowedEntityTypes) { + function prepareAllowedEntityTypesList(allowedEntityTypes, useAliasEntityTypes) { var authority = userService.getAuthority(); var entityTypes = {}; switch(authority) { @@ -726,12 +747,18 @@ function EntityService($http, $q, $filter, $translate, $log, userService, device entityTypes.rule = types.entityType.rule; entityTypes.plugin = types.entityType.plugin; entityTypes.dashboard = types.entityType.dashboard; + if (useAliasEntityTypes) { + entityTypes.current_customer = types.aliasEntityType.current_customer; + } break; case 'CUSTOMER_USER': entityTypes.device = types.entityType.device; entityTypes.asset = types.entityType.asset; entityTypes.customer = types.entityType.customer; entityTypes.dashboard = types.entityType.dashboard; + if (useAliasEntityTypes) { + entityTypes.current_customer = types.aliasEntityType.current_customer; + } break; } diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js index ce5e673f18..c969c2f48c 100644 --- a/ui/src/app/api/user.service.js +++ b/ui/src/app/api/user.service.js @@ -266,9 +266,9 @@ function UserService($http, $q, $rootScope, adminService, dashboardService, logi var pageLink = {limit: 100}; var fetchDashboardsPromise; if (currentUser.authority === 'TENANT_ADMIN') { - fetchDashboardsPromise = dashboardService.getTenantDashboards(pageLink, false); + fetchDashboardsPromise = dashboardService.getTenantDashboards(pageLink); } else { - fetchDashboardsPromise = dashboardService.getCustomerDashboards(currentUser.customerId, pageLink, false); + fetchDashboardsPromise = dashboardService.getCustomerDashboards(currentUser.customerId, pageLink); } fetchDashboardsPromise.then( function success(result) { diff --git a/ui/src/app/common/types.constant.js b/ui/src/app/common/types.constant.js index d82add14c0..6f09f3417b 100644 --- a/ui/src/app/common/types.constant.js +++ b/ui/src/app/common/types.constant.js @@ -296,6 +296,9 @@ export default angular.module('thingsboard.types', []) dashboard: "DASHBOARD", alarm: "ALARM" }, + aliasEntityType: { + current_customer: "CURRENT_CUSTOMER" + }, entityTypeTranslations: { "DEVICE": { type: 'entity.type-device', @@ -350,6 +353,10 @@ export default angular.module('thingsboard.types', []) typePlural: 'entity.type-alarms', list: 'entity.list-of-alarms', nameStartsWith: 'entity.alarm-name-starts-with' + }, + "CURRENT_CUSTOMER": { + type: 'entity.type-current-customer', + list: 'entity.type-current-customer' } }, entitySearchDirection: { diff --git a/ui/src/app/components/dashboard-autocomplete.directive.js b/ui/src/app/components/dashboard-autocomplete.directive.js index 2235b82e95..6af55853b2 100644 --- a/ui/src/app/components/dashboard-autocomplete.directive.js +++ b/ui/src/app/components/dashboard-autocomplete.directive.js @@ -48,7 +48,7 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u var promise; if (scope.dashboardsScope === 'customer' || userService.getAuthority() === 'CUSTOMER_USER') { if (scope.customerId) { - promise = dashboardService.getCustomerDashboards(scope.customerId, pageLink, false, {ignoreLoading: true}); + promise = dashboardService.getCustomerDashboards(scope.customerId, pageLink, {ignoreLoading: true}); } else { promise = $q.when({data: []}); } @@ -60,7 +60,7 @@ function DashboardAutocomplete($compile, $templateCache, $q, dashboardService, u promise = $q.when({data: []}); } } else { - promise = dashboardService.getTenantDashboards(pageLink, false, {ignoreLoading: true}); + promise = dashboardService.getTenantDashboards(pageLink, {ignoreLoading: true}); } } diff --git a/ui/src/app/components/dashboard-select.directive.js b/ui/src/app/components/dashboard-select.directive.js index ac5cd3d5b1..b1721687a6 100644 --- a/ui/src/app/components/dashboard-select.directive.js +++ b/ui/src/app/components/dashboard-select.directive.js @@ -48,12 +48,12 @@ function DashboardSelect($compile, $templateCache, $q, $mdMedia, $mdPanel, $docu var promise; if (scope.dashboardsScope === 'customer' || userService.getAuthority() === 'CUSTOMER_USER') { if (scope.customerId && scope.customerId != types.id.nullUid) { - promise = dashboardService.getCustomerDashboards(scope.customerId, pageLink, false, {ignoreLoading: true}); + promise = dashboardService.getCustomerDashboards(scope.customerId, pageLink, {ignoreLoading: true}); } else { promise = $q.when({data: []}); } } else { - promise = dashboardService.getTenantDashboards(pageLink, false, {ignoreLoading: true}); + promise = dashboardService.getTenantDashboards(pageLink, {ignoreLoading: true}); } promise.then(function success(result) { diff --git a/ui/src/app/dashboard/add-dashboards-to-customer.controller.js b/ui/src/app/dashboard/add-dashboards-to-customer.controller.js index 6fe6e7932e..b0bbef1b75 100644 --- a/ui/src/app/dashboard/add-dashboards-to-customer.controller.js +++ b/ui/src/app/dashboard/add-dashboards-to-customer.controller.js @@ -52,7 +52,7 @@ export default function AddDashboardsToCustomerController(dashboardService, $mdD fetchMoreItems_: function () { if (vm.dashboards.hasNext && !vm.dashboards.pending) { vm.dashboards.pending = true; - dashboardService.getTenantDashboards(vm.dashboards.nextPageLink, false).then( + dashboardService.getTenantDashboards(vm.dashboards.nextPageLink).then( function success(dashboards) { vm.dashboards.data = vm.dashboards.data.concat(dashboards.data); vm.dashboards.nextPageLink = dashboards.nextPageLink; diff --git a/ui/src/app/dashboard/assign-to-customer.controller.js b/ui/src/app/dashboard/assign-to-customer.controller.js deleted file mode 100644 index fa2cd0505d..0000000000 --- a/ui/src/app/dashboard/assign-to-customer.controller.js +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright © 2016-2017 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. - */ -/*@ngInject*/ -export default function AssignDashboardToCustomerController(customerService, dashboardService, $mdDialog, $q, dashboardIds, customers) { - - var vm = this; - - vm.customers = customers; - vm.searchText = ''; - - vm.assign = assign; - vm.cancel = cancel; - vm.isCustomerSelected = isCustomerSelected; - vm.hasData = hasData; - vm.noData = noData; - vm.searchCustomerTextUpdated = searchCustomerTextUpdated; - vm.toggleCustomerSelection = toggleCustomerSelection; - - vm.theCustomers = { - getItemAtIndex: function (index) { - if (index > vm.customers.data.length) { - vm.theCustomers.fetchMoreItems_(index); - return null; - } - var item = vm.customers.data[index]; - if (item) { - item.indexNumber = index + 1; - } - return item; - }, - - getLength: function () { - if (vm.customers.hasNext) { - return vm.customers.data.length + vm.customers.nextPageLink.limit; - } else { - return vm.customers.data.length; - } - }, - - fetchMoreItems_: function () { - if (vm.customers.hasNext && !vm.customers.pending) { - vm.customers.pending = true; - customerService.getCustomers(vm.customers.nextPageLink).then( - function success(customers) { - vm.customers.data = vm.customers.data.concat(customers.data); - vm.customers.nextPageLink = customers.nextPageLink; - vm.customers.hasNext = customers.hasNext; - if (vm.customers.hasNext) { - vm.customers.nextPageLink.limit = vm.customers.pageSize; - } - vm.customers.pending = false; - }, - function fail() { - vm.customers.hasNext = false; - vm.customers.pending = false; - }); - } - } - }; - - function cancel () { - $mdDialog.cancel(); - } - - function assign () { - var tasks = []; - for (var dashboardId in dashboardIds) { - tasks.push(dashboardService.assignDashboardToCustomer(vm.customers.selection.id.id, dashboardIds[dashboardId])); - } - $q.all(tasks).then(function () { - $mdDialog.hide(); - }); - } - - function noData () { - return vm.customers.data.length == 0 && !vm.customers.hasNext; - } - - function hasData () { - return vm.customers.data.length > 0; - } - - function toggleCustomerSelection ($event, customer) { - $event.stopPropagation(); - if (vm.isCustomerSelected(customer)) { - vm.customers.selection = null; - } else { - vm.customers.selection = customer; - } - } - - function isCustomerSelected (customer) { - return vm.customers.selection != null && customer && - customer.id.id === vm.customers.selection.id.id; - } - - function searchCustomerTextUpdated () { - vm.customers = { - pageSize: vm.customers.pageSize, - data: [], - nextPageLink: { - limit: vm.customers.pageSize, - textSearch: vm.searchText - }, - selection: null, - hasNext: true, - pending: false - }; - } -} diff --git a/ui/src/app/dashboard/assign-to-customer.tpl.html b/ui/src/app/dashboard/assign-to-customer.tpl.html deleted file mode 100644 index 4d7ba85452..0000000000 --- a/ui/src/app/dashboard/assign-to-customer.tpl.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - -
-

dashboard.assign-dashboard-to-customer

- - - - -
-
- - - -
-
- dashboard.assign-to-customer-text - - - - search - - - -
- customer.no-customers-text - - - - - {{ customer.title }} - - - -
-
-
-
- - - - {{ 'action.assign' | translate }} - - {{ 'action.cancel' | - translate }} - - - -
\ No newline at end of file diff --git a/ui/src/app/dashboard/dashboard-card.scss b/ui/src/app/dashboard/dashboard-card.scss new file mode 100644 index 0000000000..32f25c7aab --- /dev/null +++ b/ui/src/app/dashboard/dashboard-card.scss @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2017 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. + */ + +.tb-dashboard-assigned-customers { + display: block; + display: -webkit-box; + height: 34px; + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + margin-bottom: 4px; +} + diff --git a/ui/src/app/dashboard/dashboard-card.tpl.html b/ui/src/app/dashboard/dashboard-card.tpl.html index 636786758e..ed19fda07a 100644 --- a/ui/src/app/dashboard/dashboard-card.tpl.html +++ b/ui/src/app/dashboard/dashboard-card.tpl.html @@ -15,6 +15,6 @@ limitations under the License. --> -
{{'dashboard.assignedToCustomer' | translate}} '{{vm.item.assignedCustomer.title}}'
-
{{'dashboard.public' | translate}}
+
{{'dashboard.assignedToCustomers' | translate}}: '{{vm.item.assignedCustomersText}}'
+
{{'dashboard.public' | translate}}
diff --git a/ui/src/app/dashboard/dashboard-fieldset.tpl.html b/ui/src/app/dashboard/dashboard-fieldset.tpl.html index a54f477d11..00ee554d84 100644 --- a/ui/src/app/dashboard/dashboard-fieldset.tpl.html +++ b/ui/src/app/dashboard/dashboard-fieldset.tpl.html @@ -19,24 +19,29 @@ ng-show="!isEdit && dashboardScope === 'tenant'" class="md-raised md-primary">{{ 'dashboard.export' | translate }}
{{ 'dashboard.make-public' | translate }} -{{ 'dashboard.assign-to-customer' | translate }} -{{ isPublic ? 'dashboard.make-private' : 'dashboard.unassign-from-customer' | translate }} +{{ 'dashboard.make-private' | translate }} +{{ 'dashboard.manage-assigned-customers' | translate }} +{{ 'dashboard.unassign-from-customer' | translate }} {{ 'dashboard.delete' | translate }} - - + ng-show="!isEdit && dashboard.assignedCustomersText && dashboardScope === 'tenant'"> + + -
+
diff --git a/ui/src/app/dashboard/index.js b/ui/src/app/dashboard/index.js index 40897164fa..ac63dc55a2 100644 --- a/ui/src/app/dashboard/index.js +++ b/ui/src/app/dashboard/index.js @@ -40,8 +40,8 @@ import DashboardRoutes from './dashboard.routes'; import {DashboardsController, DashboardCardController, MakeDashboardPublicDialogController} from './dashboards.controller'; import DashboardController from './dashboard.controller'; import DashboardSettingsController from './dashboard-settings.controller'; -import AssignDashboardToCustomerController from './assign-to-customer.controller'; import AddDashboardsToCustomerController from './add-dashboards-to-customer.controller'; +import ManageAssignedCustomersController from './manage-assigned-customers.controller'; import AddWidgetController from './add-widget.controller'; import DashboardDirective from './dashboard.directive'; import EditWidgetDirective from './edit-widget.directive'; @@ -74,8 +74,8 @@ export default angular.module('thingsboard.dashboard', [ .controller('MakeDashboardPublicDialogController', MakeDashboardPublicDialogController) .controller('DashboardController', DashboardController) .controller('DashboardSettingsController', DashboardSettingsController) - .controller('AssignDashboardToCustomerController', AssignDashboardToCustomerController) .controller('AddDashboardsToCustomerController', AddDashboardsToCustomerController) + .controller('ManageAssignedCustomersController', ManageAssignedCustomersController) .controller('AddWidgetController', AddWidgetController) .directive('tbDashboardDetails', DashboardDirective) .directive('tbEditWidget', EditWidgetDirective) diff --git a/ui/src/app/dashboard/manage-assigned-customers.controller.js b/ui/src/app/dashboard/manage-assigned-customers.controller.js new file mode 100644 index 0000000000..90d3e6dc84 --- /dev/null +++ b/ui/src/app/dashboard/manage-assigned-customers.controller.js @@ -0,0 +1,69 @@ +/* + * Copyright © 2016-2017 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. + */ +/*@ngInject*/ +export default function ManageAssignedCustomersController($mdDialog, $q, types, dashboardService, actionType, dashboardIds, assignedCustomers) { + + var vm = this; + + vm.types = types; + vm.actionType = actionType; + vm.dashboardIds = dashboardIds; + vm.assignedCustomers = assignedCustomers; + if (actionType != 'manage') { + vm.assignedCustomers = []; + } + + if (actionType == 'manage') { + vm.titleText = 'dashboard.manage-assigned-customers'; + vm.labelText = 'dashboard.assigned-customers'; + vm.actionName = 'action.update'; + } else if (actionType == 'assign') { + vm.titleText = 'dashboard.assign-to-customers'; + vm.labelText = 'dashboard.assign-to-customers-text'; + vm.actionName = 'action.assign'; + } else if (actionType == 'unassign') { + vm.titleText = 'dashboard.unassign-from-customers'; + vm.labelText = 'dashboard.unassign-from-customers-text'; + vm.actionName = 'action.unassign'; + } + + vm.submit = submit; + vm.cancel = cancel; + + function cancel () { + $mdDialog.cancel(); + } + + function submit () { + var tasks = []; + for (var i=0;i + +
+ +
+

{{vm.titleText}}

+ + + + +
+
+ + + +
+
+ {{vm.labelText}} + +
+
+
+ + + + {{ vm.actionName | translate }} + + {{ 'action.cancel' | + translate }} + + + +
\ No newline at end of file diff --git a/ui/src/app/entity/entity-autocomplete.directive.js b/ui/src/app/entity/entity-autocomplete.directive.js index 62f3d6d33e..2d114dbb2e 100644 --- a/ui/src/app/entity/entity-autocomplete.directive.js +++ b/ui/src/app/entity/entity-autocomplete.directive.js @@ -38,7 +38,12 @@ export default function EntityAutocomplete($compile, $templateCache, $q, $filter if (scope.excludeEntityIds && scope.excludeEntityIds.length) { limit += scope.excludeEntityIds.length; } - entityService.getEntitiesByNameFilter(scope.entityType, searchText, limit, {ignoreLoading: true}, scope.entitySubtype).then(function success(result) { + var targetType = scope.entityType; + if (targetType == types.aliasEntityType.current_customer) { + targetType = types.entityType.customer; + } + + entityService.getEntitiesByNameFilter(targetType, searchText, limit, {ignoreLoading: true}, scope.entitySubtype).then(function success(result) { if (result) { if (scope.excludeEntityIds && scope.excludeEntityIds.length) { var entities = []; @@ -71,7 +76,11 @@ export default function EntityAutocomplete($compile, $templateCache, $q, $filter ngModelCtrl.$render = function () { if (ngModelCtrl.$viewValue) { - entityService.getEntity(scope.entityType, ngModelCtrl.$viewValue).then( + var targetType = scope.entityType; + if (targetType == types.aliasEntityType.current_customer) { + targetType = types.entityType.customer; + } + entityService.getEntity(targetType, ngModelCtrl.$viewValue).then( function success(entity) { scope.entity = entity; }, @@ -114,55 +123,61 @@ export default function EntityAutocomplete($compile, $templateCache, $q, $filter scope.selectEntityText = 'asset.select-asset'; scope.entityText = 'asset.asset'; scope.noEntitiesMatchingText = 'asset.no-assets-matching'; - scope.entityRequiredText = 'asset.asset-required' + scope.entityRequiredText = 'asset.asset-required'; break; case types.entityType.device: scope.selectEntityText = 'device.select-device'; scope.entityText = 'device.device'; scope.noEntitiesMatchingText = 'device.no-devices-matching'; - scope.entityRequiredText = 'device.device-required' + scope.entityRequiredText = 'device.device-required'; break; case types.entityType.rule: scope.selectEntityText = 'rule.select-rule'; scope.entityText = 'rule.rule'; scope.noEntitiesMatchingText = 'rule.no-rules-matching'; - scope.entityRequiredText = 'rule.rule-required' + scope.entityRequiredText = 'rule.rule-required'; break; case types.entityType.plugin: scope.selectEntityText = 'plugin.select-plugin'; scope.entityText = 'plugin.plugin'; scope.noEntitiesMatchingText = 'plugin.no-plugins-matching'; - scope.entityRequiredText = 'plugin.plugin-required' + scope.entityRequiredText = 'plugin.plugin-required'; break; case types.entityType.tenant: scope.selectEntityText = 'tenant.select-tenant'; scope.entityText = 'tenant.tenant'; scope.noEntitiesMatchingText = 'tenant.no-tenants-matching'; - scope.entityRequiredText = 'tenant.tenant-required' + scope.entityRequiredText = 'tenant.tenant-required'; break; case types.entityType.customer: scope.selectEntityText = 'customer.select-customer'; scope.entityText = 'customer.customer'; scope.noEntitiesMatchingText = 'customer.no-customers-matching'; - scope.entityRequiredText = 'customer.customer-required' + scope.entityRequiredText = 'customer.customer-required'; break; case types.entityType.user: scope.selectEntityText = 'user.select-user'; scope.entityText = 'user.user'; scope.noEntitiesMatchingText = 'user.no-users-matching'; - scope.entityRequiredText = 'user.user-required' + scope.entityRequiredText = 'user.user-required'; break; case types.entityType.dashboard: scope.selectEntityText = 'dashboard.select-dashboard'; scope.entityText = 'dashboard.dashboard'; scope.noEntitiesMatchingText = 'dashboard.no-dashboards-matching'; - scope.entityRequiredText = 'dashboard.dashboard-required' + scope.entityRequiredText = 'dashboard.dashboard-required'; break; case types.entityType.alarm: scope.selectEntityText = 'alarm.select-alarm'; scope.entityText = 'alarm.alarm'; scope.noEntitiesMatchingText = 'alarm.no-alarms-matching'; - scope.entityRequiredText = 'alarm.alarm-required' + scope.entityRequiredText = 'alarm.alarm-required'; + break; + case types.aliasEntityType.current_customer: + scope.selectEntityText = 'customer.select-default-customer'; + scope.entityText = 'customer.default-customer'; + scope.noEntitiesMatchingText = 'customer.no-customers-matching'; + scope.entityRequiredText = 'customer.default-customer-required'; break; } if (scope.entity && scope.entity.id.entityType != scope.entityType) { diff --git a/ui/src/app/entity/entity-filter.tpl.html b/ui/src/app/entity/entity-filter.tpl.html index 9f62454915..0e8ee1177b 100644 --- a/ui/src/app/entity/entity-filter.tpl.html +++ b/ui/src/app/entity/entity-filter.tpl.html @@ -32,6 +32,7 @@ @@ -78,6 +79,7 @@
@@ -123,6 +125,7 @@ the-form="theForm" tb-required="!filter.rootStateEntity" ng-disabled="filter.rootStateEntity" + use-alias-entity-types="true" ng-model="filter.rootEntity">
@@ -139,6 +142,7 @@
@@ -182,6 +186,7 @@ the-form="theForm" tb-required="!filter.rootStateEntity" ng-disabled="filter.rootStateEntity" + use-alias-entity-types="true" ng-model="filter.rootEntity"> @@ -198,6 +203,7 @@ @@ -249,6 +255,7 @@ the-form="theForm" tb-required="!filter.rootStateEntity" ng-disabled="filter.rootStateEntity" + use-alias-entity-types="true" ng-model="filter.rootEntity"> @@ -265,6 +272,7 @@ diff --git a/ui/src/app/entity/entity-select.directive.js b/ui/src/app/entity/entity-select.directive.js index 2778a79a17..8e2fbf9674 100644 --- a/ui/src/app/entity/entity-select.directive.js +++ b/ui/src/app/entity/entity-select.directive.js @@ -105,7 +105,8 @@ export default function EntitySelect($compile, $templateCache) { scope: { theForm: '=?', tbRequired: '=?', - disabled:'=ngDisabled' + disabled:'=ngDisabled', + useAliasEntityTypes: "=?" } }; } diff --git a/ui/src/app/entity/entity-select.tpl.html b/ui/src/app/entity/entity-select.tpl.html index 13e17e7af1..d37020220d 100644 --- a/ui/src/app/entity/entity-select.tpl.html +++ b/ui/src/app/entity/entity-select.tpl.html @@ -20,6 +20,7 @@ the-form="theForm" ng-disabled="disabled" tb-required="tbRequired" + use-alias-entity-types="useAliasEntityTypes" ng-model="model.entityType"> Date: Thu, 1 Mar 2018 12:58:57 +0200 Subject: [PATCH 45/57] Fix SQL upgrade script. --- .../service/install/sql/SqlDbHelper.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java b/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java index c78ceda02d..669646f1fb 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java +++ b/application/src/main/java/org/thingsboard/server/service/install/sql/SqlDbHelper.java @@ -45,11 +45,7 @@ public class SqlDbHelper { public static Path dumpTableIfExists(Connection conn, String tableName, String[] columns, String[] defaultValues, String dumpPrefix, boolean printHeader) throws Exception { - DatabaseMetaData metaData = conn.getMetaData(); - ResultSet res = metaData.getTables(null, null, tableName, - new String[] {"TABLE"}); - if (res.next()) { - res.close(); + if (tableExists(conn, tableName)) { Path dumpFile = Files.createTempFile(dumpPrefix, null); Files.deleteIfExists(dumpFile); CSVFormat csvFormat = CSV_DUMP_FORMAT; @@ -61,9 +57,9 @@ public class SqlDbHelper { try (ResultSet tableRes = stmt.executeQuery()) { ResultSetMetaData resMetaData = tableRes.getMetaData(); Map columnIndexMap = new HashMap<>(); - for (int i = 0; i < resMetaData.getColumnCount(); i++) { + for (int i = 1; i <= resMetaData.getColumnCount(); i++) { String columnName = resMetaData.getColumnName(i); - columnIndexMap.put(columnName, i); + columnIndexMap.put(columnName.toUpperCase(), i); } while(tableRes.next()) { dumpRow(tableRes, columnIndexMap, columns, defaultValues, csvPrinter); @@ -77,6 +73,15 @@ public class SqlDbHelper { } } + private static boolean tableExists(Connection conn, String tableName) { + try (Statement stmt = conn.createStatement()) { + stmt.executeQuery("select * from " + tableName + " where 1=0"); + return true; + } catch (Exception e) { + return false; + } + } + public static void loadTable(Connection conn, String tableName, String[] columns, Path sourceFile) throws Exception { loadTable(conn, tableName, columns, sourceFile, false); } @@ -89,7 +94,6 @@ public class SqlDbHelper { csvFormat = CSV_DUMP_FORMAT.withHeader(columns); } try (PreparedStatement prepared = conn.prepareStatement(createInsertStatement(tableName, columns))) { - prepared.getParameterMetaData(); try (CSVParser csvParser = new CSVParser(Files.newBufferedReader(sourceFile), csvFormat)) { csvParser.forEach(record -> { try { @@ -122,13 +126,13 @@ public class SqlDbHelper { } private static String getColumnValue(String column, String defaultValue, Map columnIndexMap, ResultSet res) { - int index = columnIndexMap.containsKey(column) ? columnIndexMap.get(column) : -1; + int index = columnIndexMap.containsKey(column.toUpperCase()) ? columnIndexMap.get(column.toUpperCase()) : -1; if (index > -1) { String str; try { Object obj = res.getObject(index); if (obj == null) { - str = ""; + return null; } else { str = obj.toString(); } From ea33ce154f7dc6c5576f6d7086a5dab53acbc878 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 1 Mar 2018 13:19:09 +0200 Subject: [PATCH 46/57] Add Fallback to English language for missing translations. --- ui/src/app/app.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/app/app.config.js b/ui/src/app/app.config.js index b520c1a9c6..506be17e18 100644 --- a/ui/src/app/app.config.js +++ b/ui/src/app/app.config.js @@ -49,6 +49,7 @@ export default function AppConfig($provide, $translateProvider.useSanitizeValueStrategy(null); $translateProvider.useMissingTranslationHandler('tbMissingTranslationHandler'); $translateProvider.addInterpolation('$translateMessageFormatInterpolation'); + $translateProvider.fallbackLanguage('en_US'); addLocaleKorean(locales); addLocaleChinese(locales); From 9151bfde83bee726622a7fc5609a09f04ee7affd Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 1 Mar 2018 20:32:04 +0200 Subject: [PATCH 47/57] Fix widgets bundles upgrade. --- .../thingsboard/server/install/ThingsboardInstallService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index d918b22bfc..1fddb9ff1e 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -94,6 +94,7 @@ public class ThingsboardInstallService { systemDataLoaderService.deleteSystemWidgetBundle("alarm_widgets"); systemDataLoaderService.deleteSystemWidgetBundle("control_widgets"); systemDataLoaderService.deleteSystemWidgetBundle("maps_v2"); + systemDataLoaderService.deleteSystemWidgetBundle("gateway_widgets"); systemDataLoaderService.loadSystemWidgets(); From 8fdc5a8a8b69949e1396b6e5e03d10543d29ba30 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Fri, 2 Mar 2018 10:36:22 +0200 Subject: [PATCH 48/57] Fix: use time-based null UUID for public userId. --- .../security/auth/jwt/RefreshTokenAuthenticationProvider.java | 3 ++- .../service/security/auth/rest/RestAuthenticationProvider.java | 3 ++- .../java/org/thingsboard/server/common/data/id/UUIDBased.java | 2 -- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java index be5e546aaf..d22d634c2d 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java @@ -25,6 +25,7 @@ import org.springframework.util.Assert; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; @@ -108,7 +109,7 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide throw new BadCredentialsException("Refresh token is not valid"); } - User user = new User(new UserId(UUIDBased.EMPTY)); + User user = new User(new UserId(EntityId.NULL_UUID)); user.setTenantId(publicCustomer.getTenantId()); user.setCustomerId(publicCustomer.getId()); user.setEmail(publicId); diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java index 661502319b..bb3a471782 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java @@ -26,6 +26,7 @@ import org.springframework.util.Assert; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.Authority; @@ -111,7 +112,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider { if (!publicCustomer.isPublic()) { throw new BadCredentialsException("Authentication Failed. Public Id is not valid."); } - User user = new User(new UserId(UUIDBased.EMPTY)); + User user = new User(new UserId(EntityId.NULL_UUID)); user.setTenantId(publicCustomer.getTenantId()); user.setCustomerId(publicCustomer.getId()); user.setEmail(publicId); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java index e39fd7a102..cf6a9ba095 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java @@ -20,8 +20,6 @@ import java.util.UUID; public abstract class UUIDBased implements Serializable { - public static final UUID EMPTY = new UUID(0L, 0L); - private static final long serialVersionUID = 1L; private final UUID id; From a593e5a9d9eec978d12e25735e6828ca69109f2d Mon Sep 17 00:00:00 2001 From: mp-loki Date: Fri, 2 Mar 2018 23:02:31 -0500 Subject: [PATCH 49/57] OPC-UA extension ui Application URI made optional --- .../extension/extensions-forms/extension-form-opc.tpl.html | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html b/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html index 6d74cc560c..3bcea1dbae 100644 --- a/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html +++ b/ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html @@ -63,10 +63,7 @@ - -
-
extension.field-required
-
+
From 8a28cd703fa63f9dbd12464088e6d25593e5aa86 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 5 Mar 2018 17:34:13 +0200 Subject: [PATCH 50/57] Update license header year to 2018. --- application/build.gradle | 3 +-- application/pom.xml | 2 +- application/src/main/assembly/windows.xml | 2 +- application/src/main/conf/logback.xml | 2 +- application/src/main/conf/thingsboard.conf | 2 +- .../src/main/data/upgrade/1.3.0/schema_update.cql | 2 +- .../src/main/data/upgrade/1.3.1/schema_update.sql | 2 +- .../src/main/data/upgrade/1.4.0/schema_update.cql | 2 +- .../src/main/data/upgrade/1.4.0/schema_update.sql | 2 +- .../server/ThingsboardInstallApplication.java | 3 +-- .../server/ThingsboardServerApplication.java | 2 +- .../server/actors/ActorSystemContext.java | 2 +- .../thingsboard/server/actors/app/AppActor.java | 2 +- .../server/actors/device/DeviceActor.java | 2 +- .../device/DeviceActorMessageProcessor.java | 2 +- .../server/actors/device/SessionInfo.java | 2 +- .../actors/device/ToDeviceRpcRequestMetadata.java | 2 +- .../server/actors/plugin/PluginActor.java | 2 +- .../plugin/PluginActorMessageProcessor.java | 2 +- .../actors/plugin/PluginCallbackMessage.java | 2 +- .../actors/plugin/PluginProcessingContext.java | 2 +- .../actors/plugin/PluginTerminationMsg.java | 2 +- .../actors/plugin/RuleToPluginMsgWrapper.java | 2 +- .../plugin/SharedPluginProcessingContext.java | 2 +- .../server/actors/plugin/TimeoutScheduler.java | 2 +- .../server/actors/plugin/ValidationCallback.java | 2 +- .../server/actors/plugin/ValidationResult.java | 2 +- .../actors/plugin/ValidationResultCode.java | 2 +- .../actors/rpc/BasicRpcSessionListener.java | 2 +- .../server/actors/rpc/RpcBroadcastMsg.java | 2 +- .../server/actors/rpc/RpcManagerActor.java | 2 +- .../server/actors/rpc/RpcSessionActor.java | 2 +- .../server/actors/rpc/RpcSessionClosedMsg.java | 2 +- .../server/actors/rpc/RpcSessionConnectedMsg.java | 2 +- .../actors/rpc/RpcSessionCreateRequestMsg.java | 2 +- .../actors/rpc/RpcSessionDisconnectedMsg.java | 2 +- .../server/actors/rpc/RpcSessionTellMsg.java | 2 +- .../server/actors/rpc/SessionActorInfo.java | 2 +- .../actors/rule/ChainProcessingContext.java | 2 +- .../actors/rule/ChainProcessingMetaData.java | 2 +- .../server/actors/rule/ComplexRuleActorChain.java | 2 +- .../actors/rule/CompoundRuleActorChain.java | 2 +- .../thingsboard/server/actors/rule/RuleActor.java | 2 +- .../server/actors/rule/RuleActorChain.java | 2 +- .../actors/rule/RuleActorMessageProcessor.java | 2 +- .../server/actors/rule/RuleActorMetaData.java | 2 +- .../actors/rule/RuleContextAwareMsgProcessor.java | 2 +- .../server/actors/rule/RuleProcessingContext.java | 2 +- .../server/actors/rule/RuleProcessingMsg.java | 2 +- .../server/actors/rule/RuleTerminationMsg.java | 2 +- .../actors/rule/RuleToPluginTimeoutMsg.java | 2 +- .../server/actors/rule/RulesProcessedMsg.java | 2 +- .../server/actors/rule/SimpleRuleActorChain.java | 2 +- .../server/actors/service/ActorService.java | 2 +- .../server/actors/service/ComponentActor.java | 2 +- .../server/actors/service/ContextAwareActor.java | 2 +- .../actors/service/ContextBasedCreator.java | 2 +- .../actors/service/DefaultActorService.java | 2 +- .../server/actors/service/RestMsgProcessor.java | 2 +- .../actors/service/WebSocketMsgProcessor.java | 2 +- .../server/actors/session/ASyncMsgProcessor.java | 2 +- .../session/AbstractSessionActorMsgProcessor.java | 2 +- .../server/actors/session/SessionActor.java | 2 +- .../actors/session/SessionManagerActor.java | 2 +- .../actors/session/SessionTerminationMsg.java | 2 +- .../server/actors/session/SyncMsgProcessor.java | 2 +- .../shared/AbstractContextAwareMsgProcessor.java | 2 +- .../server/actors/shared/ActorTerminationMsg.java | 2 +- .../actors/shared/ComponentMsgProcessor.java | 2 +- .../server/actors/shared/SessionTimeoutMsg.java | 2 +- .../actors/shared/plugin/PluginManager.java | 2 +- .../actors/shared/plugin/SystemPluginManager.java | 2 +- .../actors/shared/plugin/TenantPluginManager.java | 2 +- .../server/actors/shared/rule/RuleManager.java | 2 +- .../actors/shared/rule/SystemRuleManager.java | 2 +- .../actors/shared/rule/TenantRuleManager.java | 2 +- .../server/actors/stats/StatsActor.java | 2 +- .../server/actors/stats/StatsPersistMsg.java | 2 +- .../server/actors/stats/StatsPersistTick.java | 2 +- .../server/actors/tenant/RuleChainDeviceMsg.java | 2 +- .../server/actors/tenant/TenantActor.java | 2 +- .../server/config/AuditLogLevelProperties.java | 2 +- .../thingsboard/server/config/JwtSettings.java | 2 +- .../server/config/MvcCorsProperties.java | 2 +- .../server/config/SwaggerConfiguration.java | 2 +- .../config/ThingsboardMessageConfiguration.java | 2 +- .../config/ThingsboardSecurityConfiguration.java | 2 +- .../org/thingsboard/server/config/WebConfig.java | 2 +- .../server/config/WebSocketConfiguration.java | 2 +- .../server/controller/AdminController.java | 2 +- .../server/controller/AlarmController.java | 2 +- .../server/controller/AssetController.java | 2 +- .../server/controller/AuditLogController.java | 2 +- .../server/controller/AuthController.java | 2 +- .../server/controller/BaseController.java | 2 +- .../controller/ComponentDescriptorController.java | 2 +- .../server/controller/CustomerController.java | 2 +- .../server/controller/DashboardController.java | 2 +- .../server/controller/DeviceController.java | 2 +- .../controller/EntityRelationController.java | 2 +- .../server/controller/EventController.java | 2 +- .../server/controller/PluginController.java | 2 +- .../server/controller/RuleController.java | 2 +- .../server/controller/TenantController.java | 2 +- .../server/controller/UserController.java | 2 +- .../server/controller/WidgetTypeController.java | 2 +- .../controller/WidgetsBundleController.java | 2 +- .../controller/plugin/PluginApiController.java | 2 +- .../plugin/PluginNotFoundException.java | 2 +- .../controller/plugin/PluginWebSocketHandler.java | 2 +- .../plugin/PluginWebSocketMsgEndpoint.java | 2 +- .../server/exception/ThingsboardErrorCode.java | 2 +- .../exception/ThingsboardErrorResponse.java | 2 +- .../ThingsboardErrorResponseHandler.java | 2 +- .../server/exception/ThingsboardException.java | 2 +- .../install/ThingsboardInstallConfiguration.java | 3 +-- .../install/ThingsboardInstallException.java | 3 +-- .../server/install/ThingsboardInstallService.java | 3 +-- .../discovery/CurrentServerInstanceService.java | 2 +- .../cluster/discovery/DiscoveryService.java | 2 +- .../discovery/DiscoveryServiceListener.java | 2 +- .../cluster/discovery/DummyDiscoveryService.java | 2 +- .../service/cluster/discovery/ServerInstance.java | 2 +- .../cluster/discovery/ServerInstanceService.java | 2 +- .../cluster/discovery/ZkDiscoveryService.java | 2 +- .../cluster/routing/ClusterRoutingService.java | 2 +- .../routing/ConsistentClusterRoutingService.java | 2 +- .../service/cluster/rpc/ClusterGrpcService.java | 2 +- .../service/cluster/rpc/ClusterRpcService.java | 2 +- .../server/service/cluster/rpc/GrpcSession.java | 2 +- .../service/cluster/rpc/GrpcSessionListener.java | 2 +- .../service/cluster/rpc/RpcMsgListener.java | 2 +- .../cluster/rpc/RpcSessionCreationFuture.java | 2 +- .../AnnotationComponentDiscoveryService.java | 2 +- .../component/ComponentDiscoveryService.java | 2 +- .../environment/EnvironmentLogService.java | 2 +- .../install/CassandraDatabaseSchemaService.java | 3 +-- .../install/CassandraDatabaseUpgradeService.java | 3 +-- .../server/service/install/DatabaseHelper.java | 2 +- .../service/install/DatabaseSchemaService.java | 2 +- .../service/install/DatabaseUpgradeService.java | 3 +-- .../install/DefaultSystemDataLoaderService.java | 3 +-- .../service/install/SqlDatabaseSchemaService.java | 3 +-- .../install/SqlDatabaseUpgradeService.java | 3 +-- .../service/install/SystemDataLoaderService.java | 2 +- .../service/install/cql/CQLStatementsParser.java | 3 +-- .../service/install/cql/CassandraDbHelper.java | 3 +-- .../server/service/install/sql/SqlDbHelper.java | 2 +- .../server/service/mail/DefaultMailService.java | 2 +- .../server/service/mail/MailService.java | 2 +- .../auth/AbstractJwtAuthenticationToken.java | 2 +- .../security/auth/JwtAuthenticationToken.java | 2 +- .../security/auth/RefreshAuthenticationToken.java | 2 +- .../auth/jwt/JwtAuthenticationProvider.java | 2 +- .../JwtTokenAuthenticationProcessingFilter.java | 2 +- .../jwt/RefreshTokenAuthenticationProvider.java | 2 +- .../auth/jwt/RefreshTokenProcessingFilter.java | 2 +- .../security/auth/jwt/RefreshTokenRepository.java | 2 +- .../security/auth/jwt/RefreshTokenRequest.java | 2 +- .../security/auth/jwt/SkipPathRequestMatcher.java | 2 +- .../jwt/extractor/JwtHeaderTokenExtractor.java | 2 +- .../jwt/extractor/JwtQueryTokenExtractor.java | 2 +- .../auth/jwt/extractor/TokenExtractor.java | 2 +- .../service/security/auth/rest/LoginRequest.java | 2 +- .../security/auth/rest/PublicLoginRequest.java | 2 +- .../auth/rest/RestAuthenticationProvider.java | 2 +- .../RestAwareAuthenticationFailureHandler.java | 2 +- .../RestAwareAuthenticationSuccessHandler.java | 2 +- .../auth/rest/RestLoginProcessingFilter.java | 2 +- .../rest/RestPublicLoginProcessingFilter.java | 2 +- .../security/device/DefaultDeviceAuthService.java | 2 +- .../AuthMethodNotSupportedException.java | 2 +- .../exception/JwtExpiredTokenException.java | 2 +- .../service/security/model/SecurityUser.java | 2 +- .../service/security/model/UserPrincipal.java | 3 +-- .../security/model/token/AccessJwtToken.java | 2 +- .../service/security/model/token/JwtToken.java | 2 +- .../security/model/token/JwtTokenFactory.java | 2 +- .../security/model/token/RawAccessJwtToken.java | 2 +- .../service/update/DefaultUpdateService.java | 3 +-- .../server/service/update/UpdateService.java | 3 +-- .../service/update/model/UpdateMessage.java | 2 +- .../org/thingsboard/server/utils/MiscUtils.java | 2 +- application/src/main/proto/cluster.proto | 2 +- application/src/main/proto/discovery.proto | 2 +- application/src/main/resources/actor-system.conf | 2 +- application/src/main/resources/logback.xml | 2 +- .../main/resources/templates/account.activated.vm | 2 +- .../src/main/resources/templates/activation.vm | 2 +- .../resources/templates/password.was.reset.vm | 2 +- .../main/resources/templates/reset.password.vm | 2 +- application/src/main/resources/templates/test.vm | 2 +- application/src/main/resources/thingsboard.yml | 2 +- application/src/main/scripts/install/install.sh | 2 +- .../src/main/scripts/install/install_dev_db.sh | 2 +- application/src/main/scripts/install/logback.xml | 2 +- application/src/main/scripts/install/upgrade.sh | 2 +- .../server/actors/ActorsTestSuite.java | 2 +- .../server/actors/DefaultActorServiceTest.java | 2 +- .../thingsboard/server/actors/DummySessionID.java | 2 +- .../server/controller/AbstractControllerTest.java | 2 +- .../controller/BaseAdminControllerTest.java | 2 +- .../controller/BaseAssetControllerTest.java | 2 +- .../controller/BaseAuditLogControllerTest.java | 2 +- .../server/controller/BaseAuthControllerTest.java | 2 +- .../BaseComponentDescriptorControllerTest.java | 2 +- .../controller/BaseCustomerControllerTest.java | 2 +- .../controller/BaseDashboardControllerTest.java | 2 +- .../controller/BaseDeviceControllerTest.java | 2 +- .../controller/BasePluginControllerTest.java | 2 +- .../server/controller/BaseRuleControllerTest.java | 2 +- .../controller/BaseTenantControllerTest.java | 2 +- .../server/controller/BaseUserControllerTest.java | 2 +- .../controller/BaseWidgetTypeControllerTest.java | 2 +- .../BaseWidgetsBundleControllerTest.java | 2 +- .../controller/ControllerNoSqlTestSuite.java | 2 +- .../server/controller/ControllerSqlTestSuite.java | 2 +- .../nosql/AdminControllerNoSqlTest.java | 2 +- .../nosql/AssetControllerNoSqlTest.java | 2 +- .../nosql/AuditLogControllerNoSqlTest.java | 2 +- .../controller/nosql/AuthControllerNoSqlTest.java | 2 +- .../ComponentDescriptorControllerNoSqlTest.java | 2 +- .../nosql/CustomerControllerNoSqlTest.java | 2 +- .../nosql/DashboardControllerNoSqlTest.java | 2 +- .../nosql/DeviceControllerNoSqlTest.java | 2 +- .../nosql/PluginControllerNoSqlTest.java | 2 +- .../controller/nosql/RuleControllerNoSqlTest.java | 2 +- .../nosql/TenantControllerNoSqlTest.java | 2 +- .../controller/nosql/UserControllerNoSqlTest.java | 2 +- .../nosql/WidgetTypeControllerNoSqlTest.java | 2 +- .../nosql/WidgetsBundleControllerNoSqlTest.java | 2 +- .../controller/sql/AdminControllerSqlTest.java | 2 +- .../controller/sql/AssetControllerSqlTest.java | 2 +- .../controller/sql/AuditLogControllerSqlTest.java | 2 +- .../controller/sql/AuthControllerSqlTest.java | 2 +- .../sql/ComponentDescriptorControllerSqlTest.java | 2 +- .../controller/sql/CustomerControllerSqlTest.java | 2 +- .../sql/DashboardControllerSqlTest.java | 2 +- .../controller/sql/DeviceControllerSqlTest.java | 2 +- .../controller/sql/PluginControllerSqlTest.java | 2 +- .../controller/sql/RuleControllerSqlTest.java | 2 +- .../controller/sql/TenantControllerSqlTest.java | 2 +- .../controller/sql/UserControllerSqlTest.java | 2 +- .../sql/WidgetTypeControllerSqlTest.java | 2 +- .../sql/WidgetsBundleControllerSqlTest.java | 2 +- .../server/mqtt/MqttNoSqlTestSuite.java | 2 +- .../thingsboard/server/mqtt/MqttSqlTestSuite.java | 2 +- .../AbstractMqttServerSideRpcIntegrationTest.java | 2 +- .../MqttServerSideRpcNoSqlIntegrationTest.java | 2 +- .../sql/MqttServerSideRpcSqlIntegrationTest.java | 2 +- .../AbstractMqttTelemetryIntegrationTest.java | 2 +- .../nosql/MqttTelemetryNoSqlIntegrationTest.java | 2 +- .../sql/MqttTelemetrySqlIntegrationTest.java | 2 +- .../server/service/mail/TestMailService.java | 2 +- .../server/system/BaseHttpDeviceApiTest.java | 2 +- .../server/system/SystemNoSqlTestSuite.java | 2 +- .../server/system/SystemSqlTestSuite.java | 2 +- .../server/system/nosql/DeviceApiNoSqlTest.java | 2 +- .../server/system/sql/DeviceApiSqlTest.java | 2 +- common/data/pom.xml | 2 +- .../server/common/data/AdminSettings.java | 2 +- .../thingsboard/server/common/data/BaseData.java | 2 +- .../server/common/data/CacheConstants.java | 2 +- .../server/common/data/ContactBased.java | 2 +- .../thingsboard/server/common/data/Customer.java | 2 +- .../thingsboard/server/common/data/Dashboard.java | 2 +- .../server/common/data/DashboardInfo.java | 2 +- .../server/common/data/DataConstants.java | 2 +- .../thingsboard/server/common/data/Device.java | 2 +- .../server/common/data/EntitySubtype.java | 2 +- .../server/common/data/EntityType.java | 2 +- .../org/thingsboard/server/common/data/Event.java | 2 +- .../server/common/data/HasAdditionalInfo.java | 2 +- .../thingsboard/server/common/data/HasName.java | 2 +- .../server/common/data/SearchTextBased.java | 2 +- .../data/SearchTextBasedWithAdditionalInfo.java | 2 +- .../server/common/data/ShortCustomerInfo.java | 2 +- .../thingsboard/server/common/data/Tenant.java | 2 +- .../server/common/data/UUIDConverter.java | 2 +- .../org/thingsboard/server/common/data/User.java | 2 +- .../server/common/data/alarm/Alarm.java | 2 +- .../server/common/data/alarm/AlarmId.java | 2 +- .../server/common/data/alarm/AlarmInfo.java | 2 +- .../server/common/data/alarm/AlarmQuery.java | 2 +- .../common/data/alarm/AlarmSearchStatus.java | 3 +-- .../server/common/data/alarm/AlarmSeverity.java | 2 +- .../server/common/data/alarm/AlarmStatus.java | 2 +- .../server/common/data/asset/Asset.java | 2 +- .../common/data/asset/AssetSearchQuery.java | 2 +- .../server/common/data/audit/ActionStatus.java | 2 +- .../server/common/data/audit/ActionType.java | 2 +- .../server/common/data/audit/AuditLog.java | 2 +- .../common/data/device/DeviceSearchQuery.java | 2 +- .../server/common/data/id/AdminSettingsId.java | 2 +- .../server/common/data/id/AssetId.java | 2 +- .../server/common/data/id/AuditLogId.java | 2 +- .../common/data/id/ComponentDescriptorId.java | 2 +- .../server/common/data/id/CustomerId.java | 2 +- .../server/common/data/id/DashboardId.java | 2 +- .../common/data/id/DeviceCredentialsId.java | 2 +- .../server/common/data/id/DeviceId.java | 2 +- .../server/common/data/id/EntityId.java | 2 +- .../common/data/id/EntityIdDeserializer.java | 2 +- .../server/common/data/id/EntityIdFactory.java | 2 +- .../server/common/data/id/EntityIdSerializer.java | 2 +- .../server/common/data/id/EventId.java | 2 +- .../server/common/data/id/IdBased.java | 2 +- .../thingsboard/server/common/data/id/NodeId.java | 2 +- .../server/common/data/id/PluginId.java | 2 +- .../thingsboard/server/common/data/id/RuleId.java | 2 +- .../server/common/data/id/SessionId.java | 2 +- .../server/common/data/id/TenantId.java | 2 +- .../server/common/data/id/UUIDBased.java | 2 +- .../server/common/data/id/UserCredentialsId.java | 2 +- .../thingsboard/server/common/data/id/UserId.java | 2 +- .../server/common/data/id/WidgetTypeId.java | 2 +- .../server/common/data/id/WidgetsBundleId.java | 2 +- .../server/common/data/kv/Aggregation.java | 2 +- .../server/common/data/kv/AttributeKey.java | 2 +- .../server/common/data/kv/AttributeKvEntry.java | 2 +- .../common/data/kv/BaseAttributeKvEntry.java | 2 +- .../server/common/data/kv/BaseTsKvQuery.java | 2 +- .../server/common/data/kv/BasicKvEntry.java | 2 +- .../server/common/data/kv/BasicTsKvEntry.java | 2 +- .../server/common/data/kv/BooleanDataEntry.java | 2 +- .../server/common/data/kv/DataType.java | 2 +- .../server/common/data/kv/DoubleDataEntry.java | 2 +- .../server/common/data/kv/KvEntry.java | 2 +- .../server/common/data/kv/LongDataEntry.java | 2 +- .../server/common/data/kv/StringDataEntry.java | 2 +- .../server/common/data/kv/TsKvEntry.java | 2 +- .../server/common/data/kv/TsKvQuery.java | 2 +- .../server/common/data/page/BasePageLink.java | 2 +- .../server/common/data/page/PageDataIterable.java | 2 +- .../server/common/data/page/TextPageData.java | 2 +- .../server/common/data/page/TextPageLink.java | 2 +- .../server/common/data/page/TimePageData.java | 2 +- .../server/common/data/page/TimePageLink.java | 2 +- .../common/data/plugin/ComponentDescriptor.java | 2 +- .../data/plugin/ComponentLifecycleEvent.java | 2 +- .../data/plugin/ComponentLifecycleState.java | 2 +- .../server/common/data/plugin/ComponentScope.java | 2 +- .../server/common/data/plugin/ComponentType.java | 2 +- .../server/common/data/plugin/PluginMetaData.java | 2 +- .../common/data/relation/EntityRelation.java | 2 +- .../common/data/relation/EntityRelationInfo.java | 3 +-- .../data/relation/EntityRelationsQuery.java | 2 +- .../data/relation/EntitySearchDirection.java | 2 +- .../common/data/relation/EntityTypeFilter.java | 2 +- .../common/data/relation/RelationTypeGroup.java | 2 +- .../data/relation/RelationsSearchParameters.java | 2 +- .../server/common/data/rule/RuleMetaData.java | 2 +- .../server/common/data/rule/RuleType.java | 2 +- .../server/common/data/rule/Scope.java | 2 +- .../server/common/data/security/Authority.java | 2 +- .../common/data/security/DeviceCredentials.java | 2 +- .../data/security/DeviceCredentialsFilter.java | 2 +- .../data/security/DeviceCredentialsType.java | 2 +- .../data/security/DeviceTokenCredentials.java | 2 +- .../data/security/DeviceX509Credentials.java | 2 +- .../common/data/security/UserCredentials.java | 2 +- .../server/common/data/widget/WidgetType.java | 2 +- .../server/common/data/widget/WidgetsBundle.java | 2 +- .../server/common/data/UUIDConverterTest.java | 2 +- common/message/pom.xml | 2 +- .../thingsboard/server/common/msg/RuleMsg.java | 2 +- .../server/common/msg/aware/CustomerAwareMsg.java | 2 +- .../server/common/msg/aware/DeviceAwareMsg.java | 2 +- .../server/common/msg/aware/NodeAwareMsg.java | 2 +- .../server/common/msg/aware/PluginAwareMsg.java | 2 +- .../server/common/msg/aware/RuleAwareMsg.java | 2 +- .../server/common/msg/aware/SessionAwareMsg.java | 2 +- .../server/common/msg/aware/TenantAwareMsg.java | 2 +- .../common/msg/cluster/ClusterEventMsg.java | 2 +- .../server/common/msg/cluster/ServerAddress.java | 2 +- .../server/common/msg/cluster/ToAllNodesMsg.java | 2 +- .../common/msg/core/AttributesSubscribeMsg.java | 2 +- .../common/msg/core/AttributesUnsubscribeMsg.java | 2 +- .../msg/core/AttributesUpdateNotification.java | 2 +- .../common/msg/core/BasicCommandAckResponse.java | 2 +- .../msg/core/BasicGetAttributesRequest.java | 2 +- .../msg/core/BasicGetAttributesResponse.java | 2 +- .../server/common/msg/core/BasicRequest.java | 2 +- .../server/common/msg/core/BasicResponseMsg.java | 2 +- .../common/msg/core/BasicStatusCodeResponse.java | 2 +- .../msg/core/BasicTelemetryUploadRequest.java | 2 +- .../msg/core/BasicToDeviceSessionActorMsg.java | 2 +- .../msg/core/BasicUpdateAttributesRequest.java | 2 +- .../common/msg/core/GetAttributesRequest.java | 2 +- .../common/msg/core/GetAttributesResponse.java | 2 +- .../server/common/msg/core/ResponseMsg.java | 2 +- .../server/common/msg/core/RpcSubscribeMsg.java | 2 +- .../server/common/msg/core/RpcUnsubscribeMsg.java | 2 +- .../server/common/msg/core/RuleEngineError.java | 2 +- .../common/msg/core/RuleEngineErrorMsg.java | 2 +- .../server/common/msg/core/SessionCloseMsg.java | 2 +- .../common/msg/core/SessionCloseNotification.java | 2 +- .../server/common/msg/core/SessionOpenMsg.java | 2 +- .../common/msg/core/StatusCodeResponse.java | 2 +- .../common/msg/core/TelemetryUploadRequest.java | 2 +- .../common/msg/core/ToDeviceRpcRequestMsg.java | 2 +- .../common/msg/core/ToDeviceRpcResponseMsg.java | 2 +- .../common/msg/core/ToDeviceSessionActorMsg.java | 2 +- .../common/msg/core/ToServerRpcRequestMsg.java | 2 +- .../common/msg/core/ToServerRpcResponseMsg.java | 2 +- .../common/msg/core/UpdateAttributesRequest.java | 2 +- .../common/msg/device/BasicToDeviceActorMsg.java | 2 +- .../common/msg/device/ToDeviceActorMsg.java | 2 +- .../server/common/msg/kv/AttributesKVMsg.java | 2 +- .../server/common/msg/kv/BasicAttributeKVMsg.java | 2 +- .../common/msg/plugin/ComponentLifecycleMsg.java | 2 +- .../msg/session/AdaptorToSessionActorMsg.java | 2 +- .../session/BasicAdaptorToSessionActorMsg.java | 2 +- .../session/BasicSessionActorToAdaptorMsg.java | 2 +- .../common/msg/session/BasicSessionMsg.java | 2 +- .../msg/session/BasicToDeviceActorSessionMsg.java | 2 +- .../server/common/msg/session/FeatureType.java | 2 +- .../server/common/msg/session/FromDeviceMsg.java | 2 +- .../common/msg/session/FromDeviceRequestMsg.java | 2 +- .../server/common/msg/session/MsgType.java | 2 +- .../msg/session/SessionActorToAdaptorMsg.java | 2 +- .../server/common/msg/session/SessionContext.java | 2 +- .../server/common/msg/session/SessionCtrlMsg.java | 2 +- .../server/common/msg/session/SessionMsg.java | 2 +- .../server/common/msg/session/SessionType.java | 2 +- .../msg/session/ToDeviceActorSessionMsg.java | 2 +- .../server/common/msg/session/ToDeviceMsg.java | 2 +- .../common/msg/session/ctrl/SessionCloseMsg.java | 2 +- .../session/ex/ProcessingTimeoutException.java | 2 +- .../msg/session/ex/SessionAuthException.java | 2 +- .../common/msg/session/ex/SessionException.java | 2 +- common/pom.xml | 2 +- common/transport/pom.xml | 2 +- .../common/transport/SessionMsgProcessor.java | 2 +- .../server/common/transport/TransportAdaptor.java | 2 +- .../transport/adaptor/AdaptorException.java | 2 +- .../common/transport/adaptor/JsonConverter.java | 2 +- .../common/transport/auth/DeviceAuthResult.java | 2 +- .../common/transport/auth/DeviceAuthService.java | 2 +- .../server/common/transport/quota/Clock.java | 2 +- .../transport/quota/HostRequestLimitPolicy.java | 2 +- .../transport/quota/HostRequestsQuotaService.java | 2 +- .../common/transport/quota/QuotaService.java | 2 +- .../inmemory/HostRequestIntervalRegistry.java | 2 +- .../transport/quota/inmemory/IntervalCount.java | 2 +- .../quota/inmemory/IntervalRegistryCleaner.java | 2 +- .../quota/inmemory/IntervalRegistryLogger.java | 2 +- .../session/DeviceAwareSessionContext.java | 2 +- .../server/common/transport/quota/ClockTest.java | 2 +- .../quota/HostRequestLimitPolicyTest.java | 2 +- .../quota/HostRequestsQuotaServiceTest.java | 2 +- .../inmemory/HostRequestIntervalRegistryTest.java | 2 +- .../quota/inmemory/IntervalCountTest.java | 2 +- .../inmemory/IntervalRegistryLoggerTest.java | 2 +- dao/pom.xml | 2 +- .../main/java/org/thingsboard/server/dao/Dao.java | 2 +- .../java/org/thingsboard/server/dao/DaoUtil.java | 2 +- .../thingsboard/server/dao/EncryptionUtil.java | 2 +- .../org/thingsboard/server/dao/JpaDaoConfig.java | 2 +- .../thingsboard/server/dao/NoSqlDaoConfig.java | 2 +- .../thingsboard/server/dao/alarm/AlarmDao.java | 2 +- .../server/dao/alarm/AlarmService.java | 2 +- .../server/dao/alarm/BaseAlarmService.java | 2 +- .../server/dao/alarm/CassandraAlarmDao.java | 2 +- .../thingsboard/server/dao/asset/AssetDao.java | 2 +- .../server/dao/asset/AssetService.java | 2 +- .../server/dao/asset/AssetTypeFilter.java | 2 +- .../server/dao/asset/BaseAssetService.java | 2 +- .../server/dao/asset/CassandraAssetDao.java | 2 +- .../server/dao/attributes/AttributesDao.java | 2 +- .../server/dao/attributes/AttributesService.java | 2 +- .../dao/attributes/BaseAttributesService.java | 2 +- .../attributes/CassandraBaseAttributesDao.java | 2 +- .../thingsboard/server/dao/audit/AuditLogDao.java | 2 +- .../server/dao/audit/AuditLogLevelFilter.java | 2 +- .../server/dao/audit/AuditLogLevelMask.java | 2 +- .../server/dao/audit/AuditLogQueryCursor.java | 2 +- .../server/dao/audit/AuditLogService.java | 2 +- .../server/dao/audit/AuditLogServiceImpl.java | 2 +- .../server/dao/audit/CassandraAuditLogDao.java | 2 +- .../dao/audit/DummyAuditLogServiceImpl.java | 2 +- .../server/dao/audit/sink/AuditLogSink.java | 2 +- .../server/dao/audit/sink/DummyAuditLogSink.java | 2 +- .../dao/audit/sink/ElasticsearchAuditLogSink.java | 2 +- .../thingsboard/server/dao/cache/CacheSpecs.java | 2 +- .../dao/cache/CaffeineCacheConfiguration.java | 2 +- .../PreviousDeviceCredentialsIdKeyGenerator.java | 2 +- .../dao/cache/TBRedisCacheConfiguration.java | 2 +- .../dao/cassandra/AbstractCassandraCluster.java | 2 +- .../server/dao/cassandra/CassandraCluster.java | 2 +- .../dao/cassandra/CassandraInstallCluster.java | 3 +-- .../dao/cassandra/CassandraQueryOptions.java | 2 +- .../dao/cassandra/CassandraSocketOptions.java | 2 +- .../component/BaseComponentDescriptorService.java | 2 +- .../CassandraBaseComponentDescriptorDao.java | 2 +- .../dao/component/ComponentDescriptorDao.java | 2 +- .../dao/component/ComponentDescriptorService.java | 2 +- .../server/dao/customer/CassandraCustomerDao.java | 2 +- .../server/dao/customer/CustomerDao.java | 2 +- .../server/dao/customer/CustomerService.java | 2 +- .../server/dao/customer/CustomerServiceImpl.java | 2 +- .../dao/dashboard/CassandraDashboardDao.java | 2 +- .../dao/dashboard/CassandraDashboardInfoDao.java | 2 +- .../server/dao/dashboard/DashboardDao.java | 2 +- .../server/dao/dashboard/DashboardInfoDao.java | 2 +- .../server/dao/dashboard/DashboardService.java | 2 +- .../dao/dashboard/DashboardServiceImpl.java | 2 +- .../dao/device/CassandraDeviceCredentialsDao.java | 2 +- .../server/dao/device/CassandraDeviceDao.java | 2 +- .../server/dao/device/DeviceCredentialsDao.java | 2 +- .../dao/device/DeviceCredentialsService.java | 2 +- .../dao/device/DeviceCredentialsServiceImpl.java | 2 +- .../thingsboard/server/dao/device/DeviceDao.java | 2 +- .../server/dao/device/DeviceService.java | 2 +- .../server/dao/device/DeviceServiceImpl.java | 2 +- .../server/dao/entity/AbstractEntityService.java | 3 +-- .../server/dao/entity/BaseEntityService.java | 2 +- .../server/dao/entity/EntityService.java | 3 +-- .../server/dao/event/BaseEventService.java | 2 +- .../server/dao/event/CassandraBaseEventDao.java | 2 +- .../thingsboard/server/dao/event/EventDao.java | 2 +- .../server/dao/event/EventService.java | 2 +- .../dao/exception/DataValidationException.java | 2 +- .../server/dao/exception/DatabaseException.java | 2 +- .../exception/IncorrectParameterException.java | 2 +- .../thingsboard/server/dao/model/BaseEntity.java | 2 +- .../server/dao/model/BaseSqlEntity.java | 2 +- .../server/dao/model/EntitySubtypeEntity.java | 3 +-- .../server/dao/model/ModelConstants.java | 2 +- .../server/dao/model/SearchTextEntity.java | 2 +- .../org/thingsboard/server/dao/model/ToData.java | 2 +- .../dao/model/nosql/AdminSettingsEntity.java | 2 +- .../server/dao/model/nosql/AlarmEntity.java | 2 +- .../server/dao/model/nosql/AssetEntity.java | 2 +- .../server/dao/model/nosql/AuditLogEntity.java | 2 +- .../model/nosql/ComponentDescriptorEntity.java | 2 +- .../server/dao/model/nosql/CustomerEntity.java | 2 +- .../server/dao/model/nosql/DashboardEntity.java | 2 +- .../dao/model/nosql/DashboardInfoEntity.java | 2 +- .../dao/model/nosql/DeviceCredentialsEntity.java | 2 +- .../server/dao/model/nosql/DeviceEntity.java | 2 +- .../server/dao/model/nosql/EventEntity.java | 2 +- .../dao/model/nosql/PluginMetaDataEntity.java | 2 +- .../dao/model/nosql/RuleMetaDataEntity.java | 2 +- .../server/dao/model/nosql/TenantEntity.java | 2 +- .../dao/model/nosql/UserCredentialsEntity.java | 2 +- .../server/dao/model/nosql/UserEntity.java | 2 +- .../server/dao/model/nosql/WidgetTypeEntity.java | 2 +- .../dao/model/nosql/WidgetsBundleEntity.java | 2 +- .../server/dao/model/sql/AdminSettingsEntity.java | 2 +- .../server/dao/model/sql/AlarmEntity.java | 2 +- .../server/dao/model/sql/AssetEntity.java | 2 +- .../dao/model/sql/AttributeKvCompositeKey.java | 2 +- .../server/dao/model/sql/AttributeKvEntity.java | 2 +- .../server/dao/model/sql/AuditLogEntity.java | 2 +- .../dao/model/sql/ComponentDescriptorEntity.java | 2 +- .../server/dao/model/sql/CustomerEntity.java | 2 +- .../server/dao/model/sql/DashboardEntity.java | 2 +- .../server/dao/model/sql/DashboardInfoEntity.java | 2 +- .../dao/model/sql/DeviceCredentialsEntity.java | 2 +- .../server/dao/model/sql/DeviceEntity.java | 2 +- .../server/dao/model/sql/EventEntity.java | 2 +- .../dao/model/sql/PluginMetaDataEntity.java | 2 +- .../dao/model/sql/RelationCompositeKey.java | 2 +- .../server/dao/model/sql/RelationEntity.java | 2 +- .../server/dao/model/sql/RuleMetaDataEntity.java | 2 +- .../server/dao/model/sql/TenantEntity.java | 2 +- .../server/dao/model/sql/TsKvCompositeKey.java | 2 +- .../server/dao/model/sql/TsKvEntity.java | 2 +- .../dao/model/sql/TsKvLatestCompositeKey.java | 2 +- .../server/dao/model/sql/TsKvLatestEntity.java | 2 +- .../dao/model/sql/UserCredentialsEntity.java | 2 +- .../server/dao/model/sql/UserEntity.java | 2 +- .../server/dao/model/sql/WidgetTypeEntity.java | 2 +- .../server/dao/model/sql/WidgetsBundleEntity.java | 2 +- .../server/dao/model/type/ActionStatusCodec.java | 2 +- .../server/dao/model/type/ActionTypeCodec.java | 2 +- .../server/dao/model/type/AlarmSeverityCodec.java | 2 +- .../server/dao/model/type/AlarmStatusCodec.java | 2 +- .../server/dao/model/type/AuthorityCodec.java | 2 +- .../model/type/ComponentLifecycleStateCodec.java | 2 +- .../dao/model/type/ComponentScopeCodec.java | 2 +- .../server/dao/model/type/ComponentTypeCodec.java | 2 +- .../model/type/DeviceCredentialsTypeCodec.java | 2 +- .../server/dao/model/type/EntityTypeCodec.java | 2 +- .../server/dao/model/type/JsonCodec.java | 2 +- .../dao/model/type/RelationTypeGroupCodec.java | 2 +- .../server/dao/model/wrapper/EntityResultSet.java | 2 +- .../dao/nosql/CassandraAbstractAsyncDao.java | 2 +- .../server/dao/nosql/CassandraAbstractDao.java | 2 +- .../dao/nosql/CassandraAbstractModelDao.java | 2 +- .../dao/nosql/CassandraAbstractSearchTextDao.java | 2 +- .../dao/nosql/CassandraAbstractSearchTimeDao.java | 2 +- .../server/dao/plugin/BasePluginService.java | 2 +- .../server/dao/plugin/CassandraBasePluginDao.java | 2 +- .../thingsboard/server/dao/plugin/PluginDao.java | 2 +- .../server/dao/plugin/PluginService.java | 2 +- .../server/dao/relation/BaseRelationDao.java | 2 +- .../server/dao/relation/BaseRelationService.java | 2 +- .../server/dao/relation/RelationDao.java | 2 +- .../server/dao/relation/RelationService.java | 2 +- .../server/dao/rule/BaseRuleService.java | 2 +- .../server/dao/rule/CassandraBaseRuleDao.java | 2 +- .../org/thingsboard/server/dao/rule/RuleDao.java | 2 +- .../thingsboard/server/dao/rule/RuleService.java | 2 +- .../server/dao/service/DataValidator.java | 2 +- .../server/dao/service/PaginatedRemover.java | 2 +- .../server/dao/service/TimePaginatedRemover.java | 2 +- .../thingsboard/server/dao/service/Validator.java | 2 +- .../server/dao/settings/AdminSettingsDao.java | 2 +- .../server/dao/settings/AdminSettingsService.java | 2 +- .../dao/settings/AdminSettingsServiceImpl.java | 2 +- .../dao/settings/CassandraAdminSettingsDao.java | 2 +- .../server/dao/sql/JpaAbstractDao.java | 2 +- .../JpaAbstractDaoListeningExecutorService.java | 2 +- .../server/dao/sql/JpaAbstractSearchTextDao.java | 2 +- .../server/dao/sql/JpaAbstractSearchTimeDao.java | 2 +- .../server/dao/sql/alarm/AlarmRepository.java | 2 +- .../server/dao/sql/alarm/JpaAlarmDao.java | 2 +- .../server/dao/sql/asset/AssetRepository.java | 2 +- .../server/dao/sql/asset/JpaAssetDao.java | 2 +- .../dao/sql/attributes/AttributeKvRepository.java | 2 +- .../dao/sql/attributes/JpaAttributeDao.java | 2 +- .../server/dao/sql/audit/AuditLogRepository.java | 2 +- .../server/dao/sql/audit/JpaAuditLogDao.java | 2 +- .../component/ComponentDescriptorRepository.java | 2 +- .../component/JpaBaseComponentDescriptorDao.java | 2 +- .../dao/sql/customer/CustomerRepository.java | 2 +- .../server/dao/sql/customer/JpaCustomerDao.java | 2 +- .../sql/dashboard/DashboardInfoRepository.java | 2 +- .../dao/sql/dashboard/DashboardRepository.java | 2 +- .../server/dao/sql/dashboard/JpaDashboardDao.java | 2 +- .../dao/sql/dashboard/JpaDashboardInfoDao.java | 2 +- .../sql/device/DeviceCredentialsRepository.java | 2 +- .../server/dao/sql/device/DeviceRepository.java | 2 +- .../dao/sql/device/JpaDeviceCredentialsDao.java | 2 +- .../server/dao/sql/device/JpaDeviceDao.java | 2 +- .../server/dao/sql/event/EventRepository.java | 2 +- .../server/dao/sql/event/JpaBaseEventDao.java | 2 +- .../server/dao/sql/plugin/JpaBasePluginDao.java | 2 +- .../dao/sql/plugin/PluginMetaDataRepository.java | 2 +- .../server/dao/sql/relation/JpaRelationDao.java | 2 +- .../dao/sql/relation/RelationRepository.java | 2 +- .../server/dao/sql/rule/JpaBaseRuleDao.java | 2 +- .../dao/sql/rule/RuleMetaDataRepository.java | 2 +- .../dao/sql/settings/AdminSettingsRepository.java | 2 +- .../dao/sql/settings/JpaAdminSettingsDao.java | 2 +- .../server/dao/sql/tenant/JpaTenantDao.java | 2 +- .../server/dao/sql/tenant/TenantRepository.java | 2 +- .../dao/sql/timeseries/JpaTimeseriesDao.java | 2 +- .../dao/sql/timeseries/TsKvLatestRepository.java | 2 +- .../server/dao/sql/timeseries/TsKvRepository.java | 2 +- .../dao/sql/user/JpaUserCredentialsDao.java | 2 +- .../server/dao/sql/user/JpaUserDao.java | 2 +- .../dao/sql/user/UserCredentialsRepository.java | 2 +- .../server/dao/sql/user/UserRepository.java | 2 +- .../server/dao/sql/widget/JpaWidgetTypeDao.java | 2 +- .../dao/sql/widget/JpaWidgetsBundleDao.java | 2 +- .../dao/sql/widget/WidgetTypeRepository.java | 2 +- .../dao/sql/widget/WidgetsBundleRepository.java | 2 +- .../server/dao/tenant/CassandraTenantDao.java | 2 +- .../thingsboard/server/dao/tenant/TenantDao.java | 2 +- .../server/dao/tenant/TenantService.java | 2 +- .../server/dao/tenant/TenantServiceImpl.java | 2 +- .../timeseries/AggregatePartitionsFunction.java | 2 +- .../dao/timeseries/BaseTimeseriesService.java | 2 +- .../timeseries/CassandraBaseTimeseriesDao.java | 2 +- .../dao/timeseries/SimpleListenableFuture.java | 2 +- .../server/dao/timeseries/TimeseriesDao.java | 2 +- .../server/dao/timeseries/TimeseriesService.java | 2 +- .../dao/timeseries/TsInsertExecutorType.java | 2 +- .../server/dao/timeseries/TsKvQueryCursor.java | 2 +- .../server/dao/timeseries/TsPartitionDate.java | 2 +- .../dao/user/CassandraUserCredentialsDao.java | 2 +- .../server/dao/user/CassandraUserDao.java | 2 +- .../server/dao/user/UserCredentialsDao.java | 2 +- .../org/thingsboard/server/dao/user/UserDao.java | 2 +- .../thingsboard/server/dao/user/UserService.java | 2 +- .../server/dao/user/UserServiceImpl.java | 2 +- .../org/thingsboard/server/dao/util/NoSqlDao.java | 2 +- .../org/thingsboard/server/dao/util/SqlDao.java | 2 +- .../mapping/AbstractJsonSqlTypeDescriptor.java | 2 +- .../server/dao/util/mapping/JacksonUtil.java | 2 +- .../util/mapping/JsonStringSqlTypeDescriptor.java | 2 +- .../server/dao/util/mapping/JsonStringType.java | 2 +- .../dao/util/mapping/JsonTypeDescriptor.java | 2 +- .../server/dao/widget/CassandraWidgetTypeDao.java | 2 +- .../dao/widget/CassandraWidgetsBundleDao.java | 2 +- .../server/dao/widget/WidgetTypeDao.java | 2 +- .../server/dao/widget/WidgetTypeService.java | 2 +- .../server/dao/widget/WidgetTypeServiceImpl.java | 2 +- .../server/dao/widget/WidgetsBundleDao.java | 2 +- .../server/dao/widget/WidgetsBundleService.java | 2 +- .../dao/widget/WidgetsBundleServiceImpl.java | 2 +- dao/src/main/resources/cassandra/schema.cql | 2 +- dao/src/main/resources/cassandra/system-data.cql | 2 +- dao/src/main/resources/sql/schema.sql | 2 +- dao/src/main/resources/sql/system-data.sql | 2 +- .../server/dao/AbstractJpaDaoTest.java | 2 +- .../server/dao/CustomCassandraCQLUnit.java | 2 +- .../org/thingsboard/server/dao/CustomSqlUnit.java | 2 +- .../thingsboard/server/dao/JpaDaoTestSuite.java | 2 +- .../server/dao/JpaDbunitTestConfig.java | 2 +- .../server/dao/NoSqlDaoServiceTestSuite.java | 2 +- .../server/dao/SqlDaoServiceTestSuite.java | 2 +- .../server/dao/service/AbstractServiceTest.java | 2 +- .../dao/service/BaseAdminSettingsServiceTest.java | 2 +- .../server/dao/service/BaseAlarmServiceTest.java | 2 +- .../server/dao/service/BaseAssetServiceTest.java | 2 +- .../dao/service/BaseCustomerServiceTest.java | 2 +- .../dao/service/BaseDashboardServiceTest.java | 2 +- .../service/BaseDeviceCredentialsCacheTest.java | 2 +- .../service/BaseDeviceCredentialsServiceTest.java | 2 +- .../server/dao/service/BaseDeviceServiceTest.java | 2 +- .../server/dao/service/BaseRelationCacheTest.java | 2 +- .../dao/service/BaseRelationServiceTest.java | 2 +- .../server/dao/service/BaseTenantServiceTest.java | 2 +- .../server/dao/service/BaseUserServiceTest.java | 2 +- .../dao/service/BaseWidgetTypeServiceTest.java | 2 +- .../dao/service/BaseWidgetsBundleServiceTest.java | 2 +- .../server/dao/service/DaoNoSqlTest.java | 2 +- .../server/dao/service/DaoSqlTest.java | 2 +- .../attributes/BaseAttributesServiceTest.java | 2 +- .../nosql/AttributesServiceNoSqlTest.java | 2 +- .../attributes/sql/AttributesServiceSqlTest.java | 2 +- .../dao/service/event/BaseEventServiceTest.java | 2 +- .../event/nosql/EventServiceNoSqlTest.java | 2 +- .../service/event/sql/EventServiceSqlTest.java | 2 +- .../nosql/AdminSettingsServiceNoSqlTest.java | 2 +- .../dao/service/nosql/AlarmServiceNoSqlTest.java | 2 +- .../dao/service/nosql/AssetServiceNoSqlTest.java | 2 +- .../service/nosql/CustomerServiceNoSqlTest.java | 2 +- .../service/nosql/DashboardServiceNoSqlTest.java | 2 +- .../nosql/DeviceCredentialCacheNoSqlTest.java | 2 +- .../nosql/DeviceCredentialServiceNoSqlTest.java | 2 +- .../dao/service/nosql/DeviceServiceNoSqlTest.java | 2 +- .../dao/service/nosql/RelationCacheNoSqlTest.java | 2 +- .../service/nosql/RelationServiceNoSqlTest.java | 2 +- .../dao/service/nosql/TenantServiceNoSqlTest.java | 2 +- .../dao/service/nosql/UserServiceNoSqlTest.java | 2 +- .../service/nosql/WidgetTypeServiceNoSqlTest.java | 2 +- .../nosql/WidgetsBundleServiceNoSqlTest.java | 2 +- .../dao/service/plugin/BasePluginServiceTest.java | 2 +- .../plugin/nosql/PluginServiceNoSqlTest.java | 2 +- .../service/plugin/sql/PluginServiceSqlTest.java | 2 +- .../dao/service/rule/BaseRuleServiceTest.java | 2 +- .../service/rule/nosql/RuleServiceNoSqlTest.java | 2 +- .../dao/service/rule/sql/RuleServiceSqlTest.java | 2 +- .../service/sql/AdminSettingsServiceSqlTest.java | 2 +- .../dao/service/sql/AlarmServiceSqlTest.java | 2 +- .../dao/service/sql/AssetServiceSqlTest.java | 2 +- .../dao/service/sql/CustomerServiceSqlTest.java | 2 +- .../dao/service/sql/DashboardServiceSqlTest.java | 2 +- .../sql/DeviceCredentialsCacheSqlTest.java | 2 +- .../sql/DeviceCredentialsServiceSqlTest.java | 2 +- .../dao/service/sql/DeviceServiceSqlTest.java | 2 +- .../dao/service/sql/RelationCacheSqlTest.java | 2 +- .../dao/service/sql/RelationServiceSqlTest.java | 2 +- .../dao/service/sql/TenantServiceSqlTest.java | 2 +- .../dao/service/sql/UserServiceSqlTest.java | 2 +- .../dao/service/sql/WidgetTypeServiceSqlTest.java | 2 +- .../service/sql/WidgetsBundleServiceSqlTest.java | 2 +- .../timeseries/BaseTimeseriesServiceTest.java | 2 +- .../nosql/TimeseriesServiceNoSqlTest.java | 2 +- .../timeseries/sql/TimeseriesServiceSqlTest.java | 2 +- .../server/dao/sql/alarm/JpaAlarmDaoTest.java | 2 +- .../server/dao/sql/asset/JpaAssetDaoTest.java | 2 +- .../server/dao/sql/audit/JpaAuditLogDaoTest.java | 2 +- .../JpaBaseComponentDescriptorDaoTest.java | 2 +- .../dao/sql/customer/JpaCustomerDaoTest.java | 2 +- .../sql/dashboard/JpaDashboardInfoDaoTest.java | 2 +- .../sql/device/JpaDeviceCredentialsDaoTest.java | 2 +- .../server/dao/sql/device/JpaDeviceDaoTest.java | 2 +- .../server/dao/sql/event/JpaBaseEventDaoTest.java | 2 +- .../dao/sql/plugin/JpaBasePluginDaoTest.java | 2 +- .../server/dao/sql/rule/JpaBaseRuleDaoTest.java | 2 +- .../server/dao/sql/tenant/JpaTenantDaoTest.java | 2 +- .../dao/sql/user/JpaUserCredentialsDaoTest.java | 2 +- .../server/dao/sql/user/JpaUserDaoTest.java | 2 +- .../dao/sql/widget/JpaWidgetTypeDaoTest.java | 2 +- .../dao/sql/widget/JpaWidgetsBundleDaoTest.java | 2 +- docker/cassandra-setup/Dockerfile | 2 +- docker/cassandra-setup/install.sh | 2 +- docker/cassandra/Dockerfile | 2 +- docker/cassandra/ready-probe.sh | 2 +- docker/docker-compose-tests.yml | 2 +- docker/docker-compose.static.yml | 2 +- docker/docker-compose.yml | 2 +- docker/k8s/cassandra-setup.yaml | 2 +- docker/k8s/cassandra.yaml | 2 +- docker/k8s/common.yaml | 2 +- docker/k8s/tb.yaml | 2 +- docker/k8s/zookeeper.yaml | 2 +- docker/tb/Dockerfile | 2 +- docker/tb/run-application.sh | 2 +- docker/zookeeper/Dockerfile | 2 +- docker/zookeeper/zk-gen-config.sh | 2 +- docker/zookeeper/zk-ok.sh | 2 +- extensions-api/pom.xml | 2 +- .../server/extensions/api/component/Action.java | 2 +- .../api/component/ConfigurableComponent.java | 2 +- .../component/EmptyComponentConfiguration.java | 2 +- .../server/extensions/api/component/Filter.java | 2 +- .../server/extensions/api/component/Plugin.java | 2 +- .../extensions/api/component/Processor.java | 2 +- .../api/configuration/Configurable.java | 2 +- .../api/configuration/Configuration.java | 2 +- .../ConfigurationValidationException.java | 2 +- .../extensions/api/device/DeviceAttributes.java | 2 +- .../DeviceAttributesEventNotificationMsg.java | 2 +- .../DeviceCredentialsUpdateNotificationMsg.java | 2 +- .../extensions/api/device/DeviceMetaData.java | 2 +- .../api/device/DeviceNameOrTypeUpdateMsg.java | 2 +- .../api/device/ToDeviceActorNotificationMsg.java | 2 +- .../api/exception/AccessDeniedException.java | 2 +- .../api/exception/EntityNotFoundException.java | 2 +- .../api/exception/InternalErrorException.java | 2 +- .../api/exception/InvalidParametersException.java | 2 +- .../api/exception/ToErrorResponseEntity.java | 2 +- .../api/exception/UnauthorizedException.java | 2 +- .../api/exception/UncheckedApiException.java | 2 +- .../extensions/api/plugins/AbstractPlugin.java | 2 +- .../server/extensions/api/plugins/Plugin.java | 2 +- .../extensions/api/plugins/PluginAction.java | 2 +- .../api/plugins/PluginApiCallSecurityContext.java | 2 +- .../extensions/api/plugins/PluginCallback.java | 2 +- .../extensions/api/plugins/PluginConstants.java | 2 +- .../extensions/api/plugins/PluginContext.java | 2 +- .../extensions/api/plugins/PluginException.java | 2 +- .../plugins/PluginInitializationException.java | 2 +- .../plugins/handlers/DefaultRestMsgHandler.java | 2 +- .../plugins/handlers/DefaultRpcMsgHandler.java | 2 +- .../plugins/handlers/DefaultRuleMsgHandler.java | 2 +- .../handlers/DefaultWebsocketMsgHandler.java | 2 +- .../api/plugins/handlers/RestMsgHandler.java | 2 +- .../api/plugins/handlers/RpcMsgHandler.java | 2 +- .../api/plugins/handlers/RuleMsgHandler.java | 2 +- .../api/plugins/handlers/WebsocketMsgHandler.java | 2 +- .../api/plugins/msg/AbstractPluginToRuleMsg.java | 2 +- .../api/plugins/msg/AbstractRuleToPluginMsg.java | 2 +- .../api/plugins/msg/FromDeviceRpcResponse.java | 2 +- .../msg/GetAttributesRequestRuleToPluginMsg.java | 2 +- .../plugins/msg/GetRequestRuleToPluginMsg.java | 2 +- .../api/plugins/msg/PluginToRuleMsg.java | 2 +- .../api/plugins/msg/ResponsePluginToRuleMsg.java | 2 +- .../extensions/api/plugins/msg/RpcError.java | 2 +- .../plugins/msg/RpcRequestRuleToPluginMsg.java | 2 +- .../plugins/msg/RpcResponsePluginToRuleMsg.java | 2 +- .../api/plugins/msg/RuleToPluginMsg.java | 2 +- .../TelemetryUploadRequestRuleToPluginMsg.java | 2 +- .../extensions/api/plugins/msg/TimeoutIntMsg.java | 2 +- .../extensions/api/plugins/msg/TimeoutMsg.java | 2 +- .../api/plugins/msg/TimeoutUUIDMsg.java | 2 +- .../api/plugins/msg/ToDeviceRpcRequest.java | 2 +- .../api/plugins/msg/ToDeviceRpcRequestBody.java | 2 +- .../plugins/msg/ToDeviceRpcRequestPluginMsg.java | 2 +- .../api/plugins/msg/ToPluginActorMsg.java | 2 +- .../plugins/msg/ToPluginRpcResponseDeviceMsg.java | 2 +- .../UpdateAttributesRequestRuleToPluginMsg.java | 2 +- .../api/plugins/rest/BasicPluginRestMsg.java | 2 +- .../api/plugins/rest/PluginRestMsg.java | 2 +- .../extensions/api/plugins/rest/RestRequest.java | 2 +- .../extensions/api/plugins/rpc/PluginRpcMsg.java | 2 +- .../server/extensions/api/plugins/rpc/RpcMsg.java | 2 +- .../ws/BasicPluginWebsocketSessionRef.java | 2 +- .../api/plugins/ws/PluginWebsocketSessionRef.java | 2 +- .../extensions/api/plugins/ws/SessionEvent.java | 2 +- .../api/plugins/ws/WsSessionMetaData.java | 2 +- .../ws/msg/AbstractPluginWebSocketMsg.java | 2 +- .../plugins/ws/msg/BinaryPluginWebSocketMsg.java | 2 +- .../plugins/ws/msg/EmptyPluginWebsocketMsg.java | 2 +- .../plugins/ws/msg/PingPluginWebsocketMsg.java | 2 +- .../api/plugins/ws/msg/PluginWebsocketMsg.java | 2 +- .../plugins/ws/msg/PongPluginWebsocketMsg.java | 2 +- .../ws/msg/SessionEventPluginWebSocketMsg.java | 2 +- .../plugins/ws/msg/TextPluginWebSocketMsg.java | 2 +- .../server/extensions/api/rules/RuleContext.java | 2 +- .../extensions/api/rules/RuleException.java | 2 +- .../server/extensions/api/rules/RuleFilter.java | 2 +- .../api/rules/RuleInitializationException.java | 2 +- .../api/rules/RuleLifecycleComponent.java | 2 +- .../api/rules/RuleProcessingMetaData.java | 2 +- .../extensions/api/rules/RuleProcessor.java | 2 +- .../api/rules/SimpleRuleLifecycleComponent.java | 2 +- .../extensions/api/rules/ToRuleActorMsg.java | 2 +- extensions-core/pom.xml | 2 +- .../core/action/mail/SendMailAction.java | 2 +- .../action/mail/SendMailActionConfiguration.java | 2 +- .../core/action/mail/SendMailActionMsg.java | 2 +- .../mail/SendMailRuleToPluginActionMsg.java | 2 +- .../core/action/rpc/RpcPluginAction.java | 2 +- .../core/action/rpc/ServerSideRpcCallAction.java | 2 +- .../rpc/ServerSideRpcCallActionConfiguration.java | 2 +- .../action/rpc/ServerSideRpcCallActionMsg.java | 2 +- .../ServerSideRpcCallRuleToPluginActionMsg.java | 2 +- .../action/telemetry/TelemetryPluginAction.java | 2 +- .../TelemetryPluginActionConfiguration.java | 2 +- .../template/AbstractTemplatePluginAction.java | 2 +- .../template/TemplateActionConfiguration.java | 2 +- .../extensions/core/filter/BasicJsFilter.java | 2 +- .../core/filter/DeviceAttributesFilter.java | 2 +- .../DeviceAttributesFilterConfiguration.java | 2 +- .../core/filter/DeviceTelemetryFilter.java | 2 +- .../extensions/core/filter/DeviceTypeFilter.java | 2 +- .../filter/DeviceTypeFilterConfiguration.java | 2 +- .../core/filter/JsFilterConfiguration.java | 2 +- .../extensions/core/filter/MethodNameFilter.java | 2 +- .../filter/MethodNameFilterConfiguration.java | 2 +- .../extensions/core/filter/MsgTypeFilter.java | 2 +- .../core/filter/MsgTypeFilterConfiguration.java | 2 +- .../core/filter/NashornJsEvaluator.java | 2 +- .../core/plugin/KeyValuePluginProperties.java | 2 +- .../extensions/core/plugin/mail/MailPlugin.java | 2 +- .../core/plugin/mail/MailPluginConfiguration.java | 2 +- .../plugin/messaging/DeviceMessagingPlugin.java | 2 +- .../DeviceMessagingPluginConfiguration.java | 2 +- .../messaging/DeviceMessagingRuleMsgHandler.java | 2 +- .../messaging/PendingRpcRequestMetadata.java | 2 +- .../core/plugin/rpc/LocalRequestMetaData.java | 2 +- .../extensions/core/plugin/rpc/RpcManager.java | 2 +- .../extensions/core/plugin/rpc/RpcPlugin.java | 2 +- .../core/plugin/rpc/RpcPluginConfiguration.java | 2 +- .../core/plugin/rpc/cmd/RpcRequest.java | 2 +- .../plugin/rpc/handlers/RpcRestMsgHandler.java | 2 +- .../plugin/rpc/handlers/RpcRuleMsgHandler.java | 2 +- .../core/plugin/telemetry/AttributeData.java | 2 +- .../plugin/telemetry/SubscriptionManager.java | 2 +- .../plugin/telemetry/TelemetryStoragePlugin.java | 2 +- .../extensions/core/plugin/telemetry/TsData.java | 2 +- .../telemetry/cmd/AttributesSubscriptionCmd.java | 2 +- .../core/plugin/telemetry/cmd/GetHistoryCmd.java | 2 +- .../plugin/telemetry/cmd/SubscriptionCmd.java | 2 +- .../plugin/telemetry/cmd/TelemetryPluginCmd.java | 2 +- .../telemetry/cmd/TelemetryPluginCmdsWrapper.java | 2 +- .../telemetry/cmd/TimeseriesSubscriptionCmd.java | 2 +- .../telemetry/handlers/BiPluginCallBack.java | 2 +- .../telemetry/handlers/TelemetryFeature.java | 2 +- .../handlers/TelemetryRestMsgHandler.java | 2 +- .../handlers/TelemetryRpcMsgHandler.java | 2 +- .../handlers/TelemetryRuleMsgHandler.java | 2 +- .../handlers/TelemetryWebsocketMsgHandler.java | 2 +- .../core/plugin/telemetry/sub/Subscription.java | 2 +- .../telemetry/sub/SubscriptionErrorCode.java | 2 +- .../plugin/telemetry/sub/SubscriptionState.java | 2 +- .../plugin/telemetry/sub/SubscriptionType.java | 2 +- .../plugin/telemetry/sub/SubscriptionUpdate.java | 2 +- .../extensions/core/plugin/time/TimePlugin.java | 2 +- .../core/plugin/time/TimePluginConfiguration.java | 2 +- .../processor/AlarmDeduplicationProcessor.java | 2 +- .../AlarmDeduplicationProcessorConfiguration.java | 2 +- .../extensions/core/processor/AlarmProcessor.java | 2 +- .../processor/AlarmProcessorConfiguration.java | 2 +- .../extensions/core/utils/VelocityUtils.java | 2 +- extensions-core/src/main/proto/telemetry.proto | 2 +- .../core/filter/DeviceAttributesFilterTest.java | 2 +- extensions/extension-kafka/pom.xml | 2 +- .../extension-kafka/src/assembly/extension.xml | 2 +- .../extensions/kafka/action/KafkaActionMsg.java | 2 +- .../kafka/action/KafkaActionPayload.java | 2 +- .../kafka/action/KafkaPluginAction.java | 2 +- .../action/KafkaPluginActionConfiguration.java | 2 +- .../extensions/kafka/plugin/KafkaMsgHandler.java | 2 +- .../extensions/kafka/plugin/KafkaPlugin.java | 2 +- .../kafka/plugin/KafkaPluginConfiguration.java | 2 +- .../server/extensions/kafka/KafkaDemoClient.java | 2 +- extensions/extension-mqtt/pom.xml | 2 +- .../extension-mqtt/src/assembly/extension.xml | 2 +- .../extensions/mqtt/action/MqttActionMsg.java | 2 +- .../extensions/mqtt/action/MqttActionPayload.java | 2 +- .../extensions/mqtt/action/MqttPluginAction.java | 2 +- .../action/MqttPluginActionConfiguration.java | 2 +- .../extensions/mqtt/plugin/MqttMsgHandler.java | 2 +- .../server/extensions/mqtt/plugin/MqttPlugin.java | 2 +- .../mqtt/plugin/MqttPluginConfiguration.java | 2 +- extensions/extension-rabbitmq/pom.xml | 2 +- .../extension-rabbitmq/src/assembly/extension.xml | 2 +- .../rabbitmq/action/RabbitMqActionMsg.java | 2 +- .../rabbitmq/action/RabbitMqActionPayload.java | 2 +- .../rabbitmq/action/RabbitMqPluginAction.java | 2 +- .../action/RabbitMqPluginActionConfiguration.java | 2 +- .../rabbitmq/plugin/RabbitMqMsgHandler.java | 2 +- .../rabbitmq/plugin/RabbitMqPlugin.java | 2 +- .../plugin/RabbitMqPluginConfiguration.java | 2 +- .../extensions/rabbitmq/RabbitMqDemoClient.java | 2 +- extensions/extension-rest-api-call/pom.xml | 2 +- .../src/assembly/extension.xml | 2 +- .../rest/action/RestApiCallActionMsg.java | 2 +- .../rest/action/RestApiCallActionPayload.java | 2 +- .../rest/action/RestApiCallPluginAction.java | 2 +- .../RestApiCallPluginActionConfiguration.java | 2 +- .../rest/plugin/RestApiCallMsgHandler.java | 2 +- .../extensions/rest/plugin/RestApiCallPlugin.java | 2 +- .../plugin/RestApiCallPluginConfiguration.java | 2 +- .../extensions/rest/RestApiCallDemoClient.java | 2 +- extensions/extension-sns/pom.xml | 2 +- .../extension-sns/src/assembly/extension.xml | 2 +- .../extensions/sns/action/SnsTopicActionMsg.java | 2 +- .../sns/action/SnsTopicActionPayload.java | 2 +- .../sns/action/SnsTopicPluginAction.java | 2 +- .../action/SnsTopicPluginActionConfiguration.java | 2 +- .../extensions/sns/plugin/SnsMessageHandler.java | 2 +- .../server/extensions/sns/plugin/SnsPlugin.java | 2 +- .../sns/plugin/SnsPluginConfiguration.java | 2 +- extensions/extension-sqs/pom.xml | 2 +- .../extension-sqs/src/assembly/extension.xml | 2 +- .../sqs/action/fifo/SqsFifoQueueActionMsg.java | 2 +- .../action/fifo/SqsFifoQueueActionPayload.java | 2 +- .../sqs/action/fifo/SqsFifoQueuePluginAction.java | 2 +- .../SqsFifoQueuePluginActionConfiguration.java | 2 +- .../standard/SqsStandardQueueActionMsg.java | 2 +- .../standard/SqsStandardQueueActionPayload.java | 2 +- .../standard/SqsStandardQueuePluginAction.java | 2 +- ...SqsStandardQueuePluginActionConfiguration.java | 2 +- .../extensions/sqs/plugin/SqsMessageHandler.java | 2 +- .../server/extensions/sqs/plugin/SqsPlugin.java | 2 +- .../sqs/plugin/SqsPluginConfiguration.java | 2 +- .../server/extensions/sqs/SqsDemoClient.java | 2 +- extensions/pom.xml | 2 +- license-header-template.txt | 2 +- pom.xml | 2 +- resume.bat | 2 +- tools/pom.xml | 2 +- .../thingsboard/client/tools/MqttSslClient.java | 2 +- .../org/thingsboard/client/tools/RestClient.java | 2 +- tools/src/main/python/mqtt-send-telemetry.py | 3 +-- tools/src/main/python/one-way-ssl-mqtt-client.py | 3 +-- tools/src/main/python/simple-mqtt-client.py | 3 +-- tools/src/main/python/two-way-ssl-mqtt-client.py | 3 +-- tools/src/main/shell/client.keygen.sh | 2 +- tools/src/main/shell/server.keygen.sh | 2 +- transport/coap/pom.xml | 2 +- .../transport/coap/CoapTransportResource.java | 2 +- .../transport/coap/CoapTransportService.java | 2 +- .../coap/adaptors/CoapTransportAdaptor.java | 2 +- .../transport/coap/adaptors/JsonCoapAdaptor.java | 2 +- .../transport/coap/client/DeviceEmulator.java | 2 +- .../coap/session/CoapExchangeObserverProxy.java | 2 +- .../transport/coap/session/CoapSessionCtx.java | 2 +- .../transport/coap/session/CoapSessionId.java | 2 +- .../server/transport/coap/CoapServerTest.java | 2 +- .../coap/CoapServerTestConfiguration.java | 2 +- transport/http/pom.xml | 2 +- .../transport/http/DeviceApiController.java | 2 +- .../transport/http/session/HttpSessionCtx.java | 2 +- .../transport/http/session/HttpSessionId.java | 2 +- transport/mqtt/pom.xml | 2 +- .../transport/mqtt/MqttSslHandlerProvider.java | 2 +- .../server/transport/mqtt/MqttTopics.java | 2 +- .../transport/mqtt/MqttTransportHandler.java | 2 +- .../mqtt/MqttTransportServerInitializer.java | 2 +- .../transport/mqtt/MqttTransportService.java | 2 +- .../transport/mqtt/adaptors/JsonMqttAdaptor.java | 2 +- .../mqtt/adaptors/MqttTransportAdaptor.java | 2 +- .../transport/mqtt/session/DeviceSessionCtx.java | 2 +- .../mqtt/session/GatewayDeviceSessionCtx.java | 2 +- .../transport/mqtt/session/GatewaySessionCtx.java | 2 +- .../transport/mqtt/session/MqttSessionId.java | 2 +- .../server/transport/mqtt/util/SslUtil.java | 2 +- transport/pom.xml | 2 +- ui/pom.xml | 2 +- ui/src/app/admin/admin.controller.js | 2 +- ui/src/app/admin/admin.routes.js | 2 +- ui/src/app/admin/general-settings.tpl.html | 2 +- ui/src/app/admin/index.js | 2 +- ui/src/app/admin/outgoing-mail-settings.tpl.html | 2 +- .../app/alarm/alarm-details-dialog.controller.js | 2 +- ui/src/app/alarm/alarm-details-dialog.scss | 3 +-- ui/src/app/alarm/alarm-details-dialog.tpl.html | 2 +- ui/src/app/alarm/alarm-header.directive.js | 2 +- ui/src/app/alarm/alarm-header.tpl.html | 2 +- ui/src/app/alarm/alarm-row.directive.js | 2 +- ui/src/app/alarm/alarm-row.tpl.html | 2 +- ui/src/app/alarm/alarm-table.directive.js | 2 +- ui/src/app/alarm/alarm-table.tpl.html | 2 +- ui/src/app/alarm/alarm.scss | 3 +-- ui/src/app/alarm/index.js | 3 +-- ui/src/app/api/admin.service.js | 2 +- ui/src/app/api/alarm.service.js | 2 +- ui/src/app/api/alias-controller.js | 3 +-- ui/src/app/api/asset.service.js | 2 +- ui/src/app/api/attribute.service.js | 2 +- ui/src/app/api/audit-log.service.js | 2 +- ui/src/app/api/component-descriptor.service.js | 2 +- ui/src/app/api/customer.service.js | 2 +- ui/src/app/api/dashboard.service.js | 2 +- ui/src/app/api/data-aggregator.js | 3 +-- ui/src/app/api/datasource.service.js | 2 +- ui/src/app/api/device.service.js | 2 +- ui/src/app/api/entity-relation.service.js | 2 +- ui/src/app/api/entity.service.js | 2 +- ui/src/app/api/event.service.js | 2 +- ui/src/app/api/login.service.js | 2 +- ui/src/app/api/plugin.service.js | 2 +- ui/src/app/api/rule.service.js | 2 +- ui/src/app/api/subscription.js | 3 +-- ui/src/app/api/telemetry-websocket.service.js | 2 +- ui/src/app/api/tenant.service.js | 2 +- ui/src/app/api/time.service.js | 2 +- ui/src/app/api/user.service.js | 2 +- ui/src/app/api/widget.service.js | 2 +- ui/src/app/app.config.js | 2 +- ui/src/app/app.js | 3 +-- ui/src/app/app.run.js | 2 +- ui/src/app/asset/add-asset.tpl.html | 2 +- .../asset/add-assets-to-customer.controller.js | 2 +- ui/src/app/asset/add-assets-to-customer.tpl.html | 2 +- ui/src/app/asset/asset-card.tpl.html | 2 +- ui/src/app/asset/asset-fieldset.tpl.html | 2 +- ui/src/app/asset/asset.controller.js | 2 +- ui/src/app/asset/asset.directive.js | 2 +- ui/src/app/asset/asset.routes.js | 2 +- ui/src/app/asset/assets.tpl.html | 2 +- ui/src/app/asset/assign-to-customer.controller.js | 2 +- ui/src/app/asset/assign-to-customer.tpl.html | 2 +- ui/src/app/asset/index.js | 2 +- .../audit/audit-log-details-dialog.controller.js | 2 +- ui/src/app/audit/audit-log-details-dialog.scss | 3 +-- .../app/audit/audit-log-details-dialog.tpl.html | 2 +- ui/src/app/audit/audit-log-header.directive.js | 2 +- ui/src/app/audit/audit-log-header.tpl.html | 2 +- ui/src/app/audit/audit-log-row.directive.js | 2 +- ui/src/app/audit/audit-log-row.tpl.html | 2 +- ui/src/app/audit/audit-log-table.directive.js | 2 +- ui/src/app/audit/audit-log-table.tpl.html | 2 +- ui/src/app/audit/audit-log.routes.js | 2 +- ui/src/app/audit/audit-log.scss | 3 +-- ui/src/app/audit/audit-logs.controller.js | 3 +-- ui/src/app/audit/audit-logs.tpl.html | 3 +-- ui/src/app/audit/index.js | 3 +-- ui/src/app/common/dashboard-utils.service.js | 3 +-- ui/src/app/common/raf.provider.js | 3 +-- ui/src/app/common/types.constant.js | 2 +- ui/src/app/common/utf8-support.js | 3 +-- ui/src/app/common/utils.service.js | 3 +-- .../app/component/component-dialog.controller.js | 2 +- ui/src/app/component/component-dialog.service.js | 2 +- ui/src/app/component/component-dialog.tpl.html | 2 +- ui/src/app/component/component.directive.js | 2 +- ui/src/app/component/component.tpl.html | 2 +- ui/src/app/component/index.js | 2 +- .../app/components/circular-progress.directive.js | 2 +- .../app/components/confirm-on-exit.directive.js | 2 +- ui/src/app/components/contact-short.filter.js | 2 +- ui/src/app/components/contact.directive.js | 2 +- ui/src/app/components/contact.tpl.html | 2 +- .../dashboard-autocomplete.directive.js | 2 +- ui/src/app/components/dashboard-autocomplete.scss | 2 +- .../components/dashboard-autocomplete.tpl.html | 2 +- .../dashboard-select-panel.controller.js | 3 +-- .../components/dashboard-select-panel.tpl.html | 2 +- .../app/components/dashboard-select.directive.js | 2 +- ui/src/app/components/dashboard-select.scss | 3 +-- ui/src/app/components/dashboard-select.tpl.html | 3 +-- ui/src/app/components/dashboard.directive.js | 2 +- ui/src/app/components/dashboard.scss | 2 +- ui/src/app/components/dashboard.tpl.html | 2 +- .../datakey-config-dialog.controller.js | 2 +- .../app/components/datakey-config-dialog.tpl.html | 2 +- ui/src/app/components/datakey-config.directive.js | 2 +- ui/src/app/components/datakey-config.scss | 2 +- ui/src/app/components/datakey-config.tpl.html | 2 +- .../app/components/datasource-entity.directive.js | 2 +- ui/src/app/components/datasource-entity.scss | 2 +- ui/src/app/components/datasource-entity.tpl.html | 2 +- .../app/components/datasource-func.directive.js | 2 +- ui/src/app/components/datasource-func.scss | 2 +- ui/src/app/components/datasource-func.tpl.html | 2 +- ui/src/app/components/datasource.directive.js | 2 +- ui/src/app/components/datasource.scss | 2 +- ui/src/app/components/datasource.tpl.html | 2 +- .../app/components/datetime-period.directive.js | 2 +- ui/src/app/components/datetime-period.scss | 2 +- ui/src/app/components/datetime-period.tpl.html | 2 +- .../app/components/details-sidenav.directive.js | 2 +- ui/src/app/components/details-sidenav.scss | 2 +- ui/src/app/components/details-sidenav.tpl.html | 2 +- .../components/entity-alias-select.directive.js | 2 +- ui/src/app/components/entity-alias-select.scss | 2 +- .../app/components/entity-alias-select.tpl.html | 2 +- .../app/components/expand-fullscreen.directive.js | 2 +- ui/src/app/components/expand-fullscreen.scss | 2 +- ui/src/app/components/finish-render.directive.js | 3 +-- ui/src/app/components/grid.directive.js | 2 +- ui/src/app/components/grid.scss | 2 +- ui/src/app/components/grid.tpl.html | 2 +- ui/src/app/components/js-func.directive.js | 2 +- ui/src/app/components/js-func.scss | 2 +- ui/src/app/components/js-func.tpl.html | 2 +- ui/src/app/components/json-form.directive.js | 2 +- ui/src/app/components/json-form.scss | 2 +- ui/src/app/components/json-form.tpl.html | 2 +- ui/src/app/components/keyboard-shortcut.filter.js | 2 +- ui/src/app/components/led-light.directive.js | 2 +- .../app/components/legend-config-button.tpl.html | 2 +- .../components/legend-config-panel.controller.js | 3 +-- .../app/components/legend-config-panel.tpl.html | 2 +- ui/src/app/components/legend-config.directive.js | 3 +-- ui/src/app/components/legend-config.scss | 3 +-- ui/src/app/components/legend.directive.js | 3 +-- ui/src/app/components/legend.scss | 3 +-- ui/src/app/components/legend.tpl.html | 2 +- .../components/material-icon-select.directive.js | 3 +-- ui/src/app/components/material-icon-select.scss | 3 +-- .../app/components/material-icon-select.tpl.html | 2 +- .../material-icons-dialog.controller.js | 3 +-- ui/src/app/components/material-icons-dialog.scss | 3 +-- .../app/components/material-icons-dialog.tpl.html | 2 +- ui/src/app/components/menu-link.directive.js | 2 +- ui/src/app/components/menu-link.scss | 2 +- ui/src/app/components/menu-link.tpl.html | 2 +- ui/src/app/components/menu-toggle.tpl.html | 2 +- .../app/components/mousepoint-menu.directive.js | 2 +- ui/src/app/components/no-animate.directive.js | 2 +- ui/src/app/components/plugin-select.directive.js | 2 +- ui/src/app/components/plugin-select.scss | 2 +- ui/src/app/components/plugin-select.tpl.html | 2 +- .../app/components/react/json-form-ace-editor.jsx | 2 +- .../components/react/json-form-ace-editor.scss | 2 +- ui/src/app/components/react/json-form-array.jsx | 2 +- .../components/react/json-form-base-component.jsx | 2 +- .../app/components/react/json-form-checkbox.jsx | 2 +- ui/src/app/components/react/json-form-color.jsx | 2 +- ui/src/app/components/react/json-form-color.scss | 2 +- ui/src/app/components/react/json-form-css.jsx | 2 +- ui/src/app/components/react/json-form-date.jsx | 2 +- .../app/components/react/json-form-fieldset.jsx | 2 +- ui/src/app/components/react/json-form-html.jsx | 2 +- ui/src/app/components/react/json-form-image.jsx | 2 +- ui/src/app/components/react/json-form-image.scss | 2 +- .../app/components/react/json-form-javascript.jsx | 2 +- ui/src/app/components/react/json-form-json.jsx | 2 +- ui/src/app/components/react/json-form-number.jsx | 2 +- .../app/components/react/json-form-rc-select.jsx | 2 +- ui/src/app/components/react/json-form-react.jsx | 2 +- .../components/react/json-form-schema-form.jsx | 2 +- ui/src/app/components/react/json-form-text.jsx | 2 +- ui/src/app/components/react/json-form.scss | 2 +- .../components/react/styles/thingsboardTheme.js | 2 +- .../related-entity-autocomplete.directive.js | 2 +- .../components/related-entity-autocomplete.scss | 2 +- .../related-entity-autocomplete.tpl.html | 2 +- ui/src/app/components/scope-element.directive.js | 2 +- ui/src/app/components/side-menu.directive.js | 2 +- ui/src/app/components/side-menu.scss | 2 +- ui/src/app/components/side-menu.tpl.html | 2 +- .../app/components/socialshare-panel.directive.js | 3 +-- ui/src/app/components/socialshare-panel.tpl.html | 3 +-- ui/src/app/components/tb-event-directives.js | 2 +- ui/src/app/components/timeinterval.directive.js | 2 +- ui/src/app/components/timeinterval.scss | 2 +- ui/src/app/components/timeinterval.tpl.html | 2 +- ui/src/app/components/timewindow-button.tpl.html | 2 +- .../app/components/timewindow-panel.controller.js | 2 +- ui/src/app/components/timewindow-panel.tpl.html | 2 +- ui/src/app/components/timewindow.directive.js | 2 +- ui/src/app/components/timewindow.scss | 3 +-- ui/src/app/components/timewindow.tpl.html | 2 +- ui/src/app/components/truncate.filter.js | 2 +- .../action/manage-widget-actions.directive.js | 15 +++++++++++++++ .../widget/action/manage-widget-actions.scss | 2 +- .../widget/action/manage-widget-actions.tpl.html | 2 +- .../action/widget-action-dialog.controller.js | 3 +-- .../widget/action/widget-action-dialog.tpl.html | 2 +- .../components/widget/widget-config.directive.js | 3 +-- ui/src/app/components/widget/widget-config.scss | 2 +- .../app/components/widget/widget-config.tpl.html | 2 +- ui/src/app/components/widget/widget.controller.js | 2 +- ui/src/app/components/widget/widget.directive.js | 3 +-- ui/src/app/components/widget/widget.scss | 2 +- .../components/widgets-bundle-select.directive.js | 2 +- ui/src/app/components/widgets-bundle-select.scss | 2 +- .../app/components/widgets-bundle-select.tpl.html | 2 +- ui/src/app/customer/add-customer.tpl.html | 2 +- ui/src/app/customer/customer-card.tpl.html | 2 +- ui/src/app/customer/customer-fieldset.tpl.html | 2 +- ui/src/app/customer/customer.controller.js | 2 +- ui/src/app/customer/customer.directive.js | 2 +- ui/src/app/customer/customer.routes.js | 2 +- ui/src/app/customer/customers.tpl.html | 2 +- ui/src/app/customer/index.js | 2 +- ui/src/app/dashboard/add-dashboard.tpl.html | 2 +- .../add-dashboards-to-customer.controller.js | 2 +- .../dashboard/add-dashboards-to-customer.tpl.html | 2 +- ui/src/app/dashboard/add-widget.controller.js | 2 +- ui/src/app/dashboard/add-widget.tpl.html | 2 +- ui/src/app/dashboard/dashboard-card.scss | 3 +-- ui/src/app/dashboard/dashboard-card.tpl.html | 2 +- ui/src/app/dashboard/dashboard-fieldset.tpl.html | 2 +- .../dashboard/dashboard-settings.controller.js | 2 +- ui/src/app/dashboard/dashboard-settings.scss | 2 +- ui/src/app/dashboard/dashboard-settings.tpl.html | 2 +- .../app/dashboard/dashboard-toolbar.directive.js | 3 +-- ui/src/app/dashboard/dashboard-toolbar.scss | 3 +-- ui/src/app/dashboard/dashboard-toolbar.tpl.html | 3 +-- ui/src/app/dashboard/dashboard.controller.js | 2 +- ui/src/app/dashboard/dashboard.directive.js | 2 +- ui/src/app/dashboard/dashboard.routes.js | 2 +- ui/src/app/dashboard/dashboard.scss | 3 +-- ui/src/app/dashboard/dashboard.tpl.html | 2 +- ui/src/app/dashboard/dashboards.controller.js | 2 +- ui/src/app/dashboard/dashboards.tpl.html | 2 +- ui/src/app/dashboard/edit-widget.directive.js | 2 +- ui/src/app/dashboard/edit-widget.tpl.html | 2 +- ui/src/app/dashboard/index.js | 2 +- .../layouts/dashboard-layout.directive.js | 2 +- .../dashboard/layouts/dashboard-layout.tpl.html | 2 +- ui/src/app/dashboard/layouts/index.js | 3 +-- .../manage-dashboard-layouts.controller.js | 3 +-- .../layouts/manage-dashboard-layouts.tpl.html | 2 +- .../layouts/select-target-layout.controller.js | 3 +-- .../layouts/select-target-layout.tpl.html | 2 +- .../make-dashboard-public-dialog.tpl.html | 2 +- .../manage-assigned-customers.controller.js | 2 +- .../dashboard/manage-assigned-customers.tpl.html | 2 +- .../states/dashboard-state-dialog.controller.js | 3 +-- .../states/dashboard-state-dialog.tpl.html | 2 +- .../dashboard/states/default-state-controller.js | 3 +-- .../states/default-state-controller.scss | 3 +-- .../states/default-state-controller.tpl.html | 2 +- .../dashboard/states/entity-state-controller.js | 3 +-- .../dashboard/states/entity-state-controller.scss | 3 +-- .../states/entity-state-controller.tpl.html | 2 +- ui/src/app/dashboard/states/index.js | 3 +-- .../states/manage-dashboard-states.controller.js | 3 +-- .../dashboard/states/manage-dashboard-states.scss | 3 +-- .../states/manage-dashboard-states.tpl.html | 2 +- .../states/select-target-state.controller.js | 3 +-- .../dashboard/states/select-target-state.tpl.html | 2 +- .../states/states-component.directive.js | 3 +-- .../dashboard/states/states-controller.service.js | 3 +-- ui/src/app/device/add-device.tpl.html | 2 +- .../device/add-devices-to-customer.controller.js | 2 +- .../app/device/add-devices-to-customer.tpl.html | 2 +- .../app/device/assign-to-customer.controller.js | 2 +- ui/src/app/device/assign-to-customer.tpl.html | 2 +- ui/src/app/device/device-card.tpl.html | 2 +- .../app/device/device-credentials.controller.js | 2 +- ui/src/app/device/device-credentials.tpl.html | 2 +- ui/src/app/device/device-fieldset.tpl.html | 2 +- ui/src/app/device/device.controller.js | 2 +- ui/src/app/device/device.directive.js | 2 +- ui/src/app/device/device.routes.js | 2 +- ui/src/app/device/devices.tpl.html | 2 +- ui/src/app/device/index.js | 2 +- .../alias/aliases-entity-select-button.tpl.html | 3 +-- .../aliases-entity-select-panel.controller.js | 3 +-- .../alias/aliases-entity-select-panel.tpl.html | 2 +- .../alias/aliases-entity-select.directive.js | 3 +-- .../app/entity/alias/aliases-entity-select.scss | 3 +-- .../alias/entity-alias-dialog.controller.js | 3 +-- ui/src/app/entity/alias/entity-alias-dialog.scss | 3 +-- .../app/entity/alias/entity-alias-dialog.tpl.html | 2 +- .../app/entity/alias/entity-aliases.controller.js | 2 +- ui/src/app/entity/alias/entity-aliases.scss | 3 +-- ui/src/app/entity/alias/entity-aliases.tpl.html | 2 +- .../attribute/add-attribute-dialog.controller.js | 2 +- .../attribute/add-attribute-dialog.tpl.html | 2 +- .../add-widget-to-dashboard-dialog.controller.js | 3 +-- .../add-widget-to-dashboard-dialog.tpl.html | 2 +- .../entity/attribute/attribute-table.directive.js | 2 +- ui/src/app/entity/attribute/attribute-table.scss | 2 +- .../app/entity/attribute/attribute-table.tpl.html | 2 +- .../attribute/edit-attribute-value.controller.js | 2 +- .../attribute/edit-attribute-value.tpl.html | 2 +- .../app/entity/entity-autocomplete.directive.js | 2 +- ui/src/app/entity/entity-autocomplete.scss | 2 +- ui/src/app/entity/entity-autocomplete.tpl.html | 2 +- ui/src/app/entity/entity-filter-view.directive.js | 3 +-- ui/src/app/entity/entity-filter-view.scss | 3 +-- ui/src/app/entity/entity-filter-view.tpl.html | 3 +-- ui/src/app/entity/entity-filter.directive.js | 3 +-- ui/src/app/entity/entity-filter.scss | 3 +-- ui/src/app/entity/entity-filter.tpl.html | 2 +- ui/src/app/entity/entity-list.directive.js | 3 +-- ui/src/app/entity/entity-list.scss | 3 +-- ui/src/app/entity/entity-list.tpl.html | 3 +-- ui/src/app/entity/entity-select.directive.js | 2 +- ui/src/app/entity/entity-select.scss | 3 +-- ui/src/app/entity/entity-select.tpl.html | 2 +- .../entity-subtype-autocomplete.directive.js | 2 +- .../app/entity/entity-subtype-autocomplete.scss | 2 +- .../entity/entity-subtype-autocomplete.tpl.html | 2 +- .../app/entity/entity-subtype-list.directive.js | 3 +-- ui/src/app/entity/entity-subtype-list.scss | 3 +-- ui/src/app/entity/entity-subtype-list.tpl.html | 3 +-- .../app/entity/entity-subtype-select.directive.js | 3 +-- ui/src/app/entity/entity-subtype-select.scss | 3 +-- ui/src/app/entity/entity-subtype-select.tpl.html | 2 +- ui/src/app/entity/entity-type-list.directive.js | 3 +-- ui/src/app/entity/entity-type-list.scss | 3 +-- ui/src/app/entity/entity-type-list.tpl.html | 3 +-- ui/src/app/entity/entity-type-select.directive.js | 3 +-- ui/src/app/entity/entity-type-select.scss | 3 +-- ui/src/app/entity/entity-type-select.tpl.html | 2 +- ui/src/app/entity/index.js | 3 +-- .../entity/relation/relation-dialog.controller.js | 3 +-- ui/src/app/entity/relation/relation-dialog.scss | 3 +-- .../app/entity/relation/relation-dialog.tpl.html | 2 +- .../entity/relation/relation-filters.directive.js | 3 +-- ui/src/app/entity/relation/relation-filters.scss | 3 +-- .../app/entity/relation/relation-filters.tpl.html | 2 +- .../entity/relation/relation-table.directive.js | 2 +- ui/src/app/entity/relation/relation-table.scss | 2 +- .../app/entity/relation/relation-table.tpl.html | 2 +- .../relation-type-autocomplete.directive.js | 2 +- .../relation/relation-type-autocomplete.scss | 2 +- .../relation/relation-type-autocomplete.tpl.html | 2 +- .../app/event/event-content-dialog.controller.js | 2 +- ui/src/app/event/event-content-dialog.tpl.html | 2 +- ui/src/app/event/event-header-error.tpl.html | 2 +- ui/src/app/event/event-header-lc-event.tpl.html | 2 +- ui/src/app/event/event-header-stats.tpl.html | 2 +- ui/src/app/event/event-header.directive.js | 2 +- ui/src/app/event/event-row-error.tpl.html | 2 +- ui/src/app/event/event-row-lc-event.tpl.html | 2 +- ui/src/app/event/event-row-stats.tpl.html | 2 +- ui/src/app/event/event-row.directive.js | 2 +- ui/src/app/event/event-table.directive.js | 2 +- ui/src/app/event/event-table.tpl.html | 2 +- ui/src/app/event/event.scss | 2 +- ui/src/app/event/index.js | 2 +- .../app/extension/extension-dialog.controller.js | 3 +-- ui/src/app/extension/extension-dialog.tpl.html | 2 +- ui/src/app/extension/extension-table.directive.js | 3 +-- ui/src/app/extension/extension-table.scss | 2 +- ui/src/app/extension/extension-table.tpl.html | 3 +-- .../extension-form-http.directive.js | 3 +-- .../extensions-forms/extension-form-http.tpl.html | 2 +- .../extension-form-mqtt.directive.js | 3 +-- .../extensions-forms/extension-form-mqtt.tpl.html | 2 +- .../extension-form-opc.directive.js | 3 +-- .../extensions-forms/extension-form-opc.tpl.html | 2 +- .../extensions-forms/extension-form.scss | 2 +- ui/src/app/extension/index.js | 3 +-- ui/src/app/global-interceptor.service.js | 2 +- ui/src/app/help/help-links.constant.js | 2 +- ui/src/app/help/help.directive.js | 2 +- ui/src/app/help/help.scss | 2 +- ui/src/app/home/home-links.controller.js | 3 +-- ui/src/app/home/home-links.routes.js | 2 +- ui/src/app/home/home-links.scss | 3 +-- ui/src/app/home/home-links.tpl.html | 2 +- ui/src/app/home/index.js | 2 +- ui/src/app/ie.support.js | 3 +-- .../app/import-export/import-dialog.controller.js | 2 +- ui/src/app/import-export/import-dialog.scss | 2 +- ui/src/app/import-export/import-dialog.tpl.html | 2 +- ui/src/app/import-export/import-export.service.js | 2 +- ui/src/app/import-export/index.js | 2 +- ui/src/app/jsonform/index.js | 2 +- ui/src/app/jsonform/jsonform.controller.js | 2 +- ui/src/app/jsonform/jsonform.routes.js | 2 +- ui/src/app/jsonform/jsonform.scss | 2 +- ui/src/app/jsonform/jsonform.tpl.html | 2 +- ui/src/app/layout/breadcrumb-icon.filter.js | 2 +- ui/src/app/layout/breadcrumb-label.filter.js | 2 +- ui/src/app/layout/breadcrumb.tpl.html | 2 +- ui/src/app/layout/home.controller.js | 3 +-- ui/src/app/layout/home.routes.js | 2 +- ui/src/app/layout/home.scss | 2 +- ui/src/app/layout/home.tpl.html | 2 +- ui/src/app/layout/index.js | 2 +- ui/src/app/layout/user-menu.directive.js | 3 +-- ui/src/app/layout/user-menu.scss | 3 +-- ui/src/app/layout/user-menu.tpl.html | 2 +- ui/src/app/locale/locale.constant-es.js | 3 +-- ui/src/app/locale/locale.constant-ko.js | 3 +-- ui/src/app/locale/locale.constant-ru.js | 3 +-- ui/src/app/locale/locale.constant-zh.js | 3 +-- ui/src/app/locale/locale.constant.js | 3 +-- ui/src/app/locale/translate-handler.js | 3 +-- ui/src/app/login/create-password.controller.js | 2 +- ui/src/app/login/create-password.tpl.html | 2 +- ui/src/app/login/index.js | 2 +- ui/src/app/login/login.controller.js | 3 +-- ui/src/app/login/login.routes.js | 2 +- ui/src/app/login/login.scss | 2 +- ui/src/app/login/login.tpl.html | 2 +- .../login/reset-password-request.controller.js | 2 +- ui/src/app/login/reset-password-request.tpl.html | 2 +- ui/src/app/login/reset-password.controller.js | 2 +- ui/src/app/login/reset-password.tpl.html | 2 +- ui/src/app/plugin/add-plugin.tpl.html | 2 +- ui/src/app/plugin/index.js | 2 +- ui/src/app/plugin/plugin-card.tpl.html | 2 +- ui/src/app/plugin/plugin-fieldset.tpl.html | 2 +- ui/src/app/plugin/plugin.controller.js | 2 +- ui/src/app/plugin/plugin.directive.js | 2 +- ui/src/app/plugin/plugin.routes.js | 2 +- ui/src/app/plugin/plugin.scss | 2 +- ui/src/app/plugin/plugins.tpl.html | 2 +- ui/src/app/profile/change-password.controller.js | 2 +- ui/src/app/profile/change-password.tpl.html | 2 +- ui/src/app/profile/index.js | 2 +- ui/src/app/profile/profile.controller.js | 2 +- ui/src/app/profile/profile.routes.js | 2 +- ui/src/app/profile/profile.tpl.html | 2 +- ui/src/app/rule/add-rule.tpl.html | 2 +- ui/src/app/rule/index.js | 2 +- ui/src/app/rule/rule-card.tpl.html | 2 +- ui/src/app/rule/rule-fieldset.tpl.html | 2 +- ui/src/app/rule/rule.controller.js | 2 +- ui/src/app/rule/rule.directive.js | 2 +- ui/src/app/rule/rule.routes.js | 2 +- ui/src/app/rule/rule.scss | 2 +- ui/src/app/rule/rules.tpl.html | 2 +- ui/src/app/services/clipboard.service.js | 2 +- ui/src/app/services/error-toast.tpl.html | 2 +- ui/src/app/services/info-toast.tpl.html | 2 +- ui/src/app/services/item-buffer.service.js | 2 +- ui/src/app/services/menu.service.js | 2 +- ui/src/app/services/success-toast.tpl.html | 2 +- ui/src/app/services/toast.controller.js | 2 +- ui/src/app/services/toast.js | 2 +- ui/src/app/services/toast.scss | 3 +-- ui/src/app/services/toast.service.js | 2 +- ui/src/app/tenant/add-tenant.tpl.html | 2 +- ui/src/app/tenant/index.js | 2 +- ui/src/app/tenant/tenant-card.tpl.html | 2 +- ui/src/app/tenant/tenant-fieldset.tpl.html | 2 +- ui/src/app/tenant/tenant.controller.js | 2 +- ui/src/app/tenant/tenant.directive.js | 2 +- ui/src/app/tenant/tenant.routes.js | 2 +- ui/src/app/tenant/tenants.tpl.html | 3 +-- ui/src/app/url.handler.js | 3 +-- ui/src/app/user/activation-link.controller.js | 3 +-- ui/src/app/user/activation-link.dialog.tpl.html | 2 +- ui/src/app/user/add-user.controller.js | 3 +-- ui/src/app/user/add-user.tpl.html | 2 +- ui/src/app/user/index.js | 2 +- ui/src/app/user/user-card.tpl.html | 2 +- ui/src/app/user/user-fieldset.scss | 3 +-- ui/src/app/user/user-fieldset.tpl.html | 2 +- ui/src/app/user/user.controller.js | 2 +- ui/src/app/user/user.directive.js | 3 +-- ui/src/app/user/user.routes.js | 2 +- ui/src/app/user/users.tpl.html | 2 +- ui/src/app/widget/add-widgets-bundle.tpl.html | 2 +- ui/src/app/widget/index.js | 2 +- ui/src/app/widget/lib/CanvasDigitalGauge.js | 3 +-- ui/src/app/widget/lib/alarms-table-widget.js | 3 +-- ui/src/app/widget/lib/alarms-table-widget.scss | 3 +-- .../app/widget/lib/alarms-table-widget.tpl.html | 2 +- ui/src/app/widget/lib/analogue-compass.js | 2 +- ui/src/app/widget/lib/analogue-linear-gauge.js | 2 +- ui/src/app/widget/lib/analogue-radial-gauge.js | 2 +- ui/src/app/widget/lib/canvas-digital-gauge.js | 3 +-- ui/src/app/widget/lib/entities-table-widget.js | 3 +-- ui/src/app/widget/lib/entities-table-widget.scss | 3 +-- .../app/widget/lib/entities-table-widget.tpl.html | 2 +- ui/src/app/widget/lib/extensions-table-widget.js | 3 +-- .../app/widget/lib/extensions-table-widget.scss | 3 +-- .../widget/lib/extensions-table-widget.tpl.html | 2 +- ui/src/app/widget/lib/flot-widget.js | 3 +-- ui/src/app/widget/lib/google-map.js | 2 +- ui/src/app/widget/lib/image-map.js | 2 +- ui/src/app/widget/lib/map-widget.js | 2 +- ui/src/app/widget/lib/map-widget2.js | 2 +- ui/src/app/widget/lib/openstreet-map.js | 2 +- ui/src/app/widget/lib/rpc/index.js | 3 +-- ui/src/app/widget/lib/rpc/knob.directive.js | 3 +-- ui/src/app/widget/lib/rpc/knob.scss | 3 +-- ui/src/app/widget/lib/rpc/knob.tpl.html | 3 +-- .../app/widget/lib/rpc/led-indicator.directive.js | 3 +-- ui/src/app/widget/lib/rpc/led-indicator.scss | 3 +-- ui/src/app/widget/lib/rpc/led-indicator.tpl.html | 2 +- .../app/widget/lib/rpc/round-switch.directive.js | 3 +-- ui/src/app/widget/lib/rpc/round-switch.scss | 3 +-- ui/src/app/widget/lib/rpc/round-switch.tpl.html | 2 +- ui/src/app/widget/lib/rpc/switch.directive.js | 3 +-- ui/src/app/widget/lib/rpc/switch.scss | 3 +-- ui/src/app/widget/lib/rpc/switch.tpl.html | 3 +-- ui/src/app/widget/lib/tencent-map.js | 3 +-- ui/src/app/widget/lib/timeseries-table-widget.js | 3 +-- .../app/widget/lib/timeseries-table-widget.scss | 3 +-- .../widget/lib/timeseries-table-widget.tpl.html | 2 +- ui/src/app/widget/lib/widget-utils.js | 3 +-- .../app/widget/save-widget-type-as.controller.js | 2 +- ui/src/app/widget/save-widget-type-as.tpl.html | 2 +- .../app/widget/select-widget-type.controller.js | 2 +- ui/src/app/widget/select-widget-type.tpl.html | 2 +- ui/src/app/widget/widget-editor.controller.js | 2 +- ui/src/app/widget/widget-editor.scss | 2 +- ui/src/app/widget/widget-editor.tpl.html | 2 +- ui/src/app/widget/widget-library.controller.js | 3 +-- ui/src/app/widget/widget-library.routes.js | 2 +- ui/src/app/widget/widget-library.tpl.html | 2 +- ui/src/app/widget/widgets-bundle-card.tpl.html | 2 +- .../app/widget/widgets-bundle-fieldset.tpl.html | 2 +- ui/src/app/widget/widgets-bundle.controller.js | 2 +- ui/src/app/widget/widgets-bundle.directive.js | 2 +- ui/src/app/widget/widgets-bundles.tpl.html | 2 +- ui/src/index.html | 2 +- ui/src/scss/animations.scss | 2 +- ui/src/scss/constants.scss | 2 +- ui/src/scss/fonts.scss | 2 +- ui/src/scss/main.scss | 2 +- ui/src/scss/mixins.scss | 2 +- ui/webpack.config.dev.js | 2 +- ui/webpack.config.js | 2 +- ui/webpack.config.prod.js | 2 +- 1601 files changed, 1615 insertions(+), 1764 deletions(-) diff --git a/application/build.gradle b/application/build.gradle index 1d8a526fd1..01761692f2 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2017 The Thingsboard Authors + * Copyright © 2016-2018 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import org.apache.tools.ant.filters.ReplaceTokens buildscript { diff --git a/application/pom.xml b/application/pom.xml index e5a4a4e614..3681bd42db 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -1,6 +1,6 @@ - - -
- diff --git a/ui/src/app/dashboard/dashboard.controller.js b/ui/src/app/dashboard/dashboard.controller.js index d4d3a1c939..f672f3f03b 100644 --- a/ui/src/app/dashboard/dashboard.controller.js +++ b/ui/src/app/dashboard/dashboard.controller.js @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2017 The Thingsboard Authors + * Copyright © 2016-2018 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. diff --git a/ui/src/app/dashboard/dashboard.directive.js b/ui/src/app/dashboard/dashboard.directive.js index 39b1c02125..8f042856eb 100644 --- a/ui/src/app/dashboard/dashboard.directive.js +++ b/ui/src/app/dashboard/dashboard.directive.js @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2017 The Thingsboard Authors + * Copyright © 2016-2018 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. diff --git a/ui/src/app/dashboard/dashboard.routes.js b/ui/src/app/dashboard/dashboard.routes.js index 92bb36220f..ccb43c708f 100644 --- a/ui/src/app/dashboard/dashboard.routes.js +++ b/ui/src/app/dashboard/dashboard.routes.js @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2017 The Thingsboard Authors + * Copyright © 2016-2018 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. diff --git a/ui/src/app/dashboard/dashboard.scss b/ui/src/app/dashboard/dashboard.scss index daae8ecfc3..224dcf7e4a 100644 --- a/ui/src/app/dashboard/dashboard.scss +++ b/ui/src/app/dashboard/dashboard.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2017 The Thingsboard Authors + * Copyright © 2016-2018 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - @import "~compass-sass-mixins/lib/compass"; @import '../../scss/constants'; diff --git a/ui/src/app/dashboard/dashboard.tpl.html b/ui/src/app/dashboard/dashboard.tpl.html index fab46346da..9626509a51 100644 --- a/ui/src/app/dashboard/dashboard.tpl.html +++ b/ui/src/app/dashboard/dashboard.tpl.html @@ -1,6 +1,6 @@ -
diff --git a/ui/src/app/entity/alias/aliases-entity-select-panel.controller.js b/ui/src/app/entity/alias/aliases-entity-select-panel.controller.js index 90248b2d57..6ff7863964 100644 --- a/ui/src/app/entity/alias/aliases-entity-select-panel.controller.js +++ b/ui/src/app/entity/alias/aliases-entity-select-panel.controller.js @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2017 The Thingsboard Authors + * Copyright © 2016-2018 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - /*@ngInject*/ export default function AliasesEntitySelectPanelController(mdPanelRef, $scope, $filter, types, aliasController, onEntityAliasesUpdate) { diff --git a/ui/src/app/entity/alias/aliases-entity-select-panel.tpl.html b/ui/src/app/entity/alias/aliases-entity-select-panel.tpl.html index d197d0bd9c..bfce77cf6a 100644 --- a/ui/src/app/entity/alias/aliases-entity-select-panel.tpl.html +++ b/ui/src/app/entity/alias/aliases-entity-select-panel.tpl.html @@ -1,6 +1,6 @@ -
alias.no-entity-filter-specified
diff --git a/ui/src/app/entity/entity-filter.directive.js b/ui/src/app/entity/entity-filter.directive.js index 3cede93b39..0c8f6468f0 100644 --- a/ui/src/app/entity/entity-filter.directive.js +++ b/ui/src/app/entity/entity-filter.directive.js @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2017 The Thingsboard Authors + * Copyright © 2016-2018 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - /* eslint-disable import/no-unresolved, import/default */ import entityFilterTemplate from './entity-filter.tpl.html'; diff --git a/ui/src/app/entity/entity-filter.scss b/ui/src/app/entity/entity-filter.scss index 7e998ca674..bbaa108e09 100644 --- a/ui/src/app/entity/entity-filter.scss +++ b/ui/src/app/entity/entity-filter.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2017 The Thingsboard Authors + * Copyright © 2016-2018 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - .tb-entity-filter { #relationsQueryFilter { diff --git a/ui/src/app/entity/entity-filter.tpl.html b/ui/src/app/entity/entity-filter.tpl.html index 0e8ee1177b..f9aac3cb78 100644 --- a/ui/src/app/entity/entity-filter.tpl.html +++ b/ui/src/app/entity/entity-filter.tpl.html @@ -1,6 +1,6 @@ -
-
-
-
-
diff --git a/ui/src/app/url.handler.js b/ui/src/app/url.handler.js index cc443ff2f2..bf6dfb636d 100644 --- a/ui/src/app/url.handler.js +++ b/ui/src/app/url.handler.js @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2017 The Thingsboard Authors + * Copyright © 2016-2018 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - export default function UrlHandler($injector, $location) { var userService = $injector.get('userService'); if (userService.isUserLoaded() === true) { diff --git a/ui/src/app/user/activation-link.controller.js b/ui/src/app/user/activation-link.controller.js index 9fb76c54f4..ac0fb7967f 100644 --- a/ui/src/app/user/activation-link.controller.js +++ b/ui/src/app/user/activation-link.controller.js @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2017 The Thingsboard Authors + * Copyright © 2016-2018 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - /*@ngInject*/ export default function ActivationLinkDialogController($mdDialog, $translate, toast, activationLink) { diff --git a/ui/src/app/user/activation-link.dialog.tpl.html b/ui/src/app/user/activation-link.dialog.tpl.html index 3f55d1d446..c63d74cbc8 100644 --- a/ui/src/app/user/activation-link.dialog.tpl.html +++ b/ui/src/app/user/activation-link.dialog.tpl.html @@ -1,6 +1,6 @@ -
diff --git a/ui/src/app/widget/lib/rpc/led-indicator.directive.js b/ui/src/app/widget/lib/rpc/led-indicator.directive.js index ac0658c75b..51ab2d3b32 100644 --- a/ui/src/app/widget/lib/rpc/led-indicator.directive.js +++ b/ui/src/app/widget/lib/rpc/led-indicator.directive.js @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2017 The Thingsboard Authors + * Copyright © 2016-2018 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import './led-indicator.scss'; import tinycolor from 'tinycolor2'; diff --git a/ui/src/app/widget/lib/rpc/led-indicator.scss b/ui/src/app/widget/lib/rpc/led-indicator.scss index d086a101d6..9b68c42253 100644 --- a/ui/src/app/widget/lib/rpc/led-indicator.scss +++ b/ui/src/app/widget/lib/rpc/led-indicator.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2017 The Thingsboard Authors + * Copyright © 2016-2018 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - @import "~compass-sass-mixins/lib/compass"; $error-height: 14px; diff --git a/ui/src/app/widget/lib/rpc/led-indicator.tpl.html b/ui/src/app/widget/lib/rpc/led-indicator.tpl.html index a092f42dea..383ca25eb9 100644 --- a/ui/src/app/widget/lib/rpc/led-indicator.tpl.html +++ b/ui/src/app/widget/lib/rpc/led-indicator.tpl.html @@ -1,6 +1,6 @@ -
diff --git a/ui/src/app/widget/lib/tencent-map.js b/ui/src/app/widget/lib/tencent-map.js index 27a87cab59..e0a9c5cbe7 100644 --- a/ui/src/app/widget/lib/tencent-map.js +++ b/ui/src/app/widget/lib/tencent-map.js @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2017 The Thingsboard Authors + * Copyright © 2016-2018 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - var tmGlobals = { loadingTmId: null, tmApiKeys: {} diff --git a/ui/src/app/widget/lib/timeseries-table-widget.js b/ui/src/app/widget/lib/timeseries-table-widget.js index 28ac811f1b..684bbde1e3 100644 --- a/ui/src/app/widget/lib/timeseries-table-widget.js +++ b/ui/src/app/widget/lib/timeseries-table-widget.js @@ -1,5 +1,5 @@ /* - * Copyright © 2016-2017 The Thingsboard Authors + * Copyright © 2016-2018 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import './timeseries-table-widget.scss'; /* eslint-disable import/no-unresolved, import/default */ diff --git a/ui/src/app/widget/lib/timeseries-table-widget.scss b/ui/src/app/widget/lib/timeseries-table-widget.scss index da3ee81644..cd8f8a9dad 100644 --- a/ui/src/app/widget/lib/timeseries-table-widget.scss +++ b/ui/src/app/widget/lib/timeseries-table-widget.scss @@ -1,5 +1,5 @@ /** - * Copyright © 2016-2017 The Thingsboard Authors + * Copyright © 2016-2018 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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - tb-timeseries-table-widget { table.md-table thead.md-head>tr.md-row { height: 40px; diff --git a/ui/src/app/widget/lib/timeseries-table-widget.tpl.html b/ui/src/app/widget/lib/timeseries-table-widget.tpl.html index 349edbac5e..eb9b8caa75 100644 --- a/ui/src/app/widget/lib/timeseries-table-widget.tpl.html +++ b/ui/src/app/widget/lib/timeseries-table-widget.tpl.html @@ -1,6 +1,6 @@