diff --git a/src/main/java/org/springframework/data/jpa/repository/query/Jpa21Utils.java b/src/main/java/org/springframework/data/jpa/repository/query/Jpa21Utils.java index 2bea82ef5..84b7d6a67 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/Jpa21Utils.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/Jpa21Utils.java @@ -148,81 +148,103 @@ public class Jpa21Utils { // Sort to ensure that the intermediate entity subgraphs are created accordingly. Collections.sort(attributePaths); - Collections.reverse(attributePaths); - // We build the entity graph based on the paths with highest depth first for (String path : attributePaths) { - // Fast path - just single attribute - if (!path.contains(".")) { - - if (findAttributeNode(path, entityGraph) == null) { - entityGraph.addAttributeNodes(path); - } - - continue; - } - - // We need to build nested sub fetch graphs String[] pathComponents = StringUtils.delimitedListToStringArray(path, "."); - Subgraph parent = null; + createGraph(pathComponents, 0, entityGraph, null); + } + } - for (int c = 0; c < pathComponents.length - 1; c++) { - parent = c == 0 ? findOrCreateSubgraph(pathComponents[c], entityGraph) : parent.addSubgraph(pathComponents[c]); + private static void createGraph(String[] pathComponents, int offset, EntityGraph root, Subgraph parent) { + + String attributeName = pathComponents[offset]; + + // we found our leaf property, now let's see if it already exists and add it if not + if (pathComponents.length - 1 == offset) { + + if (parent == null && !exists(attributeName, root.getAttributeNodes())) { + root.addAttributeNodes(attributeName); + } else if (parent != null && !exists(attributeName, parent.getAttributeNodes())) { + parent.addAttributeNodes(attributeName); } + return; + } - parent.addAttributeNodes(pathComponents[pathComponents.length - 1]); + AttributeNode node = findAttributeNode(attributeName, root, parent); + if (node != null) { + + Subgraph subgraph = getSubgraph(node); + if (subgraph == null) { + subgraph = parent != null ? parent.addSubgraph(attributeName) : root.addSubgraph(attributeName); + } + createGraph(pathComponents, offset + 1, root, subgraph); + return; + } + + if (parent == null) { + createGraph(pathComponents, offset + 1, root, root.addSubgraph(attributeName)); + } else { + createGraph(pathComponents, offset + 1, root, parent.addSubgraph(attributeName)); } } /** - * Returns the {@link Subgraph} with the given name fro the given {@link EntityGraph} or creates a new one if none - * already available. - * - * @param name - * @param entityGraph + * Checks the given {@link List} of {@link AttributeNode}s for the existence of an {@link AttributeNode} matching the + * given {@literal attributeNodeName}. + * + * @param attributeNodeName + * @param nodes * @return */ - private static Subgraph findOrCreateSubgraph(String name, EntityGraph entityGraph) { - - Subgraph subgraph = findSubgraph(name, entityGraph); - - return subgraph != null ? subgraph : entityGraph.addSubgraph(name); + private static boolean exists(String attributeNodeName, List> nodes) { + return findAttributeNode(attributeNodeName, nodes) != null; } /** - * Returns the {@link Subgraph} with the given name from the given {@link EntityGraph}. - * - * @param name + * Find the {@link AttributeNode} matching the given {@literal attributeNodeName} in given {@link Subgraph} or + * {@link EntityGraph} favoring matches {@link Subgraph} over {@link EntityGraph}. + * + * @param attributeNodeName * @param entityGraph - * @return + * @param parent + * @return {@literal null} if not found. */ - private static Subgraph findSubgraph(String name, EntityGraph entityGraph) { - - AttributeNode node = findAttributeNode(name, entityGraph); - - if (node != null && !ObjectUtils.isEmpty(node.getSubgraphs())) { - return node.getSubgraphs().values().iterator().next(); - } - - return null; + private static AttributeNode findAttributeNode(String attributeNodeName, EntityGraph entityGraph, + Subgraph parent) { + return findAttributeNode(attributeNodeName, + parent != null ? parent.getAttributeNodes() : entityGraph.getAttributeNodes()); } /** - * Returns the {@link AttributeNode} with the given name if present in the given {@link EntityGraph}. - * - * @param name - * @param entityGraph must not be {@literal null}. - * @return + * Find the {@link AttributeNode} matching the given {@literal attributeNodeName} in given {@link List} of + * {@link AttributeNode}s. + * + * @param attributeNodeName + * @param nodes + * @return {@literal null} if not found. */ - private static AttributeNode findAttributeNode(String name, EntityGraph entityGraph) { + private static AttributeNode findAttributeNode(String attributeNodeName, List> nodes) { - for (AttributeNode node : entityGraph.getAttributeNodes()) { - if (ObjectUtils.nullSafeEquals(node.getAttributeName(), name)) { + for (AttributeNode node : nodes) { + if (ObjectUtils.nullSafeEquals(node.getAttributeName(), attributeNodeName)) { return node; } } return null; } + + /** + * Extracts the first {@link Subgraph} from the given {@link AttributeNode}. Ignores any potential different + * {@link Subgraph}s registered for more concrete {@link Class}es as the dynamically created graph does not + * distinguish between those. + * + * @param node + * @return + */ + private static Subgraph getSubgraph(AttributeNode node) { + return node.getSubgraphs().isEmpty() ? null : node.getSubgraphs().values().iterator().next(); + } + } diff --git a/src/test/java/org/springframework/data/jpa/domain/sample/User.java b/src/test/java/org/springframework/data/jpa/domain/sample/User.java index 40d15b2fd..1fa4065fb 100644 --- a/src/test/java/org/springframework/data/jpa/domain/sample/User.java +++ b/src/test/java/org/springframework/data/jpa/domain/sample/User.java @@ -64,7 +64,17 @@ import javax.persistence.TemporalType; attributeNodes = { @NamedAttributeNode("roles"), @NamedAttributeNode(value = "colleagues", subgraph = "User.colleagues") }, subgraphs = { @NamedSubgraph(name = "User.colleagues", - attributeNodes = { @NamedAttributeNode("colleagues"), @NamedAttributeNode("roles") }) }) }) + attributeNodes = { @NamedAttributeNode("colleagues"), + @NamedAttributeNode("roles") }) }), + @NamedEntityGraph(name = "User.deepGraph", + attributeNodes = { @NamedAttributeNode("roles"), + @NamedAttributeNode(value = "colleagues", subgraph = "User.colleagues") }, + subgraphs = { + @NamedSubgraph(name = "User.colleagues", + attributeNodes = { @NamedAttributeNode("roles"), + @NamedAttributeNode(value = "colleagues", subgraph = "User.colleaguesOfColleagues") }), + @NamedSubgraph(name = "User.colleaguesOfColleagues", + attributeNodes = { @NamedAttributeNode("roles"), }) }) }) @NamedQuery(name = "User.findByEmailAddress", query = "SELECT u FROM User u WHERE u.emailAddress = ?1") @NamedStoredProcedureQueries({ // @NamedStoredProcedureQuery(name = "User.plus1", procedureName = "plus1inout", diff --git a/src/test/java/org/springframework/data/jpa/repository/EclipseLinkEntityGraphRepositoryMethodsIntegrationTests.java b/src/test/java/org/springframework/data/jpa/repository/EclipseLinkEntityGraphRepositoryMethodsIntegrationTests.java index 6c2bd0da6..9211130f0 100644 --- a/src/test/java/org/springframework/data/jpa/repository/EclipseLinkEntityGraphRepositoryMethodsIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/EclipseLinkEntityGraphRepositoryMethodsIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import org.springframework.test.context.ContextConfiguration; /** * @author Oliver Gierke + * @author Christoph Strobl */ @ContextConfiguration("classpath:eclipselink.xml") public class EclipseLinkEntityGraphRepositoryMethodsIntegrationTests @@ -37,4 +38,16 @@ public class EclipseLinkEntityGraphRepositoryMethodsIntegrationTests @Ignore @Test public void shouldRespectDynamicFetchGraphForGetOneWithAttributeNamesById() {} + + @Ignore + @Test + public void shouldCreateDynamicGraphWithMultipleLevelsOfSubgraphs() {} + + @Ignore + @Test + public void shouldRespectConfiguredJpaEntityGraphInFindOne() {} + + @Ignore + @Test + public void shouldRespectInferFetchGraphFromMethodName() {} } diff --git a/src/test/java/org/springframework/data/jpa/repository/EntityGraphRepositoryMethodsIntegrationTests.java b/src/test/java/org/springframework/data/jpa/repository/EntityGraphRepositoryMethodsIntegrationTests.java index afdc6ed5a..f83f3ba1a 100644 --- a/src/test/java/org/springframework/data/jpa/repository/EntityGraphRepositoryMethodsIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/EntityGraphRepositoryMethodsIntegrationTests.java @@ -78,6 +78,7 @@ public class EntityGraphRepositoryMethodsIntegrationTests { ollie = repository.save(ollie); tom.getColleagues().add(ollie); + christoph.addRole(role); christoph = repository.save(christoph); ollie.getColleagues().add(christoph); @@ -91,6 +92,8 @@ public class EntityGraphRepositoryMethodsIntegrationTests { public void shouldRespectConfiguredJpaEntityGraph() { Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + em.flush(); + em.clear(); List result = repository.findAll(); @@ -106,6 +109,8 @@ public class EntityGraphRepositoryMethodsIntegrationTests { public void shouldRespectConfiguredJpaEntityGraphInFindOne() { Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + em.flush(); + em.clear(); User user = repository.findOne(tom.getId()); @@ -121,6 +126,8 @@ public class EntityGraphRepositoryMethodsIntegrationTests { public void shouldRespectInferFetchGraphFromMethodName() { Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + em.flush(); + em.clear(); User user = repository.getOneWithDefinedEntityGraphById(tom.getId()); @@ -136,6 +143,8 @@ public class EntityGraphRepositoryMethodsIntegrationTests { public void shouldRespectDynamicFetchGraphForGetOneWithAttributeNamesById() { Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + em.flush(); + em.clear(); em.flush(); em.clear(); @@ -160,6 +169,8 @@ public class EntityGraphRepositoryMethodsIntegrationTests { public void shouldRespectConfiguredJpaEntityGraphWithPaginationAndQueryDslPredicates() { Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + em.flush(); + em.clear(); Page page = repository.findAll(QUser.user.firstname.isNotNull(), new PageRequest(0, 100)); List result = page.getContent(); @@ -210,4 +221,28 @@ public class EntityGraphRepositoryMethodsIntegrationTests { assertThat(util.isLoaded(colleague, "roles"), is(true)); } } + + @Test // DATAJPA-1041, DATAJPA-1075 + public void shouldCreateDynamicGraphWithMultipleLevelsOfSubgraphs() { + + Assume.assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + em.flush(); + em.clear(); + + User user = repository.findOneWithDeepGraphById(tom.getId()); + + assertThat(user, is(notNullValue())); + + assertThat("colleagues on root should have been fetched by dynamic subgraph declaration", + Persistence.getPersistenceUtil().isLoaded(user, "colleagues"), is(true)); + + for (User colleague : user.getColleagues()) { + assertThat(Persistence.getPersistenceUtil().isLoaded(colleague, "colleagues"), is(true)); + assertThat(Persistence.getPersistenceUtil().isLoaded(colleague, "roles"), is(true)); + for (User colleagueOfColleague : colleague.getColleagues()) { + assertThat(Persistence.getPersistenceUtil().isLoaded(colleagueOfColleague, "roles"), is(true)); + assertThat(Persistence.getPersistenceUtil().isLoaded(colleagueOfColleague, "colleagues"), is(false)); + } + } + } } diff --git a/src/test/java/org/springframework/data/jpa/repository/query/EclipseLinkJpa21UtilsTests.java b/src/test/java/org/springframework/data/jpa/repository/query/EclipseLinkJpa21UtilsTests.java new file mode 100644 index 000000000..51e66c89a --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/query/EclipseLinkJpa21UtilsTests.java @@ -0,0 +1,26 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed 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.springframework.data.jpa.repository.query; + +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Christoph Strobl + */ +@ContextConfiguration("classpath:eclipselink.xml") +public class EclipseLinkJpa21UtilsTests extends Jpa21UtilsTests { + +} diff --git a/src/test/java/org/springframework/data/jpa/repository/query/Jpa21UtilsTests.java b/src/test/java/org/springframework/data/jpa/repository/query/Jpa21UtilsTests.java new file mode 100644 index 000000000..6a2e43066 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/query/Jpa21UtilsTests.java @@ -0,0 +1,146 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed 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.springframework.data.jpa.repository.query; + +import static org.junit.Assert.*; +import static org.junit.Assume.*; +import static org.springframework.data.jpa.support.EntityManagerTestUtils.*; +import static org.springframework.data.jpa.util.IsAttributeNode.*; + +import javax.persistence.AttributeNode; +import javax.persistence.EntityGraph; +import javax.persistence.EntityManager; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.domain.sample.User; +import org.springframework.data.jpa.repository.EntityGraph.EntityGraphType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Christoph Strobl + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("classpath:application-context.xml") +@Transactional +public class Jpa21UtilsTests { + + @Autowired EntityManager em; + + @Test // DATAJPA-1041, DATAJPA-1075 + public void shouldCreateGraphWithoutSubGraphCorrectly() { + + assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + + EntityGraph graph = em.createEntityGraph(User.class); + Jpa21Utils.configureFetchGraphFrom( + new JpaEntityGraph("name", EntityGraphType.FETCH, new String[] { "roles", "colleagues" }), graph); + + AttributeNode roles = findNode("roles", graph); + assertThat(roles, terminatesGraph()); + + AttributeNode colleagues = findNode("colleagues", graph); + assertThat(colleagues, terminatesGraph()); + } + + @Test // DATAJPA-1041, DATAJPA-1075 + public void shouldCreateGraphWithMultipleSubGraphCorrectly() { + + assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + + EntityGraph graph = em.createEntityGraph(User.class); + Jpa21Utils.configureFetchGraphFrom(new JpaEntityGraph("name", EntityGraphType.FETCH, + new String[] { "roles", "colleagues.roles", "colleagues.colleagues" }), graph); + + AttributeNode roles = findNode("roles", graph); + assertThat(roles, terminatesGraph()); + + AttributeNode colleagues = findNode("colleagues", graph); + assertThat(colleagues, terminatesGraphWith("roles", "colleagues")); + } + + @Test // DATAJPA-1041, DATAJPA-1075 + public void shouldCreateGraphWithDeepSubGraphCorrectly() { + + assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + + EntityGraph graph = em.createEntityGraph(User.class); + Jpa21Utils.configureFetchGraphFrom(new JpaEntityGraph("name", EntityGraphType.FETCH, + new String[] { "roles", "colleagues.roles", "colleagues.colleagues.roles" }), graph); + + AttributeNode roles = findNode("roles", graph); + assertThat(roles, terminatesGraph()); + + AttributeNode colleagues = findNode("colleagues", graph); + assertThat(colleagues, terminatesGraphWith("roles")); + assertThat(colleagues, hasSubgraphs("colleagues")); + + AttributeNode colleaguesOfColleagues = findNode("colleagues", colleagues); + assertThat(colleaguesOfColleagues, terminatesGraphWith("roles")); + } + + @Test // DATAJPA-1041, DATAJPA-1075 + public void shouldIgnoreIntermedeateSubGraphNodesThatAreNotNeeded() { + + assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + + EntityGraph graph = em.createEntityGraph(User.class); + Jpa21Utils.configureFetchGraphFrom(new JpaEntityGraph("name", EntityGraphType.FETCH, new String[] { "roles", + "colleagues", "colleagues.roles", "colleagues.colleagues", "colleagues.colleagues.roles" }), graph); + + AttributeNode roles = findNode("roles", graph); + assertThat(roles, terminatesGraph()); + + AttributeNode colleagues = findNode("colleagues", graph); + assertThat(colleagues, terminatesGraphWith("roles")); + assertThat(colleagues, hasSubgraphs("colleagues")); + + AttributeNode colleaguesOfColleagues = findNode("colleagues", colleagues); + assertThat(colleaguesOfColleagues, terminatesGraphWith("roles")); + } + + @Test // DATAJPA-1041, DATAJPA-1075 + public void orderOfSubGraphsShouldNotMatter() { + + assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + + EntityGraph graph = em.createEntityGraph(User.class); + Jpa21Utils.configureFetchGraphFrom(new JpaEntityGraph("name", EntityGraphType.FETCH, new String[] { + "colleagues.colleagues.roles", "roles", "colleagues.colleagues", "colleagues", "colleagues.roles" }), graph); + + AttributeNode roles = findNode("roles", graph); + assertThat(roles, terminatesGraph()); + + AttributeNode colleagues = findNode("colleagues", graph); + assertThat(colleagues, terminatesGraphWith("roles")); + assertThat(colleagues, hasSubgraphs("colleagues")); + + AttributeNode colleaguesOfColleagues = findNode("colleagues", colleagues); + assertThat(colleaguesOfColleagues, terminatesGraphWith("roles")); + } + + @Test(expected = Exception.class) // DATAJPA-1041, DATAJPA-1075 + public void errorsOnUnknownProperties() { + + assumeTrue(currentEntityManagerIsAJpa21EntityManager(em)); + + Jpa21Utils.configureFetchGraphFrom(new JpaEntityGraph("name", EntityGraphType.FETCH, new String[] { "¯\\_(ツ)_/¯" }), + em.createEntityGraph(User.class)); + } +} diff --git a/src/test/java/org/springframework/data/jpa/repository/query/OpenJpaJpa21UtilsTests.java b/src/test/java/org/springframework/data/jpa/repository/query/OpenJpaJpa21UtilsTests.java new file mode 100644 index 000000000..03c8468c5 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/query/OpenJpaJpa21UtilsTests.java @@ -0,0 +1,26 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed 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.springframework.data.jpa.repository.query; + +import org.springframework.test.context.ContextConfiguration; + +/** + * @author Christoph Strobl + */ +@ContextConfiguration("classpath:openjpa.xml") +public class OpenJpaJpa21UtilsTests extends Jpa21UtilsTests { + +} diff --git a/src/test/java/org/springframework/data/jpa/repository/sample/RepositoryMethodsWithEntityGraphConfigRepository.java b/src/test/java/org/springframework/data/jpa/repository/sample/RepositoryMethodsWithEntityGraphConfigRepository.java index d689c1936..5b3a915a8 100644 --- a/src/test/java/org/springframework/data/jpa/repository/sample/RepositoryMethodsWithEntityGraphConfigRepository.java +++ b/src/test/java/org/springframework/data/jpa/repository/sample/RepositoryMethodsWithEntityGraphConfigRepository.java @@ -75,4 +75,8 @@ public interface RepositoryMethodsWithEntityGraphConfigRepository // DATAJPA-1041 @EntityGraph(attributePaths = { "colleagues", "colleagues.roles", "colleagues.colleagues" }) User findOneWithMultipleSubGraphsById(Integer id); + + // DATAJPA-1041, DATAJPA-1075 + @EntityGraph(attributePaths = { "colleagues", "colleagues.roles", "colleagues.colleagues.roles" }) + User findOneWithDeepGraphById(Integer id); } diff --git a/src/test/java/org/springframework/data/jpa/util/IsAttributeNode.java b/src/test/java/org/springframework/data/jpa/util/IsAttributeNode.java new file mode 100644 index 000000000..63e76fa62 --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/util/IsAttributeNode.java @@ -0,0 +1,228 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed 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.springframework.data.jpa.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.persistence.AttributeNode; +import javax.persistence.EntityGraph; +import javax.persistence.Subgraph; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +/** + * @author Christoph Strobl + */ +public class IsAttributeNode extends TypeSafeMatcher> { + + private boolean terminatingNodeCheck = false; + private List nodes; + private List subgraphs; + private List errors = new ArrayList(); + + @Override + protected boolean matchesSafely(AttributeNode item) { + + if (item == null) { + + errors.add("AttributeNode was null!"); + return false; + } + + if (terminatingNodeCheck) { + + if (!CollectionUtils.isEmpty(item.getSubgraphs())) { + + errors.add(String.format("'%s' was expected to be a terminating node but has subgraphs %s.", + item.getAttributeName(), extractExistingAttributeNames(item.getSubgraphs().values().iterator().next()))); + return false; + } + return true; + } + + if (CollectionUtils.isEmpty(item.getSubgraphs())) { + + if (!CollectionUtils.isEmpty(nodes)) { + + errors + .add(String.format("Leaf properties %s could not be found. The node does not have any subgraphs.", nodes)); + } + if (!CollectionUtils.isEmpty(subgraphs)) { + errors.add(String.format("Subgraphs %s could not be found. The node does not have any subgraphs.", subgraphs)); + } + return false; + } + + Subgraph graph = item.getSubgraphs().values().iterator().next(); + + if (!CollectionUtils.isEmpty(nodes)) { + for (String nodeName : nodes) { + + AttributeNode node = findNode(nodeName, graph.getAttributeNodes()); + if (node == null) { + + errors.add(String.format("AttributeNode '%s' could not be found in subgraph for '%s'. Know nodes are: %s.", + nodeName, item.getAttributeName(), extractExistingAttributeNames(graph))); + return false; + } + + if (!CollectionUtils.isEmpty(node.getSubgraphs())) { + + errors.add(String.format("AttributeNode %s of subgraph %s is not a leaf property but has % SubGraph(s).", + nodeName, item.getAttributeName(), node.getSubgraphs().size())); + return false; + } + } + } + + if (!CollectionUtils.isEmpty(subgraphs)) { + for (String subgraphName : subgraphs) { + + AttributeNode node = findNode(subgraphName, graph.getAttributeNodes()); + if (node == null) { + + errors.add(String.format("Subgraph '%s' could not be found in SubGraph for '%s'. Know nodes are: %s.", + subgraphName, item.getAttributeName(), extractExistingAttributeNames(graph))); + return false; + } + + if (CollectionUtils.isEmpty(node.getSubgraphs())) { + + errors.add(String.format("'%s' of SubGraph '%s' is not a SubGraph.", subgraphName, item.getAttributeName())); + return false; + } + } + } + return true; + } + + @Override + public void describeTo(Description description) { + + for (String error : errors) { + description.appendText(error); + } + } + + /** + * Lookup the {@link AttributeNode} with given {@literal nodeName} in the root of the given {@literal graph}. + * + * @param nodeName + * @param graph + * @return + */ + public static AttributeNode findNode(String nodeName, EntityGraph graph) { + + if (graph == null) { + return null; + } + + return findNode(nodeName, graph.getAttributeNodes()); + } + + /** + * Lookup the {@link AttributeNode} with given {@literal nodeName} in the {@link List} of given {@literal nodes}. + * + * @param nodeName + * @param nodes + * @return + */ + public static AttributeNode findNode(String nodeName, List> nodes) { + + if (CollectionUtils.isEmpty(nodes)) { + return null; + } + + for (AttributeNode node : nodes) { + if (ObjectUtils.nullSafeEquals(node.getAttributeName(), nodeName)) { + return node; + } + } + + return null; + } + + /** + * Lookup the {@link AttributeNode} with given {@literal nodeName} in the first {@link Subgraph} of the given + * {@literal node}. + * + * @param attributeName + * @param node + * @return + */ + public static AttributeNode findNode(String attributeName, AttributeNode node) { + + if (CollectionUtils.isEmpty(node.getSubgraphs())) { + return null; + } + + Subgraph subgraph = node.getSubgraphs().values().iterator().next(); + return findNode(attributeName, subgraph.getAttributeNodes()); + } + + private List extractExistingAttributeNames(Subgraph graph) { + + List result = new ArrayList(graph.getAttributeNodes().size()); + for (AttributeNode node : graph.getAttributeNodes()) { + result.add(node.getAttributeName()); + } + return result; + } + + /** + * Asserts that the fetch graph terminates with {@link AttributeNode}s having the given {@literal nodeNames}. + * + * @param nodeNames + * @return + */ + public static IsAttributeNode terminatesGraphWith(String... nodeNames) { + + IsAttributeNode matcher = new IsAttributeNode(); + matcher.nodes = Arrays.asList(nodeNames); + return matcher; + } + + /** + * Asserts that the fetch graph continues with {@link AttributeNode}s having {@link AttributeNode#getSubgraphs()} with + * given {@literal subgraphNames}. + * + * @return + */ + public static IsAttributeNode hasSubgraphs(String... subgraphNames) { + + IsAttributeNode matcher = new IsAttributeNode(); + matcher.subgraphs = Arrays.asList(subgraphNames); + return matcher; + } + + /** + * Asserts that the fetch graph terminates with the given {@link AttributeNode} by checking + * {@link AttributeNode#getSubgraphs()} is empty. + * + * @return + */ + public static IsAttributeNode terminatesGraph() { + + IsAttributeNode matcher = new IsAttributeNode(); + matcher.terminatingNodeCheck = true; + return matcher; + } +}