Index: modeshape-graph/src/main/java/org/modeshape/graph/session/GraphSession.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/session/GraphSession.java (revision 2528) +++ modeshape-graph/src/main/java/org/modeshape/graph/session/GraphSession.java (working copy) @@ -187,6 +187,10 @@ public class GraphSession { return context; } + protected Graph.Batch operations() { + return operations; + } + final String readable( Name name ) { return name.getString(context.getNamespaceRegistry()); } Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrContentHandler.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrContentHandler.java (revision 2528) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrContentHandler.java (working copy) @@ -57,6 +57,7 @@ import org.modeshape.graph.property.NameFactory; import org.modeshape.graph.property.NamespaceRegistry; import org.modeshape.graph.property.Path; import org.modeshape.graph.property.PathFactory; +import org.modeshape.jcr.SessionCache.NodeEditor; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; @@ -94,6 +95,8 @@ class JcrContentHandler extends DefaultHandler { private final javax.jcr.NamespaceRegistry jcrNamespaceRegistry; private final SaveMode saveMode; protected final int uuidBehavior; + protected final boolean retentionInfoRetained; + protected final boolean lifecycleInfoRetained; protected final String primaryTypeName; protected final String mixinTypesName; @@ -102,6 +105,7 @@ class JcrContentHandler extends DefaultHandler { private AbstractJcrNode currentNode; private ContentHandler delegate; protected final List refPropsRequiringConstraintValidation = new LinkedList(); + protected final List nodesForPostProcessing = new LinkedList(); private SessionCache cache; @@ -113,7 +117,9 @@ class JcrContentHandler extends DefaultHandler { JcrContentHandler( JcrSession session, Path parentPath, int uuidBehavior, - SaveMode saveMode ) throws PathNotFoundException, RepositoryException { + SaveMode saveMode, + boolean retentionInfoRetained, + boolean lifecycleInfoRetained ) throws PathNotFoundException, RepositoryException { assert session != null; assert parentPath != null; assert uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW @@ -127,6 +133,8 @@ class JcrContentHandler extends DefaultHandler { this.pathFactory = context.getValueFactories().getPathFactory(); this.stringFactory = context.getValueFactories().getStringFactory(); this.uuidBehavior = uuidBehavior; + this.retentionInfoRetained = retentionInfoRetained; + this.lifecycleInfoRetained = lifecycleInfoRetained; this.saveMode = saveMode; switch (this.saveMode) { @@ -200,24 +208,128 @@ class JcrContentHandler extends DefaultHandler { return cache; } + protected void postProcessNodes() throws SAXException { + JcrVersionManager versions = null; + try { + for (AbstractJcrNode node : nodesForPostProcessing) { + NodeEditor editor = null; + // --------------- + // mix:versionable + // --------------- + if (node.isNodeType(JcrMixLexicon.VERSIONABLE)) { + + // At this point, we're not recovering version history information nor properly updating + // existing versionable nodes. Instead, we're just checking whether all of the versionable + // properties are valid. If they are, then the history must have been imported (or the import + // replaced a versionable node that already had history). + boolean validHistory = isValidReference(node, JcrLexicon.VERSION_HISTORY, false); + boolean validBaseVersion = isValidReference(node, JcrLexicon.BASE_VERSION, false); + boolean validPredecessors = isValidReference(node, JcrLexicon.PREDECESSORS, false); + if (validHistory) { + // There is a valid version history already ... + if (!validBaseVersion || !validPredecessors) { + // The imported base version is not valid anymore, so set it to the base version from the history ... + if (versions == null) versions = node.versionManager(); + JcrVersionHistoryNode history = versions.getVersionHistory(node); + UUID baseVersion = history.getRootVersion().uuid(); + JcrValue newValue = node.valueFrom(PropertyType.REFERENCE, baseVersion.toString()); + JcrValue[] newValues = new JcrValue[] {newValue}; + editor = node.editor(); + editor.setProperty(JcrLexicon.BASE_VERSION, newValue, true, false); + editor.setProperty(JcrLexicon.PREDECESSORS, newValues, PropertyType.REFERENCE, false); + } // otherwise the base version and predecessors are valid, too + } else { + // The version history reference is not valid, so remove all mix:versionable properties + // (they'll be re-added during save by SessionCache) ... + editor = node.editor(); + editor.removeProperty(JcrLexicon.IS_CHECKED_OUT); + editor.removeProperty(JcrLexicon.VERSION_HISTORY); + editor.removeProperty(JcrLexicon.BASE_VERSION); + editor.removeProperty(JcrLexicon.PREDECESSORS); + editor.removeProperty(JcrLexicon.MERGE_FAILED); + editor.removeProperty(JcrLexicon.ACTIVITY); + editor.removeProperty(JcrLexicon.CONFIGURATION); + } + } + + // --------------- + // mix:lockable + // --------------- + if (node.isNodeType(JcrMixLexicon.LOCKABLE) && node.isLocked()) { + // Nodes should not be locked upon import ... + node.unlock(); + } + + // --------------- + // mix:lifecycle + // --------------- + if (node.isNodeType(JcrMixLexicon.LIFECYCLE)) { + if (lifecycleInfoRetained && !isValidReference(node, JcrLexicon.LIFECYCLE_POLICY, false)) { + // The 'jcr:lifecyclePolicy' REFERENCE values is not valid or does not reference an existing node, + // so the 'jcr:lifecyclePolicy' and 'jcr:currentLifecycleState' properties should be removed... + if (editor == null) editor = node.editor(); + assert editor != null; + editor.removeProperty(JcrLexicon.LIFECYCLE_POLICY); + editor.removeProperty(JcrLexicon.CURRENT_LIFECYCLE_STATE); + } + } + + // -------------------- + // mix:managedRetention + // -------------------- + if (node.isNodeType(JcrMixLexicon.MANAGED_RETENTION)) { + if (retentionInfoRetained && !isValidReference(node, JcrLexicon.RETENTION_POLICY, false)) { + // The 'jcr:retentionPolicy' REFERENCE values is not valid or does not reference an existing node, + // so the 'jcr:retentionPolicy', 'jcr:hold' and 'jcr:isDeep' properties should be removed ... + if (editor == null) editor = node.editor(); + assert editor != null; + editor.removeProperty(JcrLexicon.HOLD); + editor.removeProperty(JcrLexicon.IS_DEEP); + editor.removeProperty(JcrLexicon.RETENTION_POLICY); + } + + } + } + } catch (RepositoryException e) { + throw new EnclosingSAXException(e); + } + } + + protected boolean isValidReference( AbstractJcrNode node, + Name propertyName, + boolean returnValueIfNoProperty ) throws RepositoryException { + AbstractJcrProperty property = node.getProperty(propertyName); + return property == null ? returnValueIfNoProperty : isValidReference(property); + } + + protected boolean isValidReference( AbstractJcrProperty property ) throws RepositoryException { + JcrPropertyDefinition defn = property.getDefinition(); + if (defn == null) return false; + if (property.isMultiple()) { + for (Value value : property.getValues()) { + if (!defn.canCastToTypeAndSatisfyConstraints(value)) { + // We know it's not valid, so return ... + return false; + } + } + // All values appeared to be valid ... + return true; + } + // Just a single value ... + return defn.canCastToTypeAndSatisfyConstraints(property.getValue()); + } + protected void validateReferenceConstraints() throws SAXException { if (refPropsRequiringConstraintValidation.isEmpty()) return; try { for (AbstractJcrProperty refProp : refPropsRequiringConstraintValidation) { - JcrPropertyDefinition defn = refProp.getDefinition(); - if (refProp.isMultiple()) { - for (Value value : refProp.getValues()) { - if (!defn.canCastToTypeAndSatisfyConstraints(value)) { - String name = stringFor(refProp.name()); - throw new ConstraintViolationException(JcrI18n.constraintViolatedOnReference.text(name, defn)); - } - } - } else { - Value value = refProp.getValue(); - if (!defn.canCastToTypeAndSatisfyConstraints(value)) { - String name = stringFor(refProp.name()); - throw new ConstraintViolationException(JcrI18n.constraintViolatedOnReference.text(name, defn)); - } + // Make sure the reference is still there ... + if (refProp.propertyInfo() == null) continue; + // It is still there, so validate it ... + if (!isValidReference(refProp)) { + JcrPropertyDefinition defn = refProp.getDefinition(); + String name = stringFor(refProp.name()); + throw new ConstraintViolationException(JcrI18n.constraintViolatedOnReference.text(name, defn)); } } } catch (RepositoryException e) { @@ -245,6 +357,7 @@ class JcrContentHandler extends DefaultHandler { */ @Override public void endDocument() throws SAXException { + postProcessNodes(); validateReferenceConstraints(); if (saveMode == SaveMode.WORKSPACE) { try { @@ -397,18 +510,47 @@ class JcrContentHandler extends DefaultHandler { } /** - * The set of properties that should be skipped on import. Currently, this list includes all properties of "mix:lockable", - * since upon import no node should be locked. + * Some nodes need additional post-processing upon import, and this set of property names is used to come up with the nodes + * that may need to be post-processed. + *

+ * Really, the nodes that need to be post-processed are best found using the node types of each node. However, that is more + * expensive to compute. Thus, we'll collect the candidate nodes that are candidates for post-processing, then in the + * post-processing we can more effectively and efficiently use the node types. + *

+ *

+ * Currently, we want to post-process nodes that contain repository-level semantics. In other words, nodes that are of the + * following node types: + *

    + *
  • mix:versionable
  • + *
  • mix:lockable
  • + *
  • mix:lifecycle
  • + *
  • mix:managedRetention
  • + *
+ * The mix:simpleVersionable would normally also be included here, except that the jcr:isCheckedOut + * property is a boolean value that doesn't need any particular post-processing. + *

+ *

+ * Some of these node types has a mandatory property, so the names of these mandatory properties are used to quickly determine + * candidates for post-processing. In cases where there is no mandatory property, then the set of all properties for that node + * type are included: + *

    + *
  • mix:versionable --> jcr:baseVersion (mandatory)
  • + *
  • mix:lockable --> jcr:lockOwner and jcr:lockIsDeep
  • + *
  • mix:lifecycle --> jcr:lifecyclePolicy and jcr:currentLifecycleState
  • + *
  • mix:managedRetention --> jcr:hold, jcr:isDeep, and + * jcr:retentionPolicy
  • + *
+ *

*/ - protected static final Set PROPERTIES_TO_SKIP_ON_IMPORT = Collections.unmodifiableSet(JcrLexicon.LOCK_IS_DEEP, - JcrLexicon.LOCK_OWNER); - - // JcrLexicon.VERSION_HISTORY, - // JcrLexicon.PREDECESSORS, - // JcrLexicon.MERGE_FAILED, - // JcrLexicon.BASE_VERSION, - // JcrLexicon.IS_CHECKED_OUT, - // ); + protected static final Set PROPERTIES_FOR_POST_PROCESSING = Collections.unmodifiableSet( + /* 'mix:lockable' has two optional properties */ + JcrLexicon.LOCK_IS_DEEP, JcrLexicon.LOCK_OWNER, + /* 'mix:versionable' has several mandatory properties, but we only need to check one */ + JcrLexicon.BASE_VERSION, + /* 'mix:lifecycle' has two optional properties */ + JcrLexicon.LIFECYCLE_POLICY, JcrLexicon.CURRENT_LIFECYCLE_STATE, + /* 'mix:managedRetention' has three optional properties */ + JcrLexicon.HOLD, JcrLexicon.IS_DEEP, JcrLexicon.RETENTION_POLICY); protected class BasicNodeHandler extends NodeHandler { private final Map> properties; @@ -416,6 +558,7 @@ class JcrContentHandler extends DefaultHandler { private NodeHandler parentHandler; private AbstractJcrNode node; private final int uuidBehavior; + private boolean postProcessed = false; protected BasicNodeHandler( Name name, NodeHandler parentHandler, @@ -453,11 +596,6 @@ class JcrContentHandler extends DefaultHandler { return parentHandler; } - protected boolean shouldNotImportProperty( Name propertyName ) { - return false; - // return PROPERTIES_TO_SKIP_ON_IMPORT.contains(propertyName); - } - @Override public void addPropertyValue( Name name, String value, @@ -468,7 +606,6 @@ class JcrContentHandler extends DefaultHandler { if (JcrLexicon.PRIMARY_TYPE.equals(name)) return; if (JcrLexicon.MIXIN_TYPES.equals(name)) return; if (JcrLexicon.UUID.equals(name)) return; - if (shouldNotImportProperty(name)) return; // ignore some properties // The node was already created, so set the property using the editor ... node.editor().setProperty(name, (JcrValue)valueFor(value, propertyType)); @@ -499,6 +636,9 @@ class JcrContentHandler extends DefaultHandler { } } } + if (!postProcessed && PROPERTIES_FOR_POST_PROCESSING.contains(name)) { + postProcessed = true; + } } catch (IOException ioe) { throw new EnclosingSAXException(ioe); } catch (RepositoryException re) { @@ -604,11 +744,6 @@ class JcrContentHandler extends DefaultHandler { continue; } - // Should we ignore this property? - if (shouldNotImportProperty(propertyName)) { - continue; - } - List values = entry.getValue(); if (values.size() == 1) { @@ -629,6 +764,12 @@ class JcrContentHandler extends DefaultHandler { } node = child; + + if (postProcessed) { + // This node needs to be post-processed ... + nodesForPostProcessing.add(node); + } + } catch (RepositoryException re) { throw new EnclosingSAXException(re); } Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrLexicon.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrLexicon.java (revision 2528) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrLexicon.java (working copy) @@ -33,10 +33,13 @@ import org.modeshape.graph.property.basic.BasicName; @Immutable public class JcrLexicon extends org.modeshape.graph.JcrLexicon { + public static final Name ACTIVITY = new BasicName(Namespace.URI, "activity"); public static final Name BASE_VERSION = new BasicName(Namespace.URI, "baseVersion"); public static final Name CHILD_VERSION_HISTORY = new BasicName(Namespace.URI, "childVersionHistory"); + public static final Name CONFIGURATION = new BasicName(Namespace.URI, "configuration"); public static final Name CONTENT = new BasicName(Namespace.URI, "content"); public static final Name COPIED_FROM = new BasicName(Namespace.URI, "copiedFrom"); + public static final Name CURRENT_LIFECYCLE_STATE = new BasicName(Namespace.URI, "currentLifecycleState"); public static final Name DATA = new BasicName(Namespace.URI, "data"); public static final Name ENCODING = new BasicName(Namespace.URI, "encoding"); public static final Name ETAG = new BasicName(Namespace.URI, "etag"); @@ -44,8 +47,11 @@ public class JcrLexicon extends org.modeshape.graph.JcrLexicon { public static final Name FROZEN_NODE = new BasicName(Namespace.URI, "frozenNode"); public static final Name FROZEN_PRIMARY_TYPE = new BasicName(Namespace.URI, "frozenPrimaryType"); public static final Name FROZEN_UUID = new BasicName(Namespace.URI, "frozenUuid"); + public static final Name HOLD = new BasicName(Namespace.URI, "hold"); public static final Name IS_CHECKED_OUT = new BasicName(Namespace.URI, "isCheckedOut"); + public static final Name IS_DEEP = new BasicName(Namespace.URI, "isDeep"); public static final Name LANGUAGE = new BasicName(Namespace.URI, "language"); + public static final Name LIFECYCLE_POLICY = new BasicName(Namespace.URI, "lifecyclePolicy"); public static final Name LOCK_IS_DEEP = new BasicName(Namespace.URI, "lockIsDeep"); public static final Name LOCK_OWNER = new BasicName(Namespace.URI, "lockOwner"); public static final Name MERGE_FAILED = new BasicName(Namespace.URI, "mergeFailed"); @@ -53,6 +59,7 @@ public class JcrLexicon extends org.modeshape.graph.JcrLexicon { /** The "jcr:path" pseudo-column used in queries */ public static final Name PATH = new BasicName(Namespace.URI, "path"); public static final Name PREDECESSORS = new BasicName(Namespace.URI, "predecessors"); + public static final Name RETENTION_POLICY = new BasicName(Namespace.URI, "retentionPolicy"); public static final Name ROOT = new BasicName(Namespace.URI, "root"); public static final Name ROOT_VERSION = new BasicName(Namespace.URI, "rootVersion"); /** The "jcr:score" pseudo-column used in queries */ Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrMixLexicon.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrMixLexicon.java (revision 2528) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrMixLexicon.java (working copy) @@ -41,5 +41,13 @@ public class JcrMixLexicon extends org.modeshape.graph.JcrMixLexicon { * The name for the "mix:shareable" mixin. */ public static final Name SHAREABLE = new BasicName(Namespace.URI, "shareable"); + /** + * The name for the "mix:lifecycle" mixin. + */ + public static final Name LIFECYCLE = new BasicName(Namespace.URI, "lifecycle"); + /** + * The name for the "mix:managedRetention" mixin. + */ + public static final Name MANAGED_RETENTION = new BasicName(Namespace.URI, "managedRetention"); } Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrSession.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrSession.java (revision 2528) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrSession.java (working copy) @@ -603,7 +603,9 @@ class JcrSession implements Session { public ContentHandler getImportContentHandler( String parentAbsPath, int uuidBehavior ) throws PathNotFoundException, RepositoryException { Path parentPath = this.executionContext.getValueFactories().getPathFactory().create(parentAbsPath); - return new JcrContentHandler(this, parentPath, uuidBehavior, SaveMode.SESSION); + boolean retainLifecycleInfo = getRepository().getDescriptorValue(Repository.OPTION_LIFECYCLE_SUPPORTED).getBoolean(); + boolean retainRetentionInfo = getRepository().getDescriptorValue(Repository.OPTION_RETENTION_SUPPORTED).getBoolean(); + return new JcrContentHandler(this, parentPath, uuidBehavior, SaveMode.SESSION, retainRetentionInfo, retainLifecycleInfo); } /** Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrVersionHistoryNode.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrVersionHistoryNode.java (revision 2528) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrVersionHistoryNode.java (working copy) @@ -84,7 +84,7 @@ class JcrVersionHistoryNode extends JcrNode implements VersionHistory { * @{inheritDoc */ @Override - public Version getRootVersion() throws RepositoryException { + public JcrVersionNode getRootVersion() throws RepositoryException { // Copied from AbstractJcrNode.getNode(String) to avoid double conversion. Needs to be refactored. Segment segment = context().getValueFactories().getPathFactory().createSegment(JcrLexicon.ROOT_VERSION); try { Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrWorkspace.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrWorkspace.java (revision 2528) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrWorkspace.java (working copy) @@ -41,6 +41,7 @@ import javax.jcr.ItemNotFoundException; import javax.jcr.NoSuchWorkspaceException; import javax.jcr.NodeIterator; import javax.jcr.PathNotFoundException; +import javax.jcr.Repository; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.UnsupportedRepositoryOperationException; @@ -691,7 +692,11 @@ class JcrWorkspace implements Workspace { CheckArg.isNotNull(parentAbsPath, "parentAbsPath"); session.checkLive(); Path parentPath = this.context.getValueFactories().getPathFactory().create(parentAbsPath); - return new JcrContentHandler(this.session, parentPath, uuidBehavior, SaveMode.WORKSPACE); + Repository repo = getSession().getRepository(); + boolean retainLifecycleInfo = repo.getDescriptorValue(Repository.OPTION_LIFECYCLE_SUPPORTED).getBoolean(); + boolean retainRetentionInfo = repo.getDescriptorValue(Repository.OPTION_RETENTION_SUPPORTED).getBoolean(); + return new JcrContentHandler(this.session, parentPath, uuidBehavior, SaveMode.WORKSPACE, retainRetentionInfo, + retainLifecycleInfo); } /** Index: modeshape-jcr/src/main/java/org/modeshape/jcr/SessionCache.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/SessionCache.java (revision 2528) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/SessionCache.java (working copy) @@ -59,6 +59,7 @@ import org.modeshape.common.util.Logger; import org.modeshape.graph.ExecutionContext; import org.modeshape.graph.Graph; import org.modeshape.graph.Location; +import org.modeshape.graph.Graph.Batch; import org.modeshape.graph.connector.RepositorySourceException; import org.modeshape.graph.property.Binary; import org.modeshape.graph.property.BinaryFactory; @@ -146,7 +147,7 @@ class SessionCache { protected final Path rootPath; protected final Name residualName; - private final GraphSession graphSession; + private final CustomGraphSession graphSession; public SessionCache( JcrSession session ) { this(session, session.workspace().getName(), session.getExecutionContext(), session.nodeTypeManager(), session.graph()); @@ -177,8 +178,7 @@ class SessionCache { this.residualName = nameFactory.create(JcrNodeType.RESIDUAL_ITEM_NAME); // Create the graph session, customized for JCR ... - this.graphSession = new GraphSession(this.store, this.workspaceName, - new JcrNodeOperations(), new JcrAuthorizer()); + this.graphSession = new CustomGraphSession(this.store, this.workspaceName, new JcrNodeOperations(), new JcrAuthorizer()); // Set the read-depth if we can... try { int depth = Integer.parseInt(session.repository().getOptions().get(Option.READ_DEPTH)); @@ -187,10 +187,33 @@ class SessionCache { } } + protected class CustomGraphSession extends GraphSession { + CustomGraphSession( Graph graph, + String workspaceName, + Operations nodeOperations, + Authorizer authorizer ) { + super(graph, workspaceName, nodeOperations, authorizer); + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.session.GraphSession#operations() + */ + @Override + protected Batch operations() { + return super.operations(); + } + } + final GraphSession graphSession() { return graphSession; } + Graph.Batch currentBatch() { + return graphSession.operations(); + } + JcrSession session() { return session; }