/** * 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.rule; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; 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.id.RuleId; 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.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentLifecycleState; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.PluginMetaData; import org.thingsboard.server.common.data.rule.RuleMetaData; import org.thingsboard.server.dao.component.ComponentDescriptorService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DatabaseException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.RuleMetaDataEntity; import org.thingsboard.server.dao.plugin.PluginService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Function; import static org.thingsboard.server.dao.DaoUtil.convertDataList; import static org.thingsboard.server.dao.DaoUtil.getData; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @Service @Slf4j public class BaseRuleService implements RuleService { private final TenantId systemTenantId = new TenantId(NULL_UUID); @Autowired public RuleDao ruleDao; @Autowired public PluginService pluginService; @Autowired private ComponentDescriptorService componentDescriptorService; @Override public RuleMetaData saveRule(RuleMetaData rule) { ruleValidator.validate(rule); if (rule.getTenantId() == null) { log.trace("Save system rule metadata with predefined id {}", systemTenantId); rule.setTenantId(systemTenantId); } if (rule.getId() != null) { RuleMetaData oldVersion = getData(ruleDao.findById(rule.getId())); if (rule.getState() == null) { rule.setState(oldVersion.getState()); } else if (rule.getState() != oldVersion.getState()) { throw new IncorrectParameterException("Use Activate/Suspend method to control state of the rule!"); } } else { if (rule.getState() == null) { rule.setState(ComponentLifecycleState.SUSPENDED); } else if (rule.getState() != ComponentLifecycleState.SUSPENDED) { throw new IncorrectParameterException("Use Activate/Suspend method to control state of the rule!"); } } validateFilters(rule.getFilters()); if (rule.getProcessor() != null && !rule.getProcessor().isNull()) { validateComponentJson(rule.getProcessor(), ComponentType.PROCESSOR); } validateComponentJson(rule.getAction(), ComponentType.ACTION); validateRuleAndPluginState(rule); return getData(ruleDao.save(rule)); } private void validateFilters(JsonNode filtersJson) { if (filtersJson == null || filtersJson.isNull()) { throw new IncorrectParameterException("Rule filters are required!"); } if (!filtersJson.isArray()) { throw new IncorrectParameterException("Filters json is not an array!"); } ArrayNode filtersArray = (ArrayNode) filtersJson; for (int i = 0; i < filtersArray.size(); i++) { validateComponentJson(filtersArray.get(i), ComponentType.FILTER); } } private void validateComponentJson(JsonNode json, ComponentType type) { if (json == null || json.isNull()) { throw new IncorrectParameterException(type.name() + " is required!"); } String clazz = getIfValid(type.name(), json, "clazz", JsonNode::isTextual, JsonNode::asText); String name = getIfValid(type.name(), json, "name", JsonNode::isTextual, JsonNode::asText); JsonNode configuration = getIfValid(type.name(), json, "configuration", JsonNode::isObject, node -> node); ComponentDescriptor descriptor = componentDescriptorService.findByClazz(clazz); if (descriptor == null) { throw new IncorrectParameterException(type.name() + " clazz " + clazz + " is not a valid component!"); } if (descriptor.getType() != type) { throw new IncorrectParameterException("Clazz " + clazz + " is not a valid " + type.name() + " component!"); } if (!componentDescriptorService.validate(descriptor, configuration)) { throw new IncorrectParameterException(type.name() + " configuration is not valid!"); } } private void validateRuleAndPluginState(RuleMetaData rule) { PluginMetaData pluginMd = pluginService.findPluginByApiToken(rule.getPluginToken()); if (pluginMd == null) { throw new IncorrectParameterException("Rule points to non-existent plugin!"); } if (!pluginMd.getTenantId().equals(systemTenantId) && !pluginMd.getTenantId().equals(rule.getTenantId())) { throw new IncorrectParameterException("Rule access plugin that belongs to different tenant!"); } if (rule.getState() == ComponentLifecycleState.ACTIVE && pluginMd.getState() != ComponentLifecycleState.ACTIVE) { throw new IncorrectParameterException("Can't save active rule that points to inactive plugin!"); } ComponentDescriptor pluginDescriptor = componentDescriptorService.findByClazz(pluginMd.getClazz()); String actionClazz = getIfValid(ComponentType.ACTION.name(), rule.getAction(), "clazz", JsonNode::isTextual, JsonNode::asText); if (!Arrays.asList(pluginDescriptor.getActions().split(",")).contains(actionClazz)) { throw new IncorrectParameterException("Rule's action is not supported by plugin with token " + rule.getPluginToken() + "!"); } } private static <T> T getIfValid(String parentName, JsonNode node, String name, Function<JsonNode, Boolean> validator, Function<JsonNode, T> extractor) { if (!node.has(name)) { throw new IncorrectParameterException(parentName + "'s " + name + " is not set!"); } else { JsonNode value = node.get(name); if (validator.apply(value)) { return extractor.apply(value); } else { throw new IncorrectParameterException(parentName + "'s " + name + " is not valid!"); } } } @Override public RuleMetaData findRuleById(RuleId ruleId) { validateId(ruleId, "Incorrect rule id for search rule request."); return getData(ruleDao.findById(ruleId.getId())); } @Override public List<RuleMetaData> findPluginRules(String pluginToken) { List<RuleMetaDataEntity> ruleEntities = ruleDao.findRulesByPlugin(pluginToken); return convertDataList(ruleEntities); } @Override public TextPageData<RuleMetaData> findSystemRules(TextPageLink pageLink) { validatePageLink(pageLink, "Incorrect PageLink object for search rule request."); List<RuleMetaDataEntity> ruleEntities = ruleDao.findByTenantIdAndPageLink(systemTenantId, pageLink); List<RuleMetaData> plugins = convertDataList(ruleEntities); return new TextPageData<>(plugins, pageLink); } @Override public TextPageData<RuleMetaData> findTenantRules(TenantId tenantId, TextPageLink pageLink) { validateId(tenantId, "Incorrect tenant id for search rule request."); validatePageLink(pageLink, "Incorrect PageLink object for search rule request."); List<RuleMetaDataEntity> ruleEntities = ruleDao.findByTenantIdAndPageLink(tenantId, pageLink); List<RuleMetaData> plugins = convertDataList(ruleEntities); return new TextPageData<>(plugins, pageLink); } @Override public List<RuleMetaData> findSystemRules() { log.trace("Executing findSystemRules"); List<RuleMetaData> rules = new ArrayList<>(); TextPageLink pageLink = new TextPageLink(300); TextPageData<RuleMetaData> pageData = null; do { pageData = findSystemRules(pageLink); rules.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageData.getNextPageLink(); } } while (pageData.hasNext()); return rules; } @Override public TextPageData<RuleMetaData> findAllTenantRulesByTenantIdAndPageLink(TenantId tenantId, TextPageLink pageLink) { log.trace("Executing findAllTenantRulesByTenantIdAndPageLink, tenantId [{}], pageLink [{}]", tenantId, pageLink); Validator.validateId(tenantId, "Incorrect tenantId " + tenantId); Validator.validatePageLink(pageLink, "Incorrect page link " + pageLink); List<RuleMetaDataEntity> rulesEntities = ruleDao.findAllTenantRulesByTenantId(tenantId.getId(), pageLink); List<RuleMetaData> rules = convertDataList(rulesEntities); return new TextPageData<>(rules, pageLink); } @Override public List<RuleMetaData> findAllTenantRulesByTenantId(TenantId tenantId) { log.trace("Executing findAllTenantRulesByTenantId, tenantId [{}]", tenantId); Validator.validateId(tenantId, "Incorrect tenantId " + tenantId); List<RuleMetaData> rules = new ArrayList<>(); TextPageLink pageLink = new TextPageLink(300); TextPageData<RuleMetaData> pageData = null; do { pageData = findAllTenantRulesByTenantIdAndPageLink(tenantId, pageLink); rules.addAll(pageData.getData()); if (pageData.hasNext()) { pageLink = pageData.getNextPageLink(); } } while (pageData.hasNext()); return rules; } @Override public void deleteRuleById(RuleId ruleId) { validateId(ruleId, "Incorrect rule id for delete rule request."); ruleDao.deleteById(ruleId); } @Override public void activateRuleById(RuleId ruleId) { updateLifeCycleState(ruleId, ComponentLifecycleState.ACTIVE); } @Override public void suspendRuleById(RuleId ruleId) { updateLifeCycleState(ruleId, ComponentLifecycleState.SUSPENDED); } private void updateLifeCycleState(RuleId ruleId, ComponentLifecycleState state) { Validator.validateId(ruleId, "Incorrect rule id for state change request."); RuleMetaDataEntity rule = ruleDao.findById(ruleId); if (rule != null) { rule.setState(state); validateRuleAndPluginState(getData(rule)); ruleDao.save(rule); } else { throw new DatabaseException("Plugin not found!"); } } @Override public void deleteRulesByTenantId(TenantId tenantId) { validateId(tenantId, "Incorrect tenant id for delete rules request."); tenantRulesRemover.removeEntitites(tenantId); } private DataValidator<RuleMetaData> ruleValidator = new DataValidator<RuleMetaData>() { @Override protected void validateDataImpl(RuleMetaData rule) { if (StringUtils.isEmpty(rule.getName())) { throw new DataValidationException("Rule name should be specified!."); } } }; private PaginatedRemover<TenantId, RuleMetaDataEntity> tenantRulesRemover = new PaginatedRemover<TenantId, RuleMetaDataEntity>() { @Override protected List<RuleMetaDataEntity> findEntities(TenantId id, TextPageLink pageLink) { return ruleDao.findByTenantIdAndPageLink(id, pageLink); } @Override protected void removeEntity(RuleMetaDataEntity entity) { ruleDao.deleteById(entity.getId()); } }; }