/* * Copyright (C) 2006-2016 DLR, Germany * * All rights reserved * * http://www.rcenvironment.de/ */ package de.rcenvironment.core.communication.common.impl; import java.io.IOException; import java.io.InvalidObjectException; import java.io.ObjectStreamException; import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; import de.rcenvironment.core.communication.api.LiveNetworkIdResolutionService; import de.rcenvironment.core.communication.api.NodeIdentifierService; import de.rcenvironment.core.communication.common.CommonIdBase; import de.rcenvironment.core.communication.common.IdType; import de.rcenvironment.core.communication.common.IdentifierException; import de.rcenvironment.core.communication.common.InstanceNodeId; import de.rcenvironment.core.communication.common.InstanceNodeSessionId; import de.rcenvironment.core.communication.common.LogicalNodeId; import de.rcenvironment.core.communication.common.LogicalNodeSessionId; import de.rcenvironment.core.communication.common.NodeIdentifierContextHolder; import de.rcenvironment.core.communication.common.ResolvableNodeId; import de.rcenvironment.core.communication.model.NodeInformationRegistry; import de.rcenvironment.core.communication.model.SharedNodeInformationHolder; import de.rcenvironment.core.communication.model.internal.SharedNodeInformationHolderImpl; import de.rcenvironment.core.utils.common.StringUtils; /** * A {@link InstanceNodeSessionId} based on a persistent, random, UUID-like identifier. All identity-defining information is immutable; the * associated display name is not. * * @author Robert Mischke */ public class NodeIdentifierImpl implements CommonIdBase, ResolvableNodeId, InstanceNodeId, InstanceNodeSessionId, LogicalNodeId, LogicalNodeSessionId { private static final long serialVersionUID = 5233747521769167502L; private static final String REGEXP_GROUP_SESSION_ID_PART = "([0-9a-f]{" + CommonIdBase.SESSION_PART_LENGTH + "})"; private static final String REGEXP_GROUP_INSTANCE_ID_PART = "([0-9a-f]{" + CommonIdBase.INSTANCE_PART_LENGTH + "})"; // note: this assumes that #DEFAULT_LOGICAL_NODE_PART is regexp-safe, ie only consists of regexp literals private static final String REGEXP_GROUP_LOGICAL_NODE_PART = "(" + CommonIdBase.DEFAULT_LOGICAL_NODE_PART + "|[0-9a-f]{1," + CommonIdBase.MAXIMUM_LOGICAL_NODE_PART_LENGTH + "}" + ")"; private static final Pattern INSTANCE_ID_STRING_PARSE_PATTERN = Pattern.compile(REGEXP_GROUP_INSTANCE_ID_PART); private static final Pattern INSTANCE_SESSION_ID_STRING_PARSE_PATTERN = Pattern.compile(REGEXP_GROUP_INSTANCE_ID_PART + STRING_FORM_PART_SEPARATOR + STRING_FORM_PART_SEPARATOR + REGEXP_GROUP_SESSION_ID_PART); private static final Pattern LOGICAL_NODE_ID_STRING_PARSE_PATTERN = Pattern.compile(REGEXP_GROUP_INSTANCE_ID_PART + STRING_FORM_PART_SEPARATOR + REGEXP_GROUP_LOGICAL_NODE_PART); private static final Pattern LOGICAL_NODE_SESSION_ID_STRING_PARSE_PATTERN = Pattern.compile(REGEXP_GROUP_INSTANCE_ID_PART + STRING_FORM_PART_SEPARATOR + REGEXP_GROUP_LOGICAL_NODE_PART + STRING_FORM_PART_SEPARATOR + REGEXP_GROUP_SESSION_ID_PART); private final IdType idType; private final String instanceIdPart; private final String sessionIdPart; // null for INSTANCE_ID type private final String logicalNodePart; // null for INSTANCE_ID or INSTANCE_SESSION_ID type private final String fullIdString; // cached concatenation of defining parts; always created, as it is needed for lookup anyway private final transient NodeInformationRegistry nodeInformationRegistry; private final transient SharedNodeInformationHolder nodeInformationHolder; private transient byte tempDeserializationTypeMarker; // not synchronized as it is used from a single thread only private transient String tempDeserializationString; // not synchronized as it is used from a single thread only /** * Constructor for the temporary instances used during deserialization; see {@link #readResolve()} implementation. */ protected NodeIdentifierImpl() { this.idType = null; this.instanceIdPart = null; this.sessionIdPart = null; this.logicalNodePart = null; this.fullIdString = null; this.nodeInformationRegistry = null; this.nodeInformationHolder = null; } /** * Public string-parsing constructor that recreates a {@link NodeIdentifierImpl} instance from a string matching one of the string forms * returned by {@link #getFullIdString()}. TODO document patterns here or at that method * * @param input the output-type-specific "full id string" to recreate the instance from * @param nodeInformationRegistry the {@link SharedNodeInformationHolder} to attach to the resulting object * @param expectedInterface the {@link InstanceNodeSessionId} subinterface determining the output object's properties and guarantees, * which also defines the accepted input pattern * @throws IdentifierException on malformed input */ public NodeIdentifierImpl(String input, NodeInformationRegistry nodeInformationRegistry, IdType type) throws IdentifierException { Objects.requireNonNull(input, "Cannot parse 'null' string to a node identifier"); // not allowed since 8.0 this.idType = type; // interface-dependent parsing final Matcher matcher; // declared outside switch() due to Java scoping rules in combination with CheckStyle switch (type) { case INSTANCE_NODE_ID: matcher = matchRegexpOrFail(INSTANCE_ID_STRING_PARSE_PATTERN, input, idType); this.instanceIdPart = matcher.group(1); // always defined and of INSTANCE_ID_LENGTH this.logicalNodePart = null; // never defined this.sessionIdPart = null; // never defined break; case INSTANCE_NODE_SESSION_ID: matcher = matchRegexpOrFail(INSTANCE_SESSION_ID_STRING_PARSE_PATTERN, input, idType); this.instanceIdPart = matcher.group(1); // always defined and of INSTANCE_ID_LENGTH this.logicalNodePart = null; // never defined this.sessionIdPart = matcher.group(2); // always defined and of SESSION_PART_LENGTH break; case LOGICAL_NODE_ID: matcher = matchRegexpOrFail(LOGICAL_NODE_ID_STRING_PARSE_PATTERN, input, idType); this.instanceIdPart = matcher.group(1); // always defined and of INSTANCE_ID_LENGTH this.logicalNodePart = matcher.group(2); // always defined and of SESSION_PART_LENGTH this.sessionIdPart = null; // never defined break; case LOGICAL_NODE_SESSION_ID: matcher = matchRegexpOrFail(LOGICAL_NODE_SESSION_ID_STRING_PARSE_PATTERN, input, idType); this.instanceIdPart = matcher.group(1); // always defined and of INSTANCE_ID_LENGTH this.logicalNodePart = matcher.group(2); // always defined and either the default part string, or // 1..MAXIMUM_LOGICAL_NODE_PART_LENGTH chars long this.sessionIdPart = matcher.group(3); // always defined and of SESSION_PART_LENGTH break; default: throw new RuntimeException("Unexpected id type requested for deserialization: " + idType); } // common fields; do this after parsing to not pollute the registry with potentially invalid keys this.fullIdString = input; this.nodeInformationRegistry = nodeInformationRegistry; this.nodeInformationHolder = selectAppropriateNodeInformationHolder(); checkBasicInternalConsistency(); } /** * Internal low-validation constructor that creates the new "full id string" from the provided id parts. Should be used when no fitting * "full id string" already exists. * * @param instanceIdPart the instance id part to use * @param logicalNodePart the logical node part to use (may be null) * @param sessionIdPart the session id part to use (may be null) * @param nodeInformationRegistry the {@link NodeInformationRegistry} to use */ protected NodeIdentifierImpl(String instanceIdPart, String logicalNodePart, String sessionIdPart, NodeInformationRegistry nodeInformationRegistry, IdType type) { this.instanceIdPart = instanceIdPart; this.sessionIdPart = sessionIdPart; this.logicalNodePart = logicalNodePart; this.nodeInformationRegistry = nodeInformationRegistry; this.idType = type; // note: not using StringUtils.format here as the benefit is unclear for these simple concatenations (using constants) switch (idType) { case INSTANCE_NODE_ID: this.fullIdString = instanceIdPart; // no separator break; case LOGICAL_NODE_ID: this.fullIdString = instanceIdPart + STRING_FORM_PART_SEPARATOR + this.logicalNodePart; // one separator break; case INSTANCE_NODE_SESSION_ID: this.fullIdString = instanceIdPart + STRING_FORM_PART_SEPARATOR + STRING_FORM_PART_SEPARATOR + sessionIdPart; // two separators break; case LOGICAL_NODE_SESSION_ID: this.fullIdString = instanceIdPart + STRING_FORM_PART_SEPARATOR + this.logicalNodePart + STRING_FORM_PART_SEPARATOR + sessionIdPart; // two s. break; default: throw new IllegalStateException(); } this.nodeInformationHolder = selectAppropriateNodeInformationHolder(); checkBasicInternalConsistency(); } /** * Internal low-validation constructor. Should be used when a fitting "full id string" already exists to avoid repeated construction. * * @param instanceIdPart the instance id part to use * @param logicalNodePart the logical node part to use (may be null) * @param sessionIdPart the session id part to use (may be null) * @param fullIdString the full id string to use * @param nodeInformationRegistry the {@link NodeInformationRegistry} to use */ protected NodeIdentifierImpl(String instanceIdPart, String logicalNodePart, String sessionIdPart, String fullIdString, NodeInformationRegistry nodeInformationRegistry, IdType type) { this.instanceIdPart = instanceIdPart; this.sessionIdPart = sessionIdPart; this.logicalNodePart = logicalNodePart; this.fullIdString = fullIdString; this.nodeInformationRegistry = nodeInformationRegistry; this.idType = type; this.nodeInformationHolder = selectAppropriateNodeInformationHolder(); checkBasicInternalConsistency(); } @Override public IdType getType() { return idType; } /** * @return an opaque string representation that can be used to reconstruct the identifier object as accurately as possible */ public String getFullIdString() { return fullIdString; } @Override public String getInstanceNodeIdString() { return instanceIdPart; } @Override public String getSessionIdPart() { return sessionIdPart; } @Override public String getLogicalNodePart() { return logicalNodePart; } @Override public String getInstanceNodeSessionIdString() { switch (idType) { case INSTANCE_NODE_SESSION_ID: return fullIdString; case LOGICAL_NODE_SESSION_ID: // convert to InstanceNodeSessionId string without creating the intermediate id object return instanceIdPart + STRING_FORM_PART_SEPARATOR + STRING_FORM_PART_SEPARATOR + sessionIdPart; default: throw newInvalidIdTypeForThisCallException(); } } @Override public String getLogicalNodeIdString() { if (idType != IdType.LOGICAL_NODE_ID) { throw newInvalidIdTypeForThisCallException(); } return fullIdString; } @Override public String getLogicalNodeSessionIdString() { if (idType != IdType.LOGICAL_NODE_SESSION_ID) { throw newInvalidIdTypeForThisCallException(); } return fullIdString; } @Override public String getLogicalNodeRecognitionPart() { if (idType != IdType.LOGICAL_NODE_ID && idType != IdType.LOGICAL_NODE_SESSION_ID) { throw newInvalidIdTypeForThisCallException(); } if (isTransientLogicalNode() || DEFAULT_LOGICAL_NODE_PART.equals(DEFAULT_LOGICAL_NODE_PART)) { return null; } // consistency check if (!logicalNodePart.startsWith(RECOGNIZABLE_LOGICAL_NODE_PART_PREFIX)) { throw new IllegalStateException("internal consistency error"); } return logicalNodePart.substring(RECOGNIZABLE_LOGICAL_NODE_PART_PREFIX.length()); } @Override public InstanceNodeId convertToInstanceNodeId() { switch (idType) {// the "from" type case INSTANCE_NODE_ID: return this; case INSTANCE_NODE_SESSION_ID: return new NodeIdentifierImpl(instanceIdPart, null, null, nodeInformationRegistry, IdType.INSTANCE_NODE_ID); default: throw new IllegalArgumentException(idType.toString()); } } @Override public InstanceNodeSessionId convertToInstanceNodeSessionId() { switch (idType) { // the "from" type case LOGICAL_NODE_SESSION_ID: return new NodeIdentifierImpl(instanceIdPart, null, sessionIdPart, nodeInformationRegistry, IdType.INSTANCE_NODE_SESSION_ID); default: throw newInvalidConversionException(idType, IdType.INSTANCE_NODE_SESSION_ID); } } @Override public LogicalNodeId convertToLogicalNodeId() { switch (idType) { // the "from" type case LOGICAL_NODE_SESSION_ID: return new NodeIdentifierImpl(instanceIdPart, logicalNodePart, null, nodeInformationRegistry, IdType.LOGICAL_NODE_ID); default: throw newInvalidConversionException(idType, IdType.LOGICAL_NODE_ID); } } @Override public LogicalNodeId convertToDefaultLogicalNodeId() { switch (idType) {// the "from" type case INSTANCE_NODE_ID: case INSTANCE_NODE_SESSION_ID: case LOGICAL_NODE_ID: return new NodeIdentifierImpl(instanceIdPart, DEFAULT_LOGICAL_NODE_PART, null, nodeInformationRegistry, IdType.LOGICAL_NODE_ID); case LOGICAL_NODE_SESSION_ID: // not providing for other types as "default" semantics would be confusing; add a new method if needed throw new RuntimeException("Invalid conversion attempt to 'default' logical node id: " + this); default: throw newInvalidConversionException(idType, IdType.LOGICAL_NODE_ID); } } @Override public LogicalNodeId expandToLogicalNodeId(String nodeIdPart) { if (idType != IdType.INSTANCE_NODE_ID) { throw newInvalidIdTypeForThisCallException(); } return new NodeIdentifierImpl(instanceIdPart, nodeIdPart, null, nodeInformationRegistry, IdType.LOGICAL_NODE_ID); } @Override public LogicalNodeSessionId convertToDefaultLogicalNodeSessionId() { switch (idType) {// the "from" type case INSTANCE_NODE_SESSION_ID: return new NodeIdentifierImpl(instanceIdPart, DEFAULT_LOGICAL_NODE_PART, sessionIdPart, nodeInformationRegistry, IdType.LOGICAL_NODE_SESSION_ID); case LOGICAL_NODE_ID: case LOGICAL_NODE_SESSION_ID: // not providing for other types as "default" semantics would be confusing; add a new method if needed throw new RuntimeException("Invalid conversion attempt to 'default' logical node id: " + this); default: // not providing for other types as "default" semantics would be confusing; add a new method if needed throw newInvalidConversionException(idType, IdType.LOGICAL_NODE_SESSION_ID); } } @Override public LogicalNodeSessionId combineWithInstanceNodeSessionId(InstanceNodeSessionId instanceSessionId) { if (idType != IdType.LOGICAL_NODE_ID) { throw newInvalidIdTypeForThisCallException(); } if (!this.getInstanceNodeIdString().equals(instanceSessionId.getInstanceNodeIdString())) { throw new IllegalStateException("Internal consistency error: The ids to combine cannot refer to different instances! " + this + " / " + instanceSessionId); } // combine return new NodeIdentifierImpl(instanceIdPart, logicalNodePart, instanceSessionId.getSessionIdPart(), nodeInformationRegistry, IdType.LOGICAL_NODE_SESSION_ID); } @Override public boolean isTransientLogicalNode() { return logicalNodePart != null && logicalNodePart.startsWith(TRANSIENT_LOGICAL_NODE_PART_PREFIX); } @Override public boolean isSameInstanceNodeAs(ResolvableNodeId otherId) { if (otherId == null) { throw new NullPointerException("The id to compare " + this + " against can not be null"); } final NodeIdentifierImpl otherIdImpl = (NodeIdentifierImpl) otherId; return instanceIdPart.equals(otherIdImpl.instanceIdPart); } @Override public boolean isSameInstanceNodeSessionAs(InstanceNodeSessionId otherId) { final NodeIdentifierImpl otherIdImpl = (NodeIdentifierImpl) otherId; if (this.idType != IdType.INSTANCE_NODE_SESSION_ID && this.idType != IdType.LOGICAL_NODE_SESSION_ID) { throw new IllegalStateException("Internal error: Tried to compare session identity on a non-session identifier"); } if (otherIdImpl.idType != IdType.INSTANCE_NODE_SESSION_ID) { throw new IllegalStateException("Unexpected parameter type: " + otherIdImpl.idType); } // note: fields cannot be null after internal consistency checks return instanceIdPart.equals(otherIdImpl.instanceIdPart) && sessionIdPart.equals(otherIdImpl.sessionIdPart); } @Override public String getRawAssociatedDisplayName() { return nodeInformationHolder.getDisplayName(); } @Override public String getAssociatedDisplayName() { String displayName = nodeInformationHolder.getDisplayName(); if (displayName == null) { displayName = DEFAULT_DISPLAY_NAME; } // TODO (p1) 9.0.0: preliminary solution, sufficient as these ids are currently only used for SSH forwarding; generalize later if (logicalNodePart != null && !DEFAULT_LOGICAL_NODE_PART.equals(logicalNodePart)) { // custom logical node id -> attach logical node part to name String suffix = getLogicalNodeRecognitionPart(); if (suffix == null) { suffix = logicalNodePart; // fallback for transient logical node ids; not used in 8.0.0, but be prepared for updates } return StringUtils.format("%s [SSH forwarding #%s]", displayName, suffix); } else { // default case, including instance node id types return displayName; } } @Override public boolean equals(Object other) { if (other instanceof NodeIdentifierImpl) { return fullIdString.equals(((NodeIdentifierImpl) other).fullIdString); } else { return false; } } @Override public int hashCode() { return fullIdString.hashCode(); } @Override public String toString() { return StringUtils.format("\"%s\" [%s]", getAssociatedDisplayName(), fullIdString); } /** * Access method for unit tests. * * @return the assigned {@link SharedNodeInformationHolderImpl} */ protected SharedNodeInformationHolder getMetaInformationHolder() { return nodeInformationHolder; } /** * Centralizes the selection of the proper SharedNodeInformationHolder. Requires {@link #nodeInformationRegistry} and all id part fields * to be initialized. */ private SharedNodeInformationHolder selectAppropriateNodeInformationHolder() { switch (idType) { case INSTANCE_NODE_ID: case LOGICAL_NODE_ID: return nodeInformationRegistry.getNodeInformationHolder(instanceIdPart); case INSTANCE_NODE_SESSION_ID: case LOGICAL_NODE_SESSION_ID: return nodeInformationRegistry.getNodeInformationHolder(getInstanceNodeSessionIdString()); default: throw new IllegalArgumentException(); } } // custom serialization hook; see JavaDoc of java.lang.Serializable private void writeObject(java.io.ObjectOutputStream out) throws IOException { // write type byte; chose this approach over a simply Enum field for efficiency out.writeByte(idType.ordinal()); out.writeUTF(fullIdString); } // custom deserialization hook; see JavaDoc of java.lang.Serializable private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { // read the serialized data and store it for the following readResolve() call that creates the immutable instance tempDeserializationTypeMarker = in.readByte(); tempDeserializationString = in.readUTF(); } // object rewriting hook called after deserialization (see JavaDoc of java.lang.Serializable); // required to properly set final/immutable fields (see http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6379948) private Object readResolve() throws ObjectStreamException { if (tempDeserializationString == null) { throw new IllegalStateException("Expected transient deserialization data"); } // TODO transitional; only needed until serialization is phased out NodeIdentifierImpl actualImmutableInstance; try { final IdType targetIdType = IdType.values()[tempDeserializationTypeMarker]; final NodeIdentifierService service = NodeIdentifierContextHolder.getDeserializationServiceForCurrentThread(); actualImmutableInstance = (NodeIdentifierImpl) service.parseSelectableTypeIdString(tempDeserializationString, targetIdType); // could also be added as to verbose logging // LogFactory.getLog(getClass()).debug("Deserialized id " + actualImmutableInstance); } catch (IdentifierException e) { throw new InvalidObjectException("Deserialization failure: " + e); } tempDeserializationString = null; // clear field as a consistency check return actualImmutableInstance; } private Matcher matchRegexpOrFail(Pattern pattern, String input, IdType type) throws IdentifierException { final Matcher matcher = pattern.matcher(input); if (!matcher.matches()) { throw new IdentifierException("'" + input + "' cannot be parsed to a valid " + type); } return matcher; } private void checkBasicInternalConsistency() { final boolean isValid; final int fullIdLength = fullIdString.length(); switch (idType) { case INSTANCE_NODE_ID: isValid = (instanceIdPart != null) && (logicalNodePart == null) && (sessionIdPart == null) && (fullIdLength == INSTANCE_ID_STRING_LENGTH); break; case INSTANCE_NODE_SESSION_ID: isValid = (instanceIdPart != null) && (logicalNodePart == null) && (sessionIdPart != null) && (fullIdLength == INSTANCE_SESSION_ID_STRING_LENGTH); break; case LOGICAL_NODE_ID: isValid = (instanceIdPart != null) && (logicalNodePart != null) && (sessionIdPart == null) && (fullIdLength >= MINIMUM_LOGICAL_NODE_ID_STRING_LENGTH) && (fullIdLength <= MAXIMUM_LOGICAL_NODE_ID_STRING_LENGTH); break; case LOGICAL_NODE_SESSION_ID: isValid = (instanceIdPart != null) && (logicalNodePart != null) && (sessionIdPart != null) && (fullIdLength >= MINIMUM_LOGICAL_NODE_SESSION_ID_STRING_LENGTH) && (fullIdLength <= MAXIMUM_LOGICAL_NODE_SESSION_ID_STRING_LENGTH); break; default: isValid = false; } if (!isValid) { throw new IllegalStateException("Internal id consistency error: " + this.toString()); } } private RuntimeException newInvalidConversionException(IdType from, IdType to) { // these are non-handleable coding errors, so using a RTE is appropriate return new RuntimeException("Converting from " + from + " to " + to + " cannot be done without active resolution; see " + LiveNetworkIdResolutionService.class.getName()); } private IllegalStateException newInvalidIdTypeForThisCallException() { return new IllegalStateException("Invalid id type " + idType + " for this call; id object: " + this.toString()); } }