
org.openremote.manager.rules.RulesService Maven / Gradle / Ivy
/*
* Copyright 2017, OpenRemote Inc.
*
* See the CONTRIBUTORS.txt file in the distribution for a
* full listing of individual contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
package org.openremote.manager.rules;
import io.micrometer.core.instrument.Tags;
import org.apache.camel.builder.RouteBuilder;
import org.openremote.container.message.MessageBrokerService;
import org.openremote.container.persistence.PersistenceService;
import org.openremote.container.timer.TimerService;
import org.openremote.manager.alarm.AlarmService;
import org.openremote.manager.asset.AssetProcessingException;
import org.openremote.manager.asset.AssetProcessingService;
import org.openremote.manager.asset.AssetStorageService;
import org.openremote.manager.datapoint.AssetDatapointService;
import org.openremote.manager.datapoint.AssetPredictedDatapointService;
import org.openremote.manager.event.AttributeEventInterceptor;
import org.openremote.manager.event.ClientEventService;
import org.openremote.manager.gateway.GatewayService;
import org.openremote.manager.notification.NotificationService;
import org.openremote.manager.rules.flow.FlowResourceImpl;
import org.openremote.manager.rules.geofence.GeofenceAssetAdapter;
import org.openremote.manager.security.ManagerIdentityService;
import org.openremote.manager.web.ManagerWebService;
import org.openremote.manager.webhook.WebhookService;
import org.openremote.model.Constants;
import org.openremote.model.Container;
import org.openremote.model.ContainerService;
import org.openremote.model.PersistenceEvent;
import org.openremote.model.asset.Asset;
import org.openremote.model.attribute.Attribute;
import org.openremote.model.attribute.AttributeEvent;
import org.openremote.model.attribute.AttributeInfo;
import org.openremote.model.query.AssetQuery;
import org.openremote.model.query.RulesetQuery;
import org.openremote.model.query.filter.LocationAttributePredicate;
import org.openremote.model.rules.*;
import org.openremote.model.rules.geofence.GeofenceDefinition;
import org.openremote.model.security.ClientRole;
import org.openremote.model.security.Realm;
import org.openremote.model.util.Pair;
import org.openremote.model.util.TextUtil;
import org.openremote.model.util.TimeUtil;
import org.openremote.model.value.MetaHolder;
import org.openremote.model.value.MetaItemType;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.logging.Level.FINEST;
import static java.util.logging.Level.SEVERE;
import static org.openremote.container.persistence.PersistenceService.PERSISTENCE_TOPIC;
import static org.openremote.container.persistence.PersistenceService.isPersistenceEventForEntityType;
import static org.openremote.container.util.MapAccess.getInteger;
import static org.openremote.container.util.MapAccess.getString;
import static org.openremote.manager.gateway.GatewayService.isNotForGateway;
/**
* Manages {@link RulesEngine}s for stored {@link Ruleset}s and processes asset attribute updates.
*
* If an updated attribute doesn't have meta {@link MetaItemType#RULE_STATE} is false and the attribute has an {@link
* org.openremote.model.asset.agent.AgentLink} meta, this implementation of {@link AttributeEventInterceptor} converts the
* update message to an {@link AttributeInfo} fact. This service keeps the facts and thus the state of rule facts are in
* sync with the asset state changes that occur. If an asset attribute value changes, the {@link AttributeInfo} in the
* rules engines will be updated to reflect the change.
*
* Each asset attribute update is processed in the following order:
*
* - Global Rulesets
* - Realm Rulesets
* - Asset Rulesets (in hierarchical order from oldest ancestor down)
*
* Processing order of rulesets with the same scope or same parent is not guaranteed.
*/
public class RulesService extends RouteBuilder implements ContainerService {
public static final int PRIORITY = LOW_PRIORITY;
public static final String OR_RULE_EVENT_EXPIRES = "OR_RULE_EVENT_EXPIRES";
public static final String OR_RULE_EVENT_EXPIRES_DEFAULT = "PT1H";
/**
* This value defines the periodic firing of the rules engines, and therefore
* has an impact on system load. If a temporary fact has a shorter expiration
* time, it's not guaranteed to be removed within that time. Any time-based
* operation, such as matching temporary facts in a sliding time window, must
* be designed with this margin in mind.
*/
public static final String OR_RULES_MIN_TEMP_FACT_EXPIRATION_MILLIS = "OR_RULES_MIN_TEMP_FACT_EXPIRATION_MILLIS";
public static final int OR_RULES_MIN_TEMP_FACT_EXPIRATION_MILLIS_DEFAULT = 50000; // Just under a minute to catch 1 min timer rules
public static final String OR_RULES_QUICK_FIRE_MILLIS = "OR_RULES_QUICK_FIRE_MILLIS";
public static final int OR_RULES_QUICK_FIRE_MILLIS_DEFAULT = 3000;
private static final Logger LOG = Logger.getLogger(RulesService.class.getName());
protected final AtomicReference> globalEngine = new AtomicReference<>();
protected final Map> realmEngines = new ConcurrentHashMap<>();
protected final Map> assetEngines = new ConcurrentHashMap<>();
protected static final Object ENGINE_LOCK = new Object();
protected List geofenceAssetAdapters = new ArrayList<>();
protected TimerService timerService;
protected ExecutorService executorService;
protected ScheduledExecutorService scheduledExecutorService;
protected PersistenceService persistenceService;
protected RulesetStorageService rulesetStorageService;
protected ManagerIdentityService identityService;
protected AssetStorageService assetStorageService;
protected NotificationService notificationService;
protected WebhookService webhookService;
protected AlarmService alarmService;
protected AssetProcessingService assetProcessingService;
protected AssetDatapointService assetDatapointService;
protected AssetPredictedDatapointService assetPredictedDatapointService;
protected ClientEventService clientEventService;
protected GatewayService gatewayService;
protected Realm[] realms;
protected AssetLocationPredicateProcessor locationPredicateRulesConsumer;
protected final Map, List> engineAssetLocationPredicateMap = new ConcurrentHashMap<>();
protected final Set assetsWithModifiedLocationPredicates = new HashSet<>();
// Keep global list of asset states that have been pushed to any engines
// The objects are already in memory inside the rule engines but keeping them
// here means we can quickly insert facts into newly started engines
protected final Set attributeEvents = ConcurrentHashMap.newKeySet();
protected final Set preInitAttributeEvents = new HashSet<>();
protected long defaultEventExpiresMillis = 1000*60*60;
protected long tempFactExpirationMillis;
protected long quickFireMillis;
protected boolean initDone;
protected boolean startDone;
protected io.micrometer.core.instrument.Timer rulesFiringTimer;
@Override
public int getPriority() {
return PRIORITY;
}
@Override
public void init(Container container) throws Exception {
executorService = container.getExecutor();
scheduledExecutorService = container.getScheduledExecutor();
timerService = container.getService(TimerService.class);
persistenceService = container.getService(PersistenceService.class);
rulesetStorageService = container.getService(RulesetStorageService.class);
identityService = container.getService(ManagerIdentityService.class);
notificationService = container.getService(NotificationService.class);
webhookService = container.getService(WebhookService.class);
alarmService = container.getService(AlarmService.class);
assetStorageService = container.getService(AssetStorageService.class);
assetProcessingService = container.getService(AssetProcessingService.class);
assetDatapointService = container.getService(AssetDatapointService.class);
assetPredictedDatapointService = container.getService(AssetPredictedDatapointService.class);
clientEventService = container.getService(ClientEventService.class);
gatewayService = container.getService(GatewayService.class);
tempFactExpirationMillis = getInteger(container.getConfig(), OR_RULES_MIN_TEMP_FACT_EXPIRATION_MILLIS, OR_RULES_MIN_TEMP_FACT_EXPIRATION_MILLIS_DEFAULT);
quickFireMillis = getInteger(container.getConfig(), OR_RULES_QUICK_FIRE_MILLIS, OR_RULES_QUICK_FIRE_MILLIS_DEFAULT);
if (initDone) {
return;
}
clientEventService.addSubscriptionAuthorizer((realm, auth, subscription) -> {
if (subscription.isEventType(RulesEngineStatusEvent.class) || subscription.isEventType(RulesetChangedEvent.class)) {
if (auth == null) {
return false;
}
if (auth.isSuperUser()) {
return true;
}
// Regular user must have role
if (!auth.hasResourceRole(ClientRole.READ_ASSETS.getValue(), Constants.KEYCLOAK_CLIENT_ID)) {
return false;
}
boolean isRestrictedUser = identityService.getIdentityProvider().isRestrictedUser(auth);
return !isRestrictedUser;
}
return false;
});
clientEventService.addSubscription(AttributeEvent.class, null, this::onAttributeEvent);
ServiceLoader.load(GeofenceAssetAdapter.class).forEach(geofenceAssetAdapter -> {
LOG.fine("Adding GeofenceAssetAdapter: " + geofenceAssetAdapter.getClass().getName());
geofenceAssetAdapters.add(geofenceAssetAdapter);
});
geofenceAssetAdapters.addAll(container.getServices(GeofenceAssetAdapter.class));
geofenceAssetAdapters.sort(Comparator.comparingInt(GeofenceAssetAdapter::getPriority));
container.getService(MessageBrokerService.class).getContext().addRoutes(this);
String defaultEventExpires = getString(container.getConfig(), OR_RULE_EVENT_EXPIRES, OR_RULE_EVENT_EXPIRES_DEFAULT);
if (!TextUtil.isNullOrEmpty(defaultEventExpires)) {
try {
defaultEventExpiresMillis = TimeUtil.parseTimeDuration(defaultEventExpires);
} catch (RuntimeException exception) {
LOG.log(Level.WARNING, "Failed to parse " + OR_RULE_EVENT_EXPIRES, exception);
}
}
container.getService(ManagerWebService.class).addApiSingleton(
new FlowResourceImpl(
container.getService(TimerService.class),
container.getService(ManagerIdentityService.class)
)
);
if (container.getMeterRegistry() != null) {
rulesFiringTimer = container.getMeterRegistry().timer("or.rules", Tags.empty());
}
initDone = true;
}
@SuppressWarnings("unchecked")
@Override
public void configure() throws Exception {
// If any ruleset was modified in the database then check its' status and undeploy, deploy, or update it
from(PERSISTENCE_TOPIC)
.routeId("Persistence-Ruleset")
.filter(isPersistenceEventForEntityType(Ruleset.class))
.filter(isNotForGateway(gatewayService))
.process(exchange -> {
PersistenceEvent> persistenceEvent = exchange.getIn().getBody(PersistenceEvent.class);
processRulesetChange((Ruleset) persistenceEvent.getEntity(), persistenceEvent.getCause());
});
// If any realm was modified in the database then check its status and undeploy, deploy or update any
// associated rulesets
from(PERSISTENCE_TOPIC)
.routeId("Persistence-RulesRealm")
.filter(isPersistenceEventForEntityType(Realm.class))
.filter(isNotForGateway(gatewayService))
.process(exchange -> {
PersistenceEvent> persistenceEvent = exchange.getIn().getBody(PersistenceEvent.class);
Realm realm = (Realm) persistenceEvent.getEntity();
processRealmChange(realm, persistenceEvent.getCause());
});
// If any asset was modified in the database, detect changed attributes
from(PERSISTENCE_TOPIC)
.routeId("Persistence-RulesAsset")
.filter(isPersistenceEventForEntityType(Asset.class))
.process(exchange -> {
PersistenceEvent> persistenceEvent = (PersistenceEvent>) exchange.getIn().getBody(PersistenceEvent.class);
final Asset> eventAsset = persistenceEvent.getEntity();
processAssetChange(eventAsset, persistenceEvent);
});
}
@Override
public void start(Container container) throws Exception {
startDone = false;
if (!geofenceAssetAdapters.isEmpty()) {
LOG.fine("GeofenceAssetAdapters found: " + geofenceAssetAdapters.size());
locationPredicateRulesConsumer = this::onEngineLocationRulesChanged;
for (GeofenceAssetAdapter geofenceAssetAdapter : geofenceAssetAdapters) {
geofenceAssetAdapter.start(container);
}
}
LOG.fine("Deploying global rulesets");
rulesetStorageService.findAll(
GlobalRuleset.class,
new RulesetQuery()
.setEnabledOnly(true)
.setFullyPopulate(true)
).forEach(this::deployGlobalRuleset);
LOG.fine("Deploying realm rulesets");
realms = Arrays.stream(identityService.getIdentityProvider().getRealms()).filter(Realm::getEnabled).toArray(Realm[]::new);
rulesetStorageService.findAll(
RealmRuleset.class,
new RulesetQuery()
.setEnabledOnly(true)
.setFullyPopulate(true))
.stream()
.filter(rd ->
Arrays.stream(realms)
.anyMatch(realm -> rd.getRealm().equals(realm.getName()))
).forEach(this::deployRealmRuleset);
LOG.fine("Deploying asset rulesets");
// Group by asset ID then realm and check realm is enabled
//noinspection ResultOfMethodCallIgnored
deployAssetRulesets(
rulesetStorageService.findAll(
AssetRuleset.class,
new RulesetQuery()
.setEnabledOnly(true)
.setFullyPopulate(true)))
.count();//Needed in order to execute the stream. TODO: can this be done differently?
LOG.fine("Loading all assets with fact attributes to initialize state of rules engines");
Stream, Stream>>> stateAttributes = findRuleStateAttributes();
// Push each attribute as an asset update through the rule engine chain
// that will ensure the insert only happens to the engines in scope
stateAttributes
.forEach(pair -> {
Asset> asset = pair.key;
pair.value.forEach(ruleAttribute -> {
AttributeEvent attributeEvent = new AttributeEvent(
asset,
ruleAttribute,
null,
ruleAttribute.getValue().orElse(null),
ruleAttribute.getTimestamp().orElse(0L),
ruleAttribute.getValue().orElse(null),
ruleAttribute.getTimestamp().orElse(0L));
insertOrUpdateAttributeInfo(attributeEvent);
});
});
// Start the engines
synchronized (ENGINE_LOCK) {
RulesEngine> globalRulesEngine = globalEngine.get();
if (globalRulesEngine != null) {
globalRulesEngine.start();
}
realmEngines.values().forEach(RulesEngine::start);
assetEngines.values().forEach(RulesEngine::start);
startDone = true;
preInitAttributeEvents.forEach(this::doProcessAttributeUpdate);
preInitAttributeEvents.clear();
}
}
@Override
public void stop(Container container) throws Exception {
for (GeofenceAssetAdapter geofenceAssetAdapter : geofenceAssetAdapters) {
try {
geofenceAssetAdapter.stop(container);
} catch (Exception e) {
LOG.log(SEVERE, "Exception thrown whilst stopping geofence adapter", e);
}
}
assetEngines.forEach((assetId, rulesEngine) -> rulesEngine.stop());
assetEngines.clear();
realmEngines.forEach((realm, rulesEngine) -> rulesEngine.stop());
realmEngines.clear();
RulesEngine> globalRulesEngine = globalEngine.get();
if (globalRulesEngine != null) {
globalRulesEngine.stop();
globalEngine.set(null);
}
attributeEvents.clear();
for (GeofenceAssetAdapter geofenceAssetAdapter : geofenceAssetAdapters) {
geofenceAssetAdapter.stop(container);
}
}
protected static boolean isRuleState(MetaHolder metaHolder) {
if (metaHolder.getMeta() == null) {
return false;
}
return metaHolder.getMeta().getValue(MetaItemType.RULE_STATE)
.orElse(metaHolder.getMeta().has(MetaItemType.AGENT_LINK));
}
/**
* React to events that have been committed to the DB and inject them into the appropriate {@link RulesEngine}s.
*/
public void onAttributeEvent(AttributeEvent event) throws AssetProcessingException {
if (!startDone) {
preInitAttributeEvents.add(event);
} else {
doProcessAttributeUpdate(event);
}
}
protected void doProcessAttributeUpdate(AttributeEvent attributeEvent) {
if (isRuleState(attributeEvent) && !attributeEvent.isDeleted()) {
insertOrUpdateAttributeInfo(attributeEvent);
} else {
retractAttributeInfo(attributeEvent);
}
}
public boolean isRulesetKnown(Ruleset ruleset) {
if (ruleset instanceof GlobalRuleset) {
RulesEngine> globalRulesEngine = globalEngine.get();
return globalRulesEngine != null
&& globalRulesEngine.deployments.containsKey(ruleset.getId())
&& globalRulesEngine.deployments.get(ruleset.getId()).ruleset.getRules().equals(ruleset.getRules());
}
if (ruleset instanceof RealmRuleset realmRuleset) {
return realmEngines.get(realmRuleset.getRealm()) != null
&& realmEngines.get(realmRuleset.getRealm()).deployments.containsKey(ruleset.getId())
&& realmEngines.get(realmRuleset.getRealm()).deployments.get(ruleset.getId()).ruleset.getRules().equals(ruleset.getRules());
}
if (ruleset instanceof AssetRuleset assetRuleset) {
return assetEngines.get(assetRuleset.getAssetId()) != null
&& assetEngines.get(assetRuleset.getAssetId()).deployments.containsKey(ruleset.getId())
&& assetEngines.get(assetRuleset.getAssetId()).deployments.get(ruleset.getId()).ruleset.getRules().equals(ruleset.getRules());
}
return false;
}
public GeofenceDefinition[] getAssetGeofences(String assetId) {
LOG.finest("Requesting geofences for asset: " + assetId);
for (GeofenceAssetAdapter geofenceAdapter : geofenceAssetAdapters) {
GeofenceDefinition[] geofences = geofenceAdapter.getAssetGeofences(assetId);
if (geofences != null) {
LOG.finest("Retrieved geofences from geofence adapter '" + geofenceAdapter.getName() + "' for asset: " + assetId);
return geofences;
}
}
return new GeofenceDefinition[0];
}
protected void processRealmChange(Realm realm, PersistenceEvent.Cause cause) {
// Check if enabled status has changed
boolean wasEnabled = Arrays.stream(realms).anyMatch(t -> realm.getName().equals(t.getName()) && realm.getId().equals(t.getId()));
boolean isEnabled = realm.getEnabled() && cause != PersistenceEvent.Cause.DELETE;
realms = Arrays.stream(identityService.getIdentityProvider().getRealms()).filter(Realm::getEnabled).toArray(Realm[]::new);
if (wasEnabled == isEnabled) {
// Nothing to do here
return;
}
if (wasEnabled) {
// Remove realm rules engine for this realm if it exists
RulesEngine realmRulesEngine = realmEngines.get(realm.getName());
if (realmRulesEngine != null) {
realmRulesEngine.stop();
realmEngines.remove(realm.getName());
}
// Remove any asset rules engines for assets in this realm
assetEngines.values().removeIf(engine -> {
boolean remove = engine.getId().getRealm().map(r -> r.equals(realm.getName())).orElse(false);
if (remove) {
engine.stop();
}
return remove;
});
} else {
// Create realm rules engines for this realm if it has any rulesets
rulesetStorageService
.findAll(
RealmRuleset.class,
new RulesetQuery()
.setRealm(realm.getName())
.setFullyPopulate(true)
.setEnabledOnly(true))
.stream()
.map(this::deployRealmRuleset)
.filter(Objects::nonNull)
.forEach(RulesEngine::start);
// Create any asset rules engines for assets in this realm that have rulesets
deployAssetRulesets(
rulesetStorageService.findAll(
AssetRuleset.class,
new RulesetQuery()
.setRealm(realm.getName())
.setEnabledOnly(true)
.setFullyPopulate(true)))
.forEach(RulesEngine::start);
}
}
protected void processAssetChange(Asset> asset, PersistenceEvent> persistenceEvent) {
switch (persistenceEvent.getCause()) {
case DELETE ->
// Remove any asset rules engines for this asset
assetEngines.values().removeIf(re -> {
if (re.getId().getAssetId().map(aId -> aId.equals(asset.getId())).orElse(false)) {
re.stop();
return true;
}
return false;
});
case UPDATE -> {
// Attribute events are also published for updated/new attributes so nothing to do here
}
}
}
protected void processRulesetChange(Ruleset ruleset, PersistenceEvent.Cause cause) {
if (cause == PersistenceEvent.Cause.DELETE || !ruleset.isEnabled()) {
if (ruleset instanceof GlobalRuleset) {
undeployGlobalRuleset((GlobalRuleset) ruleset);
} else if (ruleset instanceof RealmRuleset) {
undeployRealmRuleset((RealmRuleset) ruleset);
} else if (ruleset instanceof AssetRuleset) {
undeployAssetRuleset((AssetRuleset) ruleset);
}
} else {
if (ruleset instanceof GlobalRuleset) {
RulesEngine engine = deployGlobalRuleset((GlobalRuleset) ruleset);
engine.start();
} else if (ruleset instanceof RealmRuleset) {
RulesEngine engine = deployRealmRuleset((RealmRuleset) ruleset);
engine.start();
} else if (ruleset instanceof AssetRuleset) {
// Must reload from the database, the ruleset might not be completely hydrated on CREATE or UPDATE
AssetRuleset assetRuleset = rulesetStorageService.find(AssetRuleset.class, ruleset.getId());
RulesEngine engine = deployAssetRuleset(assetRuleset);
engine.start();
}
}
}
/**
* Deploy the ruleset into the global engine creating the engine if necessary.
*/
protected RulesEngine deployGlobalRuleset(GlobalRuleset ruleset) {
synchronized (ENGINE_LOCK) {
RulesEngine engine = globalEngine.get();
boolean isNewEngine = engine == null;
// Global rules have access to everything in the system
if (isNewEngine) {
engine = new RulesEngine<>(
timerService,
this,
identityService,
executorService,
scheduledExecutorService,
assetStorageService,
assetProcessingService,
notificationService,
webhookService,
alarmService,
clientEventService,
assetDatapointService,
assetPredictedDatapointService,
new RulesEngineId<>(),
locationPredicateRulesConsumer,
rulesFiringTimer
);
globalEngine.set(engine);
}
if (isNewEngine) {
// Push all existing facts into the engine
RulesEngine finalEngine = engine;
attributeEvents.forEach(assetState -> finalEngine.insertOrUpdateAttributeInfo(assetState, true));
}
engine.addRuleset(ruleset);
return engine;
}
}
protected void undeployGlobalRuleset(GlobalRuleset ruleset) {
synchronized (ENGINE_LOCK) {
RulesEngine engine = globalEngine.get();
if (engine == null) {
return;
}
if (engine.removeRuleset(ruleset)) {
globalEngine.set(null);
}
}
}
protected RulesEngine deployRealmRuleset(RealmRuleset ruleset) {
synchronized (ENGINE_LOCK) {
RulesEngine realmRulesEngine = realmEngines.get(ruleset.getRealm());
boolean isNewEngine = realmRulesEngine == null;
if (isNewEngine) {
realmRulesEngine = new RulesEngine<>(
timerService,
this,
identityService,
executorService,
scheduledExecutorService,
assetStorageService,
assetProcessingService,
notificationService,
webhookService,
alarmService,
clientEventService,
assetDatapointService,
assetPredictedDatapointService,
new RulesEngineId<>(ruleset.getRealm()),
locationPredicateRulesConsumer,
rulesFiringTimer
);
realmEngines.put(ruleset.getRealm(), realmRulesEngine);
// Push all existing facts into the engine
RulesEngine finalRealmRulesEngine = realmRulesEngine;
attributeEvents.forEach(assetState -> {
if (assetState.getRealm().equals(ruleset.getRealm())) {
finalRealmRulesEngine.insertOrUpdateAttributeInfo(assetState, true);
}
});
}
realmRulesEngine.addRuleset(ruleset);
return realmRulesEngine;
}
}
protected void undeployRealmRuleset(RealmRuleset ruleset) {
synchronized (ENGINE_LOCK) {
RulesEngine realmRulesEngine = realmEngines.get(ruleset.getRealm());
if (realmRulesEngine == null) {
return;
}
if (realmRulesEngine.removeRuleset(ruleset)) {
realmEngines.remove(ruleset.getRealm());
}
}
}
protected Stream> deployAssetRulesets(List rulesets) {
return rulesets
.stream()
.collect(Collectors.groupingBy(AssetRuleset::getAssetId))
.entrySet()
.stream()
.map(es ->
new Pair, List>(assetStorageService.find(es.getKey(), true), es.getValue())
)
.filter(assetAndRules -> assetAndRules.key != null)
.collect(Collectors.groupingBy(assetAndRules -> assetAndRules.key.getRealm()))
.entrySet()
.stream()
.filter(es -> Arrays
.stream(realms)
.anyMatch(at -> es.getKey().equals(at.getName())))
.flatMap(es -> {
List, List>> realmAssetAndRules = es.getValue();
// RT: Not sure we need ordering here for starting engines so removing it
// Order rulesets by asset hierarchy within this realm
return realmAssetAndRules.stream()
//.sorted(Comparator.comparingInt(item -> item.key.getPath().length))
.flatMap(assetAndRules -> assetAndRules.value.stream())
.map(this::deployAssetRuleset);
});
}
protected RulesEngine deployAssetRuleset(AssetRuleset ruleset) {
synchronized (ENGINE_LOCK) {
RulesEngine assetRulesEngine = assetEngines.get(ruleset.getAssetId());
boolean isNewEngine = assetRulesEngine == null;
if (isNewEngine) {
assetRulesEngine = new RulesEngine<>(
timerService,
this,
identityService,
executorService,
scheduledExecutorService,
assetStorageService,
assetProcessingService,
notificationService,
webhookService,
alarmService,
clientEventService,
assetDatapointService,
assetPredictedDatapointService,
new RulesEngineId<>(ruleset.getRealm(), ruleset.getAssetId()),
locationPredicateRulesConsumer,
rulesFiringTimer
);
assetEngines.put(ruleset.getAssetId(), assetRulesEngine);
// Push all existing facts for this asset (and it's children into the engine)
RulesEngine finalAssetRulesEngine = assetRulesEngine;
getAssetStatesInScope(ruleset.getAssetId())
.forEach(assetState -> finalAssetRulesEngine.insertOrUpdateAttributeInfo(assetState, true));
}
assetRulesEngine.addRuleset(ruleset);
return assetRulesEngine;
}
}
protected void undeployAssetRuleset(AssetRuleset ruleset) {
synchronized (ENGINE_LOCK) {
RulesEngine rulesEngine = assetEngines.get(ruleset.getAssetId());
if (rulesEngine == null) {
return;
}
if (rulesEngine.removeRuleset(ruleset)) {
assetEngines.remove(ruleset.getAssetId());
}
}
}
protected void insertOrUpdateAttributeInfo(AttributeEvent attributeEvent) {
if (attributeEvent.isOutdated()) {
// Attribute event is old so ignore
return;
}
LOG.log(FINEST, () -> "Inserting attribute event: " + attributeEvent);
// Remove asset state with same attribute ref as new state, add new state
boolean inserted = !attributeEvents.remove(attributeEvent);
attributeEvents.add(attributeEvent);
// Get the chain of rule engines that we need to pass through
List> rulesEngines = getEnginesInScope(attributeEvent.getRealm(), attributeEvent.getPath());
// Pass through each rules engine
for (RulesEngine> rulesEngine : rulesEngines) {
rulesEngine.insertOrUpdateAttributeInfo(attributeEvent, inserted);
}
}
protected void retractAttributeInfo(AttributeEvent attributeEvent) {
LOG.log(FINEST, () -> "Retracting attribute event: " + attributeEvent);
// Remove asset state with same attribute ref
attributeEvents.remove(attributeEvent);
// Get the chain of rule engines that we need to pass through
List> rulesEngines = getEnginesInScope(attributeEvent.getRealm(), attributeEvent.getPath());
// Pass through each rules engine
for (RulesEngine> rulesEngine : rulesEngines) {
rulesEngine.retractAttributeInfo(attributeEvent);
}
}
protected List getAssetStatesInScope(String assetId) {
return attributeEvents
.stream()
.filter(assetState -> Arrays.asList(assetState.getPath()).contains(assetId))
.collect(Collectors.toList());
}
protected List> getEnginesInScope(String realm, String[] assetPath) {
List> rulesEngines = new ArrayList<>();
// Add global engine (if it exists)
RulesEngine> globalRulesEngine = globalEngine.get();
if (globalRulesEngine != null) {
rulesEngines.add(globalRulesEngine);
}
// Add realm engine (if it exists)
RulesEngine> realmRulesEngine = realmEngines.get(realm);
if (realmRulesEngine != null) {
rulesEngines.add(realmRulesEngine);
}
// Add asset engines, iterate through asset hierarchy using asset IDs from asset path
for (String assetId : assetPath) {
RulesEngine> assetRulesEngine = assetEngines.get(assetId);
if (assetRulesEngine != null) {
rulesEngines.add(assetRulesEngine);
}
}
return rulesEngines;
}
protected Stream, Stream>>> findRuleStateAttributes() {
// Get all assets then filter out any attributes with RULE_STATE=false
List> assets = assetStorageService.findAll(new AssetQuery());
return assets.stream()
.map(asset ->
new Pair<>(asset, asset.getAttributes().stream()
.filter(RulesService::isRuleState))
);
}
/**
* Called when an engine's rules change identifying assets with location attributes that also have {@link
* LocationAttributePredicate} in the rules. The job here is to identify the asset's (via {@link AttributeInfo}) that
* have modified {@link LocationAttributePredicate}s and to notify the {@link GeofenceAssetAdapter}s.
*/
protected void onEngineLocationRulesChanged(RulesEngine> rulesEngine, List newEngineAssetStateLocationPredicates) {
synchronized (assetsWithModifiedLocationPredicates) {
int initialModifiedCount = assetsWithModifiedLocationPredicates.size();
if (newEngineAssetStateLocationPredicates == null) {
engineAssetLocationPredicateMap.computeIfPresent(rulesEngine,
(re, existingAssetStateLocationPredicates) -> {
// All location predicates have been removed so record each asset state as modified
assetsWithModifiedLocationPredicates.addAll(
existingAssetStateLocationPredicates.stream().map(
RulesEngine.AssetLocationPredicates::getAssetId).toList());
// Remove this engine from the map
return null;
});
} else {
engineAssetLocationPredicateMap.compute(rulesEngine,
(re, existingEngineAssetStateLocationPredicates) -> {
// Check if this not the first time this engine has been seen with location predicates so we can check
// for any removed asset states
if (existingEngineAssetStateLocationPredicates == null) {
// All asset states are new so record them all as modified
assetsWithModifiedLocationPredicates.addAll(
newEngineAssetStateLocationPredicates.stream().map(
RulesEngine.AssetLocationPredicates::getAssetId).toList());
} else {
// Find obsolete and modified asset states
existingEngineAssetStateLocationPredicates.forEach(
existingAssetStateLocationPredicates -> {
// Check if there are no longer any location predicates for this asset
Optional newAssetStateLocationPredicates = newEngineAssetStateLocationPredicates.stream()
.filter(assetStateLocationPredicates ->
assetStateLocationPredicates.getAssetId().equals(
existingAssetStateLocationPredicates.getAssetId()))
.findFirst();
if (newAssetStateLocationPredicates.isPresent()) {
// Compare existing and new location predicate sets if there is any change then record it
if (!newAssetStateLocationPredicates.get().getLocationPredicates().equals(
existingAssetStateLocationPredicates.getLocationPredicates())) {
assetsWithModifiedLocationPredicates.add(
existingAssetStateLocationPredicates.getAssetId());
}
} else {
// This means that there are no longer any location predicates so old ones are obsolete
assetsWithModifiedLocationPredicates.add(
existingAssetStateLocationPredicates.getAssetId());
}
});
// Check for asset states in the new map but not in the old one
newEngineAssetStateLocationPredicates.forEach(
newAssetStateLocationPredicates -> {
boolean isNewAssetState = existingEngineAssetStateLocationPredicates.stream()
.noneMatch(assetStateLocationPredicates ->
assetStateLocationPredicates.getAssetId().equals(
newAssetStateLocationPredicates.getAssetId()));
if (isNewAssetState) {
// This means that all predicates for this asset are new
assetsWithModifiedLocationPredicates.add(
newAssetStateLocationPredicates.getAssetId());
}
});
}
return newEngineAssetStateLocationPredicates;
});
}
if (assetsWithModifiedLocationPredicates.size() != initialModifiedCount) {
processModifiedGeofences();
}
}
}
protected void processModifiedGeofences() {
synchronized (assetsWithModifiedLocationPredicates) {
LOG.finest("Processing geofence modifications: modified asset geofence count=" + assetsWithModifiedLocationPredicates.size());
try {
// Find all location predicates associated with modified assets and pass through to the geofence adapters
List assetLocationPredicates = new ArrayList<>(
assetsWithModifiedLocationPredicates.size());
assetsWithModifiedLocationPredicates.forEach(assetId -> {
RulesEngine.AssetLocationPredicates locationPredicates = new RulesEngine.AssetLocationPredicates(
assetId,
new HashSet<>());
engineAssetLocationPredicateMap.forEach((rulesEngine, engineAssetStateLocationPredicates) ->
engineAssetStateLocationPredicates.stream().filter(
assetStateLocationPredicates ->
assetStateLocationPredicates.getAssetId().equals(
assetId))
.findFirst()
.ifPresent(
assetStateLocationPredicate -> {
locationPredicates.getLocationPredicates().addAll(
assetStateLocationPredicate.getLocationPredicates());
}));
assetLocationPredicates.add(locationPredicates);
});
for (GeofenceAssetAdapter geofenceAssetAdapter : geofenceAssetAdapters) {
LOG.finest("Passing modified geofences to adapter: " + geofenceAssetAdapter.getName());
geofenceAssetAdapter.processLocationPredicates(assetLocationPredicates);
if (assetLocationPredicates.isEmpty()) {
LOG.finest("All modified geofences handled");
break;
}
}
} catch (Exception e) {
LOG.log(SEVERE, "Exception thrown by geofence adapter whilst processing location predicates", e);
} finally {
// Clear modified assets ready for next batch
assetsWithModifiedLocationPredicates.clear();
}
}
}
protected Optional getRulesetDeployment(Long rulesetId) {
RulesEngine> globalRulesEngine = globalEngine.get();
if (globalRulesEngine != null) {
if (globalRulesEngine.deployments.containsKey(rulesetId)) {
return Optional.of(globalRulesEngine.deployments.get(rulesetId));
}
}
for (Map.Entry> realmAndEngine : realmEngines.entrySet()) {
if (realmAndEngine.getValue().deployments.containsKey(rulesetId)) {
return Optional.of(realmAndEngine.getValue().deployments.get(rulesetId));
}
}
for (Map.Entry> realmAndEngine : assetEngines.entrySet()) {
if (realmAndEngine.getValue().deployments.containsKey(rulesetId)) {
return Optional.of(realmAndEngine.getValue().deployments.get(rulesetId));
}
}
return Optional.empty();
}
/**
* Trigger rules engines which have the {@link org.openremote.model.value.MetaItemDescriptor} {@link org.openremote.model.rules.Ruleset#TRIGGER_ON_PREDICTED_DATA}
* and contain {@link AttributeInfo} of the specified asset id. Use this when {@link PredictedDatapoints} has changed for this asset.
* @param assetId of the asset which has new predicated data points.
*/
public void fireDeploymentsWithPredictedDataForAsset(String assetId) {
List assetStates = getAssetStatesInScope(assetId);
if (!assetStates.isEmpty()) {
String realm = assetStates.get(0).getRealm();
String[] assetPaths = assetStates.stream().flatMap(assetState -> Arrays.stream(assetState.getPath())).toArray(String[]::new);
synchronized (ENGINE_LOCK) {
for (RulesEngine> rulesEngine : getEnginesInScope(realm, assetPaths)) {
rulesEngine.scheduleFire(false);
}
}
}
}
@Override
public String toString() {
return getClass().getSimpleName() + "{}";
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy