Index: modeshape-graph/src/main/java/org/modeshape/graph/JcrMixLexicon.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/JcrMixLexicon.java (revision 1901) +++ modeshape-graph/src/main/java/org/modeshape/graph/JcrMixLexicon.java (working copy) @@ -39,4 +39,6 @@ public class JcrMixLexicon { public static final Name REFERENCEABLE = new BasicName(Namespace.URI, "referenceable"); public static final Name VERSIONABLE = new BasicName(Namespace.URI, "versionable"); public static final Name LOCKABLE = new BasicName(Namespace.URI, "lockable"); + public static final Name CREATED = new BasicName(Namespace.URI, "created"); + public static final Name LAST_MODIFIED = new BasicName(Namespace.URI, "lastModified"); } Index: modeshape-jcr/src/main/java/org/modeshape/jcr/AbstractJcrNode.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/AbstractJcrNode.java (revision 1901) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/AbstractJcrNode.java (working copy) @@ -169,6 +169,10 @@ abstract class AbstractJcrNode extends AbstractJcrItem implements javax.jcr.Node return new JcrValue(cache.factories(), cache, propertyType, value); } + final JcrValue valueFrom( String value ) { + return new JcrValue(cache.factories(), cache, PropertyType.STRING, value); + } + final JcrValue valueFrom( Calendar value ) { ValueFactories factories = cache.factories(); DateTime dateTime = factories.getDateFactory().create(value); Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrLexicon.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrLexicon.java (revision 1901) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrLexicon.java (working copy) @@ -38,6 +38,7 @@ public class JcrLexicon extends org.modeshape.graph.JcrLexicon { 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 CREATED = new BasicName(Namespace.URI, "created"); + public static final Name CREATED_BY = new BasicName(Namespace.URI, "createdBy"); public static final Name DATA = new BasicName(Namespace.URI, "data"); public static final Name ENCODING = new BasicName(Namespace.URI, "encoding"); public static final Name FROZEN_MIXIN_TYPES = new BasicName(Namespace.URI, "frozenMixinTypes"); @@ -47,6 +48,7 @@ public class JcrLexicon extends org.modeshape.graph.JcrLexicon { public static final Name IS_CHECKED_OUT = new BasicName(Namespace.URI, "isCheckedOut"); public static final Name LANGUAGE = new BasicName(Namespace.URI, "language"); public static final Name LAST_MODIFIED = new BasicName(Namespace.URI, "lastModified"); + public static final Name LAST_MODIFIED_BY = new BasicName(Namespace.URI, "lastModifiedBy"); 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"); Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrNodeType.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrNodeType.java (revision 1901) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrNodeType.java (working copy) @@ -199,6 +199,15 @@ class JcrNodeType implements NodeType { return propertyDefinitions; } + /** + * Get all of the property definitions defined on this node type and its supertypes. + * + * @return this node's explicit and inherited property definitions; never null + */ + Collection allPropertyDefinitions() { + return allDefinitions.allPropertyDefinitions(); + } + Collection allSingleValuePropertyDefinitions( Name propertyName ) { return allDefinitions.allSingleValuePropertyDefinitions(propertyName); } @@ -211,6 +220,15 @@ class JcrNodeType implements NodeType { return allDefinitions.allPropertyDefinitions(propertyName); } + /** + * Get all of the child node definitions defined on this node type and its supertypes. + * + * @return this node's explicit and inherited child node definitions; never null + */ + Collection allChildNodeDefinitions() { + return allDefinitions.allChildNodeDefinitions(); + } + Collection allChildNodeDefinitions( Name childName, boolean requireSns ) { return allDefinitions.allChildNodeDefinitions(childName, requireSns); @@ -434,6 +452,7 @@ class JcrNodeType implements NodeType { public NodeTypeIterator getDeclaredSubtypes() { return new JcrNodeTypeIterator(nodeTypeManager.declaredSubtypesFor(this)); } + /** * {@inheritDoc} * @@ -538,6 +557,14 @@ class JcrNodeType implements NodeType { return this.thisAndAllSupertypesNames.contains(nodeTypeName); } + boolean isNodeTypeOneOf( Name... nodeTypeNames ) { + if (nodeTypeNames == null || nodeTypeNames.length == 0) return false; + for (Name nodeTypeName : nodeTypeNames) { + if (this.thisAndAllSupertypesNames.contains(nodeTypeName)) return true; + } + return false; + } + /** * {@inheritDoc} * Index: modeshape-jcr/src/main/java/org/modeshape/jcr/SessionCache.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/SessionCache.java (revision 1901) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/SessionCache.java (working copy) @@ -982,6 +982,14 @@ class SessionCache { JcrValue value, boolean skipProtected ) throws AccessDeniedException, ConstraintViolationException, VersionException, RepositoryException { + return setProperty(name, value, skipProtected, false); + } + + protected AbstractJcrProperty setProperty( Name name, + JcrValue value, + boolean skipProtected, + boolean skipLastUpdated ) + throws AccessDeniedException, ConstraintViolationException, VersionException, RepositoryException { assert name != null; assert value != null; @@ -1080,6 +1088,11 @@ class SessionCache { assert jcrProp != null; JcrPropertyPayload propPayload = new JcrPropertyPayload(definition.getId(), propertyType, jcrProp); node.setProperty(dnaProp, definition.isMultiple(), propPayload); + + if (!skipLastUpdated) { + // Update the lastModified properties ... + updateLastModifiedOn(node); + } return jcrProp; } catch (ValidationException e) { throw new ConstraintViolationException(e.getMessage(), e); @@ -1139,6 +1152,16 @@ class SessionCache { boolean skipProtected ) throws AccessDeniedException, ConstraintViolationException, RepositoryException, javax.jcr.ValueFormatException, VersionException { + return setProperty(name, values, valueType, skipProtected, false); + } + + protected AbstractJcrProperty setProperty( Name name, + Value[] values, + int valueType, + boolean skipProtected, + boolean skipLastUpdated ) + throws AccessDeniedException, ConstraintViolationException, RepositoryException, javax.jcr.ValueFormatException, + VersionException { assert name != null; assert values != null; @@ -1290,6 +1313,10 @@ class SessionCache { assert jcrProp != null; JcrPropertyPayload propPayload = new JcrPropertyPayload(definition.getId(), propertyType, jcrProp); node.setProperty(dnaProp, definition.isMultiple(), propPayload); + if (!skipLastUpdated) { + // Update the lastModified properties on the node ... + updateLastModifiedOn(node); + } return jcrProp; } catch (ValidationException e) { throw new ConstraintViolationException(e.getMessage(), e); @@ -1310,6 +1337,8 @@ class SessionCache { */ public boolean removeProperty( Name name ) throws AccessDeniedException, RepositoryException { try { + // Update the lastModified properties on the node ... + updateLastModifiedOn(node); return node.removeProperty(name) != null; } catch (ValidationException e) { throw new ConstraintViolationException(e.getMessage(), e); @@ -1334,6 +1363,8 @@ class SessionCache { Path.Segment before ) throws AccessDeniedException, RepositoryException { try { node.orderChildBefore(childToBeMoved, before); + // Update the lastModified properties on the parent ... + updateLastModifiedOn(node); } catch (ValidationException e) { throw new ConstraintViolationException(e.getMessage(), e); } catch (RepositorySourceException e) { @@ -1394,6 +1425,10 @@ class SessionCache { newChildEditor.removeProperty(ModeShapeIntLexicon.NODE_DEFINITON); } } + // Update the lastModified properties on the old and new parent ... + updateLastModifiedOn(existingChild.getParent()); + updateLastModifiedOn(node); + return existingChild; } catch (ValidationException e) { throw new ConstraintViolationException(e.getMessage(), e); @@ -1446,25 +1481,48 @@ class SessionCache { private void autoCreateItemsFor( JcrNodeType nodeType ) throws InvalidItemStateException, ConstraintViolationException, AccessDeniedException, RepositoryException { - for (JcrPropertyDefinition propertyDefinition : nodeType.propertyDefinitions()) { + for (JcrPropertyDefinition propertyDefinition : nodeType.allPropertyDefinitions()) { if (propertyDefinition.isAutoCreated() && !propertyDefinition.isProtected()) { PropertyInfo autoCreatedProp = node.getProperty(propertyDefinition.getInternalName()); if (autoCreatedProp == null) { // We have to 'auto-create' the property ... - assert propertyDefinition.getDefaultValues() != null; - if (propertyDefinition.isMultiple()) { - setProperty(propertyDefinition.getInternalName(), - propertyDefinition.getDefaultValues(), - propertyDefinition.getRequiredType()); + if (propertyDefinition.getDefaultValues() != null) { + if (propertyDefinition.isMultiple()) { + setProperty(propertyDefinition.getInternalName(), + propertyDefinition.getDefaultValues(), + propertyDefinition.getRequiredType()); + } else { + assert propertyDefinition.getDefaultValues().length == 1; + setProperty(propertyDefinition.getInternalName(), + (JcrValue)propertyDefinition.getDefaultValues()[0]); + } } else { - assert propertyDefinition.getDefaultValues().length == 1; - setProperty(propertyDefinition.getInternalName(), (JcrValue)propertyDefinition.getDefaultValues()[0]); + JcrNodeType definingNodeType = propertyDefinition.declaringNodeType; + Name name = definingNodeType.getInternalName(); + if (name.equals(JcrMixLexicon.CREATED)) { + JcrNode jcrNode = (JcrNode)node.getPayload().getJcrNode(); + JcrValue now = jcrNode.valueFrom(Calendar.getInstance()); + JcrValue by = jcrNode.valueFrom(session().getUserID()); + setProperty(JcrLexicon.CREATED, now, false, true); + setProperty(JcrLexicon.CREATED_BY, by, false, true); + } else if (name.equals(JcrMixLexicon.LAST_MODIFIED)) { + JcrNode jcrNode = (JcrNode)node.getPayload().getJcrNode(); + JcrValue now = jcrNode.valueFrom(Calendar.getInstance()); + JcrValue by = jcrNode.valueFrom(session().getUserID()); + setProperty(JcrLexicon.LAST_MODIFIED, now, false, true); + setProperty(JcrLexicon.LAST_MODIFIED_BY, by, false, true); + } else if (name.equals(JcrNtLexicon.HIERARCHY_NODE)) { + JcrNode jcrNode = (JcrNode)node.getPayload().getJcrNode(); + JcrValue now = jcrNode.valueFrom(Calendar.getInstance()); + setProperty(JcrLexicon.CREATED, now, false, true); + } + // otherwise, we don't care } } } } - for (JcrNodeDefinition nodeDefinition : nodeType.childNodeDefinitions()) { + for (JcrNodeDefinition nodeDefinition : nodeType.allChildNodeDefinitions()) { if (nodeDefinition.isAutoCreated() && !nodeDefinition.isProtected()) { Name nodeName = nodeDefinition.getInternalName(); if (node.getChildrenCount(nodeName) == 0) { @@ -1592,13 +1650,31 @@ class SessionCache { JcrNode jcrNode = (JcrNode)result.getPayload().getJcrNode(); - // Fix the jcr:created protected property for nt:hierarcyNode (and descendants) + // Fix the "jcr:created", "jcr:createdBy", "jcr:lastModified" and "jcr:lastModifiedBy" properties on the new child + // ... + JcrValue now = jcrNode.valueFrom(Calendar.getInstance()); + JcrValue by = jcrNode.valueFrom(session().getUserID()); + boolean isCreatedType = primaryType.isNodeType(JcrMixLexicon.CREATED); + boolean isLastModifiedType = primaryType.isNodeType(JcrMixLexicon.LAST_MODIFIED); boolean isHierarchyNode = primaryType.isNodeType(JcrNtLexicon.HIERARCHY_NODE); - if (isHierarchyNode) { - JcrValue value = jcrNode.valueFrom(Calendar.getInstance()); - jcrNode.editor().setProperty(JcrLexicon.CREATED, value, false); + if (isHierarchyNode || isCreatedType || isLastModifiedType) { + NodeEditor editor = jcrNode.editor(); + if (isHierarchyNode) { + editor.setProperty(JcrLexicon.CREATED, now, false, true); + } + if (isCreatedType) { + editor.setProperty(JcrLexicon.CREATED, now, false, true); + editor.setProperty(JcrLexicon.CREATED_BY, by, false, true); + } + if (isLastModifiedType) { + editor.setProperty(JcrLexicon.LAST_MODIFIED, now, false, true); + editor.setProperty(JcrLexicon.LAST_MODIFIED_BY, by, false, true); + } } + // Update the lastModified properties on the parent ... + updateLastModifiedOn(node, now, by); + // The postCreateChild hook impl should populate the payloads jcrNode.editor().autoCreateItemsFor(primaryType); @@ -1613,6 +1689,45 @@ class SessionCache { } } + protected boolean updateLastModifiedOn( Node node ) throws RepositoryException { + if (isNodeType(node, JcrMixLexicon.LAST_MODIFIED)) { + JcrNode jcrNode = (JcrNode)node.getPayload().getJcrNode(); + JcrValue now = jcrNode.valueFrom(Calendar.getInstance()); + JcrValue by = jcrNode.valueFrom(session().getUserID()); + NodeEditor editor = jcrNode.editor(); + editor.setProperty(JcrLexicon.LAST_MODIFIED, now, false, true); + editor.setProperty(JcrLexicon.LAST_MODIFIED_BY, by, false, true); + return true; + } + return false; + } + + protected boolean updateLastModifiedOn( Node node, + JcrValue now, + JcrValue by ) throws RepositoryException { + if (isNodeType(node, JcrMixLexicon.LAST_MODIFIED)) { + JcrNode jcrNode = (JcrNode)node.getPayload().getJcrNode(); + NodeEditor editor = jcrNode.editor(); + editor.setProperty(JcrLexicon.LAST_MODIFIED, now, false, true); + editor.setProperty(JcrLexicon.LAST_MODIFIED_BY, by, false, true); + return true; + } + return false; + } + + protected boolean updateCreatedOn( Node node, + JcrValue now, + JcrValue by ) throws RepositoryException { + if (isNodeType(node, JcrMixLexicon.CREATED)) { + JcrNode parent = (JcrNode)node.getPayload().getJcrNode(); + NodeEditor editor = parent.editor(); + editor.setProperty(JcrLexicon.CREATED, now, false, true); + editor.setProperty(JcrLexicon.CREATED_BY, by, false, true); + return true; + } + return false; + } + /** * Destroy the child node with the supplied UUID and all nodes that exist below it, including any nodes that were created * and haven't been persisted. @@ -1626,6 +1741,8 @@ class SessionCache { throws AccessDeniedException, RepositoryException { if (!child.getParent().equals(node)) return false; try { + // Update the lastModified properties on the parent ... + updateLastModifiedOn(node); child.destroy(); } catch (AccessControlException e) { throw new AccessDeniedException(e.getMessage(), e); Index: modeshape-jcr/src/test/java/org/modeshape/jcr/AbstractJcrTest.java =================================================================== --- modeshape-jcr/src/test/java/org/modeshape/jcr/AbstractJcrTest.java (revision 1901) +++ modeshape-jcr/src/test/java/org/modeshape/jcr/AbstractJcrTest.java (working copy) @@ -130,6 +130,7 @@ public abstract class AbstractJcrTest { when(jcrSession.getRepository()).thenReturn(repository); when(workspace.getName()).thenReturn(workspaceName); when(jcrSession.isLive()).thenReturn(true); + when(jcrSession.getUserID()).thenReturn("username"); lockManager = new WorkspaceLockManager(context, repository, workspaceName, null); jcrLockManager = new JcrLockManager(jcrSession, lockManager); Index: modeshape-jcr/src/test/java/org/modeshape/jcr/AbstractSessionTest.java =================================================================== --- modeshape-jcr/src/test/java/org/modeshape/jcr/AbstractSessionTest.java (revision 1901) +++ modeshape-jcr/src/test/java/org/modeshape/jcr/AbstractSessionTest.java (working copy) @@ -182,7 +182,7 @@ public abstract class AbstractSessionTest { sessionAttributes.put("attribute1", "value1"); // Now create the workspace ... - SecurityContext mockSecurityContext = new MockSecurityContext(null, Collections.singleton(ModeShapeRoles.READWRITE)); + SecurityContext mockSecurityContext = new MockSecurityContext("username", Collections.singleton(ModeShapeRoles.READWRITE)); workspace = new JcrWorkspace(repository, workspaceName, context.with(mockSecurityContext), sessionAttributes); // Create the session and log in ... Index: modeshape-jcr/src/test/java/org/modeshape/jcr/JcrSessionTest.java =================================================================== --- modeshape-jcr/src/test/java/org/modeshape/jcr/JcrSessionTest.java (revision 1901) +++ modeshape-jcr/src/test/java/org/modeshape/jcr/JcrSessionTest.java (working copy) @@ -200,7 +200,7 @@ public class JcrSessionTest extends AbstractSessionTest { @Test public void shouldProvideUserId() throws Exception { - assertThat(session.getUserID(), nullValue()); + assertThat(session.getUserID(), notNullValue()); Principal principal = Mockito.mock(Principal.class); when(principal.getName()).thenReturn("name"); Subject subject = new Subject(false, Collections.singleton(principal), Collections.EMPTY_SET, Collections.EMPTY_SET);