
org.openremote.manager.agent.AgentService Maven / Gradle / Ivy
/*
* Copyright 2016, 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.agent;
import jakarta.persistence.EntityManager;
import org.apache.camel.builder.RouteBuilder;
import org.openremote.container.message.MessageBrokerService;
import org.openremote.container.timer.TimerService;
import org.openremote.manager.asset.AssetProcessingException;
import org.openremote.manager.asset.AssetProcessingService;
import org.openremote.manager.asset.AssetStorageService;
import org.openremote.manager.event.ClientEventService;
import org.openremote.manager.gateway.GatewayService;
import org.openremote.manager.security.ManagerIdentityService;
import org.openremote.manager.web.ManagerWebService;
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.asset.AssetFilter;
import org.openremote.model.asset.AssetTreeNode;
import org.openremote.model.asset.agent.Agent;
import org.openremote.model.asset.agent.AgentLink;
import org.openremote.model.asset.agent.ConnectionStatus;
import org.openremote.model.asset.agent.Protocol;
import org.openremote.model.attribute.*;
import org.openremote.model.protocol.ProtocolAssetDiscovery;
import org.openremote.model.protocol.ProtocolAssetImport;
import org.openremote.model.protocol.ProtocolAssetService;
import org.openremote.model.protocol.ProtocolInstanceDiscovery;
import org.openremote.model.query.AssetQuery;
import org.openremote.model.query.filter.AttributePredicate;
import org.openremote.model.query.filter.NameValuePredicate;
import org.openremote.model.query.filter.RealmPredicate;
import org.openremote.model.query.filter.StringPredicate;
import org.openremote.model.util.Pair;
import org.openremote.model.util.TextUtil;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
import static org.openremote.container.persistence.PersistenceService.PERSISTENCE_TOPIC;
import static org.openremote.container.persistence.PersistenceService.isPersistenceEventForEntityType;
import static org.openremote.manager.gateway.GatewayService.isNotForGateway;
import static org.openremote.model.attribute.Attribute.getAddedOrModifiedAttributes;
import static org.openremote.model.value.MetaItemType.AGENT_LINK;
/**
* This service's role is to communicate asset attribute writes to actuators, through protocol instances. It also
* handles redeploying {@link Protocol} instances when an {@link Attribute} of the associated {@link Agent} is modified
* either via Asset CRUD or just via an {@link AttributeEvent}.
*
* Only an {@link AttributeEvent} for an {@link Attribute} containing a
* {@link org.openremote.model.value.MetaItemType#AGENT_LINK} {@link MetaItem} will be intercepted here and passed to
* the associated {@link Protocol} instance for processing; the event will not be committed to the DB and it is up to
* the {@link Protocol} to generate a new {@link AttributeEvent} to signal that the action has been successfully handled.
*
* Any {@link AttributeEvent} that originates from an {@link Agent} {@link Protocol} will not be consumed by the source
* {@link Protocol} when it passes back through this service; this is to prevent infinite loops.
*/
public class AgentService extends RouteBuilder implements ContainerService {
protected class AgentProtocolAssetService implements ProtocolAssetService {
protected Agent,?,?> agent;
public AgentProtocolAssetService(Agent,?,?> agent) {
this.agent = agent;
}
@Override
public > T mergeAsset(T asset) {
Objects.requireNonNull(asset.getId());
if (TextUtil.isNullOrEmpty(asset.getRealm())) {
asset.setRealm(agent.getRealm());
} else if (!Objects.equals(asset.getRealm(), agent.getRealm())) {
String msg = "Protocol attempting to merge asset into another realm: " + agent;
Protocol.LOG.warning(msg);
throw new IllegalArgumentException(msg);
}
// TODO: Define access permissions for merged asset (user asset links inherit from parent agent?)
LOG.fine("Merging asset with protocol-provided: " + asset);
return assetStorageService.merge(asset, true);
}
@Override
public boolean deleteAssets(String... assetIds) {
for (String assetId: assetIds) {
Asset> asset = findAsset(assetId);
if (asset != null) {
if (!Objects.equals(asset.getRealm(), agent.getRealm())) {
Protocol.LOG.warning("Protocol attempting to delete asset from another realm: " + agent);
throw new IllegalArgumentException("Protocol attempting to delete asset from another realm");
}
}
}
LOG.fine("Deleting protocol-provided: " + Arrays.toString(assetIds));
return assetStorageService.delete(Arrays.asList(assetIds), false);
}
@SuppressWarnings("unchecked")
@Override
public > T findAsset(String assetId) {
LOG.fine("Getting protocol-provided: " + assetId);
T asset = (T)assetStorageService.find(assetId);
if (asset != null) {
if (!Objects.equals(asset.getRealm(), agent.getRealm())) {
Protocol.LOG.warning("Protocol attempting to find asset from another realm: " + agent);
throw new IllegalArgumentException("Protocol attempting to find asset from another realm");
}
}
return asset;
}
@Override
public List> findAssets(AssetQuery assetQuery) {
List> assets = assetStorageService.findAll(assetQuery.realm(new RealmPredicate(agent.getRealm())));
for (Asset> asset : assets) {
if (!Objects.equals(asset.getRealm(), agent.getRealm())) {
Protocol.LOG.warning("Protocol attempting to find asset from another realm: " + agent);
throw new IllegalArgumentException("Protocol attempting to find asset from another realm");
}
}
return assets;
}
@Override
public void sendAttributeEvent(AttributeEvent attributeEvent) {
if (TextUtil.isNullOrEmpty(attributeEvent.getRealm())) {
attributeEvent.setRealm(agent.getRealm());
} else if (!Objects.equals(attributeEvent.getRealm(), agent.getRealm())) {
Protocol.LOG.warning("Protocol attempting to send attribute event to another realm: " + agent);
throw new IllegalArgumentException("Protocol attempting to send attribute event to another realm");
}
AgentService.this.sendAttributeEvent(attributeEvent);
}
@Override
public void subscribeChildAssetChange(Consumer>> assetChangeConsumer) {
if (!getAgents().containsKey(agent.getId())) {
LOG.fine("Attempt to subscribe to child asset changes with an invalid agent ID: " + agent.getId());
return;
}
Set>>> consumers = childAssetSubscriptions
.computeIfAbsent(agent.getId(), (id) -> Collections.synchronizedSet(new HashSet<>()));
consumers.add(assetChangeConsumer);
}
@Override
public void unsubscribeChildAssetChange(Consumer>> assetChangeConsumer) {
childAssetSubscriptions.computeIfPresent(agent.getId(), (id, consumers) -> {
consumers.remove(assetChangeConsumer);
return consumers.isEmpty() ? null : consumers;
});
}
}
private static final Logger LOG = Logger.getLogger(AgentService.class.getName());
public static final int PRIORITY = MessageBrokerService.PRIORITY + 100; // Start quite late to ensure asset model etc. are initialised
protected AssetProcessingService assetProcessingService;
protected AssetStorageService assetStorageService;
protected ClientEventService clientEventService;
protected GatewayService gatewayService;
protected ExecutorService executorService;
protected Map> agentMap;
protected final Map> agentDiscoveryImportFutureMap = new ConcurrentHashMap<>();
protected final Map> protocolInstanceMap = new ConcurrentHashMap<>();
protected final Map>>>> childAssetSubscriptions = new ConcurrentHashMap<>();
protected boolean initDone;
protected Container container;
protected final Object agentLock = new Object();
@Override
public int getPriority() {
return PRIORITY;
}
@SuppressWarnings("unchecked")
@Override
public void init(Container container) throws Exception {
this.container = container;
assetProcessingService = container.getService(AssetProcessingService.class);
assetStorageService = container.getService(AssetStorageService.class);
clientEventService = container.getService(ClientEventService.class);
gatewayService = container.getService(GatewayService.class);
executorService = container.getExecutor();
if (initDone) {
return;
}
container.getService(ManagerWebService.class).addApiSingleton(
new AgentResourceImpl(
container.getService(TimerService.class),
container.getService(ManagerIdentityService.class),
assetStorageService,
this,
container.getExecutor())
);
assetProcessingService.addEventInterceptor(this::onAttributeEventIntercepted);
clientEventService.addSubscription(AttributeEvent.class, new AssetFilter().setAssetClasses(Collections.singletonList(Agent.class)), this::onAgentAttributeEvent);
initDone = true;
}
@Override
public void start(Container container) throws Exception {
container.getService(MessageBrokerService.class).getContext().addRoutes(this);
// Load all enabled agents and instantiate a protocol instance for each
LOG.fine("Loading agents...");
Collection> agents = getAgents().values();
LOG.fine("Found agent count = " + agents.size());
agents.forEach(this::doAgentInit);
}
@Override
public void stop(Container container) throws Exception {
if (agentMap != null) {
List> agents = new ArrayList<>(agentMap.values());
agents.forEach(agent -> this.undeployAgent(agent.getId()));
agentMap.clear();
}
protocolInstanceMap.clear();
}
@SuppressWarnings("unchecked")
@Override
public void configure() throws Exception {
from(PERSISTENCE_TOPIC)
.routeId("Persistence-Agent")
.filter(isPersistenceEventForEntityType(Asset.class))
.filter(isNotForGateway(gatewayService))
.process(exchange -> {
PersistenceEvent> persistenceEvent = (PersistenceEvent>)exchange.getIn().getBody(PersistenceEvent.class);
if (isPersistenceEventForEntityType(Agent.class).matches(exchange)) {
PersistenceEvent> agentEvent = (PersistenceEvent>)(PersistenceEvent>)persistenceEvent;
processAgentChange(agentEvent);
} else {
processAssetChange(persistenceEvent);
}
});
}
/**
* Called when an {@link Agent} is modified in the DB
*/
protected void processAgentChange(PersistenceEvent> persistenceEvent) {
LOG.finest("Processing agent persistence event: " + persistenceEvent.getCause());
Agent, ?, ?> agent = persistenceEvent.getEntity();
switch (persistenceEvent.getCause()) {
case CREATE, UPDATE -> deployAgent(agent);
case DELETE -> undeployAgent(agent.getId());
}
}
/**
* Deploy the {@link Agent} by creating a protocol instance, starting it and linking all attributes
*/
protected void deployAgent(Agent,?,?> agent) {
synchronized (agentLock) {
undeployAgent(agent.getId());
agent = addAgent(agent);
if (agent == null) {
return;
}
doAgentInit(agent);
}
}
/**
* Looks for new, modified and obsolete AGENT_LINK attributes and links / unlinks them
* with the protocol
*/
protected void processAssetChange(PersistenceEvent> persistenceEvent) {
LOG.finest("Processing asset persistence event: " + persistenceEvent.getCause());
Asset> asset = persistenceEvent.getEntity();
switch (persistenceEvent.getCause()) {
case CREATE ->
// Link any AGENT_LINK attributes to their referenced agent asset
getGroupedAgentLinkAttributes(
asset.getAttributes().stream(),
attribute -> true
).forEach((agent, attributes) -> this.linkAttributes(agent, asset.getId(), attributes));
case UPDATE -> {
if (!persistenceEvent.hasPropertyChanged("attributes")) {
return;
}
List> oldLinkedAttributes = ((AttributeMap) persistenceEvent.getPreviousState("attributes"))
.stream()
.filter(attr -> attr.hasMeta(AGENT_LINK))
.collect(toList());
List> newLinkedAttributes = ((AttributeMap) persistenceEvent.getCurrentState("attributes"))
.stream()
.filter(attr -> attr.hasMeta(AGENT_LINK))
.collect(Collectors.toList());
// Unlink obsolete or modified linked attributes
List> obsoleteOrModified = getAddedOrModifiedAttributes(newLinkedAttributes, oldLinkedAttributes).toList();
getGroupedAgentLinkAttributes(
obsoleteOrModified.stream(),
attribute -> true
).forEach((agent, attributes) -> unlinkAttributes(agent.getId(), asset.getId(), attributes));
// Link new or modified attributes
getGroupedAgentLinkAttributes(
newLinkedAttributes.stream().filter(attr ->
!oldLinkedAttributes.contains(attr) || obsoleteOrModified.contains(attr)),
attribute -> true)
.forEach((agent, attributes) -> linkAttributes(agent, asset.getId(), attributes));
}
case DELETE -> // Unlink any AGENT_LINK attributes from the referenced protocol
getGroupedAgentLinkAttributes(asset.getAttributes().stream(), attribute -> true)
.forEach((agent, attributes) -> unlinkAttributes(agent.getId(), asset.getId(), attributes));
}
notifyAgentAncestor(asset, persistenceEvent);
}
protected void notifyAgentAncestor(Asset> asset, PersistenceEvent> persistenceEvent) {
String parentId = asset.getParentId();
if ((asset instanceof Agent) || parentId == null) {
return;
}
String ancestorAgentId = null;
if (agentMap.containsKey(parentId)) {
ancestorAgentId = parentId;
} else {
// If path is not loaded then get the parents path as the asset might have been deleted
if (asset.getPath() == null) {
Asset> parentAsset = assetStorageService.find(parentId);
if (parentAsset != null && parentAsset.getPath() != null) {
ancestorAgentId = Arrays.stream(parentAsset.getPath())
.filter(assetId -> getAgents().containsKey(assetId))
.findFirst()
.orElse(null);
}
}
}
if (ancestorAgentId != null) {
notifyChildAssetChange(ancestorAgentId, persistenceEvent);
}
}
protected void sendAttributeEvent(AttributeEvent event) {
// Set the source so we can ignore the intercept when the event comes back through the chain
assetProcessingService.sendAttributeEvent(event, getClass().getSimpleName());
}
protected void doAgentInit(Agent,?,?> agent) {
boolean isDisabled = agent.isDisabled().orElse(false);
if (isDisabled) {
LOG.fine("Agent is disabled so not starting: " + agent);
sendAttributeEvent(new AttributeEvent(agent.getId(), Agent.STATUS.getName(), ConnectionStatus.DISABLED));
} else {
executorService.execute(() -> this.startAgent(agent));
}
}
protected void startAgent(Agent,?,?> agent) {
synchronized (agentLock) {
Protocol> protocol = null;
try {
protocol = agent.getProtocolInstance();
protocol.setAssetService(new AgentProtocolAssetService(agent));
LOG.fine("Starting protocol instance: " + protocol);
protocol.start(container);
protocolInstanceMap.put(agent.getId(), protocol);
LOG.fine("Started protocol instance: " + protocol);
LOG.finest("Linking attributes to protocol instance: " + protocol);
// Get all assets that have attributes with agent link meta for this agent
List> assets = assetStorageService.findAll(
new AssetQuery()
.attributes(
new AttributePredicate().meta(
new NameValuePredicate(AGENT_LINK, new StringPredicate(agent.getId()), false, new NameValuePredicate.Path("id"))
)
)
);
LOG.finest("Found '" + assets.size() + "' asset(s) with attributes linked to this protocol instance: " + protocol);
assets.forEach(
asset ->
getGroupedAgentLinkAttributes(
asset.getAttributes().stream(),
assetAttribute -> assetAttribute.getMetaValue(AGENT_LINK)
.map(agentLink -> agentLink.getId().equals(agent.getId()))
.orElse(false)
).forEach((agnt, attributes) -> linkAttributes(agnt, asset.getId(), attributes))
);
} catch (Exception e) {
if (protocol != null) {
try {
protocol.stop(container);
} catch (Exception ignored) {
}
}
protocolInstanceMap.remove(agent.getId());
LOG.log(Level.SEVERE, "Failed to start protocol '" + protocol + "': " + agent + " msg=" + e.getMessage());
sendAttributeEvent(new AttributeEvent(agent.getId(), Agent.STATUS.getName(), ConnectionStatus.ERROR));
}
}
}
protected void undeployAgent(String agentId) {
synchronized (agentLock) {
removeAgent(agentId);
Protocol> protocol = protocolInstanceMap.get(agentId);
if (protocol == null) {
return;
}
Map>> groupedAttributes = protocol.getLinkedAttributes().entrySet().stream().collect(
Collectors.groupingBy(entry -> entry.getKey().getId(), mapping(Map.Entry::getValue, toList()))
);
groupedAttributes.forEach((assetId, linkedAttributes) -> unlinkAttributes(agentId, assetId, linkedAttributes));
// Stop the protocol instance
try {
protocol.stop(container);
} catch (Exception e) {
LOG.log(Level.SEVERE, "Protocol instance threw an exception whilst being stopped", e);
}
// Remove child asset subscriptions for this agent
childAssetSubscriptions.remove(agentId);
protocolInstanceMap.remove(agentId);
}
}
protected void linkAttributes(Agent,?,?> agent, String assetId, Collection> attributes) {
final Protocol> protocol = getProtocolInstance(agent.getId());
if (protocol == null) {
return;
}
synchronized (protocol) {
LOG.fine("Linking asset '" + assetId + "' attributes linked to protocol: assetId=" + assetId + ", attributes=" + attributes.size() + ", protocol=" + protocol);
attributes.forEach(attribute -> {
AttributeRef attributeRef = new AttributeRef(assetId, attribute.getName());
try {
if (!protocol.getLinkedAttributes().containsKey(attributeRef)) {
LOG.finest("Linking attribute '" + attributeRef + "' to protocol: " + protocol);
protocol.linkAttribute(assetId, attribute);
}
} catch (Exception ex) {
LOG.log(Level.SEVERE, "Failed to link attribute '" + attributeRef + "' to protocol: " + protocol + " msg=" + ex.getMessage());
}
});
}
}
protected void unlinkAttributes(String agentId, String assetId, List> attributes) {
final Protocol> protocol = getProtocolInstance(agentId);
if (protocol == null) {
return;
}
synchronized (protocol) {
LOG.fine("Unlinking asset '" + assetId + "' attributes linked to protocol: assetId=" + assetId + ", attributes=" + attributes.size() + ", protocol=" + protocol);
attributes.forEach(attribute -> {
try {
AttributeRef attributeRef = new AttributeRef(assetId, attribute.getName());
if (protocol.getLinkedAttributes().containsKey(attributeRef)) {
LOG.finest("Unlinking attribute '" + attributeRef + "' to protocol: " + protocol);
protocol.unlinkAttribute(assetId, attribute);
}
} catch (Exception ex) {
LOG.log(Level.SEVERE, "Ignoring error on unlinking attribute '" + attribute + "' from protocol: " + protocol, ex);
}
});
}
}
/**
* Intercepts any {@link AttributeEvent} for an {@link Attribute} that has an
* {@link org.openremote.model.value.MetaItemType#AGENT_LINK} {@link MetaItem} and passes it to the {@link Agent}'s
* {@link Protocol#processLinkedAttributeWrite} method for handling.
*
* If the {@link AttributeEvent} originated from the {@link Agent} that the {@link Attribute} is linked to then it
* is not intercepted.
*/
protected boolean onAttributeEventIntercepted(EntityManager em,
AttributeEvent event) throws AssetProcessingException {
// Don't intercept an event that was generated by this service
if (getClass().getSimpleName().equals(event.getSource())) {
return false;
}
Optional agentLinkOptional = event.getMetaValue(AGENT_LINK);
// Never intercept events with no agent link
return agentLinkOptional.map(agentLink -> {
LOG.finest("Attribute event for agent linked attribute: agent=" + agentLink.getId() + ", ref=" + event.getRef());
if (event.isOutdated()) {
// Don't process outdated events but still intercept them
return true;
}
Protocol> protocolInstance = getProtocolInstance(agentLink.getId());
if (protocolInstance == null) {
throw new AssetProcessingException(AttributeWriteFailure.CANNOT_PROCESS, "Agent protocol instance not found, agent may be disabled or has been deleted: attributeRef=" + event.getRef() + ", agentLink=" + agentLink);
}
try {
protocolInstance.processLinkedAttributeWrite(event);
} catch (Exception e) {
AttributeWriteFailure failure = AttributeWriteFailure.UNKNOWN;
String msg = e.getMessage();
if (e instanceof AssetProcessingException assetProcessingException) {
failure = assetProcessingException.getReason();
}
throw new AssetProcessingException(failure, "An exception occurred whilst the protocol was trying to process the attribute write request: agentLink=" + agentLink + ", msg=" + msg);
}
return true; // Processing complete, skip other processors
}).orElse(false); // This is a regular attribute so allow the processing to continue
}
/**
* Called when an {@link AttributeEvent} for an {@link Agent} is broadcast on the client event bus (i.e. the
* attribute has been updated in the DB).
*
* We use this to try and react to agent changes in a generic way by re-initialising the agent to simplify each
* agent implementation.
*/
// TODO: Queue up agent attribute events and do a single agent replacement
protected void onAgentAttributeEvent(AttributeEvent event) {
synchronized (agentLock) {
Agent, ?, ?> agent = getAgent(event.getId());
if (agent == null) {
return;
}
// Check that the event has a newer timestamp than the existing agent attribute - if the attribute doesn't
// exist on the agent either then assume the agent has been modified and attribute removed
boolean eventOutdated = agent.getAttribute(event.getName()).flatMap(Attribute::getTimestamp)
.map(timestamp -> event.getTimestamp() <= timestamp)
.orElse(true);
if (eventOutdated) {
return;
}
// Ignore events that have come from the AssetStorageService (i.e. Asset merges as these are handled separately)
if (AssetStorageService.class.getSimpleName().equals(event.getSource())) {
return;
}
// Update in memory agent
agent.getAttribute(event.getName()).ifPresent(attr -> attr.setValue(event.getValue().orElse(null), event.getTimestamp()));
Protocol> protocolInstance = getProtocolInstance(agent.getId());
if (protocolInstance == null) {
if (Agent.DISABLED.getName().equals(event.getName())) {
// Maybe agent was disabled and now isn't - use standard mechanism
deployAgent(agent);
}
return;
}
LOG.finer("Notifying protocol instance of an event for one of its agent attributes: " + event.getRef());
if (protocolInstance.onAgentAttributeChanged(event)) {
LOG.info("Protocol has requested recreation following agent attribute event: " + event.getRef());
deployAgent(agent);
}
}
}
/**
* Gets all agent link attributes and their linked agent and groups them by agent
*/
protected Map, List>> getGroupedAgentLinkAttributes(Stream> attributes,
Predicate> filter) {
return attributes
.filter(attribute ->
// Exclude attributes without agent link or with agent link to not recognised agents (could be gateway agents)
attribute.getMetaValue(AGENT_LINK)
.map(agentLink -> {
if (!getAgents().containsKey(agentLink.getId())) {
LOG.finest("Agent linked attribute, agent not found or this is a gateway asset: " + attribute);
return false;
}
return true;
})
.orElse(false))
.filter(filter)
.map(attribute -> new Pair, Attribute>>(attribute.getMetaValue(AGENT_LINK).map(AgentLink::getId).map(agentId -> getAgents().get(agentId)).orElse(null), attribute))
.filter(agentAttribute -> agentAttribute.key != null)
.collect(Collectors.groupingBy(
agentAttribute -> agentAttribute.key,
Collectors.collectingAndThen(Collectors.toList(), agentAttribute -> agentAttribute.stream().map(item->item.value).collect(toList())) //TODO had to change to this because compiler has issues with inferring types, need to check for a better solution
));
}
public String toString() {
return getClass().getSimpleName() + "{" + "}";
}
protected Agent, ?, ?> addAgent(Agent, ?, ?> agent) {
// Fully load agent asset if path and parent info not loaded
if (agent.getPath() == null || (agent.getPath().length > 1 && agent.getParentId() == null)) {
LOG.fine("Agent is not fully loaded so retrieving the agent from the DB: " + agent.getId());
final Agent, ?, ?> loadedAgent = assetStorageService.find(agent.getId(), true, Agent.class);
if (loadedAgent == null) {
LOG.fine("Agent not found in the DB, maybe it has been removed: " + agent.getId());
return null;
}
agent = loadedAgent;
}
getAgents().put(agent.getId(), agent);
return agent;
}
@SuppressWarnings({"BooleanMethodIsAlwaysInverted"})
protected boolean removeAgent(String agentId) {
return getAgents().remove(agentId) != null;
}
public Agent, ?, ?> getAgent(String agentId) {
return getAgents().get(agentId);
}
protected Map> getAgents() {
synchronized (agentLock) {
if (agentMap == null) {
agentMap = assetStorageService.findAll(
new AssetQuery().types(Agent.class)
)
.stream()
.filter(asset -> gatewayService.getLocallyRegisteredGatewayId(asset.getId(), null) == null)
.collect(Collectors.toConcurrentMap(Asset::getId, agent -> (Agent, ?, ?>) agent));
}
return agentMap;
}
}
public Protocol> getProtocolInstance(Agent, ?, ?> agent) {
return getProtocolInstance(agent.getId());
}
public Protocol> getProtocolInstance(String agentId) {
return protocolInstanceMap.get(agentId);
}
protected void notifyChildAssetChange(String agentId, PersistenceEvent> assetPersistenceEvent) {
childAssetSubscriptions.computeIfPresent(agentId, (id, consumers) -> {
LOG.finest("Notifying child asset change consumers of change to agent child asset: Agent ID=" + id + ", Asset> ID=" + assetPersistenceEvent.getEntity().getId());
try {
consumers.forEach(consumer -> consumer.accept(assetPersistenceEvent));
} catch (Exception e) {
LOG.log(Level.WARNING, "Child asset change consumer threw an exception: Agent ID=" + id + ", Asset> ID=" + assetPersistenceEvent.getEntity().getId(), e);
}
return consumers;
});
}
public boolean isProtocolAssetDiscoveryOrImportRunning(String agentId) {
return agentDiscoveryImportFutureMap.containsKey(agentId);
}
public Future doProtocolInstanceDiscovery(String parentId, Class extends ProtocolInstanceDiscovery> instanceDiscoveryProviderClass, Consumer[]> onDiscovered) {
LOG.fine("Initiating protocol instance discovery: Provider = " + instanceDiscoveryProviderClass);
Runnable task = () -> {
if (parentId != null && gatewayService.getLocallyRegisteredGatewayId(parentId, null) != null) {
// TODO: Implement gateway instance discovery using client event bus
return;
}
try {
ProtocolInstanceDiscovery instanceDiscovery = instanceDiscoveryProviderClass.getDeclaredConstructor().newInstance();
Future discoveryFuture = instanceDiscovery.startInstanceDiscovery(onDiscovered);
discoveryFuture.get();
} catch (InterruptedException e) {
LOG.fine("Protocol instance discovery was cancelled");
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to do protocol instance discovery: Provider = " + instanceDiscoveryProviderClass, e);
} finally {
LOG.fine("Finished protocol instance discovery: Provider = " + instanceDiscoveryProviderClass);
}
};
return executorService.submit(task, null);
}
public Future doProtocolAssetDiscovery(Agent, ?, ?> agent, Consumer onDiscovered) throws RuntimeException {
Protocol> protocol = getProtocolInstance(agent.getId());
if (protocol == null) {
throw new UnsupportedOperationException("Agent is either invalid, disabled or mis-configured: " + agent);
}
if (!(protocol instanceof ProtocolAssetDiscovery)) {
throw new UnsupportedOperationException("Agent protocol doesn't support asset discovery");
}
LOG.fine("Initiating protocol asset discovery: Agent = " + agent);
synchronized (agentDiscoveryImportFutureMap) {
okToContinueWithImportOrDiscovery(agent.getId());
Runnable task = () -> {
try {
if (gatewayService.getLocallyRegisteredGatewayId(agent.getId(), null) != null) {
// TODO: Implement gateway instance discovery using client event bus
return;
}
ProtocolAssetDiscovery assetDiscovery = (ProtocolAssetDiscovery) protocol;
Future discoveryFuture = assetDiscovery.startAssetDiscovery(onDiscovered);
discoveryFuture.get();
} catch (InterruptedException e) {
LOG.fine("Protocol asset discovery was cancelled");
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to do protocol asset discovery: Agent = " + agent, e);
} finally {
LOG.fine("Finished protocol asset discovery: Agent = " + agent);
agentDiscoveryImportFutureMap.remove(agent.getId());
}
};
Future future = executorService.submit(task, null);
agentDiscoveryImportFutureMap.put(agent.getId(), future);
return future;
}
}
public Future doProtocolAssetImport(Agent, ?, ?> agent, byte[] fileData, Consumer onDiscovered) throws RuntimeException {
Protocol> protocol = getProtocolInstance(agent.getId());
if (protocol == null) {
throw new UnsupportedOperationException("Agent is either invalid, disabled or mis-configured: " + agent);
}
if (!(protocol instanceof ProtocolAssetImport)) {
throw new UnsupportedOperationException("Agent protocol doesn't support asset import");
}
LOG.fine("Initiating protocol asset import: Agent = " + agent);
synchronized (agentDiscoveryImportFutureMap) {
okToContinueWithImportOrDiscovery(agent.getId());
Runnable task = () -> {
try {
if (gatewayService.getLocallyRegisteredGatewayId(agent.getId(), null) != null) {
// TODO: Implement gateway instance discovery using client event bus
return;
}
ProtocolAssetImport assetImport = (ProtocolAssetImport) protocol;
Future discoveryFuture = assetImport.startAssetImport(fileData, onDiscovered);
discoveryFuture.get();
} catch (InterruptedException e) {
LOG.fine("Protocol asset import was cancelled");
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to do protocol asset import: Agent = " + agent, e);
} finally {
LOG.fine("Finished protocol asset import: Agent = " + agent);
agentDiscoveryImportFutureMap.remove(agent.getId());
}
};
Future future = executorService.submit(task, null);
agentDiscoveryImportFutureMap.put(agent.getId(), future);
return future;
}
}
protected void okToContinueWithImportOrDiscovery(String agentId) {
if (agentDiscoveryImportFutureMap.containsKey(agentId)) {
String msg = "Protocol asset discovery or import already running for requested agent: " + agentId;
LOG.fine(msg);
throw new IllegalStateException(msg);
}
}
}