/* * Copyright 2019 Arcus Project * * 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 com.iris.platform.rule.service; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.Executor; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.codahale.metrics.Timer; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; import com.iris.common.rule.RuleContext; import com.iris.common.rule.simple.SimpleContext; import com.iris.core.dao.PlaceDAO; import com.iris.core.platform.AbstractPlatformMessageListener; import com.iris.core.platform.PlatformMessageBus; import com.iris.messages.ErrorEvent; import com.iris.messages.MessageBody; import com.iris.messages.MessageConstants; import com.iris.messages.PlatformMessage; import com.iris.messages.address.Address; import com.iris.messages.address.AddressMatchers; import com.iris.messages.address.PlatformServiceAddress; import com.iris.messages.capability.Capability; import com.iris.messages.capability.RuleCapability; import com.iris.messages.capability.RuleTemplateCapability; import com.iris.messages.capability.RuleTemplateCapability.CreateRuleRequest; import com.iris.messages.errors.ErrorEventException; import com.iris.messages.errors.Errors; import com.iris.messages.errors.NotFoundException; import com.iris.messages.errors.UnauthorizedRequestException; import com.iris.messages.model.Model; import com.iris.messages.services.PlatformConstants; import com.iris.metrics.IrisMetrics; import com.iris.platform.model.ModelDao; import com.iris.platform.partition.PartitionChangedEvent; import com.iris.platform.partition.PartitionListener; import com.iris.platform.partition.Partitioner; import com.iris.platform.partition.PlatformPartition; import com.iris.platform.rule.RuleDao; import com.iris.platform.rule.RuleDefinition; import com.iris.platform.rule.RuleEnvironmentDao; import com.iris.platform.rule.catalog.RuleCatalog; import com.iris.platform.rule.catalog.RuleTemplate; import com.iris.platform.rule.catalog.selector.ListSelector; import com.iris.platform.rule.catalog.selector.Selector; import com.iris.platform.rule.environment.PlaceEnvironmentExecutor; import com.iris.platform.rule.environment.PlaceExecutorRegistry; import com.iris.platform.rule.environment.RuleModelStore; import com.iris.platform.rule.service.handler.rule.ListHistoryEntriesHandler; import com.iris.population.PlacePopulationCacheManager; import com.iris.validators.ValidationException; /** * */ @Singleton public class RuleService extends AbstractPlatformMessageListener implements PartitionListener { public static final String PROP_THREADPOOL = "service.rule.threadpool"; private static final Logger logger = LoggerFactory.getLogger(RuleService.class); private final Timer partitionLoadTimer; private final RuleEnvironmentDao ruleDao; private final PlatformMessageBus platformBus; private final Partitioner partitioner; private final RuleDao ruleDefDao; private final ModelDao modelDao; private final PlaceDAO placeDao; private final RuleCatalogLoader catalogs; private final PlaceExecutorRegistry registry; private final ListHistoryEntriesHandler listHistoryEntries; private final PlacePopulationCacheManager populationCacheMgr; @Inject public RuleService( @Named(PROP_THREADPOOL) Executor executor, RuleEnvironmentDao ruleDao, PlatformMessageBus platformBus, Partitioner partitioner, RuleDao ruleDefDao, ModelDao modelDao, PlaceDAO placeDao, RuleCatalogLoader catalogs, PlaceExecutorRegistry environments, ListHistoryEntriesHandler listHistoryEntries, PlacePopulationCacheManager populationCacheMgr ) { super(platformBus, executor); this.partitionLoadTimer = IrisMetrics.metrics("service.rules").timer("partitionloadtime"); this.ruleDao = ruleDao; this.platformBus = platformBus; this.partitioner = partitioner; this.ruleDefDao = ruleDefDao; this.modelDao = modelDao; this.placeDao = placeDao; this.catalogs = catalogs; this.registry = environments; this.listHistoryEntries = listHistoryEntries; this.populationCacheMgr = populationCacheMgr; } @Override protected void onStart() { partitioner.addPartitionListener(this); // then start listening listen(); } protected void listen() { addListeners( AddressMatchers.equals(Address.broadcastAddress()), AddressMatchers.platformService(MessageConstants.SERVICE, PlatformConstants.SERVICE_RULE), AddressMatchers.platformService(MessageConstants.SERVICE, PlatformConstants.SERVICE_RULE_TMPL) ); } @Override public void onPartitionsChanged(PartitionChangedEvent event) { logger.info("Loading rules for [{}] partitions...", event.getPartitions().size()); registry.clear(); for(PlatformPartition partition: event.getPartitions()) { executor().execute(() -> loadRulesByPartition(partition)); } } @Override protected void handleEvent(PlatformMessage message) { if(Capability.EVENT_DELETED.equals(message.getMessageType())) { Address source = message.getSource(); if(source instanceof PlatformServiceAddress && PlatformConstants.SERVICE_PLACES.equals(source.getGroup())) { onPlaceDeleted((UUID) source.getId()); return; } } UUID placeId = getPlaceId(message); if(placeId == null) { return; } PlaceEnvironmentExecutor executor = registry.getExecutor(placeId).orNull(); if(executor == null) { return; } executor.onMessageReceived(message); } private void onPlaceDeleted(UUID placeId) { // remove the executor and stop its execution registry.stop(placeId); // remove all rules, scenes and actions for the place ruleDao.deleteByPlace(placeId); } @Override protected void handleRequestAndSendResponse(PlatformMessage message) { // many rule service responses are handled in the rule service handler, which // will send a response itself, so allow null response here getMessageBus().invokeAndSendIfNotNull(message, () -> Optional.ofNullable(handleRequest(message))); } @Override protected MessageBody handleRequest(PlatformMessage message) throws Exception { UUID placeId = message.getPlaceId() == null ? null : UUID.fromString(message.getPlaceId()); Address destination = message.getDestination(); // dispatch to the specific rule -- move more in here if( RuleCapability.EnableRequest.NAME.equals(message.getMessageType()) || RuleCapability.DisableRequest.NAME.equals(message.getMessageType()) ) { PlaceEnvironmentExecutor executor = registry.getExecutor(placeId).orNull(); if(executor == null) { return Errors.notFound(message.getDestination()); } executor.handleRequest(message); return null; } // rule service requests if(com.iris.messages.service.RuleService.ListRuleTemplatesRequest.NAME.equals(message.getMessageType())) { return handleListRuleTemplates(message.getValue()); } if(com.iris.messages.service.RuleService.ListRulesRequest.NAME.equals(message.getMessageType())) { return handleListRules(message.getValue()); } if(com.iris.messages.service.RuleService.GetCategoriesRequest.NAME.equals(message.getMessageType())) { return handleGetCategories(message.getValue()); } if(com.iris.messages.service.RuleService.GetRuleTemplatesByCategoryRequest.NAME.equals(message.getMessageType())) { return handleGetRuleTemplatesByCategory(message.getValue()); } // rule template requests if(RuleTemplateCapability.ResolveRequest.NAME.equals(message.getMessageType())) { MessageBody body = message.getValue(); assertPlaceMatches(placeId, destination, body); return handleResolve(message.getDestination().getId(), body); } if(RuleTemplateCapability.CreateRuleRequest.NAME.equals(message.getMessageType())) { MessageBody body = message.getValue(); assertPlaceMatches(placeId, destination, body); return handleCreateRule(message.getDestination(), message.getDestination().getId(), body); } if(com.iris.messages.service.RuleService.ListRulesRequest.NAME.equals(message.getMessageType())) { MessageBody body = message.getValue(); assertPlaceMatches(placeId, destination, body); return handleListRules(message.getValue()); } // rule requests if(RuleCapability.DeleteRequest.NAME.equals(message.getMessageType())) { return handleDeleteRule(placeId, message.getDestination(), message.getValue()); } if(RuleCapability.UpdateContextRequest.NAME.equals(message.getMessageType())) { return handleUpdateRuleContext(placeId, message.getDestination(), message.getValue()); } if(RuleCapability.ListHistoryEntriesRequest.NAME.equals(message.getMessageType())) { return handleListHistoryEntries(message.getDestination(), message); } // base requests -- could be rule or template if(Capability.CMD_GET_ATTRIBUTES.equals(message.getMessageType())) { return handleGetAttributes(message.getDestination(), message.getValue(), message.getPlaceId()); } if(Capability.CMD_SET_ATTRIBUTES.equals(message.getMessageType())) { return handleSetAttributes(placeId, message.getDestination(), message.getValue()); } return super.handleRequest(message); } protected void loadRulesByPartition(PlatformPartition partition) { try(Timer.Context context = partitionLoadTimer.time()) { placeDao .streamByPartitionId(partition.getId()) .forEach((place) -> registry.start(place.getId())); } } // TODO optimize this private UUID getPlaceId(PlatformMessage message) { String placeId = message.getPlaceId(); return placeId == null ? null : UUID.fromString(placeId); } private UUID getPlaceId(MessageBody body) { if(body.getAttributes().get(com.iris.messages.service.RuleService.ListRuleTemplatesRequest.ATTR_PLACEID) != null) { return UUID.fromString((String) body.getAttributes().get(com.iris.messages.service.RuleService.ListRuleTemplatesRequest.ATTR_PLACEID)); } return null; } // FIXME this results in a lot of duplicated checks -- should consolidate this logic in future revisions private void assertPlaceMatches(UUID placeId, Address destination, MessageBody body) { if(placeId == null) { return; } if(!Objects.equals(placeId, getPlaceId(body))) { throw new UnauthorizedRequestException(destination, "Unauthorized access to " + destination); } } private MessageBody handleListRuleTemplates(MessageBody body) { UUID placeId = getPlaceId(body); // TODO: replace with better error events Preconditions.checkNotNull(placeId, "The place ID is required"); RuleCatalog catalog = getRuleCatalogForPlace(placeId); List<RuleTemplate> templates = catalog.getTemplates(); RuleContext context = createSimpleRuleContext(placeId); return com.iris.messages.service.RuleService.ListRuleTemplatesResponse.builder() .withRuleTemplates(templates.stream().map((rt) -> templateToMap(rt, context)).collect(Collectors.toList())) .build(); } private Map<String,Object> templateToMap(RuleTemplate template, RuleContext ruleContext) { Map<String,Object> asMap = new HashMap<>(); asMap.put(Capability.ATTR_ADDRESS, MessageConstants.SERVICE + ":" + RuleTemplateCapability.NAMESPACE + ":" + template.getId()); asMap.put(Capability.ATTR_CAPS, ImmutableSet.of(Capability.NAMESPACE, RuleTemplateCapability.NAMESPACE)); asMap.put(Capability.ATTR_ID, template.getId()); asMap.put(Capability.ATTR_TAGS, template.getTags()); asMap.put(Capability.ATTR_TYPE, RuleTemplateCapability.NAMESPACE); asMap.put(RuleTemplateCapability.ATTR_ADDED, template.getCreated()); asMap.put(RuleTemplateCapability.ATTR_KEYWORDS, template.getKeywords()); asMap.put(RuleTemplateCapability.ATTR_LASTMODIFIED, template.getModified()); asMap.put(RuleTemplateCapability.ATTR_TEMPLATE, template.getTemplate()); asMap.put(RuleTemplateCapability.ATTR_SATISFIABLE, ruleContext != null && template.isSatisfiable(ruleContext)); asMap.put(RuleTemplateCapability.ATTR_NAME, template.getName()); asMap.put(RuleTemplateCapability.ATTR_DESCRIPTION, template.getDescription()); asMap.put(RuleTemplateCapability.ATTR_CATEGORIES, template.getCategories()); asMap.put(RuleTemplateCapability.ATTR_PREMIUM, template.isPremium()); asMap.put(RuleTemplateCapability.ATTR_EXTRA, template.getExtra()); return asMap; } private MessageBody handleListRules(MessageBody body) { UUID placeId = getPlaceId(body); // TODO: replace with better error events Preconditions.checkNotNull(placeId, "The place ID is required"); List<RuleDefinition> definitions = ruleDefDao.listByPlace(placeId); return com.iris.messages.service.RuleService.ListRulesResponse.builder() .withRules(definitions.stream().map((r) -> ruleToMap(r)).collect(Collectors.toList())) .build(); } private Map<String,Object> ruleToMap(RuleDefinition rd) { Map<String,Object> asMap = new HashMap<>(); asMap.put(Capability.ATTR_ID, rd.getId().getRepresentation()); asMap.put(Capability.ATTR_ADDRESS, Address.platformService(rd.getId().getRepresentation(), RuleCapability.NAMESPACE).getRepresentation()); asMap.put(Capability.ATTR_TYPE, RuleCapability.NAMESPACE); asMap.put(Capability.ATTR_CAPS, ImmutableSet.of(Capability.NAMESPACE, RuleCapability.NAMESPACE)); asMap.put(RuleCapability.ATTR_CREATED, rd.getCreated()); asMap.put(RuleCapability.ATTR_MODIFIED, rd.getModified()); asMap.put(RuleCapability.ATTR_NAME, rd.getName()); asMap.put(RuleCapability.ATTR_STATE, rd.isDisabled() ? RuleCapability.STATE_DISABLED : RuleCapability.STATE_ENABLED); asMap.put(RuleCapability.ATTR_DESCRIPTION, rd.getDescription()); asMap.put(RuleCapability.ATTR_TEMPLATE, rd.getRuleTemplate()); asMap.put(RuleCapability.ATTR_CONTEXT, rd.getVariables()); return asMap; } private MessageBody handleGetCategories(MessageBody body) { UUID placeId = getPlaceId(body); // TODO: replace with better error events Preconditions.checkNotNull(placeId, "The place ID is required"); RuleCatalog catalog = getRuleCatalogForPlace(placeId); return com.iris.messages.service.RuleService.GetCategoriesResponse.builder() .withCategories(catalog.getRuleCountByCategory()) .build(); } private MessageBody handleGetRuleTemplatesByCategory(MessageBody body) { UUID placeId = getPlaceId(body); String category = com.iris.messages.service.RuleService.GetRuleTemplatesByCategoryRequest.getCategory(body); // TODO: replace with better error events Preconditions.checkNotNull(placeId, "The place ID is required"); Preconditions.checkNotNull(category, "The category is required"); RuleCatalog catalog = getRuleCatalogForPlace(placeId); RuleContext context = createSimpleRuleContext(placeId); List<RuleTemplate> templates = catalog.getTemplatesForCategory(category); return com.iris.messages.service.RuleService.GetRuleTemplatesByCategoryResponse.builder() .withRuleTemplates(templates.stream().map((t) -> templateToMap(t, context)).collect(Collectors.toList())) .build(); } private MessageBody handleResolve(Object templateId, MessageBody body) { // TODO: replace with better error events Preconditions.checkNotNull(templateId, "The template ID is required from the destination address"); UUID placeId = getPlaceId(body); // TODO: replace with better error events Preconditions.checkNotNull(placeId, "The place ID is required"); RuleCatalog catalog = getRuleCatalogForPlace(placeId); RuleTemplate template = catalog.getById((String) templateId); // TODO: replace with better error events Preconditions.checkNotNull(template, "No template could be found with " + templateId); RuleContext context = createSimpleRuleContext(placeId); Map<String,Selector> resolution = template.resolve(context); Map<String,Map<String,Object>> transformed = new HashMap<>(); resolution.entrySet().forEach((e) -> { transformed.put(e.getKey(), selectorToMap(e.getValue())); }); return RuleTemplateCapability.ResolveResponse.builder() .withSelectors(transformed) .build(); } private RuleContext createSimpleRuleContext(UUID placeId) { PlaceEnvironmentExecutor executor = registry.getExecutor(placeId).orNull(); Collection<Model> models = executor != null ? executor.getModelStore().getModels() : modelDao.loadModelsByPlace(placeId, RuleModelStore.TRACKED_TYPES); // use a simple context just for resolving SimpleContext context = new SimpleContext(placeId, Address.platformService("rule"), LoggerFactory.getLogger("rules." + placeId)); models.stream().filter((m) -> m != null).forEach((m) -> context.putModel(m)); return context; } private Map<String,Object> selectorToMap(Selector selector) { Map<String,Object> asMap = new HashMap<>(); asMap.put("type", selector.getType()); if(selector instanceof ListSelector) { ListSelector optionSelector = (ListSelector) selector; List<List> options = new ArrayList<>(); optionSelector.getOptions().forEach((o) -> { options.add(Arrays.asList(o.getLabel(), o.getValue())); }); asMap.put("options", options); } return asMap; } private MessageBody handleCreateRule(Address destination, Object templateId, MessageBody message) { Errors.assertRequiredParam(templateId, "templateId"); UUID placeId = getPlaceId(message); String name = RuleTemplateCapability.CreateRuleRequest.getName(message); String description = RuleTemplateCapability.CreateRuleRequest.getDescription(message); Errors.assertRequiredParam(placeId, "placeId"); Errors.assertRequiredParam(name, CreateRuleRequest.ATTR_NAME); RuleCatalog catalog = getRuleCatalogForPlace(placeId); RuleTemplate template = catalog.getById((String) templateId); if(templateId == null) { throw new ErrorEventException(Errors.notFound(Address.platformService(templateId, RuleTemplateCapability.NAMESPACE))); } Map<String,Object> variables = RuleTemplateCapability.CreateRuleRequest.getContext(message); PlaceEnvironmentExecutor executor = registry.getExecutor(placeId).orNull(); if(executor == null) { return Errors.notFound(destination); } try { Callable<RuleDefinition> save = () -> doCreateRule(placeId, template, name, description, variables); com.google.common.base.Optional<PlaceEnvironmentExecutor> executorRef = registry.getExecutor(placeId); RuleDefinition rule; if(executorRef.isPresent()) { rule = executorRef.get().submit(save).get(); } else { rule = save.call(); registry.reload(placeId); } return RuleTemplateCapability.CreateRuleResponse .builder() .withAddress(rule.getAddress()) .build(); } catch(Exception e) { logger.warn("Error creating a new rule for place [{}] for template [{}] using variables [{}]", placeId, templateId, variables, e); return Errors.fromException(e); } } // NOTE this must run in the executor thread private RuleDefinition doCreateRule(UUID placeId, RuleTemplate template, String name, String description, Map<String, Object> variables) throws ValidationException { RuleDefinition rd = template.create( placeId, name, variables ); if(StringUtils.isEmpty(description)) { rd.setDescription(template.getDescription()); } else { rd.setDescription(description); } ruleDefDao.save(rd); registry.reload(placeId); Map<String,Object> ruleDefAsMap = ruleToMap(rd); MessageBody added = MessageBody.buildMessage(Capability.EVENT_ADDED, ruleDefAsMap); PlatformMessage msg = PlatformMessage .buildBroadcast(added, Address.platformService(placeId + "." + rd.getSequenceId(), PlatformConstants.SERVICE_RULE)) .withPlaceId(rd.getPlaceId()) .withPopulation(populationCacheMgr.getPopulationByPlaceId(placeId)) .create(); platformBus.send(msg); return rd; } private MessageBody handleDeleteRule(UUID placeId, Address ruleAddress, MessageBody message) { PlaceEnvironmentExecutor executor = registry.getExecutor(placeId).orNull(); if(executor == null) { logger.debug("Received delete for rule at empty place [{}]", ruleAddress); return RuleCapability.DeleteRequest.instance(); } try { executor .submit(() -> doDelete(ruleAddress)) .get(); return RuleCapability.DeleteResponse.instance(); } catch(Exception e) { logger.warn("Error deleting rule [{}]", ruleAddress, e); return Errors.fromException(e); } } // NOTE this must run in the executor thread private void doDelete(Address ruleAddress) { RuleDefinition rd = loadRuleDefintion((PlatformServiceAddress) ruleAddress); if(rd != null) { ruleDefDao.delete(rd.getPlaceId(), rd.getSequenceId()); PlatformMessage msg = PlatformMessage .builder() .broadcast() .from(Address.platformService(rd.getPlaceId(), PlatformConstants.SERVICE_RULE, rd.getSequenceId())) .withPlaceId(rd.getPlaceId()) .withPopulation(populationCacheMgr.getPopulationByPlaceId(rd.getPlaceId())) .withPayload(Capability.EVENT_DELETED) .create(); platformBus.send(msg); registry.reload(rd.getPlaceId()); } } private MessageBody handleGetAttributes(Address ruleAddress, MessageBody message, String placeId) { Errors.assertRequiredParam(placeId, "placeId"); PlatformServiceAddress addr = (PlatformServiceAddress) ruleAddress; Collection<String> nameColl = (Collection<String>) message.getAttributes().get("names"); Set<String> names = nameColl == null ? Collections.<String>emptySet() : new HashSet<String>(nameColl); Map<String,Object> attrs = new HashMap<>(); if(addr.getGroup().equals(PlatformConstants.SERVICE_RULE_TMPL)) { RuleCatalog catalog = getRuleCatalogForPlace(UUID.fromString(placeId)); RuleTemplate rt = catalog.getById((String) addr.getId()); // TODO: better error event Preconditions.checkNotNull(rt, "No rule template can be found for " + addr.getId()); // TODO: without the place, we cannot determine satisfiability either attrs = templateToMap(rt, null); } else if(addr.getGroup().equals(PlatformConstants.SERVICE_RULE)) { RuleDefinition ruleDef = loadRuleDefintion(addr); // TODO: better error event Preconditions.checkNotNull(ruleDef, "No rule could be found for " + addr.getRepresentation()); attrs = ruleToMap(ruleDef); } return MessageBody.buildMessage(Capability.EVENT_GET_ATTRIBUTES_RESPONSE, filter(attrs, names)); } private Map<String,Object> filter(Map<String,Object> attrs, Set<String> caps) { if(caps == null || caps.isEmpty()) { return attrs; } return attrs.entrySet().stream() .filter((e) -> { return caps.contains(e.getKey()) || caps.contains(e.getKey().split(":")[0]); }) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } private MessageBody handleSetAttributes(UUID placeId, Address ruleAddress, MessageBody message) { PlatformServiceAddress addr = (PlatformServiceAddress) ruleAddress; if(addr.getGroup().equals(PlatformConstants.SERVICE_RULE_TMPL)) { return ErrorEvent.fromCode("error.attribute.not_writable", "Rule templates do not have any writable attributes"); } PlaceEnvironmentExecutor executor = registry.getExecutor(placeId).orNull(); if(executor == null) { return Errors.notFound(ruleAddress); } Set<String> readOnlyKeys = message.getAttributes().keySet().stream().filter((k) -> { return !RuleCapability.ATTR_NAME.equals(k) && !RuleCapability.ATTR_DESCRIPTION.equals(k); }) .collect(Collectors.toSet()); if(!readOnlyKeys.isEmpty()) { return ErrorEvent.fromCode("error.attribute.not_writable", readOnlyKeys.toString() + " are not writable attributes"); } RuleDefinition rd = loadRuleDefintion(addr); Map<String,Object> attributesSet = new HashMap<>(); String name = RuleCapability.getName(message); if(name != null) { attributesSet.put(RuleCapability.ATTR_NAME, name); rd.setName(name); } String desc = RuleCapability.getDescription(message); if(desc != null) { attributesSet.put(RuleCapability.ATTR_DESCRIPTION, desc); rd.setDescription(desc); } if(!attributesSet.isEmpty()) { try { executor .submit(() -> doUpdate(rd, attributesSet)) .get(); } catch(Exception e) { logger.warn("Error updating rule [{}]", ruleAddress, e); return Errors.fromException(e); } } return MessageBody.emptyMessage(); } private MessageBody handleUpdateRuleContext(UUID placeId, Address ruleAddress, MessageBody message) { PlaceEnvironmentExecutor executor = registry.getExecutor(placeId).orNull(); if(executor == null) { return Errors.notFound(ruleAddress); } RuleDefinition rd = loadRuleDefintion((PlatformServiceAddress) ruleAddress); if(rd == null) { return Errors.notFound(ruleAddress); } Map<String,Object> variables = new HashMap<>(rd.getVariables()); Map<String,Object> newVariables = RuleCapability.UpdateContextRequest.getContext(message); newVariables.entrySet().forEach((e) -> variables.put(e.getKey(), e.getValue())); rd.setVariables(variables); ImmutableMap.Builder<String, Object> changesBuilder = ImmutableMap.<String, Object>builder().put(RuleCapability.ATTR_CONTEXT, variables); String templateName = RuleCapability.UpdateContextRequest.getTemplate(message); if(!StringUtils.isEmpty(templateName)) { logger.debug("Changing rule [{}] from template [{}] to template [{}]", rd.getAddress(), rd.getRuleTemplate(), templateName); rd.setRuleTemplate(templateName); changesBuilder.put(RuleCapability.ATTR_TEMPLATE, templateName); } // Need to regenerate the rule definition or it won't actually change the rule, just the definition. RuleCatalog catalog = getRuleCatalogForPlace(rd.getPlaceId()); RuleTemplate template = catalog.getById(rd.getRuleTemplate()); // TODO: replace with better error events Preconditions.checkNotNull(template, "No template could be found with " + rd.getRuleTemplate()); try { final RuleDefinition toUpdate = template.regenerate(rd); executor .submit(() -> doUpdate(toUpdate, changesBuilder.build())) .get(); } catch (ValidationException ex) { logger.error("Error editing rule for place [{}] for template [{}] using variables [{}]", rd.getPlaceId(), rd.getRuleTemplate(), variables, ex); return Errors.fromException(ex); } catch (Exception ex) { logger.error("Error updating rule for [{}]", rd.getAddress(), ex); return Errors.fromException(ex); } return RuleCapability.UpdateContextResponse.instance(); } // NOTE this must run in the executor thread private void doUpdate(RuleDefinition rd, Map<String, Object> changes) { ruleDefDao.save(rd); registry.reload(rd.getPlaceId()); PlatformMessage msg = PlatformMessage .broadcast() .from(Address.platformService(rd.getPlaceId(), PlatformConstants.SERVICE_RULE, rd.getSequenceId())) .withPlaceId(rd.getPlaceId()) .withPopulation(populationCacheMgr.getPopulationByPlaceId(rd.getPlaceId())) .withPayload(Capability.EVENT_VALUE_CHANGE, changes) .create(); platformBus.send(msg); } private MessageBody handleListHistoryEntries(Address ruleAddress, PlatformMessage message) { RuleDefinition rd = loadRuleDefintion((PlatformServiceAddress) ruleAddress); if(rd == null) { throw new NotFoundException(ruleAddress); } Errors.assertPlaceMatches(message, rd.getPlaceId()); return listHistoryEntries.handleRequest(rd, message); } private RuleDefinition loadRuleDefintion(PlatformServiceAddress ruleAddress) { PlatformServiceAddress platformAddr = ruleAddress; UUID placeId = (UUID) platformAddr.getId(); Integer ruleSequence = platformAddr.getContextQualifier(); return ruleDefDao.findById(placeId, ruleSequence); } private RuleCatalog getRuleCatalogForPlace(UUID placeId) { return catalogs.getCatalogForPlace(placeId); } }