From 6c5054a6e835fb775a92e8ec4cc855bb8ed2bb38 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 10 Feb 2023 17:08:27 +0200 Subject: [PATCH] added caching, added PUT methods, refactored code --- .../main/data/upgrade/3.4.4/schema_update.sql | 2 +- .../server/controller/BaseController.java | 4 + .../server/controller/UserController.java | 67 +++++---- .../src/main/resources/thingsboard.yml | 3 + .../server/controller/AbstractWebTest.java | 17 +++ .../controller/BaseUserControllerTest.java | 64 ++++----- .../server/dao/user/UserService.java | 7 - .../server/dao/user/UserSettingsService.java | 43 ++++++ .../server/common/data/CacheConstants.java | 1 + .../common/data/security/UserSettings.java | 21 ++- dao/pom.xml | 4 + .../dao/entity/AbstractCachedService.java | 43 ++++++ .../server/dao/user/UserServiceImpl.java | 21 --- .../dao/user/UserSettingsCaffeineCache.java | 36 +++++ .../dao/user/UserSettingsEvictEvent.java | 24 ++++ .../dao/user/UserSettingsRedisCache.java | 38 +++++ .../dao/user/UserSettingsServiceImpl.java | 131 ++++++++++++++++++ .../dao/service/BaseUserServiceTest.java | 9 -- 18 files changed, 433 insertions(+), 102 deletions(-) create mode 100644 common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserSettingsService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/entity/AbstractCachedService.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsCaffeineCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsEvictEvent.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsRedisCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java diff --git a/application/src/main/data/upgrade/3.4.4/schema_update.sql b/application/src/main/data/upgrade/3.4.4/schema_update.sql index 598212ca2a..f8c133f352 100644 --- a/application/src/main/data/upgrade/3.4.4/schema_update.sql +++ b/application/src/main/data/upgrade/3.4.4/schema_update.sql @@ -27,6 +27,6 @@ CREATE INDEX IF NOT EXISTS idx_alarm_comment_alarm_id ON alarm_comment(alarm_id) CREATE TABLE IF NOT EXISTS user_settings ( user_id uuid NOT NULL CONSTRAINT user_settings_pkey PRIMARY KEY, - settings varchar(10000), + settings varchar(100000), CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE ); 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 3f11fc59ac..970100d938 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -128,6 +128,7 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserService; +import org.thingsboard.server.dao.user.UserSettingsService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; @@ -189,6 +190,9 @@ public abstract class BaseController { @Autowired protected UserService userService; + @Autowired + protected UserSettingsService userSettingsService; + @Autowired protected DeviceService deviceService; diff --git a/application/src/main/java/org/thingsboard/server/controller/UserController.java b/application/src/main/java/org/thingsboard/server/controller/UserController.java index fc95ee2930..ed55bcf324 100644 --- a/application/src/main/java/org/thingsboard/server/controller/UserController.java +++ b/application/src/main/java/org/thingsboard/server/controller/UserController.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.controller; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; @@ -27,6 +28,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -34,6 +36,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; @@ -59,6 +62,8 @@ import org.thingsboard.server.service.security.system.SystemSecurityService; import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; + import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID; import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.DEFAULT_DASHBOARD; @@ -87,6 +92,7 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI public class UserController extends BaseController { public static final String USER_ID = "userId"; + public static final String JSON_PATHS = "jsonPaths"; public static final String YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION = "You don't have permission to perform this operation!"; public static final String ACTIVATE_URL_PATTERN = "%s/api/noauth/activate?activateToken=%s"; @@ -382,45 +388,54 @@ public class UserController extends BaseController { } @ApiOperation(value = "Save user settings (saveUserSettings)", - notes = "Save user settings for specified user id. " ) + notes = "Save user settings represented in json format for authorized user. " ) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @PostMapping(value = "/user/{userId}/settings") - public UserSettings saveUserSettings(@ApiParam(value = USER_ID_PARAM_DESCRIPTION) - @PathVariable(USER_ID) String strUserId, @RequestBody UserSettings userSettings) throws ThingsboardException { - checkParameter(USER_ID, strUserId); + @PostMapping(value = "/user/settings") + public JsonNode saveUserSettings(@RequestBody JsonNode settings) throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); - UserId userId = new UserId(toUUID(strUserId)); - User user = checkUserId(userId, Operation.WRITE); + UserSettings userSettings = new UserSettings(); + userSettings.setSettings(settings); + userSettings.setUserId(currentUser.getId()); + return userSettingsService.saveUserSettings(currentUser.getTenantId(), userSettings).getSettings(); + } + + @ApiOperation(value = "Update user settings (saveUserSettings)", + notes = "Update user settings for authorized user. Only specified json elements will be updated." + + "Example: you have such settings: {A:5, B:{C:10, D:5}}. Updating it with {A:10, E:6} will result in" + + "{A:10, B:{C:10, D:5}}, E:6") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") + @PutMapping(value = "/user/settings") + public JsonNode putUserSettings(@RequestBody JsonNode settings) throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); - userSettings.setUserId(userId); - return userService.saveUserSettings(user.getTenantId(), userId, userSettings); + UserSettings userSettings = new UserSettings(); + userSettings.setSettings(settings); + userSettings.setUserId(currentUser.getId()); + return userSettingsService.updateUserSettings(currentUser.getTenantId(), userSettings).getSettings(); } @ApiOperation(value = "Get user settings (getUserSettings)", notes = "Fetch the User settings based on the provided User Id. " ) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @GetMapping(value = "/user/{userId}/settings") - public UserSettings getUserSettings(@ApiParam(value = USER_ID_PARAM_DESCRIPTION) - @PathVariable(USER_ID) String strUserId) throws ThingsboardException { - checkParameter(USER_ID, strUserId); - - UserId userId = new UserId(toUUID(strUserId)); - User user = checkUserId(userId, Operation.READ); + @GetMapping(value = "/user/settings") + public JsonNode getUserSettings() throws ThingsboardException { + SecurityUser currentUser = getCurrentUser(); - return checkNotNull(userService.findUserSettings(user.getTenantId(), user.getId()), "No user settingd found"); + UserSettings userSettings = userSettingsService.findUserSettings(currentUser.getTenantId(), currentUser.getId()); + return userSettings == null ? JacksonUtil.newObjectNode(): userSettings.getSettings(); } @ApiOperation(value = "Delete user settings (deleteUserSettings)", - notes = "Delete user settings based on the provided User Id. " ) + notes = "Delete user settings by specifying list of json element xpaths. \n " + + "Example: to delete B and C element in { \"A\": {\"B\": 5}, \"C\": 15} send A.B,C in jsonPaths request parameter" ) @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')") - @RequestMapping(value = "/user/{userId}/settings", method = RequestMethod.DELETE) - public void deleteUserSettings(@ApiParam(value = USER_ID_PARAM_DESCRIPTION) - @PathVariable(USER_ID) String strUserId) throws ThingsboardException { - checkParameter(USER_ID, strUserId); - - UserId userId = new UserId(toUUID(strUserId)); - User user = checkUserId(userId, Operation.WRITE); + @RequestMapping(value = "/user/settings/{jsonPaths}", method = RequestMethod.DELETE) + public void deleteUserSettings( @ApiParam(value = JSON_PATHS) + @PathVariable(JSON_PATHS) String jsonPaths) throws ThingsboardException { + checkParameter(USER_ID, jsonPaths); - userService.deleteUserSettings(user.getTenantId(), userId); + SecurityUser currentUser = getCurrentUser(); + userSettingsService.deleteUserSettings(currentUser.getTenantId(), currentUser.getId(), Arrays.asList(jsonPaths.split(","))); } } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 2fb92cdf4d..5549861950 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -457,6 +457,9 @@ cache: versionControlTask: timeToLiveInMinutes: "${CACHE_SPECS_VERSION_CONTROL_TASK_TTL:5}" maxSize: "${CACHE_SPECS_VERSION_CONTROL_TASK_MAX_SIZE:100000}" + userSettings: + timeToLiveInMinutes: "${CACHE_SPECS_USER_SETTINGS_TTL:1440}" + maxSize: "${CACHE_SPECS_USER_SETTINGS_MAX_SIZE:100000}" #Disable this because it is not required. spring.data.redis.repositories.enabled: false diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 9de2738a8e..24ce820760 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -103,6 +103,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; @@ -613,6 +614,22 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass); } + protected T doPut(String urlTemplate, T content, Class responseClass, String... params) { + try { + return readResponse(doPut(urlTemplate, content, params).andExpect(status().isOk()), responseClass); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + protected ResultActions doPut(String urlTemplate, T content, String... params) throws Exception { + MockHttpServletRequestBuilder postRequest = put(urlTemplate, params); + setJwtToken(postRequest); + String json = json(content); + postRequest.contentType(contentType).content(json); + return mockMvc.perform(postRequest); + } + protected T doDelete(String urlTemplate, Class responseClass, String... params) throws Exception { return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass); } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java index e9d516c68b..463c21ef8d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java @@ -745,48 +745,44 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { @Test public void testSaveUserSettings() throws Exception { - loginSysAdmin(); - - User user = createUser(); - User savedUser = doPost("/api/user", user, User.class); + loginCustomerUser(); - UserSettings userSettings = createUserSettings(); - UserSettings savedSettings = doPost("/api/user/" + savedUser.getId() + "/settings", userSettings, UserSettings.class); - Assert.assertEquals(savedSettings.getSettings(), savedSettings.getSettings()); + JsonNode userSettings = mapper.readTree("{\"A\":5, \"B\":10, \"E\":18}"); + JsonNode savedSettings = doPost("/api/user/settings", userSettings, JsonNode.class); + Assert.assertEquals(userSettings, savedSettings); - UserSettings retrievedSettings = doGet("/api/user/" + savedUser.getId() + "/settings", UserSettings.class); - Assert.assertEquals(retrievedSettings.getSettings(), retrievedSettings.getSettings()); - - doDelete("/api/user/" + savedUser.getId() + "/settings"); - doGet("/api/user/" + savedUser.getId() + "/settings").andExpect(status().isNotFound()); - } + JsonNode retrievedSettings = doGet("/api/user/settings", JsonNode.class); + Assert.assertEquals(retrievedSettings, userSettings); + } @Test - public void testShouldNotSaveSettingsForOtherUser() throws Exception { - loginSysAdmin(); + public void testUpdateUserSettings() throws Exception { + loginCustomerUser(); - User user = createUser(); - User savedUser = doPost("/api/user", user, User.class); + JsonNode userSettings = mapper.readTree("{\"A\":5, \"B\":10, \"E\":18}"); + JsonNode savedSettings = doPost("/api/user/settings", userSettings, JsonNode.class); + Assert.assertEquals(userSettings, savedSettings); - loginCustomerUser(); - UserSettings userSettings = createUserSettings(); - doPost("/api/user/" + savedUser.getId() + "/settings", userSettings) - .andExpect(status().isForbidden()); + JsonNode newSettings = mapper.readTree("{\"A\":10, \"B\":10, \"C\":{\"D\": 16}}"); + JsonNode updatedSettings = doPut("/api/user/settings", newSettings, JsonNode.class); + + JsonNode expectedSettings = mapper.readTree("{\"A\":10, \"B\":10, \"C\":{\"D\": 16}, \"E\":18}"); + Assert.assertEquals(expectedSettings, updatedSettings); } + @Test - public void testShouldDeleteSettingsAfterUserDeletion() throws Exception { - loginSysAdmin(); + public void testDeleteUserSettings() throws Exception { + loginCustomerUser(); - User user = createUser(); - User savedUser = doPost("/api/user", user, User.class); + JsonNode userSettings = mapper.readTree("{\"A\":10, \"B\":10, \"C\":{\"D\": 16}}"); + JsonNode savedSettings = doPost("/api/user/settings", userSettings, JsonNode.class); + Assert.assertEquals(userSettings, savedSettings); - UserSettings userSettings = createUserSettings(); - UserSettings savedSettings = doPost("/api/user/" + savedUser.getId() + "/settings", userSettings, UserSettings.class); - Assert.assertEquals(savedSettings.getSettings(), savedSettings.getSettings()); + doDelete("/api/user/settings/C.D,B"); - doDelete("/api/user/" + savedUser.getId()) - .andExpect(status().isOk()); - doGet("/api/user/" + savedUser.getId() + "/settings").andExpect(status().isNotFound()); + JsonNode retrievedSettings = doGet("/api/user/settings", JsonNode.class); + JsonNode expectedSettings = mapper.readTree("{\"A\":10, \"C\":{}}"); + Assert.assertEquals(expectedSettings, retrievedSettings); } private User createUser() throws Exception { @@ -800,10 +796,4 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { user.setLastName("Downs"); return doPost("/api/user", user, User.class); } - - private UserSettings createUserSettings() { - UserSettings userSettings = new UserSettings(); - userSettings.setSettings(JacksonUtil.newObjectNode().put("text", StringUtils.randomAlphanumeric(10))); - return userSettings; - } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index c6bfbf05b1..bb40507772 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -25,7 +25,6 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.entity.EntityDaoService; -import org.thingsboard.server.common.data.security.UserSettings; public interface UserService extends EntityDaoService { @@ -75,10 +74,4 @@ public interface UserService extends EntityDaoService { void setLastLoginTs(TenantId tenantId, UserId userId); - UserSettings saveUserSettings(TenantId tenantId, UserId userId, UserSettings userSettings); - - UserSettings findUserSettings(TenantId tenantId, UserId userId); - - void deleteUserSettings(TenantId tenantId, UserId userId); - } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserSettingsService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserSettingsService.java new file mode 100644 index 0000000000..5ed1eab92a --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserSettingsService.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2023 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.user; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserCredentialsId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.common.data.security.UserSettings; +import org.thingsboard.server.dao.entity.EntityDaoService; + +import java.util.List; + +public interface UserSettingsService { + + UserSettings updateUserSettings(TenantId tenantId, UserSettings userSettings); + + UserSettings saveUserSettings(TenantId tenantId, UserSettings userSettings); + + UserSettings findUserSettings(TenantId tenantId, UserId userId); + + void deleteUserSettings(TenantId tenantId, UserId userId, List jsonPaths); + +} 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 7cfccb2c3c..c9e6e0aa46 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 @@ -39,4 +39,5 @@ public class CacheConstants { public static final String AUTO_COMMIT_SETTINGS_CACHE = "autoCommitSettings"; public static final String TWO_FA_VERIFICATION_CODES_CACHE = "twoFaVerificationCodes"; public static final String VERSION_CONTROL_TASK_CACHE = "versionControlTask"; + public static final String USER_SETTINGS_CACHE = "userSettings"; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserSettings.java index 49d5903049..5a7fb19ffa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserSettings.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.common.data.security; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; @@ -23,7 +25,14 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.Serializable; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.getJson; +import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.setJson; @ApiModel @Data @@ -36,7 +45,17 @@ public class UserSettings implements Serializable { @ApiModelProperty(position = 2, value = "JSON object with user settings.", dataType = "com.fasterxml.jackson.databind.JsonNode") @NoXss - @Length(fieldName = "settings", max = 10000) + @Length(fieldName = "settings", max = 100000) private transient JsonNode settings; + @JsonIgnore + private byte[] settingsBytes; + + public JsonNode getSettings() { + return getJson(() -> settings, () -> settingsBytes); + } + + public void setSettings(JsonNode settings) { + setJson(settings, json -> this.settings = json, bytes -> this.settingsBytes = bytes); + } } diff --git a/dao/pom.xml b/dao/pom.xml index 901d967817..30dc1adcf0 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -233,6 +233,10 @@ org.eclipse.leshan leshan-core + + com.jayway.jsonpath + json-path + diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractCachedService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractCachedService.java new file mode 100644 index 0000000000..d48acb9fc4 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractCachedService.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2023 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.entity; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.thingsboard.server.cache.TbTransactionalCache; + +import java.io.Serializable; + +public abstract class AbstractCachedService { + + @Autowired + protected TbTransactionalCache cache; + + @Autowired + private ApplicationEventPublisher eventPublisher; + + protected void publishEvictEvent(E event) { + if (TransactionSynchronizationManager.isActualTransactionActive()) { + eventPublisher.publishEvent(event); + } else { + handleEvictEvent(event); + } + } + + public abstract void handleEvictEvent(E event); + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index e75bf3521c..c8c5175e5f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -328,27 +328,6 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic return failedLoginAttempts; } - @Override - public UserSettings saveUserSettings(TenantId tenantId, UserId userId, UserSettings userSettings) { - log.trace("Executing saveUserSettings for user [{}], [{}]", userId, userSettings); - validateId(userId, INCORRECT_USER_ID + userId); - return userSettingsDao.save(tenantId, userSettings); - } - - @Override - public UserSettings findUserSettings(TenantId tenantId, UserId userId) { - log.trace("Executing findUserSettings for user [{}]", userId); - validateId(userId, INCORRECT_USER_ID + userId); - return userSettingsDao.findById(tenantId, userId); - } - - @Override - public void deleteUserSettings(TenantId tenantId, UserId userId) { - log.trace("Executing deleteUserSettings for user [{}]", userId); - validateId(userId, INCORRECT_USER_ID + userId); - userSettingsDao.removeById(tenantId, userId); - } - private int increaseFailedLoginAttempts(User user) { JsonNode additionalInfo = user.getAdditionalInfo(); if (!(additionalInfo instanceof ObjectNode)) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsCaffeineCache.java new file mode 100644 index 0000000000..3154e5d8c0 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsCaffeineCache.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2023 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.user; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.UserSettings; +import org.thingsboard.server.dao.asset.AssetCacheKey; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("UserSettingsCache") +public class UserSettingsCaffeineCache extends CaffeineTbTransactionalCache { + + public UserSettingsCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.USER_SETTINGS_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsEvictEvent.java new file mode 100644 index 0000000000..e0d2de7ece --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsEvictEvent.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2023 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.user; + +import lombok.Data; +import org.thingsboard.server.common.data.id.UserId; + +@Data +public class UserSettingsEvictEvent { + private final UserId userId; +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsRedisCache.java new file mode 100644 index 0000000000..a510e6c864 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsRedisCache.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2023 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.user; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbFSTRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.UserSettings; +import org.thingsboard.server.dao.asset.AssetCacheKey; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("UserSettingsCache") +public class UserSettingsRedisCache extends RedisTbTransactionalCache { + + public UserSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.USER_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>()); + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java new file mode 100644 index 0000000000..a5f9d2f79f --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java @@ -0,0 +1,131 @@ +/** + * Copyright © 2016-2023 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.user; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.jayway.jsonpath.JsonPath; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionalEventListener; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.UserSettings; +import org.thingsboard.server.dao.entity.AbstractCachedService; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import static org.thingsboard.server.dao.service.Validator.validateId; + +@Service("UserSettingsDaoService") +@Slf4j +@RequiredArgsConstructor +public class UserSettingsServiceImpl extends AbstractCachedService implements UserSettingsService { + public static final String INCORRECT_USER_ID = "Incorrect userId "; + private final UserSettingsDao userSettingsDao; + + @Override + public UserSettings saveUserSettings(TenantId tenantId, UserSettings userSettings) { + log.trace("Executing saveUserSettings for user [{}], [{}]", userSettings.getUserId(), userSettings); + validateId(userSettings.getUserId(), INCORRECT_USER_ID + userSettings.getUserId()); + try { + UserSettings saved = userSettingsDao.save(tenantId, userSettings); + publishEvictEvent(new UserSettingsEvictEvent(userSettings.getUserId())); + return saved; + } catch (Exception t) { + handleEvictEvent(new UserSettingsEvictEvent(userSettings.getUserId())); + throw t; + } + } + + @Override + public UserSettings updateUserSettings(TenantId tenantId, UserSettings userSettings) { + log.trace("Executing updateUserSettings for user [{}], [{}]", userSettings.getUserId(), userSettings); + validateId(userSettings.getUserId(), INCORRECT_USER_ID + userSettings.getUserId()); + UserSettings oldSettings = userSettingsDao.findById(tenantId, userSettings.getUserId()); + userSettings.setSettings(merge(oldSettings.getSettings(), userSettings.getSettings())); + + try { + UserSettings saved = userSettingsDao.save(tenantId, userSettings); + publishEvictEvent(new UserSettingsEvictEvent(userSettings.getUserId())); + return saved; + } catch (Exception t) { + handleEvictEvent(new UserSettingsEvictEvent(userSettings.getUserId())); + throw t; + } + } + + @Override + public UserSettings findUserSettings(TenantId tenantId, UserId userId) { + log.trace("Executing findUserSettings for user [{}]", userId); + validateId(userId, INCORRECT_USER_ID + userId); + + return cache.getAndPutInTransaction(userId, + () -> userSettingsDao.findById(tenantId, userId), true); + } + + @Override + public void deleteUserSettings(TenantId tenantId, UserId userId, List jsonPaths) { + log.trace("Executing deleteUserSettings for user [{}]", userId); + validateId(userId, INCORRECT_USER_ID + userId); + UserSettings userSettings = userSettingsDao.findById(tenantId, userId); + ObjectNode settings = (ObjectNode) userSettings.getSettings(); + + try { + for (String s : jsonPaths) { + settings = new ObjectMapper().readValue(JsonPath.parse(settings.toString()).delete("$." + s).jsonString(), ObjectNode.class); + userSettings.setSettings(settings); + } + userSettingsDao.save(tenantId, userSettings); + publishEvictEvent(new UserSettingsEvictEvent(userSettings.getUserId())); + } catch (Exception t) { + handleEvictEvent(new UserSettingsEvictEvent(userSettings.getUserId())); + throw new RuntimeException(t); + } + } + + @TransactionalEventListener(classes = UserSettingsEvictEvent.class) + @Override + public void handleEvictEvent(UserSettingsEvictEvent event) { + List keys = new ArrayList<>(); + keys.add(event.getUserId()); + cache.evict(keys); + } + + public JsonNode merge(JsonNode mainNode, JsonNode updateNode) { + + Iterator fieldNames = updateNode.fieldNames(); + while (fieldNames.hasNext()) { + String fieldName = fieldNames.next(); + JsonNode jsonNode = mainNode.get(fieldName); + if (jsonNode != null && jsonNode.isObject()) { + merge(jsonNode, updateNode.get(fieldName)); + } + else { + if (mainNode instanceof ObjectNode) { + JsonNode value = updateNode.get(fieldName); + ((ObjectNode) mainNode).set(fieldName, value); + } + } + } + return mainNode; + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java index 79f78ed10e..d7885a1cb2 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java @@ -72,7 +72,6 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { customerUser = userService.saveUser(customerUser); userSettings = createUserSettings(customerUser.getId()); - userSettings = userService.saveUserSettings(TenantId.SYS_TENANT_ID, customerUser.getId(), userSettings); } @After @@ -112,14 +111,6 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { Assert.assertNotNull(userCredentials); } - @Test - public void testFindUserSettings() { - User user = userService.findUserByEmail(SYSTEM_TENANT_ID,"customer@thingsboard.org"); - Assert.assertNotNull(user); - UserSettings userSettings = userService.findUserSettings(SYSTEM_TENANT_ID, user.getId()); - Assert.assertEquals(userSettings.getSettings(), userSettings.getSettings()); - } - @Test public void testSaveUser() { User tenantAdminUser = userService.findUserByEmail(SYSTEM_TENANT_ID,"tenant@thingsboard.org");