From a529e2cd8ab2aae384993ea6b108380d689c3656 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 16 Feb 2011 19:20:53 +0100 Subject: [PATCH 01/20] DATADOC-24 - Refactored SimpleParameterAccessor to improve usability. Extracted ParameterAccessor interface that uses a simple Iterator to traverse over bindable parameters. Renamed SimpleParameterAccessor to ParametersParameterAccessor to better reflect its specialty (improved JavaDoc according to this as well). --- .../repository/query/ParameterAccessor.java | 60 ++++++++++++++++++ ....java => ParametersParameterAccessor.java} | 61 +++++++++++++------ .../query/parser/AbstractQueryCreator.java | 22 +++---- .../SimpleParameterAccessorUnitTests.java | 28 ++++----- 4 files changed, 127 insertions(+), 44 deletions(-) create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java rename spring-data-commons-core/src/main/java/org/springframework/data/repository/query/{SimpleParameterAccessor.java => ParametersParameterAccessor.java} (67%) diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java new file mode 100644 index 000000000..cd08e87e5 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java @@ -0,0 +1,60 @@ +/* + * Copyright 2011 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.repository.query; + +import java.util.Iterator; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + + +/** + * Interface to access method parameters. Allows dedicated access to parameters + * of special types + * + * @author Oliver Gierke + */ +public interface ParameterAccessor extends Iterable { + + /** + * Returns the {@link Pageable} of the parameters, if available. Returns + * {@code null} otherwise. + * + * @return + */ + Pageable getPageable(); + + + /** + * Returns the sort instance to be used for query creation. Will use a + * {@link Sort} parameter if available or the {@link Sort} contained in a + * {@link Pageable} if available. Returns {@code null} if no {@link Sort} + * can be found. + * + * @return + */ + Sort getSort(); + + + /** + * Returns an iterator over all bindable parameters. This means + * parameters implementing {@link Pageable} or {@link Sort} will not be + * included in this {@link Iterator}. + * + * @return + */ + Iterator iterator(); +} \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/SimpleParameterAccessor.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParametersParameterAccessor.java similarity index 67% rename from spring-data-commons-core/src/main/java/org/springframework/data/repository/query/SimpleParameterAccessor.java rename to spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParametersParameterAccessor.java index 9f8e8032e..51bf82b8a 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/SimpleParameterAccessor.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParametersParameterAccessor.java @@ -15,29 +15,32 @@ */ package org.springframework.data.repository.query; +import java.util.Iterator; + import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.util.Assert; /** - * {@link SimpleParameterAccessor} is used to bind method parameters. + * {@link ParameterAccessor} implementation using a {@link Parameters} instance + * to find special parameters. * * @author Oliver Gierke */ -public class SimpleParameterAccessor { +public class ParametersParameterAccessor implements ParameterAccessor { private final Parameters parameters; private final Object[] values; /** - * Creates a new {@link SimpleParameterAccessor}. + * Creates a new {@link ParametersParameterAccessor}. * * @param parameters * @param values */ - public SimpleParameterAccessor(Parameters parameters, Object[] values) { + public ParametersParameterAccessor(Parameters parameters, Object[] values) { Assert.notNull(parameters); Assert.notNull(values); @@ -50,11 +53,11 @@ public class SimpleParameterAccessor { } - /** - * Returns the {@link Pageable} of the parameters, if available. Returns - * {@code null} otherwise. + /* + * (non-Javadoc) * - * @return + * @see + * org.springframework.data.repository.query.ParameterAccessor#getPageable() */ public Pageable getPageable() { @@ -66,13 +69,11 @@ public class SimpleParameterAccessor { } - /** - * Returns the sort instance to be used for query creation. Will use a - * {@link Sort} parameter if available or the {@link Sort} contained in a - * {@link Pageable} if available. Returns {@code null} if no {@link Sort} - * can be found. + /* + * (non-Javadoc) * - * @return + * @see + * org.springframework.data.repository.query.ParameterAccessor#getSort() */ public Sort getSort() { @@ -94,11 +95,11 @@ public class SimpleParameterAccessor { } - /** - * Returns a {@link BindableParameterIterator} to traverse all bindable - * parameters. + /* + * (non-Javadoc) * - * @return + * @see + * org.springframework.data.repository.query.ParameterAccessor#iterator() */ public BindableParameterIterator iterator() { @@ -111,7 +112,7 @@ public class SimpleParameterAccessor { * * @author Oliver Gierke */ - public class BindableParameterIterator { + private class BindableParameterIterator implements Iterator { private int currentIndex = 0; @@ -125,5 +126,27 @@ public class SimpleParameterAccessor { return getBindableValue(currentIndex++); } + + + /* + * (non-Javadoc) + * + * @see java.util.Iterator#hasNext() + */ + public boolean hasNext() { + + return values.length <= currentIndex; + } + + + /* + * (non-Javadoc) + * + * @see java.util.Iterator#remove() + */ + public void remove() { + + throw new UnsupportedOperationException(); + } } } diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/AbstractQueryCreator.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/AbstractQueryCreator.java index f6997904a..3ccdf52b1 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/AbstractQueryCreator.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/AbstractQueryCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2010 the original author or authors. + * Copyright 2008-2011 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. @@ -15,9 +15,11 @@ */ package org.springframework.data.repository.query.parser; +import java.util.Iterator; + import org.springframework.data.domain.Sort; -import org.springframework.data.repository.query.SimpleParameterAccessor; -import org.springframework.data.repository.query.SimpleParameterAccessor.BindableParameterIterator; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.parser.PartTree.OrPart; import org.springframework.util.Assert; @@ -32,19 +34,18 @@ import org.springframework.util.Assert; */ public abstract class AbstractQueryCreator { - private final SimpleParameterAccessor parameters; + private final ParameterAccessor parameters; private final PartTree tree; /** * Creates a new {@link AbstractQueryCreator} for the given {@link PartTree} - * and {@link SimpleParameterAccessor}. + * and {@link ParametersParameterAccessor}. * * @param tree * @param parameters */ - public AbstractQueryCreator(PartTree tree, - SimpleParameterAccessor parameters) { + public AbstractQueryCreator(PartTree tree, ParameterAccessor parameters) { Assert.notNull(tree); Assert.notNull(parameters); @@ -75,7 +76,7 @@ public abstract class AbstractQueryCreator { private S createCriteria(PartTree tree) { S base = null; - BindableParameterIterator iterator = parameters.iterator(); + Iterator iterator = parameters.iterator(); for (OrPart node : tree) { @@ -102,7 +103,7 @@ public abstract class AbstractQueryCreator { * @param iterator * @return */ - protected abstract S create(Part part, BindableParameterIterator iterator); + protected abstract S create(Part part, Iterator iterator); /** @@ -114,8 +115,7 @@ public abstract class AbstractQueryCreator { * @param iterator * @return */ - protected abstract S and(Part part, S base, - BindableParameterIterator iterator); + protected abstract S and(Part part, S base, Iterator iterator); /** diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/SimpleParameterAccessorUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/SimpleParameterAccessorUnitTests.java index 2347fc834..db8f5b104 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/SimpleParameterAccessorUnitTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/SimpleParameterAccessorUnitTests.java @@ -26,7 +26,7 @@ import org.springframework.data.domain.Sort; /** - * Unit tests for {@link SimpleParameterAccessor}. + * Unit tests for {@link ParametersParameterAccessor}. * * @author Oliver Gierke */ @@ -52,43 +52,43 @@ public class SimpleParameterAccessorUnitTests { @Test public void testname() throws Exception { - new SimpleParameterAccessor(parameters, new Object[] { "test" }); + new ParametersParameterAccessor(parameters, new Object[] { "test" }); } @Test(expected = IllegalArgumentException.class) public void rejectsNullParameters() throws Exception { - new SimpleParameterAccessor(null, new Object[0]); + new ParametersParameterAccessor(null, new Object[0]); } @Test(expected = IllegalArgumentException.class) public void rejectsNullValues() throws Exception { - new SimpleParameterAccessor(parameters, null); + new ParametersParameterAccessor(parameters, null); } @Test(expected = IllegalArgumentException.class) public void rejectsTooLittleNumberOfArguments() throws Exception { - new SimpleParameterAccessor(parameters, new Object[0]); + new ParametersParameterAccessor(parameters, new Object[0]); } @Test(expected = IllegalArgumentException.class) public void rejectsTooManyArguments() throws Exception { - new SimpleParameterAccessor(parameters, new Object[] { "test", "test" }); + new ParametersParameterAccessor(parameters, new Object[] { "test", "test" }); } @Test public void returnsNullForPageableAndSortIfNoneAvailable() throws Exception { - SimpleParameterAccessor accessor = - new SimpleParameterAccessor(parameters, new Object[] { "test" }); + ParameterAccessor accessor = + new ParametersParameterAccessor(parameters, new Object[] { "test" }); assertThat(accessor.getPageable(), is(nullValue())); assertThat(accessor.getSort(), is(nullValue())); } @@ -98,8 +98,8 @@ public class SimpleParameterAccessorUnitTests { public void returnsSortIfAvailable() { Sort sort = new Sort("foo"); - SimpleParameterAccessor accessor = - new SimpleParameterAccessor(sortParameters, new Object[] { + ParameterAccessor accessor = + new ParametersParameterAccessor(sortParameters, new Object[] { "test", sort }); assertThat(accessor.getSort(), is(sort)); assertThat(accessor.getPageable(), is(nullValue())); @@ -110,8 +110,8 @@ public class SimpleParameterAccessorUnitTests { public void returnsPageableIfAvailable() { Pageable pageable = new PageRequest(0, 10); - SimpleParameterAccessor accessor = - new SimpleParameterAccessor(pageableParameters, new Object[] { + ParameterAccessor accessor = + new ParametersParameterAccessor(pageableParameters, new Object[] { "test", pageable }); assertThat(accessor.getPageable(), is(pageable)); assertThat(accessor.getSort(), is(nullValue())); @@ -123,8 +123,8 @@ public class SimpleParameterAccessorUnitTests { Sort sort = new Sort("foo"); Pageable pageable = new PageRequest(0, 10, sort); - SimpleParameterAccessor accessor = - new SimpleParameterAccessor(pageableParameters, new Object[] { + ParameterAccessor accessor = + new ParametersParameterAccessor(pageableParameters, new Object[] { "test", pageable }); assertThat(accessor.getPageable(), is(pageable)); assertThat(accessor.getSort(), is(sort)); From 0342d744274e30631343d36d2f2b0d03078930a7 Mon Sep 17 00:00:00 2001 From: Thomas Risberg Date: Wed, 16 Feb 2011 16:50:54 -0500 Subject: [PATCH 02/20] upgraded extension for use with Maven 3 --- pom.xml | 2 +- spring-data-commons-parent/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 770c31066..b88452e65 100644 --- a/pom.xml +++ b/pom.xml @@ -119,7 +119,7 @@ org.springframework.build.aws org.springframework.build.aws.maven - 2.0.0.RELEASE + 3.1.0.RELEASE diff --git a/spring-data-commons-parent/pom.xml b/spring-data-commons-parent/pom.xml index fc508c01d..814723b98 100644 --- a/spring-data-commons-parent/pom.xml +++ b/spring-data-commons-parent/pom.xml @@ -223,7 +223,7 @@ --> org.springframework.build.aws org.springframework.build.aws.maven - 2.0.0.RELEASE + 3.1.0.RELEASE From aeee6935f5b29f98662feac34d6c308b90173f92 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Mon, 21 Feb 2011 18:08:16 +0100 Subject: [PATCH 03/20] DATACMNS-15 - Added support to discover Distinct keyword. Query method names now support Distinct in method name prefixes. Beyond that I laxed the prefix constraints a little so that it can potentially contain anything before a 'By' separator: - findByLastname -> simply query - findUsersByLastname -> simple query - findUsersDistinctByFirstname -> distinct query - findDistinctUsersByFirstname -> distinct query - findDistinctByLastname -> distinct query --- .../repository/query/parser/PartTree.java | 51 +++++++++++++++---- .../query/parser/PartTreeUnitTests.java | 26 ++++++++++ src/main/resources/changelog.txt | 7 +++ 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java index e0e0116ca..0bf145a22 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java @@ -21,6 +21,7 @@ import static java.util.regex.Pattern.*; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.regex.Matcher; import java.util.regex.Pattern; import org.springframework.data.domain.Sort; @@ -40,11 +41,13 @@ import org.springframework.util.Assert; public class PartTree implements Iterable { private static final String ORDER_BY = "OrderBy"; - private static final String[] PREFIXES = new String[] { "findBy", "find", - "readBy", "read", "getBy", "get" }; - private static final String PREFIX_TEMPLATE = "^%s(?=[A-Z]).*"; private static final String KEYWORD_TEMPLATE = "(%s)(?=[A-Z])"; + private static final String DISTINCT = "Distinct"; + private static final Pattern PREFIX_TEMPLATE = Pattern + .compile("^(find|read|get)(\\p{Upper}.*?)??By"); + + private final boolean distinct; private final OrderBySource orderBySource; private final List nodes = new ArrayList(); @@ -61,6 +64,7 @@ public class PartTree implements Iterable { Assert.notNull(source); Assert.notNull(domainClass); + this.distinct = detectDistinct(source); String foo = strip(source); String[] parts = split(foo, ORDER_BY); @@ -109,6 +113,17 @@ public class PartTree implements Iterable { } + /** + * Returns whether we indicate distinct lookup of entities. + * + * @return + */ + public boolean isDistinct() { + + return distinct; + } + + /** * Splits the given text at the given keywords. Expects camelcase style to * only match concrete keywords and not derivatives of it. @@ -135,15 +150,33 @@ public class PartTree implements Iterable { */ private String strip(String methodName) { - for (String prefix : PREFIXES) { + Matcher matcher = PREFIX_TEMPLATE.matcher(methodName); - String regex = format(PREFIX_TEMPLATE, prefix); - if (methodName.matches(regex)) { - return methodName.substring(prefix.length()); - } + if (matcher.find()) { + return methodName.substring(matcher.group().length()); + } else { + return methodName; + } + } + + + /** + * Checks whether the given source string contains the {@link #DISTINCT} + * keyword in it's prefix. + * + * @param source + * @return + */ + private boolean detectDistinct(String source) { + + Matcher matcher = PREFIX_TEMPLATE.matcher(source); + + if (!matcher.find()) { + return false; } - return methodName; + String group = matcher.group(2); + return group != null && group.contains(DISTINCT); } /** diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java index 05b44cdff..f7223d120 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java @@ -91,6 +91,32 @@ public class PartTreeUnitTests { } + @Test + public void detectsDistinctCorrectly() throws Exception { + + PartTree tree = new PartTree("findDistinctByLastname", User.class); + assertThat(tree.isDistinct(), is(true)); + + tree = new PartTree("findUsersDistinctByLastname", User.class); + assertThat(tree.isDistinct(), is(true)); + + tree = new PartTree("findDistinctUsersByLastname", User.class); + assertThat(tree.isDistinct(), is(true)); + + tree = new PartTree("findUsersByLastname", User.class); + assertThat(tree.isDistinct(), is(false)); + + tree = new PartTree("findByLastname", User.class); + assertThat(tree.isDistinct(), is(false)); + + // Check it's non-greedy (would strip everything until Order*By* + // otherwise) + tree = new PartTree("findByLastnameOrderByFirstnameDesc", User.class); + assertThat(tree.isDistinct(), is(false)); + assertThat(tree.getSort(), is(new Sort(Direction.DESC, "firstname"))); + } + + private void assertPart(PartTree tree, Part[]... parts) { Iterator iterator = tree.iterator(); diff --git a/src/main/resources/changelog.txt b/src/main/resources/changelog.txt index 91dbfe83a..7f62426a3 100644 --- a/src/main/resources/changelog.txt +++ b/src/main/resources/changelog.txt @@ -1,6 +1,13 @@ Spring Data Commons Changelog ============================================= +Changes in version 1.0.0.M4 +---------------------------------------- + +Repository +* Improved ParameterAccessor infrastructure +* Added support for 'Distinct' keyword in finder method names + Changes in version 1.0.0.M3 (2011-02-09) ---------------------------------------- From dd45054da2e04865082fe8778d6e80e4de7e65f8 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Tue, 22 Feb 2011 21:40:04 +0100 Subject: [PATCH 04/20] DATACMNS-16, DATAJPA-29 - Cleaned up Part class. Removed operator property (as it was JPA specific) and added 'In' keyword. --- .../data/repository/query/parser/Part.java | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java index e38777ea4..c119ba542 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java @@ -129,31 +129,32 @@ public class Part { */ public static enum Type { - BETWEEN(null, 2, "Between"), + BETWEEN(2, "Between"), - IS_NOT_NULL(null, 0, "IsNotNull", "NotNull"), + IS_NOT_NULL(0, "IsNotNull", "NotNull"), - IS_NULL(null, 0, "IsNull", "Null"), + IS_NULL(0, "IsNull", "Null"), - LESS_THAN("<", "LessThan"), + LESS_THAN("LessThan"), - GREATER_THAN(">", "GreaterThan"), + GREATER_THAN("GreaterThan"), - NOT_LIKE("not like", "NotLike"), + NOT_LIKE("NotLike"), - LIKE("like", "Like"), + LIKE("Like"), - NEGATING_SIMPLE_PROPERTY("<>", "Not"), + IN("In"), - SIMPLE_PROPERTY("="); + NEGATING_SIMPLE_PROPERTY("Not"), + + SIMPLE_PROPERTY; // Need to list them again explicitly as the order is important // (esp. for IS_NULL, IS_NOT_NULL) private static final List ALL = Arrays.asList(IS_NOT_NULL, - IS_NULL, BETWEEN, LESS_THAN, GREATER_THAN, NOT_LIKE, LIKE, + IS_NULL, BETWEEN, LESS_THAN, GREATER_THAN, NOT_LIKE, LIKE, IN, NEGATING_SIMPLE_PROPERTY, SIMPLE_PROPERTY); private List keywords; - private String operator; private int numberOfArguments; @@ -166,17 +167,16 @@ public class Part { * @param numberOfArguments * @param keywords */ - private Type(String operator, int numberOfArguments, String... keywords) { + private Type(int numberOfArguments, String... keywords) { - this.operator = operator; this.numberOfArguments = numberOfArguments; this.keywords = Arrays.asList(keywords); } - private Type(String operator, String... keywords) { + private Type(String... keywords) { - this(operator, 1, keywords); + this(1, keywords); } @@ -202,12 +202,6 @@ public class Part { } - public String getOperator() { - - return this.operator; - } - - /** * Returns whether the the type supports the given raw property. Default * implementation checks whether the property ends with the registered From 7753b9b4464e5214618d6342ca44a61fffa44faf Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Tue, 22 Feb 2011 21:42:49 +0100 Subject: [PATCH 05/20] =?UTF-8?q?DATAJPA-28=20-=20Changed=20invocation=20o?= =?UTF-8?q?rder=20of=20createIsNewStrategy(=E2=80=A6).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lazily initializing the IsNewAware on first access now as classes implementing the method might wanna delegate to a template method using a constructor parameter. If createIsNewStrategy(…) is called from our constructor already the subclasses additional constructor arguments are not available yet. --- .../data/repository/support/RepositorySupport.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositorySupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositorySupport.java index 1def33e85..93ebf69a9 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositorySupport.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositorySupport.java @@ -33,7 +33,7 @@ public abstract class RepositorySupport implements Repository { private final Class domainClass; - private final IsNewAware isNewStrategy; + private IsNewAware isNewStrategy; /** @@ -45,8 +45,6 @@ public abstract class RepositorySupport implements Assert.notNull(domainClass); this.domainClass = domainClass; - this.isNewStrategy = createIsNewStrategy(domainClass); - Assert.notNull(isNewStrategy); } @@ -81,6 +79,11 @@ public abstract class RepositorySupport implements */ protected IsNewAware getIsNewStrategy() { + if (isNewStrategy == null) { + this.isNewStrategy = createIsNewStrategy(domainClass); + Assert.notNull(isNewStrategy); + } + return isNewStrategy; } } From 0721c7921c51a5b474f8a02498ad68d4312ef0c7 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 23 Feb 2011 10:07:02 +0100 Subject: [PATCH 06/20] DATACMNS-16 - Added 'NotIn' keyword. --- .../springframework/data/repository/query/parser/Part.java | 6 ++++-- src/main/resources/changelog.txt | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java index c119ba542..d16eed9cc 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java @@ -143,6 +143,8 @@ public class Part { LIKE("Like"), + NOT_IN("NotIn"), + IN("In"), NEGATING_SIMPLE_PROPERTY("Not"), @@ -152,8 +154,8 @@ public class Part { // Need to list them again explicitly as the order is important // (esp. for IS_NULL, IS_NOT_NULL) private static final List ALL = Arrays.asList(IS_NOT_NULL, - IS_NULL, BETWEEN, LESS_THAN, GREATER_THAN, NOT_LIKE, LIKE, IN, - NEGATING_SIMPLE_PROPERTY, SIMPLE_PROPERTY); + IS_NULL, BETWEEN, LESS_THAN, GREATER_THAN, NOT_LIKE, LIKE, + NOT_IN, IN, NEGATING_SIMPLE_PROPERTY, SIMPLE_PROPERTY); private List keywords; private int numberOfArguments; diff --git a/src/main/resources/changelog.txt b/src/main/resources/changelog.txt index 7f62426a3..3dec394a0 100644 --- a/src/main/resources/changelog.txt +++ b/src/main/resources/changelog.txt @@ -7,6 +7,7 @@ Changes in version 1.0.0.M4 Repository * Improved ParameterAccessor infrastructure * Added support for 'Distinct' keyword in finder method names +* Added support for 'In' and 'NotIn' keywords Changes in version 1.0.0.M3 (2011-02-09) ---------------------------------------- From 20443bb4b61a6f6d8fe6527fd7f52a430ab016fa Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Thu, 24 Feb 2011 17:31:03 +0000 Subject: [PATCH 07/20] DATACMNS-17 - Introduced metamodel for entities and repositories. Unified IsNewAware and IdAware to EntityMetadata. Refactored base repository support to use this abstraction. Introduced RepositoryMetadata and DefaultRepositoryMetadata implementation to capture all the additional information that we need around a repository interface. Adapted factory API to use this abstraction. Opened up method parameter and return types to prepare enabling the usage of the infrastructure for interfaces that do not extend Repository interface. --- .../data/domain/Persistable.java | 8 +- .../support/AbstractEntityMetadata.java | 68 ++++ .../support/DefaultRepositoryMetadata.java | 328 ++++++++++++++++++ .../{IsNewAware.java => EntityMetadata.java} | 30 +- .../data/repository/support/IdAware.java | 32 -- ...on.java => PersistableEntityMetadata.java} | 28 +- .../ReflectiveEntityInformationSupport.java | 146 -------- .../support/RepositoryFactoryBeanSupport.java | 3 +- .../support/RepositoryFactorySupport.java | 213 ++---------- .../support/RepositoryMetadata.java | 104 ++++++ .../repository/support/RepositorySupport.java | 89 ----- .../data/repository/util/ClassUtils.java | 155 --------- .../AbstractEntityMetadataUnitTests.java | 68 ++++ .../DefaultRepositoryMetadataUnitTests.java | 187 ++++++++++ .../PersistableEntityInformationTests.java | 14 +- .../PersistableEntityMetadataUnitTests.java | 62 ++++ .../repository/util/ClassUtilsUnitTests.java | 128 ------- src/main/resources/changelog.txt | 5 +- 18 files changed, 909 insertions(+), 759 deletions(-) create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AbstractEntityMetadata.java create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/repository/support/DefaultRepositoryMetadata.java rename spring-data-commons-core/src/main/java/org/springframework/data/repository/support/{IsNewAware.java => EntityMetadata.java} (61%) delete mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/repository/support/IdAware.java rename spring-data-commons-core/src/main/java/org/springframework/data/repository/support/{PersistableEntityInformation.java => PersistableEntityMetadata.java} (64%) delete mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/repository/support/ReflectiveEntityInformationSupport.java create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryMetadata.java delete mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositorySupport.java create mode 100644 spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AbstractEntityMetadataUnitTests.java create mode 100644 spring-data-commons-core/src/test/java/org/springframework/data/repository/support/DefaultRepositoryMetadataUnitTests.java create mode 100644 spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityMetadataUnitTests.java diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/domain/Persistable.java b/spring-data-commons-core/src/main/java/org/springframework/data/domain/Persistable.java index 797598368..4f8c56e82 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/domain/Persistable.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/domain/Persistable.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2010 the original author or authors. + * Copyright 2008-2011 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. @@ -22,16 +22,16 @@ import java.io.Serializable; * Simple interface for entities. * * @author Oliver Gierke - * @param the type of the identifier + * @param the type of the identifier */ -public interface Persistable extends Serializable { +public interface Persistable extends Serializable { /** * Returns the id of the entity. * * @return the id */ - PK getId(); + ID getId(); /** diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AbstractEntityMetadata.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AbstractEntityMetadata.java new file mode 100644 index 000000000..43f73433b --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AbstractEntityMetadata.java @@ -0,0 +1,68 @@ +/* + * Copyright 2011 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.repository.support; + +import org.springframework.util.Assert; + + +/** + * Base class for implementations of {@link EntityMetadata}. Considers an entity + * to be new whenever {@link #getId(Object)} returns {@literal null}. + * + * @author Oliver Gierke + */ +public abstract class AbstractEntityMetadata implements EntityMetadata { + + private final Class domainClass; + + + /** + * Creates a new {@link AbstractEntityMetadata} from the given domain class. + * + * @param domainClass + */ + public AbstractEntityMetadata(Class domainClass) { + + Assert.notNull(domainClass); + this.domainClass = domainClass; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.support.IsNewAware#isNew(java.lang + * .Object) + */ + public boolean isNew(T entity) { + + return getId(entity) == null; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.support.EntityInformation#getJavaType + * () + */ + public Class getJavaType() { + + return this.domainClass; + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/DefaultRepositoryMetadata.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/DefaultRepositoryMetadata.java new file mode 100644 index 000000000..e776bc1ed --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/DefaultRepositoryMetadata.java @@ -0,0 +1,328 @@ +/* + * Copyright 2011 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.repository.support; + +import static org.springframework.core.GenericTypeResolver.*; +import static org.springframework.data.repository.util.ClassUtils.*; + +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.data.repository.Repository; +import org.springframework.util.Assert; + + +/** + * Default implementation of {@link RepositoryMetadata}. + * + * @author Oliver Gierke + */ +public class DefaultRepositoryMetadata implements RepositoryMetadata { + + @SuppressWarnings("rawtypes") + private static final TypeVariable>[] PARAMETERS = + Repository.class.getTypeParameters(); + private static final String DOMAIN_TYPE_NAME = PARAMETERS[0].getName(); + private static final String ID_TYPE_NAME = PARAMETERS[1].getName(); + + private final Map methodCache = + new ConcurrentHashMap(); + + private final Class repositoryInterface; + private final Class repositoryBaseClass; + + + /** + * Creates a new {@link DefaultRepositoryMetadata} for the given repository + * interface and repository base class. + * + * @param repositoryInterface + */ + public DefaultRepositoryMetadata(Class repositoryInterface, + Class repositoryBaseClass) { + + Assert.notNull(repositoryInterface); + Assert.notNull(repositoryBaseClass); + this.repositoryInterface = repositoryInterface; + this.repositoryBaseClass = repositoryBaseClass; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.repository.support.RepositoryMetadata# + * getRepositoryInterface() + */ + public Class getRepositoryInterface() { + + return repositoryInterface; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.repository.support.RepositoryMetadata# + * getRepositoryBaseClass() + */ + public Class getRepositoryBaseClass() { + + return this.repositoryBaseClass; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.support.RepositoryMetadata#getDomainClass + * () + */ + public Class getDomainClass() { + + Class[] arguments = + resolveTypeArguments(repositoryInterface, Repository.class); + return arguments == null ? null : arguments[0]; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.support.RepositoryMetadata#getIdClass + * () + */ + public Class getIdClass() { + + Class[] arguments = + resolveTypeArguments(repositoryInterface, Repository.class); + return arguments == null ? null : arguments[1]; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.repository.support.RepositoryMetadata# + * getBaseClassMethod(java.lang.reflect.Method) + */ + public Method getBaseClassMethod(Method method) { + + Assert.notNull(method); + + Method result = methodCache.get(method); + + if (null != result) { + return result; + } + + result = getBaseClassMethodFor(method); + methodCache.put(method, result); + + return result; + } + + + /** + * Returns whether the given method is considered to be a repository base + * class method. + * + * @param method + * @return + */ + private boolean isBaseClassMethod(Method method) { + + Assert.notNull(method); + + if (method.getDeclaringClass().isAssignableFrom(repositoryBaseClass)) { + return true; + } + + return !method.equals(getBaseClassMethod(method)); + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.repository.support.RepositoryMetadata# + * getFinderMethods() + */ + public Iterable getQueryMethods() { + + Set result = new HashSet(); + + for (Method method : repositoryInterface.getDeclaredMethods()) { + if (!isCustomMethod(method) && !isBaseClassMethod(method)) { + result.add(method); + } + } + + return result; + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.support.RepositoryMetadata#isCustomMethod + * (java.lang.reflect.Method) + */ + public boolean isCustomMethod(Method method) { + + Class declaringClass = method.getDeclaringClass(); + + boolean isQueryMethod = declaringClass.equals(repositoryInterface); + boolean isRepositoryInterface = + isGenericRepositoryInterface(declaringClass); + boolean isBaseClassMethod = isBaseClassMethod(method); + + return !(isRepositoryInterface || isBaseClassMethod || isQueryMethod); + } + + + /** + * Returns the given base class' method if the given method (declared in the + * repository interface) was also declared at the repository base class. + * Returns the given method if the given base class does not declare the + * method given. Takes generics into account. + * + * @param method + * @return + */ + Method getBaseClassMethodFor(Method method) { + + for (Method baseClassMethod : repositoryBaseClass.getMethods()) { + + // Wrong name + if (!method.getName().equals(baseClassMethod.getName())) { + continue; + } + + // Wrong number of arguments + if (!(method.getParameterTypes().length == baseClassMethod + .getParameterTypes().length)) { + continue; + } + + // Check whether all parameters match + if (!parametersMatch(method, baseClassMethod)) { + continue; + } + + return baseClassMethod; + } + + return method; + } + + + /* + * (non-Javadoc) + * + * @see org.springframework.data.repository.support.RepositoryMetadata# + * hasCustomMethod() + */ + public boolean hasCustomMethod() { + + // No detection required if no typing interface was configured + if (isGenericRepositoryInterface(repositoryInterface)) { + return false; + } + + for (Method method : repositoryInterface.getMethods()) { + if (isCustomMethod(method) && !isBaseClassMethod(method)) { + return true; + } + } + + return false; + } + + + /** + * Checks the given method's parameters to match the ones of the given base + * class method. Matches generic arguments agains the ones bound in the + * given repository interface. + * + * @param method + * @param baseClassMethod + * @return + */ + private boolean parametersMatch(Method method, Method baseClassMethod) { + + Type[] genericTypes = baseClassMethod.getGenericParameterTypes(); + Class[] types = baseClassMethod.getParameterTypes(); + Class[] methodParameters = method.getParameterTypes(); + + for (int i = 0; i < genericTypes.length; i++) { + + Type type = genericTypes[i]; + + if (type instanceof TypeVariable) { + + String name = ((TypeVariable) type).getName(); + + if (!matchesGenericType(name, methodParameters[i])) { + return false; + } + + } else { + + if (!types[i].equals(methodParameters[i])) { + return false; + } + } + } + + return true; + } + + + /** + * Checks whether the given parameter type matches the generic type of the + * given parameter. Thus when {@literal PK} is declared, the method ensures + * that given method parameter is the primary key type declared in the given + * repository interface e.g. + * + * @param name + * @param parameterType + * @return + */ + private boolean matchesGenericType(String name, Class parameterType) { + + Class entityType = getDomainClass(); + Class idClass = getIdClass(); + + if (ID_TYPE_NAME.equals(name) && parameterType.equals(idClass)) { + return true; + } + + if (DOMAIN_TYPE_NAME.equals(name) && parameterType.equals(entityType)) { + return true; + } + + return false; + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/IsNewAware.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityMetadata.java similarity index 61% rename from spring-data-commons-core/src/main/java/org/springframework/data/repository/support/IsNewAware.java rename to spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityMetadata.java index b2eadec9e..e62845cd6 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/IsNewAware.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2010 the original author or authors. + * Copyright 2011 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. @@ -16,18 +16,34 @@ package org.springframework.data.repository.support; /** - * Interface to abstract the ways to determine if the given entity is to be - * considered as new. + * Metadata for entity types. * * @author Oliver Gierke */ -public interface IsNewAware { +public interface EntityMetadata { /** * Returns whether the given entity is considered to be new. * - * @param entity + * @param entity must never be {@literal null} * @return */ - boolean isNew(Object entity); -} \ No newline at end of file + boolean isNew(T entity); + + + /** + * Returns the id of the given entity. + * + * @param entity must never be {@literal null} + * @return + */ + Object getId(T entity); + + + /** + * Returns the actual domain class type. + * + * @return + */ + Class getJavaType(); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/IdAware.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/IdAware.java deleted file mode 100644 index 02471d509..000000000 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/IdAware.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2008-2010 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.repository.support; - -/** - * Interface to abstract the ways to retrieve the id of the given entity. - * - * @author Oliver Gierke - */ -public interface IdAware { - - /** - * Returns the id of the given entity. - * - * @param entity - * @return - */ - Object getId(Object entity); -} \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityMetadata.java similarity index 64% rename from spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityInformation.java rename to spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityMetadata.java index 86f153316..63ba9b016 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityInformation.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2010 the original author or authors. + * Copyright 2008-201 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. @@ -19,13 +19,26 @@ import org.springframework.data.domain.Persistable; /** - * Implementation of {@link IsNewAware} that assumes the entity handled + * Implementation of {@link EntityMetadata} that assumes the entity handled * implements {@link Persistable} and uses {@link Persistable#isNew()} for the * {@link #isNew(Object)} check. * * @author Oliver Gierke */ -public class PersistableEntityInformation implements IsNewAware, IdAware { +@SuppressWarnings("rawtypes") +public class PersistableEntityMetadata extends + AbstractEntityMetadata { + + /** + * Creates a new {@link PersistableEntityMetadata}. + * + * @param domainClass + */ + public PersistableEntityMetadata() { + + super(Persistable.class); + } + /* * (non-Javadoc) @@ -34,9 +47,10 @@ public class PersistableEntityInformation implements IsNewAware, IdAware { * org.springframework.data.repository.support.IsNewAware#isNew(java.lang * .Object) */ - public boolean isNew(Object entity) { + @Override + public boolean isNew(Persistable entity) { - return ((Persistable) entity).isNew(); + return entity.isNew(); } @@ -47,8 +61,8 @@ public class PersistableEntityInformation implements IsNewAware, IdAware { * org.springframework.data.repository.support.IdAware#getId(java.lang.Object * ) */ - public Object getId(Object entity) { + public Object getId(Persistable entity) { - return ((Persistable) entity).getId(); + return entity.getId(); } } \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/ReflectiveEntityInformationSupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/ReflectiveEntityInformationSupport.java deleted file mode 100644 index 4a0a1a3b3..000000000 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/ReflectiveEntityInformationSupport.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2008-2010 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.repository.support; - -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Field; -import java.lang.reflect.Method; - -import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; -import org.springframework.util.ReflectionUtils.FieldCallback; -import org.springframework.util.ReflectionUtils.MethodCallback; - - -/** - * {@link IsNewAware} and {@link IdAware} implementation that reflectively - * checks a {@link Field} or {@link Method} annotated with the given - * annotations. Subclasses usually simply have to provide the persistence - * technology specific annotations. - * - * @author Oliver Gierke - */ -public class ReflectiveEntityInformationSupport implements IsNewAware, IdAware { - - private Field field; - private Method method; - - - /** - * Creates a new {@link ReflectiveEntityInformationSupport} by inspecting - * the given class for a {@link Field} or {@link Method} for and {@link Id} - * annotation. - * - * @param domainClass not {@literal null}, must be annotated with - * {@link Entity} and carry an anootation defining the id - * property. - */ - public ReflectiveEntityInformationSupport(Class domainClass, - final Class... annotationsToScanFor) { - - Assert.notNull(domainClass); - - ReflectionUtils.doWithFields(domainClass, new FieldCallback() { - - public void doWith(Field field) { - - if (ReflectiveEntityInformationSupport.this.field != null) { - return; - } - - if (hasAnnotation(field, annotationsToScanFor)) { - ReflectiveEntityInformationSupport.this.field = field; - } - } - }); - - if (field != null) { - return; - } - - ReflectionUtils.doWithMethods(domainClass, new MethodCallback() { - - public void doWith(Method method) { - - if (ReflectiveEntityInformationSupport.this.method != null) { - return; - } - - if (hasAnnotation(method, annotationsToScanFor)) { - ReflectiveEntityInformationSupport.this.method = method; - } - } - }); - - Assert.isTrue(this.field != null || this.method != null, - "No id method or field found!"); - } - - - /** - * Checks whether the given {@link AnnotatedElement} carries one of the - * given {@link Annotation}s. - * - * @param annotatedElement - * @param annotations - * @return - */ - private boolean hasAnnotation(AnnotatedElement annotatedElement, - Class... annotations) { - - for (Class annotation : annotations) { - - if (annotatedElement.getAnnotation(annotation) != null) { - return true; - } - } - - return false; - } - - - /* - * (non-Javadoc) - * - * @see - * org.springframework.data.repository.support.RepositorySupport.IsNewAware - * #isNew(java.lang.Object) - */ - public boolean isNew(Object entity) { - - return getId(entity) == null; - } - - - /* - * (non-Javadoc) - * - * @see - * org.springframework.data.repository.support.RepositorySupport.IdAware - * #getId(java.lang.Object) - */ - public Object getId(Object entity) { - - if (field != null) { - ReflectionUtils.makeAccessible(field); - return ReflectionUtils.getField(field, entity); - } - - ReflectionUtils.makeAccessible(method); - return ReflectionUtils.invokeMethod(method, entity); - } -} \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryBeanSupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryBeanSupport.java index 3fe899c8d..0ce0d5b05 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryBeanSupport.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryBeanSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2010 the original author or authors. + * Copyright 2008-2011 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. @@ -124,7 +124,6 @@ public abstract class RepositoryFactoryBeanSupport> this.factory = createRepositoryFactory(); this.factory.setQueryLookupStrategyKey(queryLookupStrategyKey); - this.factory.validate(repositoryInterface, customImplementation); for (RepositoryProxyPostProcessor processor : getRepositoryPostProcessors()) { this.factory.addRepositoryProxyPostProcessor(processor); diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java index 7f20fdf3a..567092d3f 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java @@ -15,16 +15,12 @@ */ package org.springframework.data.repository.support; -import static org.springframework.data.repository.util.ClassUtils.*; import static org.springframework.util.ReflectionUtils.*; -import java.io.Serializable; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.aopalliance.intercept.MethodInterceptor; @@ -50,12 +46,9 @@ import org.springframework.util.Assert; */ public abstract class RepositoryFactorySupport { - private QueryLookupStrategy.Key queryLookupStrategyKey; - - private final Map methodCache = - new ConcurrentHashMap(); private final List postProcessors = new ArrayList(); + private QueryLookupStrategy.Key queryLookupStrategyKey; /** @@ -109,15 +102,17 @@ public abstract class RepositoryFactorySupport { * @param customImplementation * @return */ - @SuppressWarnings("unchecked") - public > T getRepository( - Class repositoryInterface, Object customImplementation) { + @SuppressWarnings({ "unchecked" }) + public T getRepository(Class repositoryInterface, + Object customImplementation) { + + RepositoryMetadata metadata = + new DefaultRepositoryMetadata(repositoryInterface, + getRepositoryBaseClass(repositoryInterface)); - validate(repositoryInterface, customImplementation); + validate(metadata, customImplementation); - Class domainClass = getDomainClass(repositoryInterface); - RepositorySupport target = - getTargetRepository(domainClass, repositoryInterface); + Object target = getTargetRepository(metadata); // Create proxy ProxyFactory result = new ProxyFactory(); @@ -128,32 +123,31 @@ public abstract class RepositoryFactorySupport { processor.postProcess(result); } - result.addAdvice(new QueryExecuterMethodInterceptor( - repositoryInterface, customImplementation, target)); + result.addAdvice(new QueryExecuterMethodInterceptor(metadata, + customImplementation, target)); return (T) result.getProxy(); } /** - * Create a {@link RepositorySupport} instance as backing for the query - * proxy. + * Create a repository instance as backing for the query proxy. * - * @param * @param domainClass * @return */ - protected abstract RepositorySupport getTargetRepository( - Class domainClass, Class repositoryInterface); + protected abstract Object getTargetRepository(RepositoryMetadata metadata); /** - * Determines the base class for the repository to be created. + * Returns the base class backing the actual repository instance. Make sure + * {@link #getTargetRepository(RepositoryMetadata)} returns an instance of + * this class. * + * @param repositoryInterface * @return */ - @SuppressWarnings("rawtypes") - protected abstract Class getRepositoryClass( + protected abstract Class getRepositoryBaseClass( Class repositoryInterface); @@ -166,165 +160,23 @@ public abstract class RepositoryFactorySupport { protected abstract QueryLookupStrategy getQueryLookupStrategy(Key key); - /** - * Returns if the configured repository interface has custom methods, that - * might have to be delegated to a custom implementation. This is used to - * verify repository configuration. - * - * @return - */ - private boolean hasCustomMethod( - Class> repositoryInterface) { - - boolean hasCustomMethod = false; - - // No detection required if no typing interface was configured - if (isGenericRepositoryInterface(repositoryInterface)) { - return false; - } - - for (Method method : repositoryInterface.getMethods()) { - - if (isCustomMethod(method, repositoryInterface) - && !isBaseClassMethod(method, repositoryInterface)) { - return true; - } - } - - return hasCustomMethod; - } - - - /** - * Returns whether the given method is considered to be a repository base - * class method. - * - * @param method - * @param repositoryInterface - * @return - */ - private boolean isBaseClassMethod(Method method, - Class repositoryInterface) { - - Assert.notNull(method); - - if (method.getDeclaringClass().isAssignableFrom( - getRepositoryClass(repositoryInterface))) { - return true; - } - - return !method.equals(getBaseClassMethod(method, repositoryInterface)); - } - - - /** - * Returns the base class method that is backing the given method. This can - * be necessary if a repository interface redeclares a method in - * {@link Repository} (e.g. for transaction behaviour customization). - * Returns the method itself if the base class does not implement the given - * method. - * - * @param method - * @return - */ - private Method getBaseClassMethod(Method method, - Class repositoryInterface) { - - Assert.notNull(method); - - Method result = methodCache.get(method); - - if (null != result) { - return result; - } - - result = - getBaseClassMethodFor(method, - getRepositoryClass(repositoryInterface), - repositoryInterface); - methodCache.put(method, result); - - return result; - } - - - /** - * Returns whether the given method is a custom repository method. - * - * @param method - * @param repositoryInterface - * @return - */ - private boolean isCustomMethod(Method method, Class repositoryInterface) { - - Class declaringClass = method.getDeclaringClass(); - - boolean isQueryMethod = declaringClass.equals(repositoryInterface); - boolean isRepositoryInterface = - isGenericRepositoryInterface(declaringClass); - boolean isBaseClassMethod = - isBaseClassMethod(method, repositoryInterface); - - return !(isRepositoryInterface || isBaseClassMethod || isQueryMethod); - } - - - /** - * Returns all methods considered to be finder methods. - * - * @param repositoryInterface - * @return - */ - private Iterable getFinderMethods(Class repositoryInterface) { - - Set result = new HashSet(); - - for (Method method : repositoryInterface.getDeclaredMethods()) { - if (!isCustomMethod(method, repositoryInterface) - && !isBaseClassMethod(method, repositoryInterface)) { - result.add(method); - } - } - - return result; - } - - - /** - * Validates the given repository interface. - * - * @param repositoryInterface - */ - private void validate(Class repositoryInterface) { - - Assert.notNull(repositoryInterface); - Assert.notNull( - getDomainClass(repositoryInterface), - "Could not retrieve domain class from interface. Make sure it extends GenericRepository."); - - } - - /** * Validates the given repository interface as well as the given custom * implementation. * - * @param repositoryInterface + * @param repositoryMetadata * @param customImplementation */ - protected void validate( - Class> repositoryInterface, + protected void validate(RepositoryMetadata repositoryMetadata, Object customImplementation) { - validate(repositoryInterface); - if (null == customImplementation - && hasCustomMethod(repositoryInterface)) { + && repositoryMetadata.hasCustomMethod()) { throw new IllegalArgumentException( String.format( "You have custom methods in %s but not provided a custom implementation!", - repositoryInterface)); + repositoryMetadata.getRepositoryInterface())); } } @@ -343,8 +195,8 @@ public abstract class RepositoryFactorySupport { new ConcurrentHashMap(); private final Object customImplementation; - private final Class repositoryInterface; - private final RepositorySupport target; + private final RepositoryMetadata metadata; + private final Object target; /** @@ -352,18 +204,18 @@ public abstract class RepositoryFactorySupport { * of {@link QueryMethod}s to be invoked on execution of repository * interface methods. */ - public QueryExecuterMethodInterceptor(Class repositoryInterface, - Object customImplementation, RepositorySupport target) { + public QueryExecuterMethodInterceptor( + RepositoryMetadata repositoryInterface, + Object customImplementation, Object target) { - this.repositoryInterface = repositoryInterface; + this.metadata = repositoryInterface; this.customImplementation = customImplementation; this.target = target; QueryLookupStrategy lookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey); - for (Method method : getFinderMethods(repositoryInterface)) { - + for (Method method : metadata.getQueryMethods()) { queries.put(method, lookupStrategy.resolveQuery(method)); } } @@ -393,8 +245,7 @@ public abstract class RepositoryFactorySupport { // Lookup actual method as it might be redeclared in the interface // and we have to use the repository instance nevertheless - Method actualMethod = - getBaseClassMethod(method, repositoryInterface); + Method actualMethod = metadata.getBaseClassMethod(method); return executeMethodOn(target, actualMethod, invocation.getArguments()); } @@ -449,7 +300,7 @@ public abstract class RepositoryFactorySupport { return false; } - return isCustomMethod(invocation.getMethod(), repositoryInterface); + return metadata.isCustomMethod(invocation.getMethod()); } } } diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryMetadata.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryMetadata.java new file mode 100644 index 000000000..9d75b7862 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryMetadata.java @@ -0,0 +1,104 @@ +/* + * Copyright 2011 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.repository.support; + +import java.lang.reflect.Method; + + +/** + * Metadata for repository interfaces. + * + * @author Oliver Gierke + */ +public interface RepositoryMetadata { + + /** + * Returns the repository interface. + * + * @return + */ + Class getRepositoryInterface(); + + + /** + * Returns the base class to be used to create the proxy backing instance. + * + * @return + */ + Class getRepositoryBaseClass(); + + + /** + * Returns if the configured repository interface has custom methods, that + * might have to be delegated to a custom implementation. This is used to + * verify repository configuration. + * + * @return + */ + boolean hasCustomMethod(); + + + /** + * Returns whether the given method is a custom repository method. + * + * @param method + * @param baseClass + * @return + */ + boolean isCustomMethod(Method method); + + + /** + * Returns all methods considered to be query methods. + * + * @param repositoryInterface + * @return + */ + Iterable getQueryMethods(); + + + /** + * Returns the base class method that is backing the given method. This can + * be necessary if a repository interface redeclares a method of the core + * repository interface (e.g. for transaction behaviour customization). + * Returns the method itself if the base class does not implement the given + * method. + * + * @param method + * @return + */ + Method getBaseClassMethod(Method method); + + + /** + * Returns the domain class the repository is declared for. + * + * @param clazz + * @return the domain class the repository is handling or {@code null} if + * none found. + */ + Class getDomainClass(); + + + /** + * Returns the id class the given class is declared for. + * + * @param clazz + * @return the id class of the entity managed by the repository for or + * {@code null} if none found. + */ + Class getIdClass(); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositorySupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositorySupport.java deleted file mode 100644 index 93ebf69a9..000000000 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositorySupport.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2008-2010 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.repository.support; - -import java.io.Serializable; - -import org.springframework.data.domain.Persistable; -import org.springframework.data.repository.Repository; -import org.springframework.util.Assert; - - -/** - * Abstract base class for generic repositories. Captures information about the - * domain class to be managed. - * - * @author Oliver Gierke - * @param the type of entity to be handled - */ -public abstract class RepositorySupport implements - Repository { - - private final Class domainClass; - private IsNewAware isNewStrategy; - - - /** - * Creates a new {@link RepositorySupport}. - * - * @param domainClass - */ - public RepositorySupport(Class domainClass) { - - Assert.notNull(domainClass); - this.domainClass = domainClass; - } - - - /** - * Returns the domain class to handle. - * - * @return the domain class - */ - protected Class getDomainClass() { - - return domainClass; - } - - - /** - * Return whether the given entity is to be regarded as new. Default - * implementation will inspect the given domain class and use either - * {@link PersistableEntityInformation} if the class implements - * {@link Persistable} or {@link ReflectiveEntityInformation} otherwise. - * - * @param entity - * @return - */ - protected abstract IsNewAware createIsNewStrategy(Class domainClass); - - - /** - * Returns the strategy how to determine whether an entity is to be regarded - * as new. - * - * @return the isNewStrategy - */ - protected IsNewAware getIsNewStrategy() { - - if (isNewStrategy == null) { - this.isNewStrategy = createIsNewStrategy(domainClass); - Assert.notNull(isNewStrategy); - } - - return isNewStrategy; - } -} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/util/ClassUtils.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/util/ClassUtils.java index 203a4eb5c..2fdabe3e9 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/util/ClassUtils.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/util/ClassUtils.java @@ -15,14 +15,10 @@ */ package org.springframework.data.repository.util; -import static org.springframework.core.GenericTypeResolver.*; - -import java.io.Serializable; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import java.lang.reflect.TypeVariable; import java.util.Arrays; import java.util.Collection; @@ -38,13 +34,6 @@ import org.springframework.util.StringUtils; */ public abstract class ClassUtils { - @SuppressWarnings("rawtypes") - private static final TypeVariable>[] PARAMETERS = - Repository.class.getTypeParameters(); - private static final String DOMAIN_TYPE_NAME = PARAMETERS[0].getName(); - private static final String ID_TYPE_NAME = PARAMETERS[1].getName(); - - /** * Private constructor to prevent instantiation. */ @@ -53,40 +42,6 @@ public abstract class ClassUtils { } - /** - * Returns the domain class the given class is declared for. Will introspect - * the given class for extensions of {@link Repository} and retrieve the - * domain class type from its generics declaration. - * - * @param clazz - * @return the domain class the given class is repository for or - * {@code null} if none found. - */ - public static Class getDomainClass(Class clazz) { - - Class[] arguments = resolveTypeArguments(clazz, Repository.class); - return arguments == null ? null : arguments[0]; - } - - - /** - * Returns the id class the given class is declared for. Will introspect the - * given class for extensions of {@link Repository} or and retrieve the - * {@link Serializable} type from its generics declaration. - * - * @param clazz - * @return the id class the given class is repository for or {@code null} if - * none found. - */ - @SuppressWarnings("unchecked") - public static Class getIdClass(Class clazz) { - - Class[] arguments = resolveTypeArguments(clazz, Repository.class); - return (Class) (arguments == null ? null - : arguments[1]); - } - - /** * Returns the domain class returned by the given {@link Method}. Will * extract the type from {@link Collection}s and @@ -241,114 +196,4 @@ public abstract class ClassUtils { throw ex; } - - - /** - * Returns the given base class' method if the given method (declared in the - * interface) was also declared at the base class. Returns the given method - * if the given base class does not declare the method given. Takes generics - * into account. - * - * @param method - * @param baseClass - * @param repositoryInterface - * @return - */ - public static Method getBaseClassMethodFor(Method method, - Class baseClass, Class repositoryInterface) { - - for (Method baseClassMethod : baseClass.getMethods()) { - - // Wrong name - if (!method.getName().equals(baseClassMethod.getName())) { - continue; - } - - // Wrong number of arguments - if (!(method.getParameterTypes().length == baseClassMethod - .getParameterTypes().length)) { - continue; - } - - // Check whether all parameters match - if (!parametersMatch(method, baseClassMethod, repositoryInterface)) { - continue; - } - - return baseClassMethod; - } - - return method; - } - - - /** - * Checks the given method's parameters to match the ones of the given base - * class method. Matches generic arguments agains the ones bound in the - * given repository interface. - * - * @param method - * @param baseClassMethod - * @param repositoryInterface - * @return - */ - private static boolean parametersMatch(Method method, - Method baseClassMethod, Class repositoryInterface) { - - Type[] genericTypes = baseClassMethod.getGenericParameterTypes(); - Class[] types = baseClassMethod.getParameterTypes(); - Class[] methodParameters = method.getParameterTypes(); - - for (int i = 0; i < genericTypes.length; i++) { - - Type type = genericTypes[i]; - - if (type instanceof TypeVariable) { - - String name = ((TypeVariable) type).getName(); - - if (!matchesGenericType(name, methodParameters[i], - repositoryInterface)) { - return false; - } - - } else { - - if (!types[i].equals(methodParameters[i])) { - return false; - } - } - } - - return true; - } - - - /** - * Checks whether the given parameter type matches the generic type of the - * given parameter. Thus when {@literal PK} is declared, the method ensures - * that given method parameter is the primary key type declared in the given - * repository interface e.g. - * - * @param name - * @param parameterType - * @param repositoryInterface - * @return - */ - private static boolean matchesGenericType(String name, - Class parameterType, Class repositoryInterface) { - - Class entityType = getDomainClass(repositoryInterface); - Class idClass = getIdClass(repositoryInterface); - - if (ID_TYPE_NAME.equals(name) && parameterType.equals(idClass)) { - return true; - } - - if (DOMAIN_TYPE_NAME.equals(name) && parameterType.equals(entityType)) { - return true; - } - - return false; - } } diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AbstractEntityMetadataUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AbstractEntityMetadataUnitTests.java new file mode 100644 index 000000000..c8afa7aba --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AbstractEntityMetadataUnitTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2011 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.repository.support; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; + + +/** + * Unit tests for {@link AbstractEntityMetadata}. + * + * @author Oliver Gierke + */ +public class AbstractEntityMetadataUnitTests { + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullDomainClass() throws Exception { + + new DummyAbstractEntityMetadata(null); + } + + + @Test + public void considersEntityNewIfGetIdReturnsNull() throws Exception { + + EntityMetadata metadata = + new DummyAbstractEntityMetadata(Object.class); + assertThat(metadata.isNew(null), is(true)); + assertThat(metadata.isNew(new Object()), is(false)); + } + + private static class DummyAbstractEntityMetadata extends + AbstractEntityMetadata { + + public DummyAbstractEntityMetadata(Class domainClass) { + + super(domainClass); + } + + + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.support.EntityMetadata#getId( + * java.lang.Object) + */ + public Object getId(Object entity) { + + return entity == null ? null : entity.toString(); + } + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/DefaultRepositoryMetadataUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/DefaultRepositoryMetadataUnitTests.java new file mode 100644 index 000000000..7eb6da8d0 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/DefaultRepositoryMetadataUnitTests.java @@ -0,0 +1,187 @@ +/* + * Copyright 2011 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.repository.support; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.junit.Test; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.util.ClassUtils; + + +/** + * Unit tests for {@link DefaultRepositoryMetadata}. + * + * @author Oliver Gierke + */ +public class DefaultRepositoryMetadataUnitTests { + + @SuppressWarnings("rawtypes") + static final Class REPOSITORY = + DummyGenericRepositorySupport.class; + + + @Test + public void looksUpDomainClassCorrectly() throws Exception { + + RepositoryMetadata metadata = + new DefaultRepositoryMetadata(UserRepository.class, REPOSITORY); + assertEquals(User.class, metadata.getDomainClass()); + + metadata = new DefaultRepositoryMetadata(SomeDao.class, REPOSITORY); + assertEquals(User.class, metadata.getDomainClass()); + } + + + @Test + public void findsDomainClassOnExtensionOfDaoInterface() throws Exception { + + RepositoryMetadata metadata = + new DefaultRepositoryMetadata( + ExtensionOfUserCustomExtendedDao.class, REPOSITORY); + assertEquals(User.class, metadata.getDomainClass()); + } + + + @Test + public void detectsParameterizedEntitiesCorrectly() { + + RepositoryMetadata metadata = + new DefaultRepositoryMetadata(GenericEntityRepository.class, + REPOSITORY); + assertEquals(GenericEntity.class, metadata.getDomainClass()); + } + + + @Test + public void looksUpIdClassCorrectly() throws Exception { + + RepositoryMetadata metadata = + new DefaultRepositoryMetadata(UserRepository.class, REPOSITORY); + + assertEquals(Integer.class, metadata.getIdClass()); + } + + + @Test + public void discoversRepositoryBaseClassMethod() throws Exception { + + Method method = FooDao.class.getMethod("findById", Integer.class); + DefaultRepositoryMetadata metadata = + new DefaultRepositoryMetadata(FooDao.class, REPOSITORY); + + Method reference = metadata.getBaseClassMethodFor(method); + assertEquals(REPOSITORY, reference.getDeclaringClass()); + assertThat(reference.getName(), is("findById")); + } + + + @Test + public void discoveresNonRepositoryBaseClassMethod() throws Exception { + + Method method = FooDao.class.getMethod("readById", Long.class); + + DefaultRepositoryMetadata metadata = + new DefaultRepositoryMetadata(FooDao.class, Repository.class); + + assertThat(metadata.getBaseClassMethodFor(method), is(method)); + } + + @SuppressWarnings("unused") + private class User { + + private String firstname; + + + public String getAddress() { + + return null; + } + } + + static interface UserRepository extends Repository { + + } + + /** + * Sample interface to serve two purposes: + *
    + *
  1. Check that {@link ClassUtils#getDomainClass(Class)} skips non + * {@link GenericDao} interfaces
  2. + *
  3. Check that {@link ClassUtils#getDomainClass(Class)} traverses + * interface hierarchy
  4. + *
+ * + * @author Oliver Gierke + */ + private interface SomeDao extends Serializable, UserRepository { + + Page findByFirstname(Pageable pageable, String firstname); + } + + private static interface FooDao extends Repository { + + // Redeclared method + User findById(Integer primaryKey); + + + // Not a redeclared method + User readById(Long primaryKey); + } + + /** + * Sample interface to test recursive lookup of domain class. + * + * @author Oliver Gierke + */ + static interface ExtensionOfUserCustomExtendedDao extends + UserCustomExtendedRepository { + + } + + static interface UserCustomExtendedRepository extends + Repository { + + } + + static abstract class DummyGenericRepositorySupport + implements Repository { + + public T findById(ID id) { + + return null; + } + } + + /** + * Helper class to reproduce #256. + * + * @author Oliver Gierke + */ + static class GenericEntity { + } + + static interface GenericEntityRepository extends + Repository, Long> { + + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationTests.java index 16fdc8dd7..502a093e5 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationTests.java @@ -23,7 +23,7 @@ import org.springframework.data.domain.Persistable; /** - * Unit test for {@link PersistableEntityInformation}. + * Unit test for {@link PersistableEntityMetadata}. * * @author Oliver Gierke */ @@ -32,23 +32,25 @@ public class PersistableEntityInformationTests { @Test public void detectsPersistableCorrectly() throws Exception { - PersistableEntityInformation info = new PersistableEntityInformation(); + PersistableEntityMetadata info = new PersistableEntityMetadata(); assertNewAndNoId(info, new PersistableEntity(null)); assertNotNewAndId(info, new PersistableEntity(1L), 1L); } - private void assertNewAndNoId(T info, - Object entity) { + @SuppressWarnings("rawtypes") + private > void assertNewAndNoId( + S info, Persistable entity) { assertThat(info.isNew(entity), is(true)); assertThat(info.getId(entity), is(nullValue())); } - private void assertNotNewAndId(T info, - Object entity, Object id) { + @SuppressWarnings("rawtypes") + private > void assertNotNewAndId( + S info, Persistable entity, Object id) { assertThat(info.isNew(entity), is(false)); assertThat(info.getId(entity), is(id)); diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityMetadataUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityMetadataUnitTests.java new file mode 100644 index 000000000..b22620b91 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityMetadataUnitTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2011 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.repository.support; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.domain.Persistable; + + +/** + * Unit tests for {@link PersistableEntityMetadata}. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class PersistableEntityMetadataUnitTests { + + static final PersistableEntityMetadata metadata = + new PersistableEntityMetadata(); + + @Mock + Persistable persistable; + + + @Test + @SuppressWarnings("serial") + public void usesPersistablesGetId() throws Exception { + + when(persistable.getId()).thenReturn(2L, 1L, 3L); + assertEquals(2L, metadata.getId(persistable)); + assertEquals(1L, metadata.getId(persistable)); + assertEquals(3L, metadata.getId(persistable)); + } + + + @Test + public void usesPersistablesIsNew() throws Exception { + + when(persistable.isNew()).thenReturn(true, false); + assertThat(metadata.isNew(persistable), is(true)); + assertThat(metadata.isNew(persistable), is(false)); + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/util/ClassUtilsUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/util/ClassUtilsUnitTests.java index 8f7c9989f..e8a111f35 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/repository/util/ClassUtilsUnitTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/util/ClassUtilsUnitTests.java @@ -15,18 +15,15 @@ */ package org.springframework.data.repository.util; -import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.springframework.data.repository.util.ClassUtils.*; import java.io.Serializable; -import java.lang.reflect.Method; import org.junit.Test; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.repository.Repository; -import org.springframework.data.repository.support.RepositorySupport; /** @@ -36,23 +33,6 @@ import org.springframework.data.repository.support.RepositorySupport; */ public class ClassUtilsUnitTests { - @Test - public void looksUpDomainClassCorrectly() throws Exception { - - assertEquals(User.class, getDomainClass(UserRepository.class)); - assertEquals(User.class, getDomainClass(SomeDao.class)); - assertNull(getDomainClass(Serializable.class)); - } - - - @Test - public void looksUpIdClassCorrectly() throws Exception { - - assertEquals(Integer.class, getIdClass(UserRepository.class)); - assertNull(getIdClass(Serializable.class)); - } - - @Test(expected = IllegalStateException.class) public void rejectsInvalidReturnType() throws Exception { @@ -61,14 +41,6 @@ public class ClassUtilsUnitTests { } - @Test - public void findsDomainClassOnExtensionOfDaoInterface() throws Exception { - - assertEquals(User.class, - getDomainClass(ExtensionOfUserCustomExtendedDao.class)); - } - - @Test public void determinesValidFieldsCorrectly() { @@ -77,48 +49,6 @@ public class ClassUtilsUnitTests { assertFalse(hasProperty(User.class, "address")); } - - /** - * References #256. - */ - @Test - public void detectsParameterizedEntitiesCorrectly() { - - assertEquals(GenericEntity.class, - getDomainClass(GenericEntityDao.class)); - } - - - /** - * #301 - */ - @Test - public void discoversDaoBaseClassMethod() throws Exception { - - Method method = FooDao.class.getMethod("findById", Integer.class); - - Method reference = - getBaseClassMethodFor(method, - DummyGenericRepositorySupport.class, FooDao.class); - assertEquals(DummyGenericRepositorySupport.class, - reference.getDeclaringClass()); - assertThat(reference.getName(), is("findById")); - } - - - /** - * #301 - */ - @Test - public void discoveresNonDaoBaseClassMethod() throws Exception { - - Method method = FooDao.class.getMethod("readById", Long.class); - - assertThat( - getBaseClassMethodFor(method, RepositorySupport.class, - FooDao.class), is(method)); - } - @SuppressWarnings("unused") private class User { @@ -150,62 +80,4 @@ public class ClassUtilsUnitTests { Page findByFirstname(Pageable pageable, String firstname); } - - /** - * Sample interface to test recursive lookup of domain class. - * - * @author Oliver Gierke - */ - static interface ExtensionOfUserCustomExtendedDao extends - UserCustomExtendedRepository { - - } - - static interface UserCustomExtendedRepository extends - Repository { - - } - - /** - * Helper class to reproduce #256. - * - * @author Oliver Gierke - */ - static class GenericEntity { - } - - static interface GenericEntityDao extends - Repository, Long> { - - } - - /** - * Sample DAO interface to test redeclaration of {@link GenericDao} methods. - * - * @author Oliver Gierke - */ - private static interface FooDao extends Repository { - - // Redeclared method - User findById(Integer primaryKey); - - - // Not a redeclared method - User readById(Long primaryKey); - } - - static abstract class DummyGenericRepositorySupport - extends RepositorySupport { - - public DummyGenericRepositorySupport(Class domainClass) { - - super(domainClass); - } - - - public T findById(ID id) { - - return null; - } - } } diff --git a/src/main/resources/changelog.txt b/src/main/resources/changelog.txt index 3dec394a0..6378d132e 100644 --- a/src/main/resources/changelog.txt +++ b/src/main/resources/changelog.txt @@ -6,8 +6,9 @@ Changes in version 1.0.0.M4 Repository * Improved ParameterAccessor infrastructure -* Added support for 'Distinct' keyword in finder method names -* Added support for 'In' and 'NotIn' keywords +* Added support for 'Distinct' keyword in finder method names (DATACMNS-15) +* Added support for 'In' and 'NotIn' keywords (DATACMNS-16) +* Introduced metamodel for entities and repositories (DATACMNS-17) Changes in version 1.0.0.M3 (2011-02-09) ---------------------------------------- From 9ffefbcd5011d07af9cd50b2a99e79b4d1926f21 Mon Sep 17 00:00:00 2001 From: Michael Hunger Date: Tue, 1 Mar 2011 13:40:42 +0100 Subject: [PATCH 08/20] updated aspectj to 1.6.11.M2 --- spring-data-commons-aspects/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-data-commons-aspects/pom.xml b/spring-data-commons-aspects/pom.xml index 6397e1519..7d0c99b3a 100644 --- a/spring-data-commons-aspects/pom.xml +++ b/spring-data-commons-aspects/pom.xml @@ -11,7 +11,7 @@ jar Spring Data Commons Aspects - 1.6.10.RELEASE + 1.6.11.M2 From c738effd39f007447eb1683d01bed1b60862d81e Mon Sep 17 00:00:00 2001 From: Michael Hunger Date: Tue, 1 Mar 2011 13:43:32 +0100 Subject: [PATCH 09/20] fixed bug with stateholder holding onto the temporary state for too long --- .../support/AbstractConstructorEntityInstantiator.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/spring-data-commons-core/src/main/java/org/springframework/persistence/support/AbstractConstructorEntityInstantiator.java b/spring-data-commons-core/src/main/java/org/springframework/persistence/support/AbstractConstructorEntityInstantiator.java index 6747a900b..795d7c059 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/persistence/support/AbstractConstructorEntityInstantiator.java +++ b/spring-data-commons-core/src/main/java/org/springframework/persistence/support/AbstractConstructorEntityInstantiator.java @@ -44,8 +44,13 @@ public abstract class AbstractConstructorEntityInstantiator Date: Tue, 1 Mar 2011 20:37:18 +0100 Subject: [PATCH 10/20] Added getBindableValue(int index) to ParameterAccessor. Adapted ParametersParameterAccessor accordingly. --- .../data/repository/query/ParameterAccessor.java | 13 +++++++++++++ .../query/ParametersParameterAccessor.java | 9 ++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java index cd08e87e5..a15c175b6 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParameterAccessor.java @@ -49,6 +49,19 @@ public interface ParameterAccessor extends Iterable { Sort getSort(); + /** + * Returns the bindable value with the given index. Bindable means, that + * {@link Pageable} and {@link Sort} values are skipped without noticed in + * the index. For a method signature taking {@link String}, {@link Pageable} + * , {@link String}, {@code #getBindableParameter(1)} would return the + * second {@link String} value. + * + * @param index + * @return + */ + Object getBindableValue(int index); + + /** * Returns an iterator over all bindable parameters. This means * parameters implementing {@link Pageable} or {@link Sort} will not be diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParametersParameterAccessor.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParametersParameterAccessor.java index 51bf82b8a..47c38be4f 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParametersParameterAccessor.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/ParametersParameterAccessor.java @@ -89,7 +89,14 @@ public class ParametersParameterAccessor implements ParameterAccessor { } - private Object getBindableValue(int index) { + /* + * (non-Javadoc) + * + * @see + * org.springframework.data.repository.query.ParameterAccessor#getBindableValue + * (int) + */ + public Object getBindableValue(int index) { return values[parameters.getBindableParameter(index).getIndex()]; } From 1b98d32a4fa12f1eeca70ec93427f141c7d78e82 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Tue, 1 Mar 2011 20:38:15 +0100 Subject: [PATCH 11/20] Extended resolveQuery of QueryLookupStrategy to take domain class as well. --- .../data/repository/query/QueryLookupStrategy.java | 3 ++- .../data/repository/support/RepositoryFactorySupport.java | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryLookupStrategy.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryLookupStrategy.java index e88043d1c..5633e227c 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryLookupStrategy.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryLookupStrategy.java @@ -54,7 +54,8 @@ public interface QueryLookupStrategy { * that can be executed afterwards. * * @param method + * @param domainClass * @return */ - RepositoryQuery resolveQuery(Method method); + RepositoryQuery resolveQuery(Method method, Class domainClass); } \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java index 567092d3f..5fa125449 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java @@ -216,7 +216,10 @@ public abstract class RepositoryFactorySupport { getQueryLookupStrategy(queryLookupStrategyKey); for (Method method : metadata.getQueryMethods()) { - queries.put(method, lookupStrategy.resolveQuery(method)); + queries.put( + method, + lookupStrategy.resolveQuery(method, + repositoryInterface.getDomainClass())); } } From ff844d7cbcd11573dedbb7836b75adc159a53b48 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Tue, 1 Mar 2011 20:39:52 +0100 Subject: [PATCH 12/20] Changed getReturnedDomainClass to only resolve generic type for Collections and Pages. --- .../data/repository/util/ClassUtils.java | 14 +++++++++----- .../repository/util/ClassUtilsUnitTests.java | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/util/ClassUtils.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/util/ClassUtils.java index 2fdabe3e9..ef41bee9c 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/util/ClassUtils.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/util/ClassUtils.java @@ -22,6 +22,7 @@ import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collection; +import org.springframework.data.domain.Page; import org.springframework.data.repository.Repository; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -52,15 +53,18 @@ public abstract class ClassUtils { */ public static Class getReturnedDomainClass(Method method) { - Type type = method.getGenericReturnType(); + Class returnType = method.getReturnType(); + + if (Collection.class.isAssignableFrom(returnType) + || Page.class.isAssignableFrom(returnType)) { + + Type type = method.getGenericReturnType(); - if (type instanceof ParameterizedType) { return (Class) ((ParameterizedType) type) .getActualTypeArguments()[0]; - - } else { - return method.getReturnType(); } + + return returnType; } diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/util/ClassUtilsUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/util/ClassUtilsUnitTests.java index e8a111f35..98df4b23b 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/repository/util/ClassUtilsUnitTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/util/ClassUtilsUnitTests.java @@ -41,6 +41,17 @@ public class ClassUtilsUnitTests { } + @Test + public void determinesReturnType() throws Exception { + + assertEquals(User.class, + getReturnedDomainClass(SomeDao.class.getMethod( + "findByFirstname", Pageable.class, String.class))); + assertEquals(GenericType.class, + getReturnedDomainClass(SomeDao.class.getMethod("someMethod"))); + } + + @Test public void determinesValidFieldsCorrectly() { @@ -79,5 +90,12 @@ public class ClassUtilsUnitTests { private interface SomeDao extends Serializable, UserRepository { Page findByFirstname(Pageable pageable, String firstname); + + + GenericType someMethod(); + } + + private class GenericType { + } } From 695f4ca9c00464599b57b99627759bfa9bc062b8 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Tue, 1 Mar 2011 20:42:14 +0100 Subject: [PATCH 13/20] Fixed sorting not being applied if provided via method parameter. AbstractQueryCreator now uses the Sort object from the parameters if the PartTree not already contains a Sort object (which got there due to a OrderBy directive in the method name). --- .../data/repository/query/parser/AbstractQueryCreator.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/AbstractQueryCreator.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/AbstractQueryCreator.java index 3ccdf52b1..182811375 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/AbstractQueryCreator.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/AbstractQueryCreator.java @@ -62,7 +62,10 @@ public abstract class AbstractQueryCreator { */ public T createQuery() { - return complete(createCriteria(tree), tree.getSort()); + Sort treeSort = tree.getSort(); + Sort sort = treeSort != null ? treeSort : parameters.getSort(); + + return complete(createCriteria(tree), sort); } From cae2961275e01213114c5f126654e6ca1f8a32b2 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 2 Mar 2011 09:02:18 +0100 Subject: [PATCH 14/20] DATACMNS-19 - Fixed PersistableEntityMetadata. Added generics to PersistableEntityMetadata and let it being handed the actual domain class to return. --- .../support/PersistableEntityMetadata.java | 12 +-- .../PersistableEntityInformationTests.java | 83 ------------------- .../PersistableEntityMetadataUnitTests.java | 32 ++++++- src/main/resources/changelog.txt | 1 + 4 files changed, 36 insertions(+), 92 deletions(-) delete mode 100644 spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationTests.java diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityMetadata.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityMetadata.java index 63ba9b016..bd2c16313 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityMetadata.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityMetadata.java @@ -26,17 +26,17 @@ import org.springframework.data.domain.Persistable; * @author Oliver Gierke */ @SuppressWarnings("rawtypes") -public class PersistableEntityMetadata extends - AbstractEntityMetadata { +public class PersistableEntityMetadata extends + AbstractEntityMetadata { /** * Creates a new {@link PersistableEntityMetadata}. * * @param domainClass */ - public PersistableEntityMetadata() { + public PersistableEntityMetadata(Class domainClass) { - super(Persistable.class); + super(domainClass); } @@ -48,7 +48,7 @@ public class PersistableEntityMetadata extends * .Object) */ @Override - public boolean isNew(Persistable entity) { + public boolean isNew(T entity) { return entity.isNew(); } @@ -61,7 +61,7 @@ public class PersistableEntityMetadata extends * org.springframework.data.repository.support.IdAware#getId(java.lang.Object * ) */ - public Object getId(Persistable entity) { + public Object getId(T entity) { return entity.getId(); } diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationTests.java deleted file mode 100644 index 502a093e5..000000000 --- a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationTests.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2008-2010 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.repository.support; - -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; - -import org.junit.Test; -import org.springframework.data.domain.Persistable; - - -/** - * Unit test for {@link PersistableEntityMetadata}. - * - * @author Oliver Gierke - */ -public class PersistableEntityInformationTests { - - @Test - public void detectsPersistableCorrectly() throws Exception { - - PersistableEntityMetadata info = new PersistableEntityMetadata(); - - assertNewAndNoId(info, new PersistableEntity(null)); - assertNotNewAndId(info, new PersistableEntity(1L), 1L); - } - - - @SuppressWarnings("rawtypes") - private > void assertNewAndNoId( - S info, Persistable entity) { - - assertThat(info.isNew(entity), is(true)); - assertThat(info.getId(entity), is(nullValue())); - } - - - @SuppressWarnings("rawtypes") - private > void assertNotNewAndId( - S info, Persistable entity, Object id) { - - assertThat(info.isNew(entity), is(false)); - assertThat(info.getId(entity), is(id)); - } - - static class PersistableEntity implements Persistable { - - private static final long serialVersionUID = -5898780128204716452L; - - private final Long id; - - - public PersistableEntity(Long id) { - - this.id = id; - } - - - public Long getId() { - - return id; - } - - - public boolean isNew() { - - return id == null; - } - } -} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityMetadataUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityMetadataUnitTests.java index b22620b91..a376376d9 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityMetadataUnitTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityMetadataUnitTests.java @@ -34,15 +34,15 @@ import org.springframework.data.domain.Persistable; @RunWith(MockitoJUnitRunner.class) public class PersistableEntityMetadataUnitTests { - static final PersistableEntityMetadata metadata = - new PersistableEntityMetadata(); + @SuppressWarnings("rawtypes") + static final PersistableEntityMetadata metadata = + new PersistableEntityMetadata(Persistable.class); @Mock Persistable persistable; @Test - @SuppressWarnings("serial") public void usesPersistablesGetId() throws Exception { when(persistable.getId()).thenReturn(2L, 1L, 3L); @@ -59,4 +59,30 @@ public class PersistableEntityMetadataUnitTests { assertThat(metadata.isNew(persistable), is(true)); assertThat(metadata.isNew(persistable), is(false)); } + + + @Test + public void returnsGivenClassAsEntityType() throws Exception { + + PersistableEntityMetadata info = + new PersistableEntityMetadata( + PersistableEntity.class); + + assertEquals(PersistableEntity.class, info.getJavaType()); + } + + @SuppressWarnings("serial") + static class PersistableEntity implements Persistable { + + public Long getId() { + + return null; + } + + + public boolean isNew() { + + return false; + } + } } diff --git a/src/main/resources/changelog.txt b/src/main/resources/changelog.txt index 6378d132e..be90e9b5b 100644 --- a/src/main/resources/changelog.txt +++ b/src/main/resources/changelog.txt @@ -9,6 +9,7 @@ Repository * Added support for 'Distinct' keyword in finder method names (DATACMNS-15) * Added support for 'In' and 'NotIn' keywords (DATACMNS-16) * Introduced metamodel for entities and repositories (DATACMNS-17) +* Fixed returning wrong class PersistableEntityMetadata(DATACMNS-19) Changes in version 1.0.0.M3 (2011-02-09) ---------------------------------------- From 3a6113c5e832c5501a1b0d9e61ab2d3d437f5fd7 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 2 Mar 2011 11:14:50 +0100 Subject: [PATCH 15/20] Added toString methods to make debugging easier. --- .../data/repository/query/parser/Part.java | 12 +++++++++ .../repository/query/parser/PartTree.java | 27 +++++++++++++++++++ .../query/parser/PartTreeUnitTests.java | 11 ++++++++ 3 files changed, 50 insertions(+) diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java index d16eed9cc..85eb537d8 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/Part.java @@ -113,6 +113,18 @@ public class Part { } + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + return String.format("%s %s", property.getName(), type); + } + + /** * @return the type */ diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java index 0bf145a22..826799ac5 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java @@ -27,6 +27,7 @@ import java.util.regex.Pattern; import org.springframework.data.domain.Sort; import org.springframework.data.repository.query.parser.PartTree.OrPart; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** @@ -179,6 +180,20 @@ public class PartTree implements Iterable { return group != null && group.contains(DISTINCT); } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + return String.format("%s %s", + StringUtils.collectionToDelimitedString(nodes, " or "), + orderBySource.toString()); + } + /** * A part of the parsed source that results from splitting up the resource * ar {@literal Or} keywords. Consists of {@link Part}s that have to be @@ -209,6 +224,18 @@ public class PartTree implements Iterable { } + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + + return StringUtils.collectionToDelimitedString(children, " and "); + } + + /* * (non-Javadoc) * diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java index f7223d120..1f1f9c2f2 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/query/parser/PartTreeUnitTests.java @@ -82,6 +82,17 @@ public class PartTreeUnitTests { } + @Test + public void parsesCombinedAndAndOrPropertiesCorrectly() throws Exception { + + PartTree tree = + new PartTree("firstnameAndLastnameOrLastname", User.class); + assertPart(tree, new Part[] { new Part("firstname", User.class), + new Part("lastname", User.class) }, new Part[] { new Part( + "lastname", User.class) }); + } + + @Test public void hasSortIfOrderByIsGiven() throws Exception { From 841c78acb69a2403d47d5429310225275472cafe Mon Sep 17 00:00:00 2001 From: Thomas Risberg Date: Wed, 2 Mar 2011 08:57:20 -0500 Subject: [PATCH 16/20] DATADOC-48 moving over cross-store code from previous prototype --- spring-data-commons-aspects/.classpath | 4 +- .../.settings/org.eclipse.jdt.core.prefs | 15 +- spring-data-commons-aspects/pom.xml | 12 ++ .../AsynchStoreCompletionListener.java | 32 +++++ .../persistence/EntityOperations.java | 78 +++++++++++ .../persistence/OrderedEntityOperations.java | 38 ++++++ .../persistence/RelatedEntity.java | 65 +++++++++ .../AbstractDeferredUpdateMixinFields.aj | 129 ++++++++++++++++++ .../persistence/support/ChangeSet.java | 24 ++++ .../persistence/support/ChangeSetBacked.java | 13 ++ .../support/ChangeSetConfiguration.java | 28 ++++ .../support/ChangeSetPersister.java | 47 +++++++ .../support/ChangeSetSynchronizer.java | 28 ++++ ...edSetBackedTransactionSynchronization.java | 66 +++++++++ .../persistence/support/HashMapChangeSet.java | 51 +++++++ ...SimpleReflectiveChangeSetSynchronizer.java | 96 +++++++++++++ spring-data-commons-aspects/template.mf | 4 +- 17 files changed, 723 insertions(+), 7 deletions(-) create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/AsynchStoreCompletionListener.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityOperations.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/OrderedEntityOperations.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/RelatedEntity.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/AbstractDeferredUpdateMixinFields.aj create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSet.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetBacked.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetConfiguration.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetPersister.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetSynchronizer.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangedSetBackedTransactionSynchronization.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/HashMapChangeSet.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/SimpleReflectiveChangeSetSynchronizer.java diff --git a/spring-data-commons-aspects/.classpath b/spring-data-commons-aspects/.classpath index 16f01e2ee..19ed14911 100644 --- a/spring-data-commons-aspects/.classpath +++ b/spring-data-commons-aspects/.classpath @@ -1,7 +1,7 @@ - - + + diff --git a/spring-data-commons-aspects/.settings/org.eclipse.jdt.core.prefs b/spring-data-commons-aspects/.settings/org.eclipse.jdt.core.prefs index e78b07017..c4dacddd4 100644 --- a/spring-data-commons-aspects/.settings/org.eclipse.jdt.core.prefs +++ b/spring-data-commons-aspects/.settings/org.eclipse.jdt.core.prefs @@ -1,6 +1,13 @@ -#Wed Nov 17 12:20:43 EST 2010 +#Tue Mar 01 12:59:14 EST 2011 eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 -org.eclipse.jdt.core.compiler.compliance=1.5 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.source=1.5 +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/spring-data-commons-aspects/pom.xml b/spring-data-commons-aspects/pom.xml index 7d0c99b3a..9bbc0d239 100644 --- a/spring-data-commons-aspects/pom.xml +++ b/spring-data-commons-aspects/pom.xml @@ -81,6 +81,13 @@ true + + + org.hibernate.javax.persistence + hibernate-jpa-2.0-api + 1.0.0.Final + + org.mockito mockito-all @@ -108,6 +115,11 @@ Springframework Maven SNAPSHOT Repository http://maven.springframework.org/snapshot + + jboss-repository + JBoss Public Repository + http://repository.jboss.org/nexus/content/groups/public-jboss + diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/AsynchStoreCompletionListener.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/AsynchStoreCompletionListener.java new file mode 100644 index 000000000..4f417bb57 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/AsynchStoreCompletionListener.java @@ -0,0 +1,32 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; + +/** + * Listener interface for asynchronous storage operations. + * Can be annotated with OnlyOnFailure as an optimization + * if the listener is only interested in compensating transactions + * in the event of write failure. + * + * @author Rod Johnson + * + * @param new value type + */ +public interface AsynchStoreCompletionListener { + + /** + * Constant indicating no store completion action + */ + class NONE implements AsynchStoreCompletionListener { + public void onCompletion(AsynchStoreCompletionListener.StoreResult result, Object newValue, Field foreignStore) {} + } + + enum StoreResult { + SUCCESS, + FAILURE, + INDETERMINATE + }; + + void onCompletion(StoreResult result, V newValue, Field foreignStore); + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityOperations.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityOperations.java new file mode 100644 index 000000000..df6aa2f49 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityOperations.java @@ -0,0 +1,78 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; + +import org.springframework.dao.DataAccessException; + +/** + * Interface to be implemented for each persistence technology, + * handling operations for the relevant entity type. + * Parameters: Key=K, Entity class=E + * + * @author Rod Johnson + */ +public interface EntityOperations { + + /** + * Is this clazz supported by the current EntityOperations? + * @param entityClass + * @param fs ForeignStore annotation, may be null + * @return + */ + boolean supports(Class entityClass, RelatedEntity fs); + + /** + * Return null if not found + * @param + * @param entityClass + * @param pk + * @return + * @throws DataAccessException + */ + E findEntity(Class entityClass, K pk) throws DataAccessException; + + /** + * Find the unique key for the given entity whose class this EntityOperations + * understands. For example, it might be the id property value. + * @param entity + * @return + * @throws DataAccessException + */ + K findUniqueKey(E entity) throws DataAccessException; + + /** + * + * @param entityClass + * @return the type of the unique key for this supported entity + * @throws DataAccessException + */ + Class uniqueKeyType(Class entityClass) throws DataAccessException; + + boolean isTransient(E entity) throws DataAccessException; + + /** + * Persist. Will cause key to be non-null. + * @param owner Persistent root entity, which has the RelatedEntity field + * @param entity + * @param f Foreign store field for entity being persisted + * @param fs ForeignStore annotation + * @throws DataAccessException + * @return the new unique key + */ + K makePersistent(Object owner, E entity, Field f, RelatedEntity fs) throws DataAccessException; + + /** + * Is this type of entity transactional? + * @return + */ + boolean isTransactional(); + + /** + * Should the field be cached in the entity? For some entity types + * such as streams, there should be no caching, and the value + * should be retrieved from the persistent store every time. + * @return + */ + boolean cacheInEntity(); + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/OrderedEntityOperations.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/OrderedEntityOperations.java new file mode 100644 index 000000000..e1cf11a83 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/OrderedEntityOperations.java @@ -0,0 +1,38 @@ +package org.springframework.persistence; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.Ordered; +import org.springframework.dao.DataAccessException; + +/** + * Convenient base class for EntityOperations implementations + * that adds ordering support. + * @author Rod Johnson + * + * @param + * @param + */ +public abstract class OrderedEntityOperations implements EntityOperations, Ordered { + + protected final Log log = LogFactory.getLog(getClass()); + + private int order = Integer.MAX_VALUE; + + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + + /** + * Convenient default. Subclasses with non-Long key types can override this if they wish. + */ + @Override + public Class uniqueKeyType(Class entityClass) throws DataAccessException { + return Long.class; + } +} \ No newline at end of file diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/RelatedEntity.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/RelatedEntity.java new file mode 100644 index 000000000..f9e538c82 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/RelatedEntity.java @@ -0,0 +1,65 @@ +package org.springframework.persistence; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation indicating that a field may be stored in a foreign store + * and specifying the necessary guarantees. Conceptual rather than + * implementation-specific. + * @see ForeignStoreKeyManager + * + * @author Rod Johnson + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface RelatedEntity { + + /** + * + * Optional information as to how to compute or locate the key value. + * Some strategies may take this into account. + */ + String keyExpression() default ""; + + /** + * Should we use the key of the present entity + * @return + */ + boolean sameKey() default false; + + /** + * Policies for persistence + * @return + */ + PersistencePolicy policy() default @PersistencePolicy(); + + /** + * Name for the preferred data store. Merely a hint. May not be followed. + * @return + */ + String preferredStore() default ""; + + /** + * Is asynchronous store OK? + * @return + */ + boolean asynchStore() default false; + + // TODO - indicates if an asynchronous write should begin + // only after commit of a transaction + boolean afterCommit() default false; + + /** + * Completion listener class. Only used if asynchStore is true. + * Must have a no-arg constructor. + * @return + */ + @SuppressWarnings("unchecked") + Class storeCompletionListenerClass() default AsynchStoreCompletionListener.NONE.class; + + String storeCompletionListenerBeanName() default ""; + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/AbstractDeferredUpdateMixinFields.aj b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/AbstractDeferredUpdateMixinFields.aj new file mode 100644 index 000000000..bdfe6ce5b --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/AbstractDeferredUpdateMixinFields.aj @@ -0,0 +1,129 @@ +package org.springframework.persistence.support; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; + +import org.aspectj.lang.reflect.FieldSignature; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Aspect that saves field access in a ChangeSet + * + * @author Rod Johnson + * @author Thomas Risberg + */ +public abstract aspect AbstractDeferredUpdateMixinFields extends AbstractTypeAnnotatingMixinFields { + + //------------------------------------------------------------------------- + // Configure aspect for whole system. + // init() method can be invoked automatically if the aspect is a Spring + // bean, or called in user code. + //------------------------------------------------------------------------- + // Aspect shared config + + private ChangeSetPersister changeSetPersister; + + private ChangeSetSynchronizer changeSetManager; + + public void setChangeSetConfiguration(ChangeSetConfiguration changeSetConfiguration) { + this.changeSetPersister = changeSetConfiguration.getChangeSetPersister(); + this.changeSetManager = changeSetConfiguration.getChangeSetManager(); + } + + //------------------------------------------------------------------------- + // Advise user-defined constructors of ChangeSetBacked objects to create a new + // backing ChangeSet + //------------------------------------------------------------------------- + pointcut arbitraryUserConstructorOfChangeSetBackedObject(ChangeSetBacked entity) : + execution((@ET ChangeSetBacked+).new(..)) && + !execution((@ET ChangeSetBacked+).new(ChangeSet)) && + this(entity); + + // Or could use cflow + pointcut finderConstructorOfChangeSetBackedObject(ChangeSetBacked entity, ChangeSet cs) : + execution((@ET ChangeSetBacked+).new(ChangeSet)) && + this(entity) && + args(cs); + + + before(ChangeSetBacked entity) : arbitraryUserConstructorOfChangeSetBackedObject(entity) { + entity.itdChangeSetPersister = changeSetPersister; + log.info("User-defined constructor called on ChangeSetBacked object of class " + entity.getClass()); + // Populate all properties + ChangeSet changeSet = new HashMapChangeSet(); + changeSetManager.populateChangeSet(changeSet, entity); + entity.setChangeSet(changeSet); + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + throw new InvalidDataAccessResourceUsageException("No transaction synchronization is active"); + } + TransactionSynchronizationManager.registerSynchronization(new ChangedSetBackedTransactionSynchronization(changeSetPersister, entity)); + } + + before(ChangeSetBacked entity, ChangeSet changeSet) : finderConstructorOfChangeSetBackedObject(entity, changeSet) { + entity.itdChangeSetPersister = changeSetPersister; + changeSetManager.populateEntity(changeSet, entity); + + // Now leave an empty ChangeSet to listen only to future changes + entity.setChangeSet(new HashMapChangeSet()); + + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + throw new InvalidDataAccessResourceUsageException("No transaction synchronization is active"); + } + TransactionSynchronizationManager.registerSynchronization(new ChangedSetBackedTransactionSynchronization(changeSetPersister, entity)); + } + + + //------------------------------------------------------------------------- + // ChangeSet-related mixins + //------------------------------------------------------------------------- + // Introduced field + private ChangeSet ChangeSetBacked.changeSet; + + private ChangeSetPersister ChangeSetBacked.itdChangeSetPersister; + + public void ChangeSetBacked.setChangeSet(ChangeSet cs) { + this.changeSet = cs; + } + + public ChangeSet ChangeSetBacked.getChangeSet() { + return changeSet; + } + + // Flush the entity state to the persistent store + public void ChangeSetBacked.flush() { + itdChangeSetPersister.persistState(this.getClass(), this.changeSet); + } + + public Object ChangeSetBacked.getId() { + return itdChangeSetPersister.getPersistentId(this.getClass(), this.changeSet); + } + + + //------------------------------------------------------------------------- + // Around advice for field get/set + //------------------------------------------------------------------------- + // Nothing to do on field get unless laziness desired + + Object around(ChangeSetBacked entity, Object newVal) : entityFieldSet(entity, newVal) { + Field f = ((FieldSignature) thisJoinPoint.getSignature()).getField(); + + String propName = f.getName();//getRedisPropertyName(thisJoinPoint.getSignature()); + if (newVal instanceof Number) { + log.info("SET " + f + " -> ChangeSet number value property [" + propName + "] with value=[" + newVal + "]"); + entity.getChangeSet().set(propName, (Number) newVal); + } + else if (newVal instanceof String) { + log.info("SET " + f + " -> ChangeSet string value property [" + propName + "] with value=[" + newVal + "]"); + entity.getChangeSet().set(propName, (String) newVal); + } + else { + log.info("Don't know how to SET " + f + " with value=[" + newVal + "]"); + } + return proceed(entity, newVal); + } + +} \ No newline at end of file diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSet.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSet.java new file mode 100644 index 000000000..2fa0ff0d6 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSet.java @@ -0,0 +1,24 @@ +package org.springframework.persistence.support; + +import java.util.Map; + +import org.springframework.core.convert.ConversionService; + +/** + * Interface representing the set of changes in an entity. + * + * @author Rod Johnson + * @author Thomas Risberg + * + */ +public interface ChangeSet { + + T get(String key, Class requiredClass, ConversionService cs); + + void set(String key, Object o); + + Map getValues(); + + Object removeProperty(String k); + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetBacked.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetBacked.java new file mode 100644 index 000000000..cf1994e74 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetBacked.java @@ -0,0 +1,13 @@ +package org.springframework.persistence.support; + + +/** + * Interface introduced to objects exposing ChangeSet information + * @author Rod Johnson + * @author Thomas Risberg + */ +public interface ChangeSetBacked { + + ChangeSet getChangeSet(); + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetConfiguration.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetConfiguration.java new file mode 100644 index 000000000..3bdd97f1b --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetConfiguration.java @@ -0,0 +1,28 @@ +package org.springframework.persistence.support; + +public class ChangeSetConfiguration { + + private ChangeSetPersister changeSetPersister; + + private ChangeSetSynchronizer changeSetManager; + + public ChangeSetPersister getChangeSetPersister() { + return changeSetPersister; + } + + public void setChangeSetPersister(ChangeSetPersister changeSetPersister) { + this.changeSetPersister = changeSetPersister; + } + + public ChangeSetSynchronizer getChangeSetManager() { + return changeSetManager; + } + + public void setChangeSetManager( + ChangeSetSynchronizer changeSetManager) { + this.changeSetManager = changeSetManager; + } + + + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetPersister.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetPersister.java new file mode 100644 index 000000000..2f63414b2 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetPersister.java @@ -0,0 +1,47 @@ +package org.springframework.persistence.support; + +import org.springframework.dao.DataAccessException; + +/** + * Interface to be implemented by classes that can synchronize + * between data stores and ChangeSets. + * @author Rod Johnson + * + * @param entity key + */ +public interface ChangeSetPersister { + + String ID_KEY = "_id"; + + String CLASS_KEY = "_class"; + + /** + * TODO how to tell when not found? throw exception? + */ + void getPersistentState(Class entityClass, K key, ChangeSet changeSet) throws DataAccessException, NotFoundException; + + /** + * Return id + * @param cs + * @return + * @throws DataAccessException + */ + K getPersistentId(Class entityClass, ChangeSet cs) throws DataAccessException; + + /** + * Return key + * @param cs Key may be null if not persistent + * @return + * @throws DataAccessException + */ + K persistState(Class entityClass, ChangeSet cs) throws DataAccessException; + + /** + * Exception thrown in alternate control flow if getPersistentState + * finds no entity data. + */ + class NotFoundException extends Exception { + + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetSynchronizer.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetSynchronizer.java new file mode 100644 index 000000000..098365e1a --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetSynchronizer.java @@ -0,0 +1,28 @@ +package org.springframework.persistence.support; + +import java.util.Map; + +import org.springframework.dao.DataAccessException; + +/** + * Interface to be implemented by classes that can synchronize + * between entities and ChangeSets. + * @author Rod Johnson + * + * @param + */ +public interface ChangeSetSynchronizer { + + Map> persistentFields(Class entityClassClass); + + /** + * Take all entity fields into a changeSet. + * @param entity + * @return + * @throws DataAccessException + */ + void populateChangeSet(ChangeSet changeSet, E entity) throws DataAccessException; + + void populateEntity(ChangeSet changeSet, E entity) throws DataAccessException; + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangedSetBackedTransactionSynchronization.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangedSetBackedTransactionSynchronization.java new file mode 100644 index 000000000..82cf1c3a6 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangedSetBackedTransactionSynchronization.java @@ -0,0 +1,66 @@ +package org.springframework.persistence.support; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.transaction.support.TransactionSynchronization; + +public class ChangedSetBackedTransactionSynchronization implements TransactionSynchronization { + + protected final Log log = LogFactory.getLog(getClass()); + + private ChangeSetPersister changeSetPersister; + + private ChangeSetBacked entity; + + private int changeSetTxStatus = -1; + + public ChangedSetBackedTransactionSynchronization(ChangeSetPersister changeSetPersister, ChangeSetBacked entity) { + this.changeSetPersister = changeSetPersister; + this.entity = entity; + } + + @Override + public void afterCommit() { + log.debug("After Commit called for " + entity); + changeSetPersister.persistState(entity.getClass(), entity.getChangeSet()); + changeSetTxStatus = 0; + } + + @Override + public void afterCompletion(int status) { + log.debug("After Completion called with status = " + status); + if (changeSetTxStatus == 0) { + if (status == STATUS_COMMITTED) { + // this is good + log.debug("ChangedSetBackedTransactionSynchronization completed successfully for " + this.entity); + } + else { + // this could be bad - TODO: compensate + log.error("ChangedSetBackedTransactionSynchronization failed for " + this.entity); + } + } + } + + @Override + public void beforeCommit(boolean readOnly) { + } + + @Override + public void beforeCompletion() { + } + + @Override + public void flush() { + } + + @Override + public void resume() { + throw new IllegalStateException("ChangedSetBackedTransactionSynchronization does not support transaction suspension currently."); + } + + @Override + public void suspend() { + throw new IllegalStateException("ChangedSetBackedTransactionSynchronization does not support transaction suspension currently."); + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/HashMapChangeSet.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/HashMapChangeSet.java new file mode 100644 index 000000000..e9fe86104 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/HashMapChangeSet.java @@ -0,0 +1,51 @@ +package org.springframework.persistence.support; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.core.convert.ConversionService; + +/** + * Simple ChangeSet implementation backed by a HashMap. + * @author Thomas Risberg + * @author Rod Johnson + */ +public class HashMapChangeSet implements ChangeSet { + + private Map values; + + public HashMapChangeSet(Map values) { + this.values = values; + } + + public HashMapChangeSet() { + this(new HashMap()); + } + + @Override + public void set(String key, Object o) { + values.put(key, o); + } + + @Override + public String toString() { + return "HashMapChangeSet: values=[" + values + "]"; + } + + @Override + public Map getValues() { + return Collections.unmodifiableMap(values); + } + + @Override + public Object removeProperty(String k) { + return this.values.remove(k); + } + + @Override + public T get(String key, Class requiredClass, ConversionService conversionService) { + return conversionService.convert(values.get(key), requiredClass); + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/SimpleReflectiveChangeSetSynchronizer.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/SimpleReflectiveChangeSetSynchronizer.java new file mode 100644 index 000000000..43b6f6b51 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/SimpleReflectiveChangeSetSynchronizer.java @@ -0,0 +1,96 @@ +package org.springframework.persistence.support; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.convert.ConversionService; +import org.springframework.dao.DataAccessException; +import org.springframework.persistence.RelatedEntity; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.FieldCallback; +import org.springframework.util.ReflectionUtils.FieldFilter; + +/** + * Synchronizes fields to ChangeSets, regardless of visibility. + * + * @author Rod Johnson + */ +public class SimpleReflectiveChangeSetSynchronizer implements ChangeSetSynchronizer { + + /** + * Filter matching infrastructure fields, so they can be excluded + */ + private static FieldFilter PERSISTABLE_FIELDS = new FieldFilter() { + @Override + public boolean matches(Field f) { + return !( + f.isSynthetic() || + Modifier.isStatic(f.getModifiers()) || + Modifier.isTransient(f.getModifiers()) || + f.getName().startsWith("ajc$") || + f.isAnnotationPresent(RelatedEntity.class) + ); + } + }; + + private final Log log = LogFactory.getLog(getClass()); + + private final ConversionService conversionService; + + @Autowired + public SimpleReflectiveChangeSetSynchronizer(ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Override + public Map> persistentFields(Class entityClass) { + final Map> fields = new HashMap>(); + ReflectionUtils.doWithFields(entityClass, new FieldCallback() { + @Override + public void doWith(Field f) throws IllegalArgumentException, IllegalAccessException { + fields.put(f.getName(), f.getType()); + } + }, PERSISTABLE_FIELDS); + return fields; + } + + @Override + public void populateChangeSet(final ChangeSet changeSet, final ChangeSetBacked entity) throws DataAccessException { + ReflectionUtils.doWithFields(entity.getClass(), new FieldCallback() { + @Override + public void doWith(Field f) throws IllegalArgumentException, IllegalAccessException { + f.setAccessible(true); + if (log.isDebugEnabled()) { + log.debug("POPULATE ChangeSet value from entity field: " + f); + } + changeSet.set(f.getName(), f.get(entity)); + } + }, PERSISTABLE_FIELDS); + String classShortName = ClassUtils.getShortName(entity.getClass()); + changeSet.set("_class", classShortName); + } + + @Override + public void populateEntity(final ChangeSet changeSet, final ChangeSetBacked entity) throws DataAccessException { + ReflectionUtils.doWithFields(entity.getClass(), new FieldCallback() { + @Override + public void doWith(Field f) throws IllegalArgumentException, IllegalAccessException { + if (changeSet.getValues().containsKey(f.getName())) { + f.setAccessible(true); + if (log.isDebugEnabled()) { + log.debug("POPULATE entity from ChangeSet for field: " + f); + } + Object val = changeSet.get(f.getName(), f.getType(), conversionService); + f.set(entity, val); + } + } + }, PERSISTABLE_FIELDS); + } + +} diff --git a/spring-data-commons-aspects/template.mf b/spring-data-commons-aspects/template.mf index 1acd51461..84c7abd31 100644 --- a/spring-data-commons-aspects/template.mf +++ b/spring-data-commons-aspects/template.mf @@ -4,13 +4,15 @@ Bundle-Vendor: SpringSource Bundle-ManifestVersion: 2 Import-Package: sun.reflect;version="0";resolution:=optional +Excluded-Imports: + org.springframework.persistence Import-Template: org.springframework.beans.*;version="[3.0.0, 4.0.0)", org.springframework.core.*;version="[3.0.0, 4.0.0)", org.springframework.dao.*;version="[3.0.0, 4.0.0)", + org.springframework.transaction..*;version="[3.0.0, 4.0.0)", org.springframework.util.*;version="[3.0.0, 4.0.0)", org.springframework.data.core.*;version="[1.0.0, 2.0.0)", - org.springframework.data.persistence.*;version="[1.0.0, 2.0.0)", org.aopalliance.*;version="[1.0.0, 2.0.0)";resolution:=optional, org.apache.commons.logging.*;version="[1.1.1, 2.0.0)", org.aspectj.*;version="[1.6.5, 2.0.0)", From af99e4e8257cc639b452143b1c217de4f3b38143 Mon Sep 17 00:00:00 2001 From: Thomas Risberg Date: Wed, 2 Mar 2011 10:07:58 -0500 Subject: [PATCH 17/20] moved DocBook to pre-site --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b88452e65..d83db4c97 100644 --- a/pom.xml +++ b/pom.xml @@ -133,7 +133,7 @@ generate-html generate-pdf - package + pre-site From 67c2c58b42b61885a8a8b408957f0f606ffab086 Mon Sep 17 00:00:00 2001 From: Michael Hunger Date: Fri, 4 Mar 2011 02:14:19 +0100 Subject: [PATCH 18/20] set both target and source to 1.6 in the pom --- spring-data-commons-parent/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-data-commons-parent/pom.xml b/spring-data-commons-parent/pom.xml index 814723b98..af749539a 100644 --- a/spring-data-commons-parent/pom.xml +++ b/spring-data-commons-parent/pom.xml @@ -269,8 +269,8 @@ org.apache.maven.plugins maven-compiler-plugin - 1.5 - 1.5 + 1.6 + 1.6 -Xlint:all true false From e157f380bb2109b7a6fe45a909c495b67e7b6ed3 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Sun, 27 Feb 2011 18:33:34 +0100 Subject: [PATCH 19/20] DATADOC-34 - Infrastructure for callbacks on RepositoryQuery creation. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduced QueryCreationListener that allows plugging in functionality after a query was created. Heavily refactored QueryMethod and RepositoryQuery subsystem. Splitted up simple EntityMetadata (domain class and potentially other stuff) from the more advanced methods like getId(Object) and isNew(…). QueryMethod now carries a type enum that allows switching over it to determine the execution. Beyond that QueryMethod now returns a EntityMetadata rather than a plain class. RepositoryQuery in turn exposes the query method. PartTree allows iterating over all contained Parts as well now. Introduced AbstractEntityInformation that regards an entity as new if its id is null. Simplified handling of RepositoryProxyPostProcessors and aligned them to QueryCreationListener handling. --- .../org/springframework/data/domain/Sort.java | 18 +++ .../data/repository/query/QueryMethod.java | 61 +++++++--- .../repository/query/RepositoryQuery.java | 10 ++ .../repository/query/parser/PartTree.java | 20 +++ ...ta.java => AbstractEntityInformation.java} | 12 +- .../repository/support/EntityInformation.java | 42 +++++++ .../repository/support/EntityMetadata.java | 18 --- ...java => PersistableEntityInformation.java} | 8 +- .../support/QueryCreationListener.java | 35 ++++++ .../support/RepositoryFactoryBeanSupport.java | 20 --- .../support/RepositoryFactorySupport.java | 47 ++++++- ...sactionalRepositoryFactoryBeanSupport.java | 29 +++-- ...> AbstractEntityInformationUnitTests.java} | 16 +-- ...ersistableEntityInformationUnitTests.java} | 10 +- .../RepositoryFactorySupportUnitTests.java | 115 ++++++++++++++++++ 15 files changed, 367 insertions(+), 94 deletions(-) rename spring-data-commons-core/src/main/java/org/springframework/data/repository/support/{AbstractEntityMetadata.java => AbstractEntityInformation.java} (77%) create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityInformation.java rename spring-data-commons-core/src/main/java/org/springframework/data/repository/support/{PersistableEntityMetadata.java => PersistableEntityInformation.java} (86%) create mode 100644 spring-data-commons-core/src/main/java/org/springframework/data/repository/support/QueryCreationListener.java rename spring-data-commons-core/src/test/java/org/springframework/data/repository/support/{AbstractEntityMetadataUnitTests.java => AbstractEntityInformationUnitTests.java} (76%) rename spring-data-commons-core/src/test/java/org/springframework/data/repository/support/{PersistableEntityMetadataUnitTests.java => PersistableEntityInformationUnitTests.java} (86%) create mode 100644 spring-data-commons-core/src/test/java/org/springframework/data/repository/support/RepositoryFactorySupportUnitTests.java diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/domain/Sort.java b/spring-data-commons-core/src/main/java/org/springframework/data/domain/Sort.java index 8d733568f..d300a618a 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/domain/Sort.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/domain/Sort.java @@ -111,6 +111,24 @@ public class Sort implements } + /** + * Returns the order registered for the given property. + * + * @param property + * @return + */ + public Order getOrderFor(String property) { + + for (Order order : this) { + if (order.getProperty().equals(property)) { + return order; + } + } + + return null; + } + + /* * (non-Javadoc) * diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryMethod.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryMethod.java index 80fa9241b..f05f5e720 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryMethod.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/QueryMethod.java @@ -23,6 +23,8 @@ import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.repository.support.EntityMetadata; +import org.springframework.data.repository.util.ClassUtils; import org.springframework.util.Assert; @@ -35,6 +37,11 @@ import org.springframework.util.Assert; */ public class QueryMethod { + public static enum Type { + + SINGLE_ENTITY, PAGING, COLLECTION, MODIFYING; + } + private final Method method; private final Parameters parameters; @@ -82,27 +89,21 @@ public class QueryMethod { } - /** - * Returns whether the given - * - * @param number - * @return - */ - public boolean isCorrectNumberOfParameters(int number) { + public EntityMetadata getEntityMetadata() { - return number == parameters.getBindableParameters() - .getNumberOfParameters(); + return new EntityMetadata() { + + public Class getJavaType() { + + return getDomainClass(); + } + }; } - /** - * Returns the domain class for this method. - * - * @return - */ - public Class getDomainClass() { + protected Class getDomainClass() { - return getReturnedDomainClass(method); + return ClassUtils.getReturnedDomainClass(method); } @@ -112,7 +113,7 @@ public class QueryMethod { * * @return */ - public boolean isCollectionQuery() { + protected boolean isCollectionQuery() { Class returnType = method.getReturnType(); return org.springframework.util.ClassUtils.isAssignable(List.class, @@ -125,7 +126,7 @@ public class QueryMethod { * * @return */ - public boolean isPageQuery() { + protected boolean isPageQuery() { Class returnType = method.getReturnType(); return org.springframework.util.ClassUtils.isAssignable(Page.class, @@ -133,6 +134,30 @@ public class QueryMethod { } + public Type getType() { + + if (isModifyingQuery()) { + return Type.MODIFYING; + } + + if (isPageQuery()) { + return Type.PAGING; + } + + if (isCollectionQuery()) { + return Type.COLLECTION; + } + + return Type.SINGLE_ENTITY; + } + + + protected boolean isModifyingQuery() { + + return false; + } + + /** * Returns the {@link Parameters} wrapper to gain additional information * about {@link Method} parameters. diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/RepositoryQuery.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/RepositoryQuery.java index 860750b24..69bf0f0f1 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/RepositoryQuery.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/RepositoryQuery.java @@ -15,6 +15,8 @@ */ package org.springframework.data.repository.query; + + /** * Interface for a query abstraction. * @@ -30,4 +32,12 @@ public interface RepositoryQuery { * @return */ public Object execute(Object[] parameters); + + + /** + * Returns the + * + * @return + */ + public QueryMethod getQueryMethod(); } \ No newline at end of file diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java index 826799ac5..a9f644c67 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/query/parser/PartTree.java @@ -125,6 +125,26 @@ public class PartTree implements Iterable { } + /** + * Returns an {@link Iterable} of all parts contained in the + * {@link PartTree}. + * + * @return + */ + public Iterable getParts() { + + List result = new ArrayList(); + + for (OrPart orPart : this) { + for (Part part : orPart) { + result.add(part); + } + } + + return result; + } + + /** * Splits the given text at the given keywords. Expects camelcase style to * only match concrete keywords and not derivatives of it. diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AbstractEntityMetadata.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AbstractEntityInformation.java similarity index 77% rename from spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AbstractEntityMetadata.java rename to spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AbstractEntityInformation.java index 43f73433b..fd715e23e 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AbstractEntityMetadata.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/AbstractEntityInformation.java @@ -19,22 +19,24 @@ import org.springframework.util.Assert; /** - * Base class for implementations of {@link EntityMetadata}. Considers an entity - * to be new whenever {@link #getId(Object)} returns {@literal null}. + * Base class for implementations of {@link EntityInformation}. Considers an + * entity to be new whenever {@link #getId(Object)} returns {@literal null}. * * @author Oliver Gierke */ -public abstract class AbstractEntityMetadata implements EntityMetadata { +public abstract class AbstractEntityInformation implements + EntityInformation { private final Class domainClass; /** - * Creates a new {@link AbstractEntityMetadata} from the given domain class. + * Creates a new {@link AbstractEntityInformation} from the given domain + * class. * * @param domainClass */ - public AbstractEntityMetadata(Class domainClass) { + public AbstractEntityInformation(Class domainClass) { Assert.notNull(domainClass); this.domainClass = domainClass; diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityInformation.java new file mode 100644 index 000000000..33797d33f --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityInformation.java @@ -0,0 +1,42 @@ +/* + * Copyright 2011 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.repository.support; + +/** + * Extension of {@link EntityMetadata} to add functionality to query information + * of entity instances. + * + * @author Oliver Gierke + */ +public interface EntityInformation extends EntityMetadata { + + /** + * Returns whether the given entity is considered to be new. + * + * @param entity must never be {@literal null} + * @return + */ + boolean isNew(T entity); + + + /** + * Returns the id of the given entity. + * + * @param entity must never be {@literal null} + * @return + */ + Object getId(T entity); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityMetadata.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityMetadata.java index e62845cd6..ffc93e070 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityMetadata.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/EntityMetadata.java @@ -22,24 +22,6 @@ package org.springframework.data.repository.support; */ public interface EntityMetadata { - /** - * Returns whether the given entity is considered to be new. - * - * @param entity must never be {@literal null} - * @return - */ - boolean isNew(T entity); - - - /** - * Returns the id of the given entity. - * - * @param entity must never be {@literal null} - * @return - */ - Object getId(T entity); - - /** * Returns the actual domain class type. * diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityMetadata.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityInformation.java similarity index 86% rename from spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityMetadata.java rename to spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityInformation.java index bd2c16313..ffb873a39 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityMetadata.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/PersistableEntityInformation.java @@ -26,15 +26,15 @@ import org.springframework.data.domain.Persistable; * @author Oliver Gierke */ @SuppressWarnings("rawtypes") -public class PersistableEntityMetadata extends - AbstractEntityMetadata { +public class PersistableEntityInformation extends + AbstractEntityInformation { /** - * Creates a new {@link PersistableEntityMetadata}. + * Creates a new {@link PersistableEntityInformation}. * * @param domainClass */ - public PersistableEntityMetadata(Class domainClass) { + public PersistableEntityInformation(Class domainClass) { super(domainClass); } diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/QueryCreationListener.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/QueryCreationListener.java new file mode 100644 index 000000000..b704dfa78 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/QueryCreationListener.java @@ -0,0 +1,35 @@ +/* + * Copyright 2011 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.repository.support; + +import org.springframework.data.repository.query.RepositoryQuery; + + +/** + * Callback for listeners that want to execute functionality on + * {@link RepositoryQuery} creation. + * + * @author Oliver Gierke + */ +public interface QueryCreationListener { + + /** + * Will be invoked just after the {@link RepositoryQuery} was created. + * + * @param query + */ + void onCreation(T query); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryBeanSupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryBeanSupport.java index 0ce0d5b05..00ab4b49b 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryBeanSupport.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactoryBeanSupport.java @@ -15,9 +15,6 @@ */ package org.springframework.data.repository.support; -import java.util.Collections; -import java.util.List; - import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Required; @@ -124,23 +121,6 @@ public abstract class RepositoryFactoryBeanSupport> this.factory = createRepositoryFactory(); this.factory.setQueryLookupStrategyKey(queryLookupStrategyKey); - - for (RepositoryProxyPostProcessor processor : getRepositoryPostProcessors()) { - this.factory.addRepositoryProxyPostProcessor(processor); - } - } - - - /** - * Returns all {@link RepositoryProxyPostProcessor} to be added to the - * repository factory to be created. Default implementation will return an - * empty list. - * - * @return - */ - protected List getRepositoryPostProcessors() { - - return Collections.emptyList(); } diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java index 5fa125449..ecb978219 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/RepositoryFactorySupport.java @@ -26,6 +26,7 @@ import java.util.concurrent.ConcurrentHashMap; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.springframework.aop.framework.ProxyFactory; +import org.springframework.core.GenericTypeResolver; import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; @@ -49,6 +50,8 @@ public abstract class RepositoryFactorySupport { private final List postProcessors = new ArrayList(); private QueryLookupStrategy.Key queryLookupStrategyKey; + private List> queryPostProcessors = + new ArrayList>(); /** @@ -62,6 +65,20 @@ public abstract class RepositoryFactorySupport { } + /** + * Adds a {@link QueryCreationListener} to the factory to plug in + * functionality triggered right after creation of {@link RepositoryQuery} + * instances. + * + * @param listener + */ + public void addQueryCreationListener(QueryCreationListener listener) { + + Assert.notNull(listener); + this.queryPostProcessors.add(listener); + } + + /** * Adds {@link RepositoryProxyPostProcessor}s to the factory to allow * manipulation of the {@link ProxyFactory} before the proxy gets created. @@ -71,7 +88,7 @@ public abstract class RepositoryFactorySupport { * * @param processor */ - protected void addRepositoryProxyPostProcessor( + public void addRepositoryProxyPostProcessor( RepositoryProxyPostProcessor processor) { Assert.notNull(processor); @@ -205,10 +222,10 @@ public abstract class RepositoryFactorySupport { * interface methods. */ public QueryExecuterMethodInterceptor( - RepositoryMetadata repositoryInterface, + RepositoryMetadata repositoryMetadata, Object customImplementation, Object target) { - this.metadata = repositoryInterface; + this.metadata = repositoryMetadata; this.customImplementation = customImplementation; this.target = target; @@ -216,10 +233,28 @@ public abstract class RepositoryFactorySupport { getQueryLookupStrategy(queryLookupStrategyKey); for (Method method : metadata.getQueryMethods()) { - queries.put( - method, + RepositoryQuery query = lookupStrategy.resolveQuery(method, - repositoryInterface.getDomainClass())); + repositoryMetadata.getDomainClass()); + invokeListeners(query, metadata); + queries.put(method, query); + } + } + + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void invokeListeners(RepositoryQuery query, + RepositoryMetadata metadata) { + + for (QueryCreationListener listener : queryPostProcessors) { + Class typeArgument = + GenericTypeResolver.resolveTypeArgument( + listener.getClass(), + QueryCreationListener.class); + if (typeArgument != null + && typeArgument.isAssignableFrom(query.getClass())) { + listener.onCreation(query); + } } } diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/TransactionalRepositoryFactoryBeanSupport.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/TransactionalRepositoryFactoryBeanSupport.java index 4f2df71b2..4a189163f 100644 --- a/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/TransactionalRepositoryFactoryBeanSupport.java +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/support/TransactionalRepositoryFactoryBeanSupport.java @@ -15,9 +15,6 @@ */ package org.springframework.data.repository.support; -import java.util.Arrays; -import java.util.List; - import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.ListableBeanFactory; @@ -59,20 +56,32 @@ public abstract class TransactionalRepositoryFactoryBeanSupport getRepositoryPostProcessors() { + protected final RepositoryFactorySupport createRepositoryFactory() { - return Arrays.asList(txPostProcessor); + RepositoryFactorySupport factory = doCreateRepositoryFactory(); + factory.addRepositoryProxyPostProcessor(txPostProcessor); + return factory; } + /** + * Creates the actual {@link RepositoryFactorySupport} instance. + * + * @return + */ + protected abstract RepositoryFactorySupport doCreateRepositoryFactory(); + + /* * (non-Javadoc) * diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AbstractEntityMetadataUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AbstractEntityInformationUnitTests.java similarity index 76% rename from spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AbstractEntityMetadataUnitTests.java rename to spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AbstractEntityInformationUnitTests.java index c8afa7aba..9b114f0b2 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AbstractEntityMetadataUnitTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/AbstractEntityInformationUnitTests.java @@ -22,32 +22,32 @@ import org.junit.Test; /** - * Unit tests for {@link AbstractEntityMetadata}. + * Unit tests for {@link AbstractEntityInformation}. * * @author Oliver Gierke */ -public class AbstractEntityMetadataUnitTests { +public class AbstractEntityInformationUnitTests { @Test(expected = IllegalArgumentException.class) public void rejectsNullDomainClass() throws Exception { - new DummyAbstractEntityMetadata(null); + new DummyAbstractEntityInformation(null); } @Test public void considersEntityNewIfGetIdReturnsNull() throws Exception { - EntityMetadata metadata = - new DummyAbstractEntityMetadata(Object.class); + EntityInformation metadata = + new DummyAbstractEntityInformation(Object.class); assertThat(metadata.isNew(null), is(true)); assertThat(metadata.isNew(new Object()), is(false)); } - private static class DummyAbstractEntityMetadata extends - AbstractEntityMetadata { + private static class DummyAbstractEntityInformation extends + AbstractEntityInformation { - public DummyAbstractEntityMetadata(Class domainClass) { + public DummyAbstractEntityInformation(Class domainClass) { super(domainClass); } diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityMetadataUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationUnitTests.java similarity index 86% rename from spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityMetadataUnitTests.java rename to spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationUnitTests.java index a376376d9..e14b917fd 100644 --- a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityMetadataUnitTests.java +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/PersistableEntityInformationUnitTests.java @@ -32,11 +32,11 @@ import org.springframework.data.domain.Persistable; * @author Oliver Gierke */ @RunWith(MockitoJUnitRunner.class) -public class PersistableEntityMetadataUnitTests { +public class PersistableEntityInformationUnitTests { @SuppressWarnings("rawtypes") - static final PersistableEntityMetadata metadata = - new PersistableEntityMetadata(Persistable.class); + static final PersistableEntityInformation metadata = + new PersistableEntityInformation(Persistable.class); @Mock Persistable persistable; @@ -64,8 +64,8 @@ public class PersistableEntityMetadataUnitTests { @Test public void returnsGivenClassAsEntityType() throws Exception { - PersistableEntityMetadata info = - new PersistableEntityMetadata( + PersistableEntityInformation info = + new PersistableEntityInformation( PersistableEntity.class); assertEquals(PersistableEntity.class, info.getJavaType()); diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/RepositoryFactorySupportUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/RepositoryFactorySupportUnitTests.java new file mode 100644 index 000000000..8043fe941 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/repository/support/RepositoryFactorySupportUnitTests.java @@ -0,0 +1,115 @@ +/* + * Copyright 2011 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.repository.support; + +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.springframework.data.repository.query.RepositoryQuery; + + +/** + * Unit tests for {@link RepositoryFactorySupport}. + * + * @author Oliver Gierke + */ +@RunWith(MockitoJUnitRunner.class) +public class RepositoryFactorySupportUnitTests { + + RepositoryFactorySupport factory = new DummyRepositoryFactory(); + + @Mock + MyQueryCreationListener listener; + @Mock + PlainQueryCreationListener otherListener; + + + @Test + public void invokesCustomQueryCreationListenerForSpecialRepositoryQueryOnly() + throws Exception { + + factory.addQueryCreationListener(listener); + factory.addQueryCreationListener(otherListener); + + factory.getRepository(ObjectRepository.class); + + verify(listener, times(1)).onCreation(any(MyRepositoryQuery.class)); + verify(otherListener, times(2)).onCreation(any(RepositoryQuery.class)); + + } + + class DummyRepositoryFactory extends RepositoryFactorySupport { + + @Override + protected Object getTargetRepository(RepositoryMetadata metadata) { + + return new Object(); + } + + + @Override + protected Class getRepositoryBaseClass(Class repositoryInterface) { + + return Object.class; + } + + + @Override + protected QueryLookupStrategy getQueryLookupStrategy(Key key) { + + MyRepositoryQuery queryOne = mock(MyRepositoryQuery.class); + RepositoryQuery queryTwo = mock(RepositoryQuery.class); + + QueryLookupStrategy strategy = mock(QueryLookupStrategy.class); + when(strategy.resolveQuery(any(Method.class), any(Class.class))) + .thenReturn(queryOne, queryTwo); + + return strategy; + } + } + + interface ObjectRepository extends Repository { + + Object findByClass(Class clazz); + + + Object findByFoo(); + } + + interface PlainQueryCreationListener extends + QueryCreationListener { + + } + + interface MyQueryCreationListener extends + QueryCreationListener { + + } + + interface MyRepositoryQuery extends RepositoryQuery { + + } +} From c8465d8d034c3799fcbd11b0e3331e677ec86378 Mon Sep 17 00:00:00 2001 From: Thomas Risberg Date: Tue, 8 Mar 2011 23:22:22 -0500 Subject: [PATCH 20/20] DATADOC-48 adding cross-store code to support Mongo->JPA relationships --- .../.settings/org.eclipse.jdt.core.prefs | 2 +- spring-data-commons-aspects/pom.xml | 11 ++ .../ChainingEntityOperationsLocator.java | 51 ++++++ ...ChainingForeignStoreKeyManagerLocator.java | 52 ++++++ .../EntityManagerJpaEntityOperations.java | 88 +++++++++ .../persistence/EntityOperationsLocator.java | 22 +++ .../persistence/ForeignStoreKeyManager.java | 58 ++++++ .../ForeignStoreKeyManagerLocator.java | 24 +++ .../GeneratedFieldForeignStoreKeyManager.java | 65 +++++++ .../InvalidFieldAnnotationException.java | 20 ++ .../JpaEntityForeignStoreFieldTransience.aj | 17 ++ .../persistence/MappingValidator.java | 20 ++ .../OrderedForeignStoreKeyManager.java | 45 +++++ .../PresentKeyForeignStoreKeyManager.java | 44 +++++ .../RooConventionEntityOperations.java | 99 ++++++++++ .../Roo_GeneratedForeignStoreKeys.java | 10 + .../persistence/StoreSpanning.aj | 172 ++++++++++++++++++ .../UnknownEntityClassException.java | 12 ++ ...hangeSetConstructorEntityInstantiator.java | 17 ++ .../ChangeSetForeignStoreKeyManager.java | 94 ++++++++++ .../support/DefaultManagedSet.java | 162 +++++++++++++++++ .../persistence/support/ManagedSet.java | 27 +++ .../support/SimpleMappingValidator.java | 34 ++++ spring-data-commons-aspects/template.mf | 7 +- 24 files changed, 1151 insertions(+), 2 deletions(-) create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/ChainingEntityOperationsLocator.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/ChainingForeignStoreKeyManagerLocator.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityManagerJpaEntityOperations.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityOperationsLocator.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/ForeignStoreKeyManager.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/ForeignStoreKeyManagerLocator.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/GeneratedFieldForeignStoreKeyManager.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/InvalidFieldAnnotationException.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/JpaEntityForeignStoreFieldTransience.aj create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/MappingValidator.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/OrderedForeignStoreKeyManager.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/PresentKeyForeignStoreKeyManager.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/RooConventionEntityOperations.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/Roo_GeneratedForeignStoreKeys.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/StoreSpanning.aj create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/UnknownEntityClassException.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetConstructorEntityInstantiator.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetForeignStoreKeyManager.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/DefaultManagedSet.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ManagedSet.java create mode 100644 spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/SimpleMappingValidator.java diff --git a/spring-data-commons-aspects/.settings/org.eclipse.jdt.core.prefs b/spring-data-commons-aspects/.settings/org.eclipse.jdt.core.prefs index c4dacddd4..942f0c46e 100644 --- a/spring-data-commons-aspects/.settings/org.eclipse.jdt.core.prefs +++ b/spring-data-commons-aspects/.settings/org.eclipse.jdt.core.prefs @@ -1,4 +1,4 @@ -#Tue Mar 01 12:59:14 EST 2011 +#Tue Mar 08 11:29:43 EST 2011 eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 diff --git a/spring-data-commons-aspects/pom.xml b/spring-data-commons-aspects/pom.xml index 9bbc0d239..54152dc05 100644 --- a/spring-data-commons-aspects/pom.xml +++ b/spring-data-commons-aspects/pom.xml @@ -80,6 +80,17 @@ jsr250-api true + + javax.validation + validation-api + 1.0.0.GA + + + org.hibernate + hibernate-validator + 4.0.2.GA + test + diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ChainingEntityOperationsLocator.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ChainingEntityOperationsLocator.java new file mode 100644 index 000000000..e28b401a4 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ChainingEntityOperationsLocator.java @@ -0,0 +1,51 @@ +package org.springframework.persistence; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.core.OrderComparator; +import org.springframework.dao.DataAccessException; + +/** + * Chaining implementation of entity operations that automatically configures itself + * from a Spring context if available. + * + * @author Rod Johnson + */ +public class ChainingEntityOperationsLocator implements EntityOperationsLocator { + + private List entityOperationsList = new LinkedList(); + + @Autowired + public void init(ApplicationContext context) { + Map beansOfType = context.getBeansOfType(EntityOperations.class); + List l = new LinkedList(); + for (EntityOperations eo : beansOfType.values()) { + l.add(eo); + } + Collections.sort(l, new OrderComparator()); + for (EntityOperations eo : l) { + add(eo); + } + } + + public void add(EntityOperations ef) { + entityOperationsList.add(ef); + } + + @Override + public EntityOperations entityOperationsFor(Class entityClass, RelatedEntity fs) + throws DataAccessException { + for (EntityOperations eo : entityOperationsList) { + if (eo.supports(entityClass, fs)) { + return eo; + } + } + throw new UnknownEntityClassException(entityClass); + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ChainingForeignStoreKeyManagerLocator.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ChainingForeignStoreKeyManagerLocator.java new file mode 100644 index 000000000..3a5357cfa --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ChainingForeignStoreKeyManagerLocator.java @@ -0,0 +1,52 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.core.OrderComparator; +import org.springframework.dao.DataAccessException; + +/** + * Chaining implementation of ForeignStoreKeyManagerLocator that can be parameterized + * from a Spring ApplicationContext. + * + * @author Rod Johnson + * + */ +public class ChainingForeignStoreKeyManagerLocator implements ForeignStoreKeyManagerLocator { + + private List delegates = new LinkedList(); + + public void add(ForeignStoreKeyManager fskm) { + delegates.add(fskm); + } + + @Autowired + public void init(ApplicationContext context) { + Map beansOfType = context.getBeansOfType(ForeignStoreKeyManager.class); + List l = new LinkedList(); + for (ForeignStoreKeyManager fskm : beansOfType.values()) { + l.add(fskm); + } + Collections.sort(l, new OrderComparator()); + for (ForeignStoreKeyManager fskm : l) { + add(fskm); + } + } + + @Override + public ForeignStoreKeyManager foreignStoreKeyManagerFor(Class entityClass, Field f) throws DataAccessException { + for (ForeignStoreKeyManager fskm : delegates) { + if (fskm.isSupportedField(entityClass, f)) { + return fskm; + } + } + throw new IllegalArgumentException("No ForeignStoreKeyManager for " + entityClass + " on " + f); + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityManagerJpaEntityOperations.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityManagerJpaEntityOperations.java new file mode 100644 index 000000000..3c956ab57 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityManagerJpaEntityOperations.java @@ -0,0 +1,88 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import javax.persistence.Entity; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.springframework.dao.DataAccessException; + +/** + * Implementation of entity operations that works on any entity + * that adheres to Roo persist() and static finder conventions. + * Does not depend on Roo, merely on Roo conventions, which can + * also be implemented by hand. + * + * @author Rod Johnson + */ +public class EntityManagerJpaEntityOperations extends OrderedEntityOperations { + + @PersistenceContext + private EntityManager entityManager; + + public static Object invoke(Class clazz, String methodName, + Object target, Class[] argTypes, Object... args) { + try { + Method m = clazz.getMethod(methodName, argTypes); + return m.invoke(target, (Object[]) args); + } catch (Exception ex) { + // TODO FIX ME + // System.out.println(ex + ": checked exceptions are stupid"); + throw new IllegalArgumentException(ex); + } + } + + public static Object invokeNoArgMethod(Class clazz, String methodName, Object target) { + return invoke(clazz, methodName, target, (Class[]) null, + (Object[]) null); + } + + @Override + public Object findEntity(Class entityClass, Object pk) + throws DataAccessException { + String findMethod = "find" + entityClass.getSimpleName(); + Object found = entityManager.find(entityClass, pk); + log.info("Lookup [" + entityClass.getName() + "] by pk=[" + pk + "] using EntityManager.find() - found [" + found + "]"); + return found; + } + + + @Override + public Object findUniqueKey(Object entity) throws DataAccessException { + String idMethodName = "getId"; + return invokeNoArgMethod(entity.getClass(), idMethodName, entity); + } + + @Override + public boolean isTransient(Object entity) throws DataAccessException { + return findUniqueKey(entity) == null; + } + + @Override + public Object makePersistent(Object owner, Object entity, Field f, RelatedEntity fs) throws DataAccessException { + if (log.isDebugEnabled()) { + log.debug("Making entity persistent: BEFORE [" + entity + "]"); + } + entityManager.persist(entity); + Object key = findUniqueKey(entity); + log.info("Making entity persistent: AFTER [" + entity + "]"); + return key; + } + + @Override + public boolean supports(Class entityClass, RelatedEntity fs) { + return entityClass.isAnnotationPresent(Entity.class); + } + + @Override + public boolean cacheInEntity() { + return false; + } + + @Override + public boolean isTransactional() { + // TODO Need to have a better test + return true; + }} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityOperationsLocator.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityOperationsLocator.java new file mode 100644 index 000000000..64a70cd26 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/EntityOperationsLocator.java @@ -0,0 +1,22 @@ +package org.springframework.persistence; + +import org.springframework.dao.DataAccessException; + +/** + * Interface to be implemented by classes that can find EntityOperations + * implementations to handle particular classes. + * + * @author Rod Johnson + */ +public interface EntityOperationsLocator { + + /** + * Find the EntityOperations for this class. + * @param fs ForeignStore annotation (may be null) + * @param entityClass + * @return + * @throws DataAccessException if no EntityOperations can be found. + */ + EntityOperations entityOperationsFor(Class entityClass, RelatedEntity fs) throws DataAccessException; + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ForeignStoreKeyManager.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ForeignStoreKeyManager.java new file mode 100644 index 000000000..5efe6b21c --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ForeignStoreKeyManager.java @@ -0,0 +1,58 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; +import java.util.Set; + +import org.springframework.dao.DataAccessException; + +/** + * Interface to be implemented to infer or compute foreign store + * key values and possibly store them. + * + * @author Rod Johnson + * + */ +public interface ForeignStoreKeyManager { + + /** + * Is this entity class one we can store additional + * state in related to fields annotated with ForeignStore. + * @param entityClass + * @param foreignStore + * @return + */ + boolean isSupportedField(Class entityClass, Field foreignStore); + + /** + * + * @param entity + * @param foreignStore + * @return null if not yet persistent + * @throws DataAccessException if the key cannot be computed or stored + */ + K findForeignStoreKey(T entity, Field foreignStore, Class keyClass) throws DataAccessException; + + /** + * Can be a NOP if the key is inferred + * @param entity + * @param foreignStore + * @param pk + * @throws DataAccessException + */ + void storeForeignStoreKey(T entity, Field foreignStore, Object pk) throws DataAccessException; + + /** + * Clear out the foreign key value + * Can be a NOP if the key is inferred + * @param entity + * @param foreignStore + * @param keyClass class of the key + * @throws DataAccessException + */ + void clearForeignStoreKey(T entity, Field foreignStore, Class keyClass) throws DataAccessException; + + Set findForeignStoreKeySet(T entity, Field foreignStore, Class keyClass) throws DataAccessException; + + void storeForeignStoreKeySet(T entity, Field foreignStore, Set keys) throws DataAccessException; + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ForeignStoreKeyManagerLocator.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ForeignStoreKeyManagerLocator.java new file mode 100644 index 000000000..fbe475dc1 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/ForeignStoreKeyManagerLocator.java @@ -0,0 +1,24 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; + +import org.springframework.dao.DataAccessException; + +/** + * Interface to be implemented by classes that can find ForeignStoreKeyManager + * implementations to handle particular entities. + * + * @author Rod Johnson + */ +public interface ForeignStoreKeyManagerLocator { + + /** + * Find the ForeignStoreKeyManager for this class. + * @param f field the RelatedEntity annotation is on + * @param entityClass + * @return + * @throws DataAccessException if no ForeignStoreKeyManager can be found. + */ + ForeignStoreKeyManager foreignStoreKeyManagerFor(Class entityClass, Field f) throws DataAccessException; + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/GeneratedFieldForeignStoreKeyManager.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/GeneratedFieldForeignStoreKeyManager.java new file mode 100644 index 000000000..8a8209bb2 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/GeneratedFieldForeignStoreKeyManager.java @@ -0,0 +1,65 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.dao.DataAccessException; + +/** + * Stores keys in generated additional persistent fields + * that Roo will add. e.g. + * + *
+ * atForeignStore
+ * Person Person;
+ * 
+ * long person_id;
+ * 
+ * 
+ * @author + * + */ +public class GeneratedFieldForeignStoreKeyManager extends + OrderedForeignStoreKeyManager { + + private final Log log = LogFactory.getLog(getClass()); + + + @Override + public Object findForeignStoreKey(Roo_GeneratedForeignStoreKeys entity, Field foreignStore, Class requiredClass) + throws DataAccessException { + String methodName = "get" + propertyName(foreignStore); + Object key = RooConventionEntityOperations.invokeNoArgMethod(entity.getClass(), methodName, entity); + log.info("FIND foreign store property " + foreignStore + " <- Entity generated String property [" + methodName + "] returned [" + key + "]"); + return key; + } + + @Override + public void storeForeignStoreKey(Roo_GeneratedForeignStoreKeys entity, Field foreignStore, + Object key) throws DataAccessException { + String methodName = "set" + propertyName(foreignStore); + RooConventionEntityOperations.invoke(entity.getClass(), methodName, entity, new Class[] { key.getClass()}, key); + log.info("STORE foreign store property " + foreignStore + " -> Entity generated String property [" + methodName + "] with key value [" + key + "]"); + } + + @Override + public void clearForeignStoreKey(Roo_GeneratedForeignStoreKeys entity, Field foreignStore, Class keyClass) throws DataAccessException { + String methodName = "set" + propertyName(foreignStore); + RooConventionEntityOperations.invoke(entity.getClass(), methodName, entity, new Class[] { keyClass }, null); + log.info("CKEAR foreign store property " + foreignStore + " -> Entity generated String property [" + methodName + "]"); + } + + + @Override + public boolean isSupportedField(Class clazz, Field f) { + // Check for marker interface + return Roo_GeneratedForeignStoreKeys.class.isAssignableFrom(clazz); + } + + + private String propertyName(Field f) { + return "_" + f.getName() + "_Id"; + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/InvalidFieldAnnotationException.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/InvalidFieldAnnotationException.java new file mode 100644 index 000000000..3741b96c2 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/InvalidFieldAnnotationException.java @@ -0,0 +1,20 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; + +import org.springframework.dao.InvalidDataAccessApiUsageException; + +/** + * Exception thrown on an attempt to use a field with an invalid + * RelatedEntity annotation. + * + * @author Rod Johnson + */ +public class InvalidFieldAnnotationException extends + InvalidDataAccessApiUsageException { + + public InvalidFieldAnnotationException(Class entityClass, Field f, String reason) { + super("Field [" + f.getName() + "] has invalid RelatedEntity annotation: reason='" + reason + "'", null); + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/JpaEntityForeignStoreFieldTransience.aj b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/JpaEntityForeignStoreFieldTransience.aj new file mode 100644 index 000000000..05b6f47d2 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/JpaEntityForeignStoreFieldTransience.aj @@ -0,0 +1,17 @@ +package org.springframework.persistence; + +import javax.persistence.Transient; +import javax.persistence.Entity; + +/** + * Aspect to annotate @ForeignStore fields as JPA @Transient to stop + * JPA trying to manage them itself + * @author Rod Johnson + * + */ +public privileged aspect JpaEntityForeignStoreFieldTransience { + + declare @field : @RelatedEntity * (@Entity *).* : @Transient; + + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/MappingValidator.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/MappingValidator.java new file mode 100644 index 000000000..8143d0543 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/MappingValidator.java @@ -0,0 +1,20 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; + +import org.springframework.dao.InvalidDataAccessApiUsageException; + +/** + * Interface to validate RelatedAnnotation annotation usage + * and other mapping constructs. + * + * @author Rod Johnson + * + */ +public interface MappingValidator { + + void validateGet(Class entityClass, Field f, RelatedEntity re) throws InvalidDataAccessApiUsageException; + + void validateSetTo(Class entityClass, Field f, RelatedEntity re, Object newVal) throws InvalidDataAccessApiUsageException, IllegalArgumentException; + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/OrderedForeignStoreKeyManager.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/OrderedForeignStoreKeyManager.java new file mode 100644 index 000000000..c4211a01a --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/OrderedForeignStoreKeyManager.java @@ -0,0 +1,45 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.Ordered; +import org.springframework.dao.DataAccessException; + +/** + * Convenient base class for ForeignStoreKeyManager implementations that adds + * ordering support. + * + * @author Rod Johnson + */ +public abstract class OrderedForeignStoreKeyManager implements ForeignStoreKeyManager, Ordered { + + protected final Log log = LogFactory.getLog(getClass()); + + private int order = Integer.MAX_VALUE; + + @Override + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + + /** + * Subclasses can override if they support collection management. + */ + @Override + public Set findForeignStoreKeySet(T entity, Field foreignStore, Class keyClass) throws DataAccessException { + throw new UnsupportedOperationException(); + } + + @Override + public void storeForeignStoreKeySet(T entity, Field foreignStore, Set keys) throws DataAccessException { + throw new UnsupportedOperationException(); + } + +} \ No newline at end of file diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/PresentKeyForeignStoreKeyManager.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/PresentKeyForeignStoreKeyManager.java new file mode 100644 index 000000000..c2308ee42 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/PresentKeyForeignStoreKeyManager.java @@ -0,0 +1,44 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; + +import org.springframework.dao.DataAccessException; + +/** + * ForeignStoreKeyManager implementation that uses the key of the present + * entity. + * + * @author Rod Johnson + * + */ +public class PresentKeyForeignStoreKeyManager extends OrderedForeignStoreKeyManager { + + private final EntityOperationsLocator eoLocator; + + public PresentKeyForeignStoreKeyManager(EntityOperationsLocator eoLocator) { + this.eoLocator = eoLocator; + } + + @Override + public Object findForeignStoreKey(Object entity, Field foreignStore, Class requiredClass) throws DataAccessException { + EntityOperations eo = eoLocator.entityOperationsFor(entity.getClass(), foreignStore.getAnnotation(RelatedEntity.class)); + return eo.findUniqueKey(entity); + } + + @Override + public boolean isSupportedField(Class clazz, Field foreignStore) { + RelatedEntity fs = foreignStore.getAnnotation(RelatedEntity.class); + return fs.sameKey(); + } + + @Override + public void storeForeignStoreKey(Object entity, Field foreignStore, Object pk) throws DataAccessException { + // Nothing to do + } + + @Override + public void clearForeignStoreKey(Object entity, Field foreignStore, Class keyClass) throws DataAccessException { + // Nothing to do + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/RooConventionEntityOperations.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/RooConventionEntityOperations.java new file mode 100644 index 000000000..e1d811bd7 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/RooConventionEntityOperations.java @@ -0,0 +1,99 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import org.springframework.dao.DataAccessException; + +/** + * Implementation of entity operations that works on any entity + * that adheres to Roo persist() and static finder conventions. + * Does not depend on Roo, merely on Roo conventions, which can + * also be implemented by hand. + * + * @author Rod Johnson + */ +public class RooConventionEntityOperations extends OrderedEntityOperations { + + /** + * Utility method + * + * @param clazz + * @param methodName + * @param target + * @param args + * @return + */ + public static Object invoke(Class clazz, String methodName, + Object target, Class[] argTypes, Object... args) { + try { + Method m = clazz.getMethod(methodName, argTypes); + return m.invoke(target, (Object[]) args); + } catch (Exception ex) { + // TODO FIX ME + // System.out.println(ex + ": checked exceptions are stupid"); + throw new IllegalArgumentException(ex); + } + } + + public static Object invokeNoArgMethod(Class clazz, String methodName, Object target) { + return invoke(clazz, methodName, target, (Class[]) null, + (Object[]) null); + } + + @Override + public Object findEntity(Class entityClass, Object pk) + throws DataAccessException { + String findMethod = "find" + entityClass.getSimpleName(); + Object found = invoke(entityClass, findMethod, entityClass, + new Class[] { pk.getClass() }, pk); + log.info("Lookup [" + entityClass.getName() + "] by pk=[" + pk + "] using static finder method '" + + findMethod + "' found [" + found + "]"); + return found; + } + + + @Override + public Object findUniqueKey(Object entity) throws DataAccessException { + String idMethodName = "getId"; + return invokeNoArgMethod(entity.getClass(), idMethodName, entity); + } + + @Override + public boolean isTransient(Object entity) throws DataAccessException { + return findUniqueKey(entity) == null; + } + + @Override + public Object makePersistent(Object owner, Object entity, Field f, RelatedEntity fs) throws DataAccessException { + if (log.isDebugEnabled()) { + log.debug("Making entity persistent: BEFORE [" + entity + "]"); + } + String persistMethodName = "persist"; + invokeNoArgMethod(entity.getClass(), persistMethodName, entity); + Object key = findUniqueKey(entity); + log.info("Making entity persistent: AFTER [" + entity + "]"); + return key; + } + + @Override + public boolean supports(Class clazz, RelatedEntity fs) { + try { + // TODO fix this + clazz.getMethod("getId"); + return true; + } catch (Exception ex) { + return false; + } + } + + @Override + public boolean cacheInEntity() { + return false; + } + + @Override + public boolean isTransactional() { + // TODO Need to have a better test + return true; + }} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/Roo_GeneratedForeignStoreKeys.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/Roo_GeneratedForeignStoreKeys.java new file mode 100644 index 000000000..ab9706613 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/Roo_GeneratedForeignStoreKeys.java @@ -0,0 +1,10 @@ +package org.springframework.persistence; + +/** + * Tag interface introduced to objects that have introduced foreign store keys + * @author Rod Johnson + * + */ +public interface Roo_GeneratedForeignStoreKeys { + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/StoreSpanning.aj b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/StoreSpanning.aj new file mode 100644 index 000000000..2152def30 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/StoreSpanning.aj @@ -0,0 +1,172 @@ +package org.springframework.persistence; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.aspectj.lang.reflect.FieldSignature; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.persistence.support.DefaultManagedSet; +import org.springframework.persistence.support.ManagedSet; +import org.springframework.persistence.support.ManagedSet.ChangeListener; + +/** + * Aspect to handle ForeignStore annotation indicating navigation to a + * potentially different persistence store. + * + * Can be configured via invoking init() method or through Spring + * autowiring if beans named "entityOperationsLocator" and + * "foreignStoreKeyManager" are provided. + * + * @author Rod Johnson + */ +public privileged aspect StoreSpanning { + + private final Log log = LogFactory.getLog(getClass()); + + private EntityOperationsLocator entityOperationsLocator; + + private ForeignStoreKeyManagerLocator foreignStoreKeyManagerLocator; + + private MappingValidator mappingValidator; + + @Autowired + public void init(EntityOperationsLocator eol, ForeignStoreKeyManagerLocator fskml) { + this.entityOperationsLocator = eol; + this.foreignStoreKeyManagerLocator = fskml; + } + + @Autowired(required=false) + public void setMappingValidator(MappingValidator mv) { + this.mappingValidator = mv; + } + + + public pointcut foreignEntityFieldGet(Object entity, RelatedEntity fs) : + get(@RelatedEntity * *) && + this(entity) && + @annotation(fs); + + public pointcut foreignEntityFieldSet(Object entity, RelatedEntity fs, Object newVal) : + set(@RelatedEntity * *) && + this(entity) && + @annotation(fs) && + args(newVal); + + @SuppressWarnings("unchecked") + Object around(Object entity, RelatedEntity fs) : foreignEntityFieldGet(entity, fs) { + Field f = ((FieldSignature) thisJoinPoint.getSignature()).getField(); + log.info("GET: Handling foreign store " + f); + if (this.mappingValidator != null) { + this.mappingValidator.validateGet(entity.getClass(), f, fs); + } + + Object fieldValue = proceed(entity, fs); + // What if it was set to null? + if (fieldValue != null) { + log.info("GET " + f + ": returning actual field value"); + return fieldValue; + } + + // Must retrieve + if (Set.class.isAssignableFrom(f.getType())) { + // TODO empty set, store class + log.info("GET " + f + ": Retrieving ManagedSet"); + ForeignStoreKeyManager foreignStoreKeyManager = foreignStoreKeyManagerLocator.foreignStoreKeyManagerFor(entity.getClass(), f); + + // TODO fix me, this is fragile + ParameterizedType genericType = (ParameterizedType) f.getGenericType(); + Class entityClass = (Class) genericType.getActualTypeArguments()[0]; + Class keyClass = entityOperationsLocator.entityOperationsFor(entityClass, fs).uniqueKeyType(entityClass); + Set keySet = foreignStoreKeyManager.findForeignStoreKeySet(entity, f, keyClass); + ManagedSet managedSet = DefaultManagedSet.fromKeySet(keySet, entityClass, entityOperationsLocator); + return managedSet; + } + else if (Collection.class.isAssignableFrom(f.getType())) { + throw new UnsupportedOperationException("Unsupported collection type " + f.getType() + " in entity class " + entity.getClass()); + } + else { + return findScalarEntity(entity, f, fs); + } + } + + private Object findScalarEntity(Object entity, Field f, RelatedEntity fs) { + EntityOperations eo = entityOperationsLocator.entityOperationsFor(f.getType(), fs); + Class keyType = eo.uniqueKeyType(f.getType()); + ForeignStoreKeyManager foreignStoreKeyManager = foreignStoreKeyManagerLocator.foreignStoreKeyManagerFor(entity.getClass(), f); + Object pk = foreignStoreKeyManager.findForeignStoreKey(entity, f, keyType); + if (pk != null) { + log.debug("GET " + f + ": entity find for key=[" + pk + "] of class [" + pk.getClass() + "]"); + Object found = eo.findEntity(f.getType(), pk); + log.info("GET " + f + ": entity find for key=[" + pk + "] found [" + found + "]"); + return found; + } + else { + log.info("GET " + f + ": no key found, returning null"); + return null; + } + } + + // TODO handle explicit set to null + @SuppressWarnings("unchecked") + Object around(final Object entity, RelatedEntity fs, Object newVal) : foreignEntityFieldSet(entity, fs, newVal) { + final Field f = ((FieldSignature) thisJoinPoint.getSignature()).getField(); + if (this.mappingValidator != null) { + this.mappingValidator.validateSetTo(entity.getClass(), f, fs, newVal); + } + log.info("SET: Handling foreign store " + f); + + if (newVal != null) { + if (Set.class.isAssignableFrom(f.getType())) { + log.info("Setting set: Creating ManagedSet"); + final ManagedSet managedSet = DefaultManagedSet.fromEntitySet((Set) newVal, entityOperationsLocator); + final ForeignStoreKeyManager foreignStoreKeyManager = foreignStoreKeyManagerLocator.foreignStoreKeyManagerFor(entity.getClass(), f); + foreignStoreKeyManager.storeForeignStoreKeySet(entity, f, managedSet.getKeySet()); + managedSet.addListener(new ChangeListener() { + @Override + public void onDirty() { + foreignStoreKeyManager.storeForeignStoreKeySet(entity, f, managedSet.getKeySet()); + } + }); + return proceed(entity, fs, managedSet); + } + else if (Collection.class.isAssignableFrom(f.getType())) { + throw new UnsupportedOperationException("Unsupported collection type " + f.getType() + " in entity class " + entity.getClass()); + } + else { + EntityOperations eo = handleScalarFieldSet(entity, f, fs, newVal); + + // Don't store it in the entity if the entity type doesn't support + // it, for example + // because it shouldn't be read repeatedly (as with a stream) + if (!eo.cacheInEntity()) { + return null; + } + } + } + return proceed(entity, fs, newVal); + } + + private EntityOperations handleScalarFieldSet(Object entity, Field f, RelatedEntity fs, Object newVal) { + EntityOperations eo = entityOperationsLocator.entityOperationsFor(f.getType(), fs); + Object pk = eo.findUniqueKey(newVal); + + System.err.println("TODO: test whether current entity is persistent"); + if (pk == null) { + // Entity is transient for now + log.info("SET " + f + ": no foreign store key to store; entity has no persistent identity, MAKING PERSISTENT"); + pk = eo.makePersistent(entity,newVal, f, fs); + } + + if (pk != null) { + ForeignStoreKeyManager foreignStoreKeyManager = foreignStoreKeyManagerLocator.foreignStoreKeyManagerFor(entity.getClass(), f); + foreignStoreKeyManager.storeForeignStoreKey(entity, f, pk); + log.info("SET " + f + ": stored foreign store key=[" + pk + "]"); + } + return eo; + } +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/UnknownEntityClassException.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/UnknownEntityClassException.java new file mode 100644 index 000000000..f17c13d9f --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/UnknownEntityClassException.java @@ -0,0 +1,12 @@ +package org.springframework.persistence; + +import org.springframework.dao.UncategorizedDataAccessException; + +public class UnknownEntityClassException extends + UncategorizedDataAccessException { + + public UnknownEntityClassException(Class entityClass) { + super("Unknown entity class [" + entityClass.getName() + "]", null); + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetConstructorEntityInstantiator.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetConstructorEntityInstantiator.java new file mode 100644 index 000000000..47fb10322 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetConstructorEntityInstantiator.java @@ -0,0 +1,17 @@ +package org.springframework.persistence.support; + + +/** + * Try for a constructor taking a ChangeSet: failing that, try a no-arg + * constructor and then setChangeSet(). + * + * @author Rod Johnson + */ +public class ChangeSetConstructorEntityInstantiator extends AbstractConstructorEntityInstantiator{ + + @Override + protected void setState(ChangeSetBacked entity, ChangeSet cs) { + entity.setChangeSet(cs); + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetForeignStoreKeyManager.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetForeignStoreKeyManager.java new file mode 100644 index 000000000..695331726 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ChangeSetForeignStoreKeyManager.java @@ -0,0 +1,94 @@ +package org.springframework.persistence.support; + +import java.lang.reflect.Field; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.convert.ConversionService; +import org.springframework.dao.DataAccessException; +import org.springframework.persistence.OrderedForeignStoreKeyManager; + +/** + * ForeignStoreKeyManager implementation that backs the foreign key to a + * ChangeSet. + * + * @author Thomas Risberg + * @author Rod Johnson + */ +public class ChangeSetForeignStoreKeyManager extends OrderedForeignStoreKeyManager { + + public static final String FOREIGN_STORE_SET_PREFIX = "S"; + + protected final Log log = LogFactory.getLog(getClass()); + + private String fieldDelimiter = "."; + + private final ConversionService conversionService; + + public String getFieldDelimiter() { + return fieldDelimiter; + } + + public void setFieldDelimiter(String fieldDelimiter) { + this.fieldDelimiter = fieldDelimiter; + } + + @Autowired + public ChangeSetForeignStoreKeyManager(ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Override + public void clearForeignStoreKey(ChangeSetBacked entity, Field foreignStore, Class keyClass) throws DataAccessException { + String propName = propertyName(entity, foreignStore); + entity.getChangeSet().removeProperty(propName); + log.info("CLEAR foreign store property " + foreignStore + " <- ChangeSetBacked foreign key property [" + propName + "]"); + } + + @Override + public K findForeignStoreKey(ChangeSetBacked entity, Field foreignStore, Class keyClass) throws DataAccessException { + String propName = propertyName(entity, foreignStore); + System.err.println("+++ " + entity.getChangeSet().getValues()); + K key = entity.getChangeSet().get(propName, keyClass, this.conversionService); + log.info("FIND foreign store property " + foreignStore + " <- ChangeSetBacked foreign key property [" + propName + "] returned [" + + key + "]"); + return key; + } + + @Override + public boolean isSupportedField(Class entityClass, Field foreignStore) { + return ChangeSetBacked.class.isAssignableFrom(entityClass); + } + + @Override + public void storeForeignStoreKey(ChangeSetBacked entity, Field foreignStore, Object pk) throws DataAccessException { + String propName = propertyName(entity, foreignStore); + entity.getChangeSet().set(propName, pk); + log.info("STORE foreign store property " + foreignStore + " -> ChangeSetBacked foreign key property [" + propName + + "] with key value [" + pk + "]"); + } + + @Override + public Set findForeignStoreKeySet(ChangeSetBacked entity, Field foreignStore, Class keyClass) throws DataAccessException { + Set keySet = entity.getChangeSet().get(foreignStoreKeyName(foreignStore), Set.class, this.conversionService); +// if (keySet != null && !keySet.isEmpty()) +// System.out.println("KeySET=**************" + keySet + ", 0th type=" + keySet.iterator().next().getClass()); + return keySet; + } + + private String foreignStoreKeyName(Field foreignStore) { + return FOREIGN_STORE_SET_PREFIX + getFieldDelimiter() + foreignStore.getName(); + } + + @Override + public void storeForeignStoreKeySet(ChangeSetBacked entity, Field foreignStore, Set keys) throws DataAccessException { + entity.getChangeSet().set(foreignStoreKeyName(foreignStore), keys); + } + + private String propertyName(ChangeSetBacked rb, Field f) { + return rb.getClass().getSimpleName() + getFieldDelimiter() + f.getName(); + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/DefaultManagedSet.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/DefaultManagedSet.java new file mode 100644 index 000000000..072a36143 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/DefaultManagedSet.java @@ -0,0 +1,162 @@ +package org.springframework.persistence.support; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.springframework.dao.DataAccessException; +import org.springframework.persistence.EntityOperations; +import org.springframework.persistence.EntityOperationsLocator; + +public class DefaultManagedSet implements ManagedSet { + + public static DefaultManagedSet fromEntitySet(Set entitySet, EntityOperationsLocator eol) { + DefaultManagedSet dms = new DefaultManagedSet(eol); + if (entitySet != null) { + for (Object entity : entitySet) { + dms.add(entity); + } + } + return dms; + } + + public static DefaultManagedSet fromKeySet(Set keySet, Class entityClass, EntityOperationsLocator eol) throws DataAccessException { + DefaultManagedSet dms = new DefaultManagedSet(eol); + if (keySet != null) { + for (Object key : keySet) { + dms.keySet.add(key); + EntityOperations eo = eol.entityOperationsFor(entityClass, null); + dms.entitySet.add(eo.findEntity(entityClass, key)); + } + } + return dms; + } + + private final Set keySet = new HashSet(); + + private final Set entitySet = new HashSet(); + + private boolean dirty; + + private List listeners = new LinkedList(); + + private final EntityOperationsLocator entityOperationsLocator; + + private DefaultManagedSet(EntityOperationsLocator eol) { + this.entityOperationsLocator = eol; + } + + @Override + public void addListener(ChangeListener l) { + this.listeners.add(l); + } + + + protected void publishEvent() { + this.dirty = true; + for (ChangeListener l : listeners) { + l.onDirty(); + } + } + + @Override + public Set getKeySet() { + return this.keySet; + } + + @Override + public boolean isDirty() { + return this.dirty; + } + + + @Override + public boolean add(Object e) { + if (entitySet.contains(e)) { + return false; + } + EntityOperations eo = entityOperationsLocator.entityOperationsFor(e.getClass(), null); + Object key = eo.findUniqueKey(e); + if (key == null) { + eo.makePersistent(null, e, null, null); + } + key = eo.findUniqueKey(e); + keySet.add(key); + entitySet.add(e); + publishEvent(); + return true; + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + this.entitySet.clear(); + this.keySet.clear(); + publishEvent(); + } + + @Override + public boolean contains(Object o) { + return entitySet.contains(o); + } + + @Override + public boolean containsAll(Collection c) { + return entitySet.containsAll(c); + } + + @Override + public boolean isEmpty() { + return keySet.isEmpty(); + } + + @Override + public Iterator iterator() { + return entitySet.iterator(); + } + + @Override + public boolean remove(Object e) { + if (!entitySet.contains(e)) { + return false; + } + EntityOperations eo = entityOperationsLocator.entityOperationsFor(e.getClass(), null); + keySet.remove(eo.findUniqueKey(e)); + entitySet.remove(e); + publishEvent(); + return true; + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public int size() { + return keySet.size(); + } + + @Override + public Object[] toArray() { + throw new UnsupportedOperationException(); + } + + @Override + public Object[] toArray(Object[] a) { + throw new UnsupportedOperationException(); + } + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ManagedSet.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ManagedSet.java new file mode 100644 index 000000000..430865fa2 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/ManagedSet.java @@ -0,0 +1,27 @@ +package org.springframework.persistence.support; + +import java.util.Set; + +public interface ManagedSet extends Set { + + Set getKeySet(); + + void addListener(ChangeListener l); + + boolean isDirty(); + + interface ChangeListener { + void onDirty(); + } + + // TODO move into managed collection + + // TODO insertions, deletions + // after markSynchronized() + + // This may be wrong, shouldn't it give something back for a ChangeSet? +// void retrieve(EntityOperationsLocator eol) throws DataAccessException; +// +// void persist(EntityOperationsLocator eol) throws DataAccessException; + +} diff --git a/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/SimpleMappingValidator.java b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/SimpleMappingValidator.java new file mode 100644 index 000000000..e06212ca1 --- /dev/null +++ b/spring-data-commons-aspects/src/main/java/org/springframework/persistence/support/SimpleMappingValidator.java @@ -0,0 +1,34 @@ +package org.springframework.persistence.support; + +import java.lang.reflect.Field; + +import javax.validation.constraints.NotNull; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.persistence.AsynchStoreCompletionListener; +import org.springframework.persistence.InvalidFieldAnnotationException; +import org.springframework.persistence.MappingValidator; +import org.springframework.persistence.RelatedEntity; + +// TODO fancier version could discover many rules, with annotations etc. +// Also invoke the relevant EntityOperations +public class SimpleMappingValidator implements MappingValidator { + + @Override + public void validateGet(Class entityClass, Field f, RelatedEntity re) throws InvalidDataAccessApiUsageException { + // Validate the annotation + if (!AsynchStoreCompletionListener.NONE.class.equals(re.storeCompletionListenerClass()) + && !"".equals(re.storeCompletionListenerBeanName())) { + throw new InvalidFieldAnnotationException(entityClass, f, + "Can't have storeCompletionListener class and bean name on same annotation"); + } + } + + public void validateSetTo(Class entityClass, Field f, RelatedEntity re, Object newVal) throws InvalidDataAccessApiUsageException, + IllegalArgumentException { + if (newVal == null && f.isAnnotationPresent(NotNull.class)) { + throw new IllegalArgumentException("Can't set non-null field [" + f.getName() + " to null"); + } + } + +} diff --git a/spring-data-commons-aspects/template.mf b/spring-data-commons-aspects/template.mf index 84c7abd31..97bcff638 100644 --- a/spring-data-commons-aspects/template.mf +++ b/spring-data-commons-aspects/template.mf @@ -5,14 +5,19 @@ Bundle-ManifestVersion: 2 Import-Package: sun.reflect;version="0";resolution:=optional Excluded-Imports: - org.springframework.persistence + org.springframework.persistence, + org.springframework.persistence.support Import-Template: org.springframework.beans.*;version="[3.0.0, 4.0.0)", + org.springframework.context.*;version="[3.0.0, 4.0.0)", org.springframework.core.*;version="[3.0.0, 4.0.0)", org.springframework.dao.*;version="[3.0.0, 4.0.0)", + org.springframework.orm.*;version="[3.0.0, 4.0.0)", org.springframework.transaction..*;version="[3.0.0, 4.0.0)", org.springframework.util.*;version="[3.0.0, 4.0.0)", org.springframework.data.core.*;version="[1.0.0, 2.0.0)", + javax.validation.*;version="[1.0.0, 2.0.0)";resolution:=optional, + javax.persistence.*;version="[1.0.0, 3.0.0)";resolution:=optional, org.aopalliance.*;version="[1.0.0, 2.0.0)";resolution:=optional, org.apache.commons.logging.*;version="[1.1.1, 2.0.0)", org.aspectj.*;version="[1.6.5, 2.0.0)",