Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/federation/OffsetMirrorProjector.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/connector/federation/OffsetMirrorProjector.java (revision 2336) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/federation/OffsetMirrorProjector.java (working copy) @@ -32,8 +32,9 @@ import org.modeshape.graph.property.PathFactory; /** * A Projector for federated repository configurations that are an offset, direct one-for-one mirror against a single source - * repository that is projected below the federated root. In other words, the federated repository has a single projection with a - * single "/something/below/root => /" rule. + * repository with a subgraph projected below the federated root. In other words, the federated repository has a single projection + * with a single "/something/below/root => /source/node" rule. Note that this project works even when the path in source is the + * root node, such as "/something/below/root => /". */ @Immutable final class OffsetMirrorProjector extends ProjectorWithPlaceholders { @@ -50,30 +51,42 @@ final class OffsetMirrorProjector extends ProjectorWithPlaceholders { List projections ) { assert projections != null; assert context != null; + // There must be a single projection ... if (projections.size() != 1) return null; Projection projection = projections.get(0); assert projection != null; + // That projection may only have one rule ... if (projection.getRules().size() != 1) return null; PathFactory pathFactory = context.getValueFactories().getPathFactory(); + + // The # of paths in repository must be 1 ... List topLevelPaths = projection.getRules().get(0).getTopLevelPathsInRepository(pathFactory); if (topLevelPaths.size() != 1) return null; Path topLevelPath = topLevelPaths.get(0); assert topLevelPath != null; + // The federated path may not be the root ... if (topLevelPath.isRoot()) return null; - return new OffsetMirrorProjector(context, projections, topLevelPath); + + // The corresponding source path may or may not be the root ... + Path sourcePath = projection.getRules().get(0).getPathInSource(topLevelPath, pathFactory); + + return new OffsetMirrorProjector(context, projections, topLevelPath, sourcePath); } private final Projection projection; private final Path offset; private final int offsetSize; + private final Path sourcePath; private OffsetMirrorProjector( ExecutionContext context, List projections, - Path offset ) { + Path offset, + Path sourcePath ) { super(context, projections); this.projection = projections.get(0); this.offset = offset; this.offsetSize = offset.size(); + this.sourcePath = sourcePath; } /** @@ -94,14 +107,17 @@ final class OffsetMirrorProjector extends ProjectorWithPlaceholders { if (path.size() == offsetSize) { // Make sure the path is the same ... if (path.equals(offset)) { - locationInSource = location.with(context.getValueFactories().getPathFactory().createRootPath()); + locationInSource = location.with(sourcePath); } else { return null; // not in the path } } else { // Make sure the path begins with the offset ... - if (path.isAtOrBelow(offset)) { - locationInSource = location.with(path.subpath(offsetSize)); + if (path.isDecendantOf(offset)) { + Path pathBelowOffset = path.relativeTo(offset); + PathFactory pathFactory = context.getValueFactories().getPathFactory(); + Path pathInSource = pathFactory.create(sourcePath, pathBelowOffset); + locationInSource = location.with(pathInSource); } else { // Not in the path return null; Index: modeshape-graph/src/main/java/org/modeshape/graph/connector/federation/Projection.java =================================================================== --- modeshape-graph/src/main/java/org/modeshape/graph/connector/federation/Projection.java (revision 2336) +++ modeshape-graph/src/main/java/org/modeshape/graph/connector/federation/Projection.java (working copy) @@ -755,7 +755,8 @@ public class Projection implements Comparable, Serializable { */ protected Path projectPathInSourceToPathInRepository( Path pathInSource, PathFactory factory ) { - if (!this.sourcePath.isAtOrAbove(pathInSource)) return null; + if (this.sourcePath.equals(pathInSource)) return this.repositoryPath; + if (!this.sourcePath.isAncestorOf(pathInSource)) return null; // Remove the leading source path ... Path relativeSourcePath = pathInSource.relativeTo(this.sourcePath); // Prepend the region's root path ... @@ -773,7 +774,8 @@ public class Projection implements Comparable, Serializable { */ protected Path projectPathInRepositoryToPathInSource( Path pathInRepository, PathFactory factory ) { - if (!this.repositoryPath.isAtOrAbove(pathInRepository)) return null; + if (this.repositoryPath.equals(pathInRepository)) return this.sourcePath; + if (!this.repositoryPath.isAncestorOf(pathInRepository)) return null; // Find the relative path from the root of this region ... Path pathInRegion = pathInRepository.relativeTo(this.repositoryPath); // Prepend the path in source ... Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/federation/AbstractFederatedRepositorySourceIntegrationTest.java =================================================================== --- modeshape-graph/src/test/java/org/modeshape/graph/connector/federation/AbstractFederatedRepositorySourceIntegrationTest.java (revision 2336) +++ modeshape-graph/src/test/java/org/modeshape/graph/connector/federation/AbstractFederatedRepositorySourceIntegrationTest.java (working copy) @@ -33,12 +33,18 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import org.junit.AfterClass; +import org.junit.Before; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import org.modeshape.common.statistic.Stopwatch; import org.modeshape.common.util.CheckArg; -import org.modeshape.graph.ModeShapeLexicon; import org.modeshape.graph.ExecutionContext; import org.modeshape.graph.Graph; import org.modeshape.graph.Location; +import org.modeshape.graph.ModeShapeLexicon; import org.modeshape.graph.Node; import org.modeshape.graph.Results; import org.modeshape.graph.Subgraph; @@ -52,12 +58,6 @@ import org.modeshape.graph.property.Name; import org.modeshape.graph.property.Path; import org.modeshape.graph.property.PathNotFoundException; import org.modeshape.graph.property.Property; -import org.junit.AfterClass; -import org.junit.Before; -import org.mockito.MockitoAnnotations; -import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; /** * @@ -153,8 +153,8 @@ public abstract class AbstractFederatedRepositorySourceIntegrationTest { @AfterClass public static void afterAll() { - System.out.println("Results for federated reads: " + FEDERATED_TIMER.getSimpleStatistics()); - System.out.println("Results for source reads: " + FEDERATED_TIMER.getSimpleStatistics()); + // System.out.println("Results for federated reads: " + FEDERATED_TIMER.getSimpleStatistics()); + // System.out.println("Results for source reads: " + FEDERATED_TIMER.getSimpleStatistics()); } /** @@ -266,6 +266,32 @@ public abstract class AbstractFederatedRepositorySourceIntegrationTest { } /** + * Assert that the node does not exist in the federated repository given by the supplied path but does exist in the underlying + * source given by the path, source name, and workspace name. + * + * @param pathInFederated + * @param pathInSource + * @param sourceName + * @param workspaceName + */ + protected void assertNotFederated( String pathInFederated, + String pathInSource, + String sourceName, + String workspaceName ) { + try { + FEDERATED_TIMER.start(); + federated.getNodeAt(pathInFederated); + FEDERATED_TIMER.stop(); + fail("Did not expect to find federated node \"" + pathInFederated + "\""); + } catch (PathNotFoundException e) { + // expected + } + SOURCE_TIMER.start(); + graphFor(sourceName, workspaceName).getNodeAt(pathInSource); + SOURCE_TIMER.stop(); + } + + /** * Assert that the node in the federated repository given by the supplied path represents the same node in the underlying * source given by the path, source name, and workspace name. * @@ -286,20 +312,26 @@ public abstract class AbstractFederatedRepositorySourceIntegrationTest { SOURCE_TIMER.start(); Node sourceNode = graphFor(sourceName, workspaceName).getNodeAt(pathInSource); SOURCE_TIMER.stop(); - // The name should match ... + Path fedPath = fedNode.getLocation().getPath(); Path sourcePath = sourceNode.getLocation().getPath(); - if (!fedPath.isRoot() && !sourcePath.isRoot()) { - assertThat(fedNode.getLocation().getPath().getLastSegment().getName(), is(sourceNode.getLocation() - .getPath() - .getLastSegment() - .getName())); - } + if (fedPath.isRoot() || sourcePath.isRoot()) { + UUID fedUuid = fedNode.getLocation().getUuid(); + UUID sourceUuid = sourceNode.getLocation().getUuid(); + assertThat(fedUuid, is(sourceUuid)); + } else { + Name fedName = fedPath.getLastSegment().getName(); + Name sourceNodeName = sourcePath.getLastSegment().getName(); - // The UUID should match ... - UUID fedUuid = fedNode.getLocation().getUuid(); - UUID sourceUuid = sourceNode.getLocation().getUuid(); - assertThat(fedUuid, is(sourceUuid)); + // If the node names match, then the nodes should be projected and the UUIDs should match. + // Otherwise, the federated node is likely a placeholder node, and thus will have a different UUID. + if (fedName.equals(sourceNodeName)) { + // The UUID should match ... + UUID fedUuid = fedNode.getLocation().getUuid(); + UUID sourceUuid = sourceNode.getLocation().getUuid(); + assertThat(fedUuid, is(sourceUuid)); + } + } // The children should match ... List fedChildren = new ArrayList(); Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/federation/AbstractProjectorTest.java =================================================================== --- modeshape-graph/src/test/java/org/modeshape/graph/connector/federation/AbstractProjectorTest.java (revision 2336) +++ modeshape-graph/src/test/java/org/modeshape/graph/connector/federation/AbstractProjectorTest.java (working copy) @@ -30,11 +30,11 @@ import static org.junit.Assert.assertThat; import java.util.ArrayList; import java.util.List; import java.util.UUID; +import org.junit.Before; import org.modeshape.graph.ExecutionContext; import org.modeshape.graph.Location; import org.modeshape.graph.connector.federation.Projection.Rule; import org.modeshape.graph.property.Path; -import org.junit.Before; /** * @param @@ -82,6 +82,10 @@ public abstract class AbstractProjectorTest { projections.add(new Projection(sourceName, workspaceName, false, rule(rules))); } + protected void clearProjections() { + projections.clear(); + } + protected Path.Segment segment( String segment ) { return context.getValueFactories().getPathFactory().createSegment(segment); } Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/federation/FederatedRepositorySourceProjectionTest.java new file mode 100644 =================================================================== --- /dev/null (revision 2336) +++ modeshape-graph/src/test/java/org/modeshape/graph/connector/federation/FederatedRepositorySourceProjectionTest.java (working copy) @@ -0,0 +1,132 @@ +/* + * 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.graph.connector.federation; + +import org.junit.Before; +import org.junit.Test; +import org.modeshape.graph.Graph; + +/** + * An integration test that verifies the behavior of a {@link FederatedRepositorySource} configured with a federated workspace + * using various projections to an underlying source. + */ +public class FederatedRepositorySourceProjectionTest extends AbstractFederatedRepositorySourceIntegrationTest { + + private String offsetSourceName; + private String offsetWorkspaceName; + + @Override + @Before + public void beforeEach() throws Exception { + super.beforeEach(); + + // Set up the projection ... + offsetSourceName = "Offset Source"; + offsetWorkspaceName = "Offset Workspace"; + + // Add some data to the source ... + Graph source = graphFor(offsetSourceName, offsetWorkspaceName); + source.importXmlFrom(getClass().getClassLoader().getResource("cars.xml").toURI()).into("/"); + } + + /** + * Assert that the node in the source repository given by the supplied path represents the equivalent offset node in the + * federated repository given by a path offset from the supplied path. + * + * @param federatedPath the path in the federated repository + * @param pathToSourceNode the path to the node in the source repository + */ + protected void assertSameNode( String federatedPath, + String pathToSourceNode ) { + assertSameNode(federatedPath, pathToSourceNode, offsetSourceName, offsetWorkspaceName); + } + + /** + * Assert that the node in the source repository given by the supplied path does not exist in the source or in the federated + * repository. + * + * @param federatedPath the path in the federated repository + * @param pathToSourceNode the path to the node in the source repository + */ + protected void assertNotFederated( String federatedPath, + String pathToSourceNode ) { + assertNotFederated(federatedPath, pathToSourceNode, offsetSourceName, offsetWorkspaceName); + } + + @Test + public void shouldProjectRootNodeInSourceIntoFederatedUsingOffset() throws Exception { + // Set up the projection ... + addProjection("fedSpace", "Offset Projection", offsetSourceName, offsetWorkspaceName, "/v1/v2 => /"); + assertSameNode("/v1/v2/Cars", "/Cars"); + assertSameNode("/v1/v2/Cars/Hybrid", "/Cars/Hybrid"); + assertSameNode("/v1/v2/Cars/Hybrid/Toyota Prius", "/Cars/Hybrid/Toyota Prius"); + assertSameNode("/v1/v2/Cars/Hybrid/Toyota Highlander", "/Cars/Hybrid/Toyota Highlander"); + assertSameNode("/v1/v2/Cars/Hybrid/Nissan Altima", "/Cars/Hybrid/Nissan Altima"); + assertSameNode("/v1/v2/Cars/Sports/Aston Martin DB9", "/Cars/Sports/Aston Martin DB9"); + assertSameNode("/v1/v2/Cars/Sports/Infiniti G37", "/Cars/Sports/Infiniti G37"); + assertSameNode("/v1/v2/Cars/Luxury/Cadillac DTS", "/Cars/Luxury/Cadillac DTS"); + assertSameNode("/v1/v2/Cars/Luxury/Bentley Continental", "/Cars/Luxury/Bentley Continental"); + assertSameNode("/v1/v2/Cars/Luxury/Lexus IS350", "/Cars/Luxury/Lexus IS350"); + assertSameNode("/v1/v2/Cars/Utility/Land Rover LR2", "/Cars/Utility/Land Rover LR2"); + assertSameNode("/v1/v2/Cars/Utility/Land Rover LR3", "/Cars/Utility/Land Rover LR3"); + assertSameNode("/v1/v2/Cars/Utility/Hummer H3", "/Cars/Utility/Hummer H3"); + assertSameNode("/v1/v2/Cars/Utility/Ford F-150", "/Cars/Utility/Ford F-150"); + } + + @Test + public void shouldProjectNonRootNodeInSourceIntoFederatedUsingOffset() throws Exception { + // Set up the projection ... + addProjection("fedSpace", "Offset Projection", offsetSourceName, offsetWorkspaceName, "/v1/v2/Cars => /Cars"); + assertSameNode("/v1/v2/Cars", "/Cars"); + assertSameNode("/v1/v2/Cars/Hybrid", "/Cars/Hybrid"); + assertSameNode("/v1/v2/Cars/Hybrid/Toyota Prius", "/Cars/Hybrid/Toyota Prius"); + assertSameNode("/v1/v2/Cars/Hybrid/Toyota Highlander", "/Cars/Hybrid/Toyota Highlander"); + assertSameNode("/v1/v2/Cars/Hybrid/Nissan Altima", "/Cars/Hybrid/Nissan Altima"); + assertSameNode("/v1/v2/Cars/Sports/Aston Martin DB9", "/Cars/Sports/Aston Martin DB9"); + assertSameNode("/v1/v2/Cars/Sports/Infiniti G37", "/Cars/Sports/Infiniti G37"); + assertSameNode("/v1/v2/Cars/Luxury/Cadillac DTS", "/Cars/Luxury/Cadillac DTS"); + assertSameNode("/v1/v2/Cars/Luxury/Bentley Continental", "/Cars/Luxury/Bentley Continental"); + assertSameNode("/v1/v2/Cars/Luxury/Lexus IS350", "/Cars/Luxury/Lexus IS350"); + assertSameNode("/v1/v2/Cars/Utility/Land Rover LR2", "/Cars/Utility/Land Rover LR2"); + assertSameNode("/v1/v2/Cars/Utility/Land Rover LR3", "/Cars/Utility/Land Rover LR3"); + assertSameNode("/v1/v2/Cars/Utility/Hummer H3", "/Cars/Utility/Hummer H3"); + assertSameNode("/v1/v2/Cars/Utility/Ford F-150", "/Cars/Utility/Ford F-150"); + } + + @Test + public void shouldProjectNonRootNodeWithSiblingsInSourceIntoFederatedUsingOffset() throws Exception { + // Set up the projection ... + addProjection("fedSpace", "Offset Projection", offsetSourceName, offsetWorkspaceName, "/v1/v2/Lux => /Cars/Luxury"); + assertSameNode("/v1/v2/Lux/Cadillac DTS", "/Cars/Luxury/Cadillac DTS"); + assertSameNode("/v1/v2/Lux/Bentley Continental", "/Cars/Luxury/Bentley Continental"); + assertSameNode("/v1/v2/Lux/Lexus IS350", "/Cars/Luxury/Lexus IS350"); + // Car should not be projected ... + assertNotFederated("/v1/Car", "/Cars"); + assertNotFederated("/v1/v2/Car", "/Cars"); + // The other children of Car should not be projected ... + assertNotFederated("/v1/v2/Hybrid", "/Cars/Hybrid"); + assertNotFederated("/v1/v2/Sports", "/Cars/Sports"); + assertNotFederated("/v1/v2/Utility", "/Cars/Utility"); + } +} Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/federation/OffsetFullMirrorProjectorTest.java new file mode 100644 =================================================================== --- /dev/null (revision 2336) +++ modeshape-graph/src/test/java/org/modeshape/graph/connector/federation/OffsetFullMirrorProjectorTest.java (working copy) @@ -0,0 +1,83 @@ +/* + * 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.graph.connector.federation; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; +import org.junit.Before; +import org.junit.Test; +import org.modeshape.graph.Location; + +/** + * + */ +public class OffsetFullMirrorProjectorTest extends AbstractProjectorTest { + + private String mirrorSourceName; + private String mirrorWorkspaceName; + + @Before + @Override + public void beforeEach() { + super.beforeEach(); + this.mirrorSourceName = "source1"; + this.mirrorWorkspaceName = "workspace1"; + addProjection(mirrorSourceName, mirrorWorkspaceName, "/a/b/c => /"); + this.projector = OffsetMirrorProjector.with(context, projections); + } + + protected void assertProjectedIntoMirror( String federatedPath, + String pathInSource ) { + Location location = location(federatedPath); + ProjectedNode node = projector.project(context, location, false); + assertThat(node.isProxy(), is(true)); + ProxyNode proxy = node.asProxy(); + assertThat(proxy.location().getPath(), is(path(pathInSource))); + assertThat(proxy.source(), is(mirrorSourceName)); + assertThat(proxy.workspaceName(), is(mirrorWorkspaceName)); + assertThat(proxy.hasNext(), is(false)); + } + + @Test + public void shouldAlwaysReturnProxyNodeForLocationAboveMirrorSource() { + assertPlacholderHasChildren("/", "a"); + assertPlacholderHasChildren("/a", "b"); + assertPlacholderHasChildren("/a/b", "c"); + assertProjectedIntoMirror("/a/b/c", "/"); + } + + @Test + public void shouldAlwaysReturnProxyNodeForLocationWithinMirror() { + assertProjectedIntoMirror("/a/b/c", "/"); + assertProjectedIntoMirror("/a/b/c/d", "/d"); + assertProjectedIntoMirror("/a/b/c/d/e", "/d/e"); + } + + @Test + public void shouldReturnNoProjectedNodeForLocationOffPathToSourceBranch() { + assertNoProjectedNodeAt("/a[2]"); + assertNoProjectedNodeAt("/a/b[2]"); + assertNoProjectedNodeAt("/a/b/c[2]"); + } +} Index: modeshape-graph/src/test/java/org/modeshape/graph/connector/federation/OffsetMirrorProjectorTest.java =================================================================== --- modeshape-graph/src/test/java/org/modeshape/graph/connector/federation/OffsetMirrorProjectorTest.java (revision 2336) +++ modeshape-graph/src/test/java/org/modeshape/graph/connector/federation/OffsetMirrorProjectorTest.java (working copy) @@ -25,9 +25,9 @@ package org.modeshape.graph.connector.federation; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; -import org.modeshape.graph.Location; import org.junit.Before; import org.junit.Test; +import org.modeshape.graph.Location; /** * @@ -43,7 +43,7 @@ public class OffsetMirrorProjectorTest extends AbstractProjectorTest /"); + addProjection(mirrorSourceName, mirrorWorkspaceName, "/a/b/c => /d/e"); this.projector = OffsetMirrorProjector.with(context, projections); } @@ -64,14 +64,14 @@ public class OffsetMirrorProjectorTest extends AbstractProjectorTest