Index: docs/reference/src/main/docbook/en-US/content/jcr/configuration.xml =================================================================== --- docs/reference/src/main/docbook/en-US/content/jcr/configuration.xml (revision 2627) +++ docs/reference/src/main/docbook/en-US/content/jcr/configuration.xml (working copy) @@ -90,7 +90,7 @@ --> - + Index: extensions/modeshape-search-lucene/src/main/java/org/modeshape/search/lucene/IndexRules.java =================================================================== --- extensions/modeshape-search-lucene/src/main/java/org/modeshape/search/lucene/IndexRules.java (revision 2627) +++ extensions/modeshape-search-lucene/src/main/java/org/modeshape/search/lucene/IndexRules.java (working copy) @@ -415,6 +415,24 @@ public class IndexRules { } /** + * Define a path-based field in the indexes. This method will overwrite any existing definition in this builder. + * + * @param name the name of the field + * @param store the storage setting, or null if the field should be {@link Store#YES stored} + * @param index the index setting, or null if the field should be indexed but {@link Index#NOT_ANALYZED not analyzed} + * @return this builder for convenience and method chaining; never null + */ + public Builder pathField( Name name, + Field.Store store, + Field.Index index ) { + if (store == null) store = Field.Store.YES; + if (index == null) index = Field.Index.NOT_ANALYZED; + Rule rule = new TypedRule(FieldType.STRING, store, index, false, false); + rulesByName.put(name, rule); + return this; + } + + /** * Define a reference-based field in the indexes. This method will overwrite any existing definition in this builder. * * @param name the name of the field Index: modeshape-integration-tests/pom.xml =================================================================== --- modeshape-integration-tests/pom.xml (revision 2627) +++ modeshape-integration-tests/pom.xml (working copy) @@ -18,6 +18,13 @@ --> + com.oracle + ojdbc14 + + 10.0.2.0 + test + + org.modeshape modeshape-common Index: modeshape-integration-tests/src/test/java/org/modeshape/test/integration/sequencer/AbstractSequencerTest.java =================================================================== --- modeshape-integration-tests/src/test/java/org/modeshape/test/integration/sequencer/AbstractSequencerTest.java (revision 2627) +++ modeshape-integration-tests/src/test/java/org/modeshape/test/integration/sequencer/AbstractSequencerTest.java (working copy) @@ -23,11 +23,171 @@ */ package org.modeshape.test.integration.sequencer; +import static org.junit.Assert.fail; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.observation.Event; +import javax.jcr.observation.EventIterator; +import javax.jcr.observation.EventListener; +import org.modeshape.common.SystemFailureException; +import org.modeshape.common.text.Inflector; import org.modeshape.test.integration.AbstractSingleUseModeShapeTest; -/** - * - */ public abstract class AbstractSequencerTest extends AbstractSingleUseModeShapeTest { + /** + * Return a new listener that accumulates the nodes that have been deleted. + * + * @return the new listener + * @throws RepositoryException + */ + public DeleteListener registerListenerForDeletes() throws RepositoryException { + return registerListenerForDeletesBelow("/"); + } + + public DeleteListener registerListenerForDeletesBelow( String path ) throws RepositoryException { + DeleteListener listener = new DeleteListener(session(), path); + listener.register(); + return listener; + } + + public static class DeleteListener implements EventListener { + + private final Session session; + private final String path; + private List> deletedPaths = new ArrayList>(); + private boolean isRegistered = false; + + protected DeleteListener( Session session, + String path ) { + this.session = session; + this.path = path; + } + + /** + * {@inheritDoc} + * + * @see javax.jcr.observation.EventListener#onEvent(javax.jcr.observation.EventIterator) + */ + @Override + public void onEvent( EventIterator events ) { + try { + List deleted = new ArrayList(); + while (events.hasNext()) { + Event event = events.nextEvent(); + deleted.add(event.getPath()); + } + deletedPaths.add(deleted); + } catch (RepositoryException e) { + throw new SystemFailureException(e); + } + } + + public void clear() { + this.deletedPaths.clear(); + } + + public int size() { + int count = 0; + for (List list : deletedPaths) { + count += list.size(); + } + return count; + } + + /** + * @return deletedPaths + */ + public List> getDeletedPaths() { + return deletedPaths; + } + + public void register() throws RepositoryException { + if (isRegistered) return; + session.getWorkspace().getObservationManager().addEventListener(this, + Event.NODE_REMOVED, + path, + true, + null, + null, + false); + isRegistered = true; + } + + public void unregister() throws RepositoryException { + this.session.getWorkspace().getObservationManager().removeEventListener(this); + isRegistered = false; + } + + /** + * Wait at most for the specified time until delete events for nodes the supplied paths have been received. If not all + * events are seen after the supplied time, this method will cause a test failure. + * + * @param maxTimeToWait the maximum time to wait + * @param unit the time unit for the maximum time to wait + * @param paths the paths that are to be deleted (must not be descendants of the deleted nodes) + * @return the set of paths that were not found after timeout; if empty, then all expected events were found + */ + public Set waitForDeleted( long maxTimeToWait, + TimeUnit unit, + String... paths ) { + return waitForDeleted(maxTimeToWait, unit, true, paths); + } + + /** + * Wait at most for the specified time until delete events for nodes the supplied paths have been received. + * + * @param maxTimeToWait the maximum time to wait + * @param unit the time unit for the maximum time to wait + * @param failIfNotAllFound true if this method should cause a test failure should not all events be found, or false if + * this method should return upon failure + * @param paths the paths that are to be deleted (must not be descendants of the deleted nodes) + * @return the set of paths that were not found after timeout; if empty, then all expected events were found + */ + public Set waitForDeleted( long maxTimeToWait, + TimeUnit unit, + boolean failIfNotAllFound, + String... paths ) { + assert paths != null; + Set remainingPaths = new HashSet(); + for (String path : paths) { + if (path != null) remainingPaths.add(path); + } + if (remainingPaths.isEmpty()) return remainingPaths; + + long maxTimeToWaitInMillis = unit.toMillis(maxTimeToWait); + long startTime = System.currentTimeMillis(); + long waitedInMillis = 0L; + while (waitedInMillis < maxTimeToWaitInMillis) { + for (List list : deletedPaths) { + remainingPaths.removeAll(list); + if (remainingPaths.isEmpty()) return remainingPaths; + } + waitedInMillis = System.currentTimeMillis() - startTime; + if (waitedInMillis < maxTimeToWaitInMillis) { + // We haven't reached the tim limit, so wait ... + try { + Thread.sleep(Math.min(50L, maxTimeToWaitInMillis - waitedInMillis)); + } catch (InterruptedException e) { + break; + } + waitedInMillis = System.currentTimeMillis() - startTime; + } + } + if (failIfNotAllFound && !remainingPaths.isEmpty()) { + Inflector inflector = new Inflector(); + String unitName = inflector.pluralize(unit.toString().toLowerCase(), (int)maxTimeToWait); + fail("Waited for " + maxTimeToWait + " " + unitName + " but didn't see events for deletion of: " + remainingPaths + + ". Did see these deletions: " + deletedPaths); + } + return remainingPaths; + } + + } + } Index: modeshape-integration-tests/src/test/java/org/modeshape/test/integration/sequencer/DeleteDerivedContentIntegrationTest.java new file mode 100644 =================================================================== --- /dev/null (revision 2627) +++ modeshape-integration-tests/src/test/java/org/modeshape/test/integration/sequencer/DeleteDerivedContentIntegrationTest.java (working copy) @@ -0,0 +1,115 @@ +/* + * ModeShape (http://www.modeshape.org) + * See the COPYRIGHT.txt file distributed with this work for information + * regarding copyright ownership. Some portions may be licensed + * to Red Hat, Inc. under one or more contributor license agreements. + * See the AUTHORS.txt file in the distribution for a full listing of + * individual contributors. + * + * ModeShape is free software. Unless otherwise indicated, all code in ModeShape + * is licensed to you under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * ModeShape is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.modeshape.test.integration.sequencer; + +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.notNullValue; +import static org.junit.Assert.assertThat; +import java.util.concurrent.TimeUnit; +import javax.jcr.Node; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class DeleteDerivedContentIntegrationTest extends AbstractSequencerTest { + + /** + * {@inheritDoc} + * + * @see org.modeshape.test.integration.sequencer.AbstractSequencerTest#getResourcePathToConfigurationFile() + */ + @Override + protected String getResourcePathToConfigurationFile() { + return "config/configRepositoryForCndSequencing.xml"; + } + + /** + * {@inheritDoc} + * + * @see org.modeshape.test.integration.AbstractSingleUseModeShapeTest#getRepositoryName() + */ + @Override + protected String getRepositoryName() { + return "Content"; + } + + @Before + @Override + public void beforeEach() throws Exception { + super.beforeEach(); + } + + @After + @Override + public void afterEach() throws Exception { + super.afterEach(); + } + + @Test + public void shouldDeleteDerivedContentWhenOriginalFileIsDeleted() throws Exception { + // print = true; + for (int i = 0; i != 2; ++i) { + uploadFile("sequencers/cnd/jsr_283_builtins.cnd", "/files/"); + waitUntilSequencedNodesIs(1 * (i + 1)); + Thread.sleep(200); // wait a bit while the new content is indexed + // printSubgraph(assertNode("/")); + + // Find the sequenced node ... + String derivedPath = "/sequenced/cnd/jsr_283_builtins.cnd"; + Node cnd = assertNode(derivedPath, "nt:unstructured"); + printSubgraph(cnd); + + Node file1 = assertNode(derivedPath + "/nt:activity", "nt:nodeType", "mode:derived"); + assertThat(file1, is(notNullValue())); + + printQuery("SELECT * FROM [mode:derived]", 34); + // printQuery("SELECT * FROM [nt:nodeType]", 34); + // printQuery("SELECT * FROM [nt:propertyDefinition]", 86); + // printQuery("SELECT * FROM [nt:childNodeDefinition]", 10); + + // Register a delete listener ... + DeleteListener listener = registerListenerForDeletes(); + + // Now delete the original node ... + String filePath = "/files/jsr_283_builtins.cnd"; + assertNode(filePath); + session().removeItem(filePath); + session().save(); + + // And wait for the events signalling the original and derived content were deleted. + // The CND sequencer outputs multiple node type definitions, not a single parent node under which these nodes appear. + // Therefore, we need to see delete events for each node definition. + listener.waitForDeleted(5, + TimeUnit.SECONDS, + filePath, + derivedPath + "/nt:activity", + derivedPath + "/nt:base", + derivedPath + "/mode:defined", + derivedPath + "/mix:referenceable", + derivedPath + "/nt:query"); + + printQuery("SELECT * FROM [mode:derived]", 0); + } + } +} Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrEngine.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrEngine.java (revision 2627) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrEngine.java (working copy) @@ -333,6 +333,11 @@ public class JcrEngine extends ModeShapeEngine implements Repositories { } } + // Disable the derived content removal option if not explicitly set and no sequencers ... + if (!options.containsKey(Option.REMOVE_DERIVED_CONTENT_WITH_ORIGINAL) && getSequencingService().getSequencers().isEmpty()) { + options.put(Option.REMOVE_DERIVED_CONTENT_WITH_ORIGINAL, Boolean.FALSE.toString()); + } + // Read the descriptors ... Node descriptorsNode = subgraph.getNode(ModeShapeLexicon.DESCRIPTORS); if (descriptorsNode != null) { Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrI18n.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrI18n.java (revision 2627) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrI18n.java (working copy) @@ -135,6 +135,8 @@ public final class JcrI18n { public static I18n errorImportingNodeTypeContent; public static I18n nodeTypesNotFoundInXml; + public static I18n failedToQueryForDerivedContent; + public static I18n systemSourceNameOptionValueDoesNotReferenceExistingSource; public static I18n systemSourceNameOptionValueDoesNotReferenceValidWorkspace; public static I18n systemSourceNameOptionValueIsNotFormattedCorrectly; Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrRepository.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrRepository.java (revision 2627) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrRepository.java (working copy) @@ -71,9 +71,11 @@ import org.modeshape.graph.ExecutionContext; import org.modeshape.graph.Graph; import org.modeshape.graph.GraphI18n; import org.modeshape.graph.JaasSecurityContext; +import org.modeshape.graph.Location; import org.modeshape.graph.SecurityContext; import org.modeshape.graph.Subgraph; import org.modeshape.graph.Workspace; +import org.modeshape.graph.Graph.Batch; import org.modeshape.graph.connector.RepositoryConnection; import org.modeshape.graph.connector.RepositoryConnectionFactory; import org.modeshape.graph.connector.RepositoryContext; @@ -86,8 +88,10 @@ import org.modeshape.graph.connector.federation.ProjectionParser; import org.modeshape.graph.connector.inmemory.InMemoryRepositorySource; import org.modeshape.graph.connector.xmlfile.XmlFileRepositorySource; import org.modeshape.graph.observe.Changes; +import org.modeshape.graph.observe.NetChangeObserver; import org.modeshape.graph.observe.Observable; import org.modeshape.graph.observe.Observer; +import org.modeshape.graph.property.DateTime; import org.modeshape.graph.property.Name; import org.modeshape.graph.property.NamespaceRegistry; import org.modeshape.graph.property.Path; @@ -95,8 +99,16 @@ import org.modeshape.graph.property.PathFactory; import org.modeshape.graph.property.Property; import org.modeshape.graph.property.PropertyFactory; import org.modeshape.graph.property.ValueFactories; +import org.modeshape.graph.property.ValueFactory; import org.modeshape.graph.property.basic.GraphNamespaceRegistry; +import org.modeshape.graph.query.QueryBuilder; +import org.modeshape.graph.query.QueryResults; +import org.modeshape.graph.query.QueryBuilder.ConstraintBuilder; +import org.modeshape.graph.query.model.QueryCommand; +import org.modeshape.graph.query.model.Visitors; import org.modeshape.graph.query.parse.QueryParsers; +import org.modeshape.graph.query.plan.PlanHints; +import org.modeshape.graph.query.validate.Schemata; import org.modeshape.graph.request.ChangeRequest; import org.modeshape.graph.request.InvalidWorkspaceException; import org.modeshape.jcr.RepositoryQueryManager.PushDown; @@ -193,7 +205,7 @@ public class JcrRepository implements Repository { /** * The depth of the subgraphs that should be loaded from the connectors during indexing operations. The default value is - * 10. + * 4. */ INDEX_READ_DEPTH, @@ -216,7 +228,7 @@ public class JcrRepository implements Repository { /** * A boolean flag that specifies whether this repository is expected to execute searches and queries. If client - * applications will never perform searches or queries, then maintaining the query indexes is an unncessary overhead, and + * applications will never perform searches or queries, then maintaining the query indexes is an unnecessary overhead, and * can be disabled. Note that this is merely a hint, and that searches and queries might still work when this is set to * 'false'. *

@@ -325,7 +337,22 @@ public class JcrRepository implements Repository { * {@link VersionHistoryOption#FLAT} or {@link VersionHistoryOption#HIERARCHICAL} values. *

*/ - VERSION_HISTORY_STRUCTURE; + VERSION_HISTORY_STRUCTURE, + + /** + * A boolean option that dictates whether content derived from other content (e.g., by sequencers) should be automatically + * removed when the content from which it was derived is removed from the repository. + *

+ * For example, consider that a file is uploaded and sequenced, and that the content derived from the file is stored in + * the repository. When that file is removed, this option dictates whether the derived content should also be removed + * automatically. + *

+ *

+ * A value of 'true' will ensure that all content derived from deleted content is also deleted. A value of 'false' will + * leave the derived content. The default value is 'true'. + *

+ */ + REMOVE_DERIVED_CONTENT_WITH_ORIGINAL, ; /** * Determine the option given the option name. This does more than {@link Option#valueOf(String)}, since this method first @@ -447,6 +474,11 @@ public class JcrRepository implements Repository { * The default value for the {@link Option#VERSION_HISTORY_STRUCTURE} option is {@value} . */ public static final String VERSION_HISTORY_STRUCTURE = VersionHistoryOption.HIERARCHICAL; + + /** + * The default value for the {@link Option#REMOVE_DERIVED_CONTENT_WITH_ORIGINAL} option is {@value} . + */ + public static final String REMOVE_DERIVED_CONTENT_WITH_ORIGINAL = Boolean.TRUE.toString(); } /** @@ -508,6 +540,7 @@ public class JcrRepository implements Repository { defaults.put(Option.EXPOSE_WORKSPACE_NAMES_IN_DESCRIPTOR, DefaultOption.EXPOSE_WORKSPACE_NAMES_IN_DESCRIPTOR); defaults.put(Option.VERSION_HISTORY_STRUCTURE, DefaultOption.VERSION_HISTORY_STRUCTURE); defaults.put(Option.REPOSITORY_JNDI_LOCATION, DefaultOption.REPOSITORY_JNDI_LOCATION); + defaults.put(Option.REMOVE_DERIVED_CONTENT_WITH_ORIGINAL, DefaultOption.REMOVE_DERIVED_CONTENT_WITH_ORIGINAL); DEFAULT_OPTIONS = Collections.unmodifiableMap(defaults); } @@ -850,6 +883,11 @@ public class JcrRepository implements Repository { repositoryObservationManager.register(new SystemChangeObserver(Arrays.asList(new JcrSystemObserver[] { repositoryLockManager, namespaceObserver, repositoryTypeManager}))); + if (Boolean.valueOf(this.options.get(Option.REMOVE_DERIVED_CONTENT_WITH_ORIGINAL))) { + // Add an observer that moves/removes derived content when the original is moved/removed ... + repositoryObservationManager.register(new DerivedContentSynchronizer()); + } + // If the JNDI Location is set and not trivial, attempt the bind. String jndiLocation = this.options.get(Option.REPOSITORY_JNDI_LOCATION); if (!jndiLocation.equals("")) { @@ -2009,4 +2047,126 @@ public class JcrRepository implements Repository { } } } + + class DerivedContentSynchronizer extends NetChangeObserver { + /** + * {@inheritDoc} + * + * @see org.modeshape.graph.observe.NetChangeObserver#notify(org.modeshape.graph.observe.NetChangeObserver.NetChanges) + */ + @Override + protected void notify( NetChanges netChanges ) { + // Go through the changes and look for moves or removes, and accumulate the paths that we should search for ... + Map> pathsByWorkspaceName = null; + for (NetChange change : netChanges.getNetChanges()) { + String workspaceName = change.getRepositoryWorkspaceName(); + + // Don't watch the system workspace ... + if (getSystemWorkspaceName().equals(workspaceName)) continue; + + // Go through each net change, and only process node/property adds and property changes ... + if (change.includes(ChangeType.NODE_REMOVED, ChangeType.NODE_MOVED)) { + if (pathsByWorkspaceName == null) pathsByWorkspaceName = new HashMap>(); + Map paths = pathsByWorkspaceName.get(change.getRepositoryWorkspaceName()); + if (paths == null) { + paths = new HashMap(); + pathsByWorkspaceName.put(change.getRepositoryWorkspaceName(), paths); + } + Path newPath = null; + if (change.includes(ChangeType.NODE_MOVED)) { + newPath = change.getLocation().getPath(); + } + paths.put(change.getPath(), newPath); + } + } + + if (pathsByWorkspaceName == null) { + // No removes or deletes ... + return; + } + + // We should have at least one query ... + final ExecutionContext context = getExecutionContext(); + QueryBuilder builder = new QueryBuilder(context.getValueFactories().getTypeSystem()); + DateTime timestamp = netChanges.getTimestamp(); + Schemata schemata = getRepositoryTypeManager().getRepositorySchemata(); + Map variables = null; + QueryCommand query = null; + String workspaceName = null; + PathFactory pathFactory = context.getValueFactories().getPathFactory(); + ValueFactory strings = context.getValueFactories().getStringFactory(); + + try { + + // Query for 'mode:derived' nodes that were derived from any content at/under these paths ... + for (Map.Entry> entry : pathsByWorkspaceName.entrySet()) { + workspaceName = entry.getKey(); + Map newPathByOld = entry.getValue(); + Set paths = newPathByOld.keySet(); + + // Build a query for each workspace ... + ConstraintBuilder constraint = builder.select("jcr:path", "mode:derivedFrom", "mode:derivedAt") + .from("mode:derived") + .where(); + constraint = constraint.propertyValue("mode:derived", "mode:derivedAt") + .isLessThanOrEqualTo() + .literal(timestamp) + .and() + .openParen(); + boolean first = true; + for (Path path : paths) { + if (first) first = false; + else constraint = constraint.or(); + constraint = constraint.propertyValue("mode:derived", "mode:derivedFrom") + .isEqualTo() + .literal(strings.create(path)) + .or() + .propertyValue("mode:derived", "mode:derivedFrom") + .isLike(strings.create(path) + "/%"); + } + constraint = constraint.closeParen(); + query = constraint.end().query(); + + // Submit the query ... + PlanHints hints = new PlanHints(); + QueryResults results = queryManager().query(workspaceName, query, schemata, hints, variables); + int locIndex = results.getColumns().getLocationIndex("mode:derived"); + int fromIndex = results.getColumns().getColumnIndexForName("mode:derivedFrom"); + Batch batch = createWorkspaceGraph(workspaceName, context).batch(); + for (Object[] tuple : results.getTuples()) { + Location derivedLocation = (Location)tuple[locIndex]; + Path derivedFrom = pathFactory.create(tuple[fromIndex]); + // Find out which of the changed paths this corresponds. Note that we have to walk the changed paths + // because the changed paths may be ancestors) .. + for (Path path : paths) { + if (derivedFrom.isAtOrBelow(path)) { + // The derived location should only be below one of the changed paths ... + Path changedToPath = newPathByOld.get(path); + if (changedToPath != null) { + // This is a move, so figure out the new derivedFrom path ... + Path relative = derivedFrom.relativeTo(path); + Path newDerivedFrom = relative.resolveAgainst(changedToPath); + batch.set(ModeShapeLexicon.DERIVED_FROM).on(derivedLocation).to(newDerivedFrom).and(); + } else { + // The changed node was deleted ... + batch.delete(derivedLocation).and(); + } + break; + } + } + } + builder.clear(); + + // Execute the batch ... + batch.execute(); + } + } catch (RepositoryException e) { + String queryStr = Visitors.readable(query); + Logger.getLogger(JcrRepository.this.getClass()).error(e, + JcrI18n.failedToQueryForDerivedContent, + workspaceName, + queryStr); + } + } + } } Index: modeshape-jcr/src/main/java/org/modeshape/jcr/NodeTypeSchemata.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/NodeTypeSchemata.java (revision 2627) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/NodeTypeSchemata.java (working copy) @@ -326,8 +326,7 @@ class NodeTypeSchemata implements Schemata { store = Store.NO; builder.referenceField(defn.getInternalName(), store, index); } else if (typeSystem.getPathFactory().getTypeName().equals(type)) { - store = Store.NO; - builder.weakReferenceField(defn.getInternalName(), store, index, defn.isFullTextSearchable()); + builder.pathField(defn.getInternalName(), store, index); } else { // Everything else gets stored as a string ... builder.stringField(defn.getInternalName(), store, index, canBeReference, defn.isFullTextSearchable()); Index: modeshape-jcr/src/main/resources/org/modeshape/jcr/JcrI18n.properties =================================================================== --- modeshape-jcr/src/main/resources/org/modeshape/jcr/JcrI18n.properties (revision 2627) +++ modeshape-jcr/src/main/resources/org/modeshape/jcr/JcrI18n.properties (working copy) @@ -133,6 +133,8 @@ supertypeNotFound=Could not find type "{0}" which is a required supertype of typ errorImportingNodeTypeContent = Error importing node types from "{0}": {1} nodeTypesNotFoundInXml = No valid node types elements found in the XML in "{0}" +failedToQueryForDerivedContent = Error while querying for content in workspace "{0}" derived from (re)moved paths, using this query: {1} + systemSourceNameOptionValueDoesNotReferenceExistingSource = The JCR Repository 'SYSTEM_SOURCE_NAME' option value "{0}" references an invalid or non-existant source "{1}" systemSourceNameOptionValueDoesNotReferenceValidWorkspace = The JCR Repository 'SYSTEM_SOURCE_NAME' option value "{0}" references an invalid or non-existant workspace in the "{1}" source systemSourceNameOptionValueIsNotFormattedCorrectly = The JCR Repository 'SYSTEM_SOURCE_NAME' option value "{0}" is invalid or improperly formatted Index: modeshape-jcr/src/main/resources/org/modeshape/jcr/modeshape_builtins.cnd =================================================================== --- modeshape-jcr/src/main/resources/org/modeshape/jcr/modeshape_builtins.cnd (revision 2627) +++ modeshape-jcr/src/main/resources/org/modeshape/jcr/modeshape_builtins.cnd (working copy) @@ -89,4 +89,5 @@ [mode:publishArea] > mix:title mixin [mode:derived] mixin -- mode:derivedFrom (path) +- mode:derivedFrom (path) // the location of the original information from which this was derived +- mode:derivedAt (date) // the timestamp of the last change to the original information from which this was derived Index: modeshape-jcr/src/test/java/org/modeshape/jcr/JcrConfigurationTest.java =================================================================== --- modeshape-jcr/src/test/java/org/modeshape/jcr/JcrConfigurationTest.java (revision 2627) +++ modeshape-jcr/src/test/java/org/modeshape/jcr/JcrConfigurationTest.java (working copy) @@ -260,6 +260,11 @@ public class JcrConfigurationTest { options.put(Option.EXPOSE_WORKSPACE_NAMES_IN_DESCRIPTOR, DefaultOption.EXPOSE_WORKSPACE_NAMES_IN_DESCRIPTOR); options.put(Option.VERSION_HISTORY_STRUCTURE, DefaultOption.VERSION_HISTORY_STRUCTURE); options.put(Option.REPOSITORY_JNDI_LOCATION, DefaultOption.REPOSITORY_JNDI_LOCATION); + String defaultRemoveDerivedValue = DefaultOption.REMOVE_DERIVED_CONTENT_WITH_ORIGINAL; + if (engine.getSequencingService().getSequencers().isEmpty()) { + defaultRemoveDerivedValue = Boolean.FALSE.toString(); + } + options.put(Option.REMOVE_DERIVED_CONTENT_WITH_ORIGINAL, defaultRemoveDerivedValue); assertThat(repository.getOptions(), is(options)); } Index: modeshape-jcr/src/test/resources/tck/default/configRepository.xml =================================================================== --- modeshape-jcr/src/test/resources/tck/default/configRepository.xml (revision 2627) +++ modeshape-jcr/src/test/resources/tck/default/configRepository.xml (working copy) @@ -69,10 +69,10 @@ Store - + - + + - + Index: modeshape-repository/src/main/java/org/modeshape/repository/ModeShapeLexicon.java =================================================================== --- modeshape-repository/src/main/java/org/modeshape/repository/ModeShapeLexicon.java (revision 2627) +++ modeshape-repository/src/main/java/org/modeshape/repository/ModeShapeLexicon.java (working copy) @@ -53,6 +53,7 @@ public class ModeShapeLexicon extends org.modeshape.graph.ModeShapeLexicon { public static final Name CLUSTER_NAME = new BasicName(Namespace.URI, "clusterName"); public static final Name DERIVED = new BasicName(Namespace.URI, "derived"); public static final Name DERIVED_FROM = new BasicName(Namespace.URI, "derivedFrom"); + public static final Name DERIVED_AT = new BasicName(Namespace.URI, "derivedAt"); public static final Name INITIAL_CONTENT = new BasicName(Namespace.URI, "initialContent"); public static final Name CONTENT = new BasicName(Namespace.URI, "content"); Index: modeshape-repository/src/main/java/org/modeshape/repository/sequencer/SequencerContext.java =================================================================== --- modeshape-repository/src/main/java/org/modeshape/repository/sequencer/SequencerContext.java (revision 2627) +++ modeshape-repository/src/main/java/org/modeshape/repository/sequencer/SequencerContext.java (working copy) @@ -5,6 +5,7 @@ import org.modeshape.graph.ExecutionContext; import org.modeshape.graph.Graph; import org.modeshape.graph.io.Destination; import org.modeshape.graph.io.GraphBatchDestination; +import org.modeshape.graph.property.DateTime; /** * The sequencer context represents the complete context of a sequencer invocation, including the execution context (which @@ -20,19 +21,23 @@ public class SequencerContext { private final Graph sourceGraph; private final Graph destinationGraph; private final Destination destination; + private final DateTime timestamp; public SequencerContext( ExecutionContext executionContext, Graph sourceGraph, - Graph outputGraph ) { + Graph outputGraph, + DateTime timestamp ) { super(); assert executionContext != null; assert sourceGraph != null; + assert timestamp != null; this.executionContext = executionContext; this.sourceGraph = sourceGraph; this.destinationGraph = outputGraph != null ? outputGraph : sourceGraph; this.destination = new GraphBatchDestination(destinationGraph.batch()); + this.timestamp = timestamp; } /** @@ -45,6 +50,15 @@ public class SequencerContext { } /** + * Get the timestamp of the sequencing. This is always the timestamp of the change event that is being processed. + * + * @return timestamp the "current" timestamp; never null + */ + public DateTime getTimestamp() { + return timestamp; + } + + /** * Returns the I/O environment in which this sequencer context operates * * @return the I/O environment in which this sequencer context operates Index: modeshape-repository/src/main/java/org/modeshape/repository/sequencer/SequencingService.java =================================================================== --- modeshape-repository/src/main/java/org/modeshape/repository/sequencer/SequencingService.java (revision 2627) +++ modeshape-repository/src/main/java/org/modeshape/repository/sequencer/SequencingService.java (working copy) @@ -489,7 +489,8 @@ public class SequencingService implements AdministeredService { Graph outputGraph = Graph.create(outputSource, context); final SimpleProblems problems = new SimpleProblems(); - SequencerContext sequencerContext = new SequencerContext(context, sourceGraph, outputGraph); + SequencerContext sequencerContext = new SequencerContext(context, sourceGraph, outputGraph, + changes.getTimestamp()); try { sequencer.execute(node, propertyName, change, outputPathsInSource, sequencerContext, problems); sequencerContext.getDestination().submit(); Index: modeshape-repository/src/main/java/org/modeshape/repository/sequencer/StreamSequencerAdapter.java =================================================================== --- modeshape-repository/src/main/java/org/modeshape/repository/sequencer/StreamSequencerAdapter.java (revision 2627) +++ modeshape-repository/src/main/java/org/modeshape/repository/sequencer/StreamSequencerAdapter.java (working copy) @@ -440,7 +440,7 @@ public class StreamSequencerAdapter implements Sequencer { Property newMixinTypes = propertyFactory.create(JcrLexicon.MIXIN_TYPES, values); propertiesByName.put(newMixinTypes.getName(), newMixinTypes); - // Add the other 'mode:derived' property/properties ... + // Add the 'mode:derivedFrom' property ... Property derivedFrom = propertiesByName.get(ModeShapeLexicon.DERIVED_FROM); if (derivedFrom == null) { // Only do this if the sequencer didn't already do this ... @@ -448,6 +448,15 @@ public class StreamSequencerAdapter implements Sequencer { propertiesByName.put(derivedFrom.getName(), derivedFrom); } + // Add the 'mode:derivedAt' property ... + Property derivedOn = propertiesByName.get(ModeShapeLexicon.DERIVED_AT); + if (derivedOn == null) { + // Only do this if the sequencer didn't already do this ... + // The timestamp should match that of the change event. + derivedOn = propertyFactory.create(ModeShapeLexicon.DERIVED_AT, context.getTimestamp()); + propertiesByName.put(derivedOn.getName(), derivedOn); + } + // Return the properties ... return propertiesByName.values(); } Index: modeshape-repository/src/test/java/org/modeshape/repository/sequencer/StreamSequencerAdapterTest.java =================================================================== --- modeshape-repository/src/test/java/org/modeshape/repository/sequencer/StreamSequencerAdapterTest.java (revision 2627) +++ modeshape-repository/src/test/java/org/modeshape/repository/sequencer/StreamSequencerAdapterTest.java (working copy) @@ -52,6 +52,7 @@ import org.modeshape.graph.Node; import org.modeshape.graph.connector.inmemory.InMemoryRepositorySource; import org.modeshape.graph.observe.NetChangeObserver.ChangeType; import org.modeshape.graph.observe.NetChangeObserver.NetChange; +import org.modeshape.graph.property.DateTime; import org.modeshape.graph.property.Name; import org.modeshape.graph.property.Path; import org.modeshape.graph.property.PathNotFoundException; @@ -82,11 +83,13 @@ public class StreamSequencerAdapterTest { private Problems problems; private Graph graph; private Property sequencedProperty; + private DateTime now; @Before public void beforeEach() { problems = new SimpleProblems(); this.context = new ExecutionContext(); + this.now = this.context.getValueFactories().getDateFactory().create(); this.sequencerOutput = new SequencerOutputMap(this.context.getValueFactories()); final SequencerOutputMap finalOutput = sequencerOutput; @@ -111,7 +114,7 @@ public class StreamSequencerAdapterTest { } }; sequencer = new StreamSequencerAdapter(streamSequencer, false); - seqContext = new SequencerContext(context, graph, graph); + seqContext = new SequencerContext(context, graph, graph, now); } protected Path path( String path ) { @@ -606,7 +609,7 @@ public class StreamSequencerAdapterTest { InMemoryRepositorySource source2 = new InMemoryRepositorySource(); source2.setName(repositorySourceName2); Graph graph2 = Graph.create(source2.getConnection(), context); - seqContext = new SequencerContext(context, graph, graph2); + seqContext = new SequencerContext(context, graph, graph2, now); // Set up the node that will be sequenced ... graph.create("/a").and().create("/a/b").and().create("/a/b/c").and(); @@ -675,10 +678,12 @@ public class StreamSequencerAdapterTest { props = nodeA.getPropertiesByName(); assertThat(nodeA.getChildren().size(), is(2)); - assertThat(props.size(), is(4)); // Need to add one to account for dna:uuid, jcr:mixinTypes and mode:derivedFrom + assertThat(props.size(), is(5)); // Need to add one to account for dna:uuid, jcr:mixinTypes, mode:derivedFrom, + // mode:derivedAt assertThat(props.get(nameFor("property1")).getFirstValue().toString(), is("value1")); assertThat(props.get(JcrLexicon.MIXIN_TYPES).getFirstValue(), is((Object)ModeShapeLexicon.DERIVED)); assertThat(props.get(ModeShapeLexicon.DERIVED_FROM).getFirstValue(), is((Object)inputPath)); + assertThat(props.get(ModeShapeLexicon.DERIVED_AT).getFirstValue(), is((Object)now)); Node nodeB = graph.getNodeAt("/a/b[1]"); props = nodeB.getPropertiesByName();