From ddcf4124bb78092bac6af54144ce4b019df98c8d Mon Sep 17 00:00:00 2001 From: Jukka Zitting Date: Tue, 4 Feb 2014 13:31:24 -0500 Subject: [PATCH] OAK-533: Define behaviour for and implement mix:lastModified Updated patch based on Radu Cotescu's earlier work --- .../plugins/lastmodified/LastModifiedEditor.java | 130 +++++++++++++++++ .../lastmodified/LastModifiedEditorProvider.java | 57 ++++++++ .../jackrabbit/oak/jcr/MixLastModifiedTest.java | 156 +++++++++++++++++++++ 3 files changed, 343 insertions(+) create mode 100644 oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/lastmodified/LastModifiedEditor.java create mode 100644 oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/lastmodified/LastModifiedEditorProvider.java create mode 100644 oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/MixLastModifiedTest.java diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/lastmodified/LastModifiedEditor.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/lastmodified/LastModifiedEditor.java new file mode 100644 index 0000000..f525864 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/lastmodified/LastModifiedEditor.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.lastmodified; + +import static org.apache.jackrabbit.JcrConstants.JCR_LASTMODIFIED; +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.JCR_LASTMODIFIEDBY; + +import java.util.Calendar; + +import org.apache.jackrabbit.oak.api.CommitFailedException; +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.plugins.nodetype.TypePredicate; +import org.apache.jackrabbit.oak.spi.commit.Editor; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; +import org.apache.jackrabbit.util.ISO8601; + +class LastModifiedEditor implements Editor { + + private final LastModifiedEditor parent; + + private final String name; + + private NodeBuilder builder; + + private final TypePredicate isMixLastModified; + + private final String user; + + private final String date; + + private boolean propertiesChanged; + + private boolean lastModifiedChanged; + + public LastModifiedEditor( + NodeBuilder builder, TypePredicate isMixLastModified, String user) { + this.parent = null; + this.name = null; + this.builder = builder; + this.isMixLastModified = isMixLastModified; + this.user = user; + this.date = ISO8601.format(Calendar.getInstance()); + } + + private LastModifiedEditor(LastModifiedEditor parent, String name) { + this.parent = parent; + this.name = name; + this.builder = null; // initialized lazily + this.isMixLastModified = parent.isMixLastModified; + this.user = parent.user; + this.date = parent.date; + } + + private NodeBuilder getBuilder() { + if (builder == null) { // implies parent != null + builder = parent.getBuilder().getChildNode(name); + } + return builder; + } + + @Override + public void enter(NodeState before, NodeState after) { + propertiesChanged = false; + lastModifiedChanged = false; + } + + @Override + public void leave(NodeState before, NodeState after) { + if (propertiesChanged && !lastModifiedChanged + && isMixLastModified.apply(after)) { + getBuilder().setProperty(JCR_LASTMODIFIED, date); + } + } + + @Override + public void propertyAdded(PropertyState after) throws CommitFailedException { + String name = after.getName(); + if (JCR_LASTMODIFIED.equals(name) || JCR_LASTMODIFIEDBY.equals(name)) { + lastModifiedChanged = true; + } else { + propertiesChanged = true; + } + } + + @Override + public void propertyChanged(PropertyState before, PropertyState after) { + String name = after.getName(); + if (JCR_LASTMODIFIED.equals(name) || JCR_LASTMODIFIEDBY.equals(name)) { + lastModifiedChanged = true; + } else { + propertiesChanged = true; + } + } + + @Override + public void propertyDeleted(PropertyState before) { + propertiesChanged = true; + } + + @Override + public Editor childNodeAdded(String name, NodeState after) { + return new LastModifiedEditor(this, name); + } + + @Override + public Editor childNodeChanged( + String name, NodeState before, NodeState after) { + return new LastModifiedEditor(this, name); + } + + @Override + public Editor childNodeDeleted(String name, NodeState before) { + return null; + } +} diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/lastmodified/LastModifiedEditorProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/lastmodified/LastModifiedEditorProvider.java new file mode 100644 index 0000000..59a8141 --- /dev/null +++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/plugins/lastmodified/LastModifiedEditorProvider.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.plugins.lastmodified; + +import static org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants.MIX_LASTMODIFIED; + +import org.apache.jackrabbit.oak.plugins.nodetype.TypePredicate; +import org.apache.jackrabbit.oak.spi.commit.Editor; +import org.apache.jackrabbit.oak.spi.commit.EditorProvider; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +/** + * This class provides an editor for nodes of type {@code mix:lastModified} + * from section 3.7.11.8 of JSR283: + * + *
+ * [mix:lastModified] mixin
+ * - jcr:lastModified (DATE) autocreated protected? OPV?
+ * - jcr:lastModifiedBy (STRING) autocreated protected? OPV?
+ * 
+ * + * When encountering such nodes, unless either of these properties has + * explicitly been changed, the editor provided by this class will + * automatically set these values. + */ +public class LastModifiedEditorProvider implements EditorProvider { + + private final String user; + + public LastModifiedEditorProvider(String user) { + this.user = user; + } + + @Override + public Editor getRootEditor( + NodeState before, NodeState after, NodeBuilder builder) { + TypePredicate isMixLastModified = + new TypePredicate(after, MIX_LASTMODIFIED); + return new LastModifiedEditor(builder, isMixLastModified, user); + } + +} diff --git a/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/MixLastModifiedTest.java b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/MixLastModifiedTest.java new file mode 100644 index 0000000..f360315 --- /dev/null +++ b/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/MixLastModifiedTest.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.jackrabbit.oak.jcr; + +import static org.junit.Assert.assertTrue; + +import java.util.Calendar; + +import javax.jcr.LoginException; +import javax.jcr.Node; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.api.JackrabbitSession; +import org.apache.jackrabbit.api.security.user.User; +import org.apache.jackrabbit.api.security.user.UserManager; +import org.apache.jackrabbit.oak.plugins.lastmodified.LastModifiedEditorProvider; +import org.apache.jackrabbit.oak.plugins.nodetype.NodeTypeConstants; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class MixLastModifiedTest { + + public static final String TEST_USER_NAME = "user1"; + public static final String TEST_USER_PASSWORD = "user1"; + + private static Repository repository; + + private static Session adminSession; + private static Session session; + private static User user; + + @BeforeClass + public static void beforeClass() throws LoginException, RepositoryException { + repository = new Jcr().with(new LastModifiedEditorProvider("test")).createRepository(); + adminSession = createAdminSession(); + UserManager usrMgr = ((JackrabbitSession) adminSession).getUserManager(); + user = usrMgr.createUser(TEST_USER_NAME, TEST_USER_PASSWORD); + adminSession.save(); + session = repository.login(new SimpleCredentials(user.getID(), TEST_USER_PASSWORD.toCharArray())); + } + + @AfterClass + public static void tearDown() { + if (adminSession != null) { + adminSession.logout(); + } + if (session != null) { + session.logout(); + } + } + + @Test + public void testLastModifiedDate() throws LoginException, RepositoryException { + Node root = adminSession.getRootNode(); + Node foo = root.addNode("foo"); + adminSession.save(); + Node foo1 = foo.addNode("foo1"); + Node foo2 = root.addNode("foo2"); + foo1.addMixin(NodeTypeConstants.MIX_LASTMODIFIED); + Node bar = foo1.addNode("bar"); + foo2.addMixin(NodeTypeConstants.MIX_LASTMODIFIED); + + adminSession.save(); + /** + * /foo/foo1[jcr:mixinTypes=[mix:lastModfified]]/bar + * /foo2[jcr:mixinTypes=[mix:lastModfified]] + */ + + long foo2CreatedAt = foo2.getProperty(JcrConstants.JCR_LASTMODIFIED).getLong(); + + Node foobar = bar.addNode("foobar"); + foobar.addNode("node"); + long foo1TimeBeforeSave = System.currentTimeMillis(); + + adminSession.save(); + /** + * /foo/foo1/bar/foobar/node + * /foo2 + */ + + long foo1TimeAfterSave = System.currentTimeMillis(); + session.refresh(true); + foo1 = session.getNode("/foo/foo1"); + foo2 = session.getNode("/foo2"); + long foo1LastModifiedAt = foo1.getProperty(JcrConstants.JCR_LASTMODIFIED).getLong(); + long foo2LastModifiedAt = foo2.getProperty(JcrConstants.JCR_LASTMODIFIED).getLong(); + assertTrue(foo1TimeBeforeSave < foo1LastModifiedAt && foo1LastModifiedAt < foo1TimeAfterSave); + assertTrue(foo2CreatedAt == foo2LastModifiedAt); + + foo1TimeBeforeSave = System.currentTimeMillis(); + foobar.remove(); + adminSession.save(); + session.refresh(true); + foo1 = session.getNode("/foo/foo1"); + foo1LastModifiedAt = foo1.getProperty(JcrConstants.JCR_LASTMODIFIED).getLong(); + foo1TimeAfterSave = System.currentTimeMillis(); + assertTrue(foo1TimeBeforeSave < foo1LastModifiedAt && foo1LastModifiedAt < foo1TimeAfterSave); + + Calendar calendar = Calendar.getInstance(); + long explicitLastModifiedDate = calendar.getTimeInMillis(); + foo1 = adminSession.getNode("/foo/foo1"); + foo1.setProperty(JcrConstants.JCR_LASTMODIFIED, calendar); + adminSession.save(); + session.refresh(true); + foo1 = session.getNode("/foo/foo1"); + foo1LastModifiedAt = foo1.getProperty(JcrConstants.JCR_LASTMODIFIED).getLong(); + assertTrue(explicitLastModifiedDate == foo1LastModifiedAt); + } + + @Test + public void testLastModifiedBy() throws RepositoryException { + adminSession.refresh(true); + Node foo1 = adminSession.getNode("/foo/foo1"); + Node foo2 = adminSession.getNode("/foo2"); + foo1.setProperty(NodeTypeConstants.JCR_LASTMODIFIEDBY, TEST_USER_NAME); + adminSession.save(); + + session.refresh(true); + foo1 = session.getNode("/foo/foo1"); + foo2 = session.getNode("/foo2"); + assertTrue(TEST_USER_NAME.equals(foo1.getProperty(NodeTypeConstants.JCR_LASTMODIFIEDBY).getString())); + assertTrue("admin".equals(foo2.getProperty(NodeTypeConstants.JCR_LASTMODIFIEDBY).getString())); + + adminSession.refresh(true); + foo1 = adminSession.getNode("/foo/foo1"); + foo1.setProperty("foo1", "foo1"); + adminSession.save(); + + session.refresh(true); + foo1 = session.getNode("/foo/foo1"); + assertTrue("admin".equals(foo1.getProperty(NodeTypeConstants.JCR_LASTMODIFIEDBY).getString())); + } + + private static Session createAdminSession() throws LoginException, RepositoryException { + return repository.login(new SimpleCredentials("admin", "admin".toCharArray())); + } +} -- 1.8.3.msysgit.0