Browse Source

added caching, added PUT methods, refactored code

pull/8051/head
dashevchenko 4 years ago
parent
commit
6c5054a6e8
  1. 2
      application/src/main/data/upgrade/3.4.4/schema_update.sql
  2. 4
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  3. 67
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  4. 3
      application/src/main/resources/thingsboard.yml
  5. 17
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  6. 64
      application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java
  7. 7
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java
  8. 43
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserSettingsService.java
  9. 1
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  10. 21
      common/data/src/main/java/org/thingsboard/server/common/data/security/UserSettings.java
  11. 4
      dao/pom.xml
  12. 43
      dao/src/main/java/org/thingsboard/server/dao/entity/AbstractCachedService.java
  13. 21
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java
  14. 36
      dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsCaffeineCache.java
  15. 24
      dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsEvictEvent.java
  16. 38
      dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsRedisCache.java
  17. 131
      dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java
  18. 9
      dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java

2
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
);

4
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;

67
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(",")));
}
}

3
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

17
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> T doPut(String urlTemplate, T content, Class<T> responseClass, String... params) {
try {
return readResponse(doPut(urlTemplate, content, params).andExpect(status().isOk()), responseClass);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected <T> 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> T doDelete(String urlTemplate, Class<T> responseClass, String... params) throws Exception {
return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass);
}

64
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;
}
}

7
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);
}

43
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<String> jsonPaths);
}

1
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";
}

21
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);
}
}

4
dao/pom.xml

@ -233,6 +233,10 @@
<groupId>org.eclipse.leshan</groupId>
<artifactId>leshan-core</artifactId>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
</dependency>
</dependencies>
<build>
<plugins>

43
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<K extends Serializable, V extends Serializable, E> {
@Autowired
protected TbTransactionalCache<K, V> 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);
}

21
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)) {

36
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<UserId, UserSettings> {
public UserSettingsCaffeineCache(CacheManager cacheManager) {
super(cacheManager, CacheConstants.USER_SETTINGS_CACHE);
}
}

24
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;
}

38
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<UserId, UserSettings> {
public UserSettingsRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) {
super(CacheConstants.USER_SETTINGS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbFSTRedisSerializer<>());
}
}

131
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<UserId, UserSettings, UserSettingsEvictEvent> 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<String> 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<UserId> keys = new ArrayList<>();
keys.add(event.getUserId());
cache.evict(keys);
}
public JsonNode merge(JsonNode mainNode, JsonNode updateNode) {
Iterator<String> 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;
}
}

9
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");

Loading…
Cancel
Save