/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.component.internal; import java.io.IOException; import java.util.ArrayList; 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.Map.Entry; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.codehaus.jackson.map.ObjectMapper; import de.rcenvironment.core.communication.common.InstanceNodeSessionId; import de.rcenvironment.core.communication.common.LogicalNodeId; import de.rcenvironment.core.communication.common.ResolvableNodeId; import de.rcenvironment.core.communication.nodeproperties.NodePropertiesService; import de.rcenvironment.core.communication.nodeproperties.NodeProperty; import de.rcenvironment.core.communication.nodeproperties.spi.NodePropertiesChangeListener; import de.rcenvironment.core.communication.nodeproperties.spi.NodePropertiesChangeListenerAdapter; import de.rcenvironment.core.component.api.DistributedComponentKnowledge; import de.rcenvironment.core.component.api.DistributedComponentKnowledgeService; import de.rcenvironment.core.component.model.api.ComponentInstallation; import de.rcenvironment.core.component.model.impl.ComponentInstallationImpl; import de.rcenvironment.core.component.spi.DistributedComponentKnowledgeListener; import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils; import de.rcenvironment.core.utils.common.JsonUtils; import de.rcenvironment.core.utils.common.service.AdditionalServiceDeclaration; import de.rcenvironment.core.utils.common.service.AdditionalServicesProvider; import de.rcenvironment.core.utils.incubator.DebugSettings; import de.rcenvironment.toolkit.modules.concurrency.api.AsyncCallback; import de.rcenvironment.toolkit.modules.concurrency.api.AsyncCallbackExceptionPolicy; import de.rcenvironment.toolkit.modules.concurrency.api.AsyncOrderedCallbackManager; /** * Default {@link DistributedComponentKnowledgeService} implementation. * * @author Robert Mischke */ public class DistributedComponentKnowledgeServiceImpl implements DistributedComponentKnowledgeService, AdditionalServicesProvider { // private static final String LIST_OF_INSTALLATIONS_PROPERTY = "componentInstallations"; private static final String SINGLE_INSTALLATION_PROPERTY_PREFIX = "componentInstallation/"; private NodePropertiesService nodePropertiesService; private final AsyncOrderedCallbackManager<DistributedComponentKnowledgeListener> componentKnowledgeCallbackManager = ConcurrencyUtils.getFactory().createAsyncOrderedCallbackManager(AsyncCallbackExceptionPolicy.LOG_AND_CANCEL_LISTENER); private volatile DistributedComponentKnowledge currentSnapshot; private final InternalModel internalModel; private final boolean verboseLogging = DebugSettings.getVerboseLoggingEnabled(getClass()); private final Log log = LogFactory.getLog(getClass()); /** * Internal holder for the dynamic model state. Introduced to make the outer class easier to understand (especially with regards to * synchronization). * * @author Robert Mischke */ private static final class InternalModel { private List<ComponentInstallation> immutableLocalState = Collections.unmodifiableList(new ArrayList<ComponentInstallation>()); private Map<InstanceNodeSessionId, Map<String, ComponentInstallation>> dynamicReceivedState = new HashMap<InstanceNodeSessionId, Map<String, ComponentInstallation>>(); private Map<String, String> dynamicPublishedState = new HashMap<String, String>(); } /** * Local {@link DistributedComponentKnowledge} implementation that represents a snapshot of the known component installations on * reachable nodes. * * Note that this class is intended to be immutable for thread-safety, but the contained objects are not immutable yet. * * @author Robert Mischke */ private static final class DistributedComponentKnowledgeSnapshot implements DistributedComponentKnowledge { private final List<ComponentInstallation> localStateAsList; private final List<ComponentInstallation> distributedStateAsList; // keys are instance id strings for now; switch to logical node ids to enable publishing by logical node id private final Map<String, Collection<ComponentInstallation>> distributedStateAsMap; DistributedComponentKnowledgeSnapshot(InternalModel internalModel) { synchronized (internalModel) { // simply copy the local state object as it is immutable localStateAsList = internalModel.immutableLocalState; // convert the dynamic received state into a thread-safe, immutable copy List<ComponentInstallation> tempGlobalList = new ArrayList<ComponentInstallation>(); // flatten map to list; make lists in map unmodifiable Map<String, Collection<ComponentInstallation>> tempMap = new HashMap<String, Collection<ComponentInstallation>>(); // extracted to fix formatter/checkstyle issue in for() line final Set<Entry<InstanceNodeSessionId, Map<String, ComponentInstallation>>> entrySet = internalModel.dynamicReceivedState.entrySet(); for (Map.Entry<InstanceNodeSessionId, Map<String, ComponentInstallation>> entry : entrySet) { // extract InstanceNodeSessionId nodeId = entry.getKey(); Collection<ComponentInstallation> componentsOfNode = entry.getValue().values(); // copy/convert tempGlobalList.addAll(componentsOfNode); //FIXME temporary fix for the problem that published components for a node were overwritten by an empty components //list if older instance session ids for the same node were contained in the internal model //It should be checked if the old instance session ids should be in the model at all (bode_br) ArrayList<ComponentInstallation> componentsToAdd = new ArrayList<ComponentInstallation>(componentsOfNode); if (tempMap.get(nodeId.getInstanceNodeIdString()) != null) { componentsToAdd.addAll(tempMap.get(nodeId.getInstanceNodeIdString())); } tempMap.put(nodeId.getInstanceNodeIdString(), Collections.unmodifiableCollection(componentsToAdd)); } distributedStateAsList = Collections.unmodifiableList(tempGlobalList); distributedStateAsMap = Collections.unmodifiableMap(tempMap); } } @Override public Collection<ComponentInstallation> getPublishedInstallationsOnNode(ResolvableNodeId nodeId) { return distributedStateAsMap.get(nodeId.getInstanceNodeIdString()); // contains immutable collection } @Override public Collection<ComponentInstallation> getAllPublishedInstallations() { return distributedStateAsList; // immutable list } @Override public Collection<ComponentInstallation> getLocalInstallations() { return localStateAsList; } @Override public Collection<ComponentInstallation> getAllInstallations() { Set<ComponentInstallation> allInstallations = new HashSet<ComponentInstallation>( distributedStateAsList.size() + localStateAsList.size()); allInstallations.addAll(distributedStateAsList); allInstallations.addAll(localStateAsList); return allInstallations; } @Override public String toString() { // for debug output only return "Local: " + localStateAsList + ", Distributed: " + distributedStateAsMap; } } public DistributedComponentKnowledgeServiceImpl() { internalModel = new InternalModel(); // create placeholder to use until the first update currentSnapshot = new DistributedComponentKnowledgeSnapshot(internalModel); addDistributedComponentKnowledgeListener(new DistributedComponentKnowledgeListener() { @Override public void onDistributedComponentKnowledgeChanged(DistributedComponentKnowledge newState) { if (verboseLogging) { log.debug("Component knowledge updated: " + newState); } } }); } @Override public Collection<AdditionalServiceDeclaration> defineAdditionalServices() { Collection<AdditionalServiceDeclaration> listenerDeclarations = new ArrayList<AdditionalServiceDeclaration>(); listenerDeclarations.add(new AdditionalServiceDeclaration(NodePropertiesChangeListener.class, new NodePropertiesChangeListenerAdapter() { @Override public void onReachableNodePropertiesChanged(Collection<? extends NodeProperty> addedProperties, Collection<? extends NodeProperty> updatedProperties, Collection<? extends NodeProperty> removedProperties) { // forward to outer class updateOnReachableNodePropertiesChanged(addedProperties, updatedProperties, removedProperties); } })); return listenerDeclarations; } @Override public void setLocalComponentInstallations(Collection<ComponentInstallation> allInstallations, Collection<ComponentInstallation> installationsToPublish) { Map<String, String> delta = new HashMap<String, String>(); SortedSet<String> uniqueIds = new TreeSet<String>(); // update the local state and notify listeners synchronized (internalModel) { internalModel.immutableLocalState = Collections.unmodifiableList(new ArrayList<ComponentInstallation>(allInstallations)); DistributedComponentKnowledgeSnapshot newSnapshot = new DistributedComponentKnowledgeSnapshot(internalModel); // note: callbacks are asynchronous, so triggering them with locks held is safe setNewSnapshot(newSnapshot); // distribute the new published state if necessary for (ComponentInstallation installation : installationsToPublish) { String uniqueId = installation.getInstallationId(); uniqueIds.add(SINGLE_INSTALLATION_PROPERTY_PREFIX + uniqueId); // always serialize to detect changes; could be optimized if changes become frequent String serialized = serializeComponentInstallationData(installation); String propertyId = SINGLE_INSTALLATION_PROPERTY_PREFIX + uniqueId; if (!serialized.equals(internalModel.dynamicPublishedState.get(propertyId))) { // new or modified log.debug("Publishing component descriptor " + uniqueId); delta.put(propertyId, serialized); } } for (String oldId : internalModel.dynamicPublishedState.keySet()) { if (!uniqueIds.contains(oldId) && internalModel.dynamicPublishedState.get(oldId) != null) { // already published, but not public anymore -> remove log.debug("Unpublishing component id " + oldId); delta.put(oldId, null); } } internalModel.dynamicPublishedState.putAll(delta); // add "ids of published installations" meta property // NOTE: currently unused // delta.put(LIST_OF_INSTALLATIONS_PROPERTY, // StringUtils.escapeAndConcat(uniqueIds.toArray(new String[uniqueIds.size()]))); if (!delta.isEmpty()) { nodePropertiesService.addOrUpdateLocalNodeProperties(delta); } } } @Override public DistributedComponentKnowledge getCurrentComponentKnowledge() { // TODO ensure that any caller that registers a listener before calling this method // can never miss intermediate updates return currentSnapshot; } private void updateOnReachableNodePropertiesChanged(Collection<? extends NodeProperty> addedProperties, Collection<? extends NodeProperty> updatedProperties, Collection<? extends NodeProperty> removedProperties) { DistributedComponentKnowledgeSnapshot newSnapshot = null; boolean modified = false; synchronized (internalModel) { for (NodeProperty property : addedProperties) { if (isComponentInstallationProperty(property)) { if (verboseLogging) { log.debug("Parsing new component installation property: " + property); } boolean success = processAddedOrUpdatedProperty(property, false); if (success) { modified = true; } } } for (NodeProperty property : updatedProperties) { if (isComponentInstallationProperty(property)) { if (verboseLogging) { log.debug("Parsing updated component installation property: " + property); } boolean success = processAddedOrUpdatedProperty(property, true); if (success) { modified = true; } } } for (NodeProperty property : removedProperties) { if (isComponentInstallationProperty(property)) { if (verboseLogging) { log.debug("Removing disconnected component installation property: " + property); } boolean success = processRemovedProperty(property); if (success) { modified = true; } } } if (modified) { newSnapshot = new DistributedComponentKnowledgeSnapshot(internalModel); // note: callbacks are asynchronous, so triggering them with locks held is safe setNewSnapshot(newSnapshot); } } } protected void addDistributedComponentKnowledgeListener(DistributedComponentKnowledgeListener listener) { final DistributedComponentKnowledge knowledgeOnRegistrationTime = currentSnapshot; componentKnowledgeCallbackManager.addListenerAndEnqueueCallback(listener, new AsyncCallback<DistributedComponentKnowledgeListener>() { @Override public void performCallback(DistributedComponentKnowledgeListener listener) { listener.onDistributedComponentKnowledgeChanged(knowledgeOnRegistrationTime); } }); } protected void removeDistributedComponentKnowledgeListener(DistributedComponentKnowledgeListener listener) { componentKnowledgeCallbackManager.removeListener(listener); } protected void bindNodePropertiesService(NodePropertiesService newInstance) { nodePropertiesService = newInstance; } private boolean processAddedOrUpdatedProperty(NodeProperty property, boolean isUpdate) { final InstanceNodeSessionId sourceNodeId = property.getInstanceNodeSessionId(); final String propertyKey = property.getKey().substring(SINGLE_INSTALLATION_PROPERTY_PREFIX.length()); final String value = property.getValue(); ComponentInstallation componentInstallation = deserializeComponentInstallationData(value); if (componentInstallation == null) { log.warn("Ignoring invalid component installation entry published by " + sourceNodeId); return false; } // sanity check: installation property published by same node? final LogicalNodeId declaredNodeIdObject = componentInstallation.fetchNodeIdAsObject(); // TODO >=8.0: improve in case of potential instance id collisions? if (!declaredNodeIdObject.isSameInstanceNodeAs(sourceNodeId)) { log.error("Ignoring invalid component installation entry: published by node " + sourceNodeId + ", but allegedly installed on node " + componentInstallation.getNodeId()); return false; } if (componentInstallation.getComponentRevision() == null) { log.error("Ignoring invalid component installation entry: 'null' component revision"); return false; } if (componentInstallation.getComponentRevision().getComponentInterface() == null) { log.error("Ignoring invalid component installation entry: 'null' component interface"); return false; } if (componentInstallation.getComponentRevision().getComponentInterface().getIdentifier() == null) { log.error("Ignoring invalid component installation entry: 'null' component interface id"); return false; } String componentDescriptionId; try { componentDescriptionId = componentInstallation.getComponentRevision() .getComponentInterface().getIdentifier(); } catch (NullPointerException e) { log.warn("Parsed component installation data caused a NPE; ignoring", e); return false; } log.debug("Successfully parsed component installation published by " + sourceNodeId + ": " + componentDescriptionId); Map<String, ComponentInstallation> nodeState = internalModel.dynamicReceivedState.get(sourceNodeId); if (nodeState == null) { nodeState = new HashMap<String, ComponentInstallation>(); internalModel.dynamicReceivedState.put(sourceNodeId, nodeState); } ComponentInstallation previousEntry = nodeState.put(propertyKey, componentInstallation); // internal consistency checks if (isUpdate) { if (previousEntry == null) { log.warn("Unexpected state: received a property update, but there was no previously registered component for key '" + propertyKey + "'; maybe the previous property could not be parsed?"); } } else { if (previousEntry != null) { log.warn("Unexpected state: received a new property, but there was a previously registered component already; key=" + propertyKey); } } return true; } private boolean processRemovedProperty(NodeProperty property) { final InstanceNodeSessionId nodeId = property.getInstanceNodeSessionId(); final String propertyKey = property.getKey().substring(SINGLE_INSTALLATION_PROPERTY_PREFIX.length()); Map<String, ComponentInstallation> nodeState = internalModel.dynamicReceivedState.get(nodeId); if (nodeState == null) { // a component was unpublished, but it was not known to this node before, so ignore it return false; } ComponentInstallation previousEntry = nodeState.remove(propertyKey); if (previousEntry == null) { // a component was unpublished, but it was not known to this node before, so ignore it return false; } log.debug("Successfully removed a component installation previously published by " + nodeId + " (key: " + propertyKey + ")"); return true; } private boolean isComponentInstallationProperty(NodeProperty property) { return property.getKey().startsWith(SINGLE_INSTALLATION_PROPERTY_PREFIX); } private void setNewSnapshot(final DistributedComponentKnowledge newSnapshot) { currentSnapshot = newSnapshot; componentKnowledgeCallbackManager.enqueueCallback(new AsyncCallback<DistributedComponentKnowledgeListener>() { @Override public void performCallback(DistributedComponentKnowledgeListener listener) { listener.onDistributedComponentKnowledgeChanged(newSnapshot); } }); } private String serializeComponentInstallationData(ComponentInstallation ci) { ObjectMapper mapper = JsonUtils.getDefaultObjectMapper(); try { return mapper.writeValueAsString(ci); } catch (IOException e) { log.error("Error serializing component descriptor", e); return null; } } private ComponentInstallation deserializeComponentInstallationData(String jsonData) { ObjectMapper mapper = JsonUtils.getDefaultObjectMapper(); try { return mapper.readValue(jsonData, ComponentInstallationImpl.class); } catch (IOException e) { log.error("Error deserializing component descriptor from JSON data: " + jsonData, e); return null; } } }