diff --git a/.travis.yml b/.travis.yml index dc2311804d..7bb709bc37 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: java + jdk: - oraclejdk8 + env: matrix: - PROFILE=ci @@ -8,12 +10,22 @@ env: - PROFILE=spring5 - PROFILE=spring5-next - PROFILE=querydsl-next + +addons: + apt: + packages: + - oracle-java8-installer + cache: directories: - $HOME/.m2 + sudo: false + install: true + script: "mvn clean dependency:list test -P${PROFILE} -Dsort" + notifications: slack: secure: PWyr3+7uTnHzZSrJY2DrowwdYODlIeFZB6tzuq+i/vYqMlX2GGXGQN9YmqbKPeaVZBdSI7YrI1UDa5cPAAXoTjn/JewkL0RbdTeIS6PGCpcpAb5rFMYtOhEDBrFB/SRiMH4tw86bRNKq/SrUWmpkzDtTzLrw7JceumgyqnrT6GY= diff --git a/pom.xml b/pom.xml index 19255544a4..e3fdc53620 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,7 @@ DATACMNS + 2.0.4 2.11.7 1.4.8 @@ -143,6 +144,13 @@ true + + io.javaslang + javaslang + ${javaslang} + true + + javax.el el-api diff --git a/src/main/asciidoc/repositories.adoc b/src/main/asciidoc/repositories.adoc index 5e7a4ce98d..e51830c1bb 100644 --- a/src/main/asciidoc/repositories.adoc +++ b/src/main/asciidoc/repositories.adoc @@ -218,7 +218,7 @@ NOTE: Note, that the intermediate repository interface is annotated with `@NoRep Using a unique Spring Data module in your application makes things simple hence, all repository interfaces in the defined scope are bound to the Spring Data module. Sometimes applications require using more than one Spring Data module. In such case, it's required for a repository definition to distinguish between persistence technologies. Spring Data enters strict repository configuration mode because it detects multiple repository factories on the class path. Strict configuration requires details on the repository or the domain class to decide about Spring Data module binding for a repository definition: 1. If the repository definition <>, then it's a valid candidate for the particular Spring Data module. -2. If the domain class is <>, then it's a valid candidate for the particular Spring Data module. Spring Data modules accept either 3rd party annotations (such as JPA's `@Entity`) or provide own annotations such as `@Document` for Spring Data MongoDB/Spring Data Elasticsearch. +2. If the domain class is <>, then it's a valid candidate for the particular Spring Data module. Spring Data modules accept either 3rd party annotations (such as JPA's `@Entity`) or provide own annotations such as `@Document` for Spring Data MongoDB/Spring Data Elasticsearch. [[repositories.multiple-modules.types]] .Repository definitions using Module-specific Interfaces @@ -306,7 +306,7 @@ public class Person { This example shows a domain class using both JPA and Spring Data MongoDB annotations. It defines two repositories, `JpaPersonRepository` and `MongoDBPersonRepository`. One is intended for JPA and the other for MongoDB usage. Spring Data is no longer able to tell the repositories apart which leads to undefined behavior. ==== -<> and <> are used for strict repository configuration identify repository candidates for a particular Spring Data module. Using multiple persistence technology-specific annotations on the same domain type is possible to reuse domain types across multiple persistence technologies, but then Spring Data is no longer able to determine a unique module to bind the repository. +<> and <> are used for strict repository configuration identify repository candidates for a particular Spring Data module. Using multiple persistence technology-specific annotations on the same domain type is possible to reuse domain types across multiple persistence technologies, but then Spring Data is no longer able to determine a unique module to bind the repository. The last way to distinguish repositories is scoping repository base packages. Base packages define the starting points for scanning for repository interface definitions which implies to have repository definitions located in the appropriate packages. By default, annotation-driven configuration uses the package of the configuration class. The <> is mandatory. @@ -414,7 +414,7 @@ List findByLastname(String lastname, Pageable pageable); ---- ==== -The first method allows you to pass an `org.springframework.data.domain.Pageable` instance to the query method to dynamically add paging to your statically defined query. A `Page` knows about the total number of elements and pages available. It does so by the infrastructure triggering a count query to calculate the overall number. As this might be expensive depending on the store used, `Slice` can be used as return instead. A `Slice` only knows about whether there's a next `Slice` available which might be just sufficient when walking thought a larger result set. +The first method allows you to pass an `org.springframework.data.domain.Pageable` instance to the query method to dynamically add paging to your statically defined query. A `Page` knows about the total number of elements and pages available. It does so by the infrastructure triggering a count query to calculate the overall number. As this might be expensive depending on the store used, `Slice` can be used as return instead. A `Slice` only knows about whether there's a next `Slice` available which might be just sufficient when walking through a larger result set. Sorting options are handled through the `Pageable` instance too. If you only need sorting, simply add an `org.springframework.data.domain.Sort` parameter to your method. As you also can see, simply returning a `List` is possible as well. In this case the additional metadata required to build the actual `Page` instance will not be created (which in turn means that the additional count query that would have been necessary not being issued) but rather simply restricts the query to look up only the given range of entities. @@ -736,6 +736,36 @@ A corresponding attribute is available in the XML namespace. ---- ==== +[[core.domain-events]] +== Publishing events from aggregate roots + +Entities managed by repositories are aggregate roots. +In a Domain-Driven Design application, these aggregate roots usually publish domain events. +Spring Data provides an annotation `@DomainEvents` you can use on a method of your aggregate root to make that publication as easy as possible. + +.Exposing domain events from an aggregate root +==== +[source, java] +---- +class AnAggreagteRoot { + + @DomainEvents <1> + Collection domainEvents() { + // … return events you want to get published here + } + + @AfterDomainEventsPublication <2> + void callbackMethod() { + // … potentially clean up domain events list + } +} +---- +<1> The method using `@DomainEvents` can either return a single event instance or a collection of events. It must not take any arguments. +<2> After all events have been published, a method annotated with `@AfterDomainEventsPublication`. It e.g. can be used to potentially clean the list of events to be published. +==== + +The methods will be called every time one of a Spring Data repository's `save(…)` methods is called. + [[core.extensions]] == Spring Data extensions diff --git a/src/main/asciidoc/repository-query-return-types-reference.adoc b/src/main/asciidoc/repository-query-return-types-reference.adoc index 48ed372398..12277e8e4d 100644 --- a/src/main/asciidoc/repository-query-return-types-reference.adoc +++ b/src/main/asciidoc/repository-query-return-types-reference.adoc @@ -19,6 +19,7 @@ NOTE: Geospatial types like (`GeoResult`, `GeoResults`, `GeoPage`) are only avai |`Collection`|A `Collection`. |`List`|A `List`. |`Optional`|A Java 8 or Guava `Optional`. Expects the query method to return one result at most. In case no result is found `Optional.empty()`/`Optional.absent()` is returned. More than one result will trigger an `IncorrectResultSizeDataAccessException`. +|`Option`|An either Scala or JavaSlang `Option` type. Semantically same behavior as Java 8's `Optional` described above. |`Stream`|A Java 8 `Stream`. |`Future`|A `Future`. Expects method to be annotated with `@Async` and requires Spring's asynchronous method execution capability enabled. |`CompletableFuture`|A Java 8 `CompletableFuture`. Expects method to be annotated with `@Async` and requires Spring's asynchronous method execution capability enabled. diff --git a/src/main/java/org/springframework/data/convert/Jsr310Converters.java b/src/main/java/org/springframework/data/convert/Jsr310Converters.java index 262017d681..27f2540287 100644 --- a/src/main/java/org/springframework/data/convert/Jsr310Converters.java +++ b/src/main/java/org/springframework/data/convert/Jsr310Converters.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-2016 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,10 +19,12 @@ import static java.time.LocalDateTime.*; import static java.time.ZoneId.*; +import java.time.Duration; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.Period; import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; @@ -38,6 +40,7 @@ * Helper class to register JSR-310 specific {@link Converter} implementations in case the we're running on Java 8. * * @author Oliver Gierke + * @author Barak Schoster */ public abstract class Jsr310Converters { @@ -66,6 +69,10 @@ public abstract class Jsr310Converters { converters.add(InstantToDateConverter.INSTANCE); converters.add(ZoneIdToStringConverter.INSTANCE); converters.add(StringToZoneIdConverter.INSTANCE); + converters.add(DurationToStringConverter.INSTANCE); + converters.add(StringToDurationConverter.INSTANCE); + converters.add(PeriodToStringConverter.INSTANCE); + converters.add(StringToPeriodConverter.INSTANCE); return converters; } @@ -181,4 +188,48 @@ public ZoneId convert(String source) { return ZoneId.of(source); } } + + @WritingConverter + public static enum DurationToStringConverter implements Converter { + + INSTANCE; + + @Override + public String convert(Duration duration) { + return duration.toString(); + } + } + + @ReadingConverter + public static enum StringToDurationConverter implements Converter { + + INSTANCE; + + @Override + public Duration convert(String s) { + return Duration.parse(s); + } + } + + @WritingConverter + public static enum PeriodToStringConverter implements Converter { + + INSTANCE; + + @Override + public String convert(Period period) { + return period.toString(); + } + } + + @ReadingConverter + public static enum StringToPeriodConverter implements Converter { + + INSTANCE; + + @Override + public Period convert(String s) { + return Period.parse(s); + } + } } diff --git a/src/main/java/org/springframework/data/domain/AbstractAggregateRoot.java b/src/main/java/org/springframework/data/domain/AbstractAggregateRoot.java new file mode 100644 index 0000000000..1be2d258da --- /dev/null +++ b/src/main/java/org/springframework/data/domain/AbstractAggregateRoot.java @@ -0,0 +1,64 @@ +/* + * Copyright 2016 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.domain; + +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * Convenience base class for aggregate roots that exposes a {@link #registerEvent(Object)} to capture domain events and + * expose them via {@link #getDomainEvents()}. The implementation is using the general event publication mechanism + * implied by {@link DomainEvents} and {@link AfterDomainEventPublication}. If in doubt or need to customize anything + * here, rather build your own base class and use the annotations directly. + * + * @author Oliver Gierke + * @since 1.13 + */ +public class AbstractAggregateRoot { + + /** + * All domain events currently captured by the aggregate. + */ + @Getter(onMethod = @__(@DomainEvents)) // + private transient final List domainEvents = new ArrayList(); + + /** + * Registers the given event object for publication on a call to a Spring Data repository's save method. + * + * @param event must not be {@literal null}. + * @return + */ + protected T registerEvent(T event) { + + Assert.notNull(event, "Domain event must not be null!"); + + this.domainEvents.add(event); + return event; + } + + /** + * Clears all domain events currently held. Usually invoked by the infrastructure in place in Spring Data + * repositories. + */ + @AfterDomainEventPublication + public void clearDomainEvents() { + this.domainEvents.clear(); + } +} diff --git a/src/main/java/org/springframework/data/domain/AfterDomainEventPublication.java b/src/main/java/org/springframework/data/domain/AfterDomainEventPublication.java new file mode 100644 index 0000000000..48d3d47331 --- /dev/null +++ b/src/main/java/org/springframework/data/domain/AfterDomainEventPublication.java @@ -0,0 +1,36 @@ +/* + * Copyright 2016 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.domain; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to be used on a method of a Spring Data managed aggregate to get invoked after the events of an aggregate + * have been published. + * + * @author Oliver Gierke + * @see DomainEvents + * @since 1.13 + * @soundtrack Benny Greb - September (Moving Parts Live) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +public @interface AfterDomainEventPublication { + +} diff --git a/src/main/java/org/springframework/data/domain/DomainEvents.java b/src/main/java/org/springframework/data/domain/DomainEvents.java new file mode 100644 index 0000000000..a184d9cf74 --- /dev/null +++ b/src/main/java/org/springframework/data/domain/DomainEvents.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016 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.domain; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.ApplicationEventPublisher; + +/** + * {@link DomainEvents} can be used on methods of aggregate roots managed by Spring Data repositories to publish the + * events returned by that method as Spring application events. + * + * @author Oliver Gierke + * @see ApplicationEventPublisher + * @see AfterDomainEventPublication + * @since 1.13 + * @soundtrack Benny Greb - Soulfood (Moving Parts Live) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +public @interface DomainEvents { +} diff --git a/src/main/java/org/springframework/data/domain/ExampleMatcher.java b/src/main/java/org/springframework/data/domain/ExampleMatcher.java index 233fe9f327..2788fb616d 100644 --- a/src/main/java/org/springframework/data/domain/ExampleMatcher.java +++ b/src/main/java/org/springframework/data/domain/ExampleMatcher.java @@ -608,7 +608,7 @@ public static GenericPropertyMatcher startsWith() { * @return */ public static GenericPropertyMatcher exact() { - return new GenericPropertyMatcher().startsWith(); + return new GenericPropertyMatcher().exact(); } /** diff --git a/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java b/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java index e408065e49..c1c23fb62a 100644 --- a/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java +++ b/src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java @@ -30,6 +30,8 @@ import java.util.Set; import java.util.TreeSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.annotation.TypeAlias; import org.springframework.data.mapping.Association; @@ -58,7 +60,9 @@ */ public class BasicPersistentEntity> implements MutablePersistentEntity { + private static final Logger LOGGER = LoggerFactory.getLogger(BasicPersistentEntity.class); private static final String TYPE_MISMATCH = "Target bean of type %s is not of type of the persistent entity (%s)!"; + private static final String NULL_ASSOCIATION = "%s.addAssociation(…) was called with a null association! Usually indicates a problem in a Spring Data MappingContext implementation. Be sure to file a bug at https://jira.spring.io!"; private final PreferredConstructor constructor; private final TypeInformation information; @@ -236,11 +240,17 @@ protected P returnPropertyIfBetterIdPropertyCandidateOrNull(P property) { return property; } - /* (non-Javadoc) + /* + * (non-Javadoc) * @see org.springframework.data.mapping.MutablePersistentEntity#addAssociation(org.springframework.data.mapping.model.Association) */ public void addAssociation(Association

association) { + if (association == null) { + LOGGER.warn(String.format(NULL_ASSOCIATION, this.getClass().getName())); + return; + } + if (!associations.contains(association)) { associations.add(association); } diff --git a/src/main/java/org/springframework/data/querydsl/QueryDslUtils.java b/src/main/java/org/springframework/data/querydsl/QueryDslUtils.java index abc87f0fbb..a6091e828b 100644 --- a/src/main/java/org/springframework/data/querydsl/QueryDslUtils.java +++ b/src/main/java/org/springframework/data/querydsl/QueryDslUtils.java @@ -21,6 +21,7 @@ import com.querydsl.core.types.Path; import com.querydsl.core.types.PathMetadata; +import com.querydsl.core.types.PathType; /** * Utility class for Querydsl. @@ -64,6 +65,10 @@ private static String toDotPath(Path path, String tail) { return tail; } + if (metadata.getPathType().equals(PathType.DELEGATE)) { + return toDotPath(parent, tail); + } + Object element = metadata.getElement(); if (element == null || !StringUtils.hasText(element.toString())) { diff --git a/src/main/java/org/springframework/data/querydsl/binding/PathInformation.java b/src/main/java/org/springframework/data/querydsl/binding/PathInformation.java new file mode 100644 index 0000000000..071bf97dd8 --- /dev/null +++ b/src/main/java/org/springframework/data/querydsl/binding/PathInformation.java @@ -0,0 +1,76 @@ +/* + * Copyright 2016 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.querydsl.binding; + +import java.beans.PropertyDescriptor; + +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.querydsl.EntityPathResolver; + +import com.querydsl.core.types.Path; + +/** + * Internal abstraction of mapped paths. + * + * @author Oliver Gierke + * @since 1.13 + */ +interface PathInformation { + + /** + * The type of the leaf property. + * + * @return + */ + Class getLeafType(); + + /** + * The type of the leaf property's parent type. + * + * @return + */ + Class getLeafParentType(); + + /** + * The name of the leaf property. + * + * @return + */ + String getLeafProperty(); + + /** + * The {@link PropertyDescriptor} for the leaf property. + * + * @return + */ + PropertyDescriptor getLeafPropertyDescriptor(); + + /** + * The dot separated representation of the current path. + * + * @return + */ + String toDotPath(); + + /** + * Tries to reify a Querydsl {@link Path} from the given {@link PropertyPath} and base. + * + * @param path must not be {@literal null}. + * @param base can be {@literal null}. + * @return + */ + Path reifyPath(EntityPathResolver resolver); +} diff --git a/src/main/java/org/springframework/data/querydsl/binding/PropertyPathInformation.java b/src/main/java/org/springframework/data/querydsl/binding/PropertyPathInformation.java new file mode 100644 index 0000000000..4634028090 --- /dev/null +++ b/src/main/java/org/springframework/data/querydsl/binding/PropertyPathInformation.java @@ -0,0 +1,141 @@ +/* + * Copyright 2016 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.querydsl.binding; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.Field; + +import org.springframework.beans.BeanUtils; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.querydsl.EntityPathResolver; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.ReflectionUtils; + +import com.querydsl.core.types.Path; +import com.querydsl.core.types.dsl.CollectionPathBase; + +/** + * {@link PropertyPath} based implementation of {@link PathInformation}. + * + * @author Oliver Gierke + * @since 1.13 + */ +@ToString +@EqualsAndHashCode +@RequiredArgsConstructor(staticName = "of", access = AccessLevel.PRIVATE) +class PropertyPathInformation implements PathInformation { + + private final PropertyPath path; + + /** + * Creates a new {@link PropertyPathInformation} for the given path and type. + * + * @param path must not be {@literal null} or empty. + * @param type must not be {@literal null}. + * @return + */ + public static PropertyPathInformation of(String path, Class type) { + return PropertyPathInformation.of(PropertyPath.from(path, type)); + } + + /** + * Creates a new {@link PropertyPathInformation} for the given path and {@link TypeInformation}. + * + * @param path must not be {@literal null} or empty. + * @param type must not be {@literal null}. + * @return + */ + public static PropertyPathInformation of(String path, TypeInformation type) { + return PropertyPathInformation.of(PropertyPath.from(path, type)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.binding.MappedPath#getLeafType() + */ + @Override + public Class getLeafType() { + return path.getLeafProperty().getType(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.binding.MappedPath#getLeafParentType() + */ + @Override + public Class getLeafParentType() { + return path.getLeafProperty().getOwningType().getType(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.binding.MappedPath#getLeafProperty() + */ + @Override + public String getLeafProperty() { + return path.getLeafProperty().getSegment(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.binding.MappedPath#getLeafPropertyDescriptor() + */ + @Override + public PropertyDescriptor getLeafPropertyDescriptor() { + return BeanUtils.getPropertyDescriptor(getLeafParentType(), getLeafProperty()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.binding.MappedPath#toDotPath() + */ + @Override + public String toDotPath() { + return path.toDotPath(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.binding.PathInformation#reifyPath(org.springframework.data.querydsl.EntityPathResolver) + */ + @Override + public Path reifyPath(EntityPathResolver resolver) { + return reifyPath(resolver, path, null); + } + + private static Path reifyPath(EntityPathResolver resolver, PropertyPath path, Path base) { + + if (base instanceof CollectionPathBase) { + return reifyPath(resolver, path, (Path) ((CollectionPathBase) base).any()); + } + + Path entityPath = base != null ? base : resolver.createPath(path.getOwningType().getType()); + + Field field = ReflectionUtils.findField(entityPath.getClass(), path.getSegment()); + Object value = ReflectionUtils.getField(field, entityPath); + + if (path.hasNext()) { + return reifyPath(resolver, path.next(), (Path) value); + } + + return (Path) value; + } +} diff --git a/src/main/java/org/springframework/data/querydsl/binding/QuerydslBindings.java b/src/main/java/org/springframework/data/querydsl/binding/QuerydslBindings.java index 69a0f45f0b..14c4847e7e 100644 --- a/src/main/java/org/springframework/data/querydsl/binding/QuerydslBindings.java +++ b/src/main/java/org/springframework/data/querydsl/binding/QuerydslBindings.java @@ -17,6 +17,10 @@ import static org.springframework.data.querydsl.QueryDslUtils.*; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Value; + import java.util.Arrays; import java.util.Collection; import java.util.HashSet; @@ -197,7 +201,7 @@ boolean isPathAvailable(String path, TypeInformation type) { * @return can be {@literal null}. */ @SuppressWarnings("unchecked") - public , T> MultiValueBinding getBindingForPath(PropertyPath path) { + public , T> MultiValueBinding getBindingForPath(PathInformation path) { Assert.notNull(path, "PropertyPath must not be null!"); @@ -212,7 +216,7 @@ public , T> MultiValueBinding getBindingForPat } } - pathAndBinding = (PathAndBinding) typeSpecs.get(path.getLeafProperty().getType()); + pathAndBinding = (PathAndBinding) typeSpecs.get(path.getLeafType()); return pathAndBinding == null ? null : pathAndBinding.getBinding(); } @@ -223,7 +227,7 @@ public , T> MultiValueBinding getBindingForPat * @param path must not be {@literal null}. * @return */ - Path getExistingPath(PropertyPath path) { + Path getExistingPath(PathInformation path) { Assert.notNull(path, "PropertyPath must not be null!"); @@ -232,24 +236,27 @@ Path getExistingPath(PropertyPath path) { } /** - * @param path - * @param type + * Returns the {@link PathInformation} for the given path and {@link TypeInformation}. + * + * @param path must not be {@literal null}. + * @param type must not be {@literal null}. * @return */ - PropertyPath getPropertyPath(String path, TypeInformation type) { + PathInformation getPropertyPath(String path, TypeInformation type) { Assert.notNull(path, "Path must not be null!"); + Assert.notNull(type, "Type information must not be null!"); if (!isPathVisible(path)) { return null; } if (pathSpecs.containsKey(path)) { - return PropertyPath.from(toDotPath(pathSpecs.get(path).getPath()), type); + return QuerydslPathInformation.of(pathSpecs.get(path).getPath()); } try { - PropertyPath propertyPath = PropertyPath.from(path, type); + PathInformation propertyPath = PropertyPathInformation.of(path, type); return isPathVisible(propertyPath) ? propertyPath : null; } catch (PropertyReferenceException o_O) { return null; @@ -262,7 +269,7 @@ PropertyPath getPropertyPath(String path, TypeInformation type) { * @param path * @return */ - private boolean isPathVisible(PropertyPath path) { + private boolean isPathVisible(PathInformation path) { List segments = Arrays.asList(path.toDotPath().split("\\.")); @@ -436,21 +443,10 @@ protected void registerBinding(PathAndBinding binding) { * * @author Oliver Gierke */ + @RequiredArgsConstructor public final class TypeBinder { - private final Class type; - - /** - * Creates a new {@link TypeBinder} for the given type. - * - * @param type must not be {@literal null}. - */ - private TypeBinder(Class type) { - - Assert.notNull(type, "Type must not be null!"); - - this.type = type; - } + private final @NonNull Class type; /** * Configures the given {@link SingleValueBinding} to be used for the current type. @@ -479,32 +475,14 @@ public

> void all(MultiValueBinding binding) { * A pair of a {@link Path} and the registered {@link MultiValueBinding}. * * @author Christoph Strobl + * @author Oliver Gierke * @since 1.11 */ + @Value private static class PathAndBinding, T> { - private final Path path; - private final MultiValueBinding binding; - - /** - * Creates a new {@link PathAndBinding} for the given {@link Path} and {@link MultiValueBinding}. - * - * @param path must not be {@literal null}. - * @param binding must not be {@literal null}. - */ - public PathAndBinding(S path, MultiValueBinding binding) { - - this.path = path; - this.binding = binding; - } - - public Path getPath() { - return path; - } - - public MultiValueBinding getBinding() { - return binding; - } + Path path; + MultiValueBinding binding; } /** @@ -513,26 +491,18 @@ public MultiValueBinding getBinding() { * * @author Oliver Gierke */ - static class MultiValueBindingAdapter, S> implements MultiValueBinding { - - private final SingleValueBinding delegate; + @RequiredArgsConstructor + static class MultiValueBindingAdapter

, T> implements MultiValueBinding { - /** - * Creates a new {@link MultiValueBindingAdapter} for the given {@link SingleValueBinding}. - * - * @param delegate must not be {@literal null}. - */ - public MultiValueBindingAdapter(SingleValueBinding delegate) { - this.delegate = delegate; - } + private final @NonNull SingleValueBinding delegate; /* * (non-Javadoc) * @see org.springframework.data.web.querydsl.MultiValueBinding#bind(com.mysema.query.types.Path, java.util.Collection) */ @Override - public Predicate bind(T path, Collection value) { - Iterator iterator = value.iterator(); + public Predicate bind(P path, Collection value) { + Iterator iterator = value.iterator(); return delegate.bind(path, iterator.hasNext() ? iterator.next() : null); } } diff --git a/src/main/java/org/springframework/data/querydsl/binding/QuerydslPathInformation.java b/src/main/java/org/springframework/data/querydsl/binding/QuerydslPathInformation.java new file mode 100644 index 0000000000..1f78dc3c2c --- /dev/null +++ b/src/main/java/org/springframework/data/querydsl/binding/QuerydslPathInformation.java @@ -0,0 +1,95 @@ +/* + * Copyright 2016 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.querydsl.binding; + +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +import java.beans.PropertyDescriptor; + +import org.springframework.beans.BeanUtils; +import org.springframework.data.querydsl.EntityPathResolver; +import org.springframework.data.querydsl.QueryDslUtils; + +import com.querydsl.core.types.Path; + +/** + * {@link PathInformation} based on a Querydsl {@link Path}. + * + * @author Oliver Gierke + * @since 1.13 + */ +@ToString +@EqualsAndHashCode +@RequiredArgsConstructor(staticName = "of") +class QuerydslPathInformation implements PathInformation { + + private final Path path; + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.binding.MappedPath#getLeafType() + */ + @Override + public Class getLeafType() { + return path.getType(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.binding.MappedPath#getLeafParentType() + */ + @Override + public Class getLeafParentType() { + return path.getMetadata().getParent().getType(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.binding.MappedPath#getLeafProperty() + */ + @Override + public String getLeafProperty() { + return path.getMetadata().getElement().toString(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.binding.MappedPath#getLeafPropertyDescriptor() + */ + @Override + public PropertyDescriptor getLeafPropertyDescriptor() { + return BeanUtils.getPropertyDescriptor(getLeafParentType(), getLeafProperty()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.binding.MappedPath#toDotPath() + */ + @Override + public String toDotPath() { + return QueryDslUtils.toDotPath(path); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.binding.PathInformation#reifyPath(org.springframework.data.querydsl.EntityPathResolver) + */ + public Path reifyPath(EntityPathResolver resolver) { + return path; + } +} diff --git a/src/main/java/org/springframework/data/querydsl/binding/QuerydslPredicateBuilder.java b/src/main/java/org/springframework/data/querydsl/binding/QuerydslPredicateBuilder.java index bc53abdcd6..bd230fab1e 100644 --- a/src/main/java/org/springframework/data/querydsl/binding/QuerydslPredicateBuilder.java +++ b/src/main/java/org/springframework/data/querydsl/binding/QuerydslPredicateBuilder.java @@ -16,7 +16,6 @@ package org.springframework.data.querydsl.binding; import java.beans.PropertyDescriptor; -import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -25,7 +24,6 @@ import java.util.Map; import java.util.Map.Entry; -import org.springframework.beans.BeanUtils; import org.springframework.beans.PropertyValues; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.Property; @@ -41,7 +39,6 @@ import com.querydsl.core.BooleanBuilder; import com.querydsl.core.types.Path; import com.querydsl.core.types.Predicate; -import com.querydsl.core.types.dsl.CollectionPathBase; /** * Builder assembling {@link Predicate} out of {@link PropertyValues}. @@ -54,7 +51,7 @@ public class QuerydslPredicateBuilder { private final ConversionService conversionService; private final MultiValueBinding defaultBinding; - private final Map> paths; + private final Map> paths; private final EntityPathResolver resolver; /** @@ -70,7 +67,7 @@ public QuerydslPredicateBuilder(ConversionService conversionService, EntityPathR this.defaultBinding = new QuerydslDefaultBinding(); this.conversionService = conversionService; - this.paths = new HashMap>(); + this.paths = new HashMap>(); this.resolver = resolver; } @@ -106,7 +103,7 @@ public Predicate getPredicate(TypeInformation type, MultiValueMap type, MultiValueMap values) { + private Predicate invokeBinding(PathInformation dotPath, QuerydslBindings bindings, Collection values) { Path path = getPath(dotPath, bindings); @@ -151,7 +148,7 @@ private Predicate invokeBinding(PropertyPath dotPath, QuerydslBindings bindings, * @param bindings must not be {@literal null}. * @return */ - private Path getPath(PropertyPath path, QuerydslBindings bindings) { + private Path getPath(PathInformation path, QuerydslBindings bindings) { Path resolvedPath = bindings.getExistingPath(path); @@ -165,37 +162,12 @@ private Path getPath(PropertyPath path, QuerydslBindings bindings) { return resolvedPath; } - resolvedPath = reifyPath(path, null); + resolvedPath = path.reifyPath(resolver); paths.put(path, resolvedPath); return resolvedPath; } - /** - * Tries to reify a Querydsl {@link Path} from the given {@link PropertyPath} and base. - * - * @param path must not be {@literal null}. - * @param base can be {@literal null}. - * @return - */ - private Path reifyPath(PropertyPath path, Path base) { - - if (base instanceof CollectionPathBase) { - return reifyPath(path, (Path) ((CollectionPathBase) base).any()); - } - - Path entityPath = base != null ? base : resolver.createPath(path.getOwningType().getType()); - - Field field = ReflectionUtils.findField(entityPath.getClass(), path.getSegment()); - Object value = ReflectionUtils.getField(field, entityPath); - - if (path.hasNext()) { - return reifyPath(path.next(), (Path) value); - } - - return (Path) value; - } - /** * Converts the given source values into a collection of elements that are of the given {@link PropertyPath}'s type. * Considers a single element list with an empty {@link String} an empty collection because this basically indicates @@ -205,10 +177,9 @@ private Path reifyPath(PropertyPath path, Path base) { * @param path must not be {@literal null}. * @return */ - private Collection convertToPropertyPathSpecificType(List source, PropertyPath path) { + private Collection convertToPropertyPathSpecificType(List source, PathInformation path) { - PropertyPath leafProperty = path.getLeafProperty(); - Class targetType = leafProperty.getOwningType().getProperty(leafProperty.getSegment()).getType(); + Class targetType = path.getLeafType(); if (source.isEmpty() || isSingleElementCollectionWithoutText(source)) { return Collections.emptyList(); @@ -226,26 +197,25 @@ private Collection convertToPropertyPathSpecificType(List source } /** - * Returns the target {@link TypeDescriptor} for the given {@link PropertyPath} by either inspecting the field or + * Returns the target {@link TypeDescriptor} for the given {@link PathInformation} by either inspecting the field or * property (the latter preferred) to pick up annotations potentially defined for formatting purposes. * * @param path must not be {@literal null}. * @return */ - private static TypeDescriptor getTargetTypeDescriptor(PropertyPath path) { + private static TypeDescriptor getTargetTypeDescriptor(PathInformation path) { - PropertyPath leafProperty = path.getLeafProperty(); - Class owningType = leafProperty.getOwningType().getType(); + PropertyDescriptor descriptor = path.getLeafPropertyDescriptor(); - PropertyDescriptor descriptor = BeanUtils.getPropertyDescriptor(owningType, leafProperty.getSegment()); + Class owningType = path.getLeafParentType(); + String leafProperty = path.getLeafProperty(); if (descriptor == null) { - return TypeDescriptor.nested(ReflectionUtils.findField(owningType, leafProperty.getSegment()), 0); + return TypeDescriptor.nested(ReflectionUtils.findField(owningType, leafProperty), 0); } - return TypeDescriptor.nested( - new Property(owningType, descriptor.getReadMethod(), descriptor.getWriteMethod(), leafProperty.getSegment()), - 0); + return TypeDescriptor + .nested(new Property(owningType, descriptor.getReadMethod(), descriptor.getWriteMethod(), leafProperty), 0); } /** diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java index e28124c3a2..68f0d754c4 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java @@ -82,13 +82,13 @@ public BeanDefinitionBuilder build(RepositoryConfiguration configuration) { Assert.notNull(resourceLoader, "ResourceLoader must not be null!"); String factoryBeanName = configuration.getRepositoryFactoryBeanName(); - factoryBeanName = StringUtils.hasText(factoryBeanName) ? factoryBeanName : extension - .getRepositoryFactoryClassName(); + factoryBeanName = StringUtils.hasText(factoryBeanName) ? factoryBeanName + : extension.getRepositoryFactoryClassName(); BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(factoryBeanName); builder.getRawBeanDefinition().setSource(configuration.getSource()); - builder.addPropertyValue("repositoryInterface", configuration.getRepositoryInterface()); + builder.addConstructorArgValue(configuration.getRepositoryInterface()); builder.addPropertyValue("queryLookupStrategyKey", configuration.getQueryLookupStrategyKey()); builder.addPropertyValue("lazyInit", configuration.isLazyInit()); builder.addPropertyValue("repositoryBaseClass", configuration.getRepositoryBaseClassName()); @@ -127,8 +127,8 @@ private String registerCustomImplementation(RepositoryConfiguration configura return beanName; } - AbstractBeanDefinition beanDefinition = implementationDetector.detectCustomImplementation( - configuration.getImplementationClassName(), configuration.getBasePackages()); + AbstractBeanDefinition beanDefinition = implementationDetector + .detectCustomImplementation(configuration.getImplementationClassName(), configuration.getBasePackages()); if (null == beanDefinition) { return null; diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryBeanNameGenerator.java b/src/main/java/org/springframework/data/repository/config/RepositoryBeanNameGenerator.java index 667adcc205..1020be8b89 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryBeanNameGenerator.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryBeanNameGenerator.java @@ -65,7 +65,7 @@ public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry */ private Class getRepositoryInterfaceFrom(BeanDefinition beanDefinition) { - Object value = beanDefinition.getPropertyValues().getPropertyValue("repositoryInterface").getValue(); + Object value = beanDefinition.getConstructorArgumentValues().getArgumentValue(0, Class.class).getValue(); if (value instanceof Class) { return (Class) value; diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java index 950e504cb6..6b0718d916 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java @@ -25,12 +25,11 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; -import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.core.env.Environment; import org.springframework.core.env.EnvironmentCapable; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.ResourceLoader; -import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.util.Assert; @@ -48,7 +47,6 @@ public class RepositoryConfigurationDelegate { private static final String REPOSITORY_REGISTRATION = "Spring Data {} - Registering repository: {} - Interface: {} - Factory: {}"; private static final String MULTIPLE_MODULES = "Multiple Spring Data modules found, entering strict repository configuration mode!"; - private static final String MODULE_DETECTION_PACKAGE = "org.springframework.data.**.repository.support"; static final String FACTORY_BEAN_OBJECT_TYPE = "factoryBeanObjectType"; @@ -160,17 +158,13 @@ public List registerRepositoriesIn(BeanDefinitionRegist */ private boolean multipleStoresDetected() { - ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false); - scanner.setEnvironment(environment); - scanner.setResourceLoader(resourceLoader); - scanner.addIncludeFilter(new AssignableTypeFilter(RepositoryFactorySupport.class)); - - if (scanner.findCandidateComponents(MODULE_DETECTION_PACKAGE).size() > 1) { + boolean multipleModulesFound = SpringFactoriesLoader + .loadFactoryNames(RepositoryFactorySupport.class, resourceLoader.getClassLoader()).size() > 1; + if (multipleModulesFound) { LOGGER.info(MULTIPLE_MODULES); - return true; } - return false; + return multipleModulesFound; } } diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java index 1596b67e45..04f5850a3e 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java @@ -33,7 +33,6 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; -import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -50,8 +49,6 @@ public abstract class RepositoryConfigurationExtensionSupport implements Reposit private static final String CLASS_LOADING_ERROR = "%s - Could not load type %s using class loader %s."; private static final String MULTI_STORE_DROPPED = "Spring Data {} - Could not safely identify store assignment for repository candidate {}."; - private static final String FACTORY_BEAN_TYPE_PREDICTING_POST_PROCESSOR = "org.springframework.data.repository.core.support.FactoryBeanTypePredictingBeanPostProcessor"; - /* * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#getModuleName() @@ -114,18 +111,8 @@ public String getDefaultNamedQueryLocation() { * (non-Javadoc) * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#registerBeansForRoot(org.springframework.beans.factory.support.BeanDefinitionRegistry, org.springframework.data.repository.config.RepositoryConfigurationSource) */ - public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConfigurationSource configurationSource) { - - String typeName = RepositoryFactoryBeanSupport.class.getName(); - - BeanDefinitionBuilder builder = BeanDefinitionBuilder - .rootBeanDefinition(FACTORY_BEAN_TYPE_PREDICTING_POST_PROCESSOR); - builder.addConstructorArgValue(typeName); - builder.addConstructorArgValue("repositoryInterface"); - - registerIfNotAlreadyRegistered(builder.getBeanDefinition(), registry, typeName.concat("_Predictor"), - configurationSource.getSource()); - } + public void registerBeansForRoot(BeanDefinitionRegistry registry, + RepositoryConfigurationSource configurationSource) {} /** * Returns the prefix of the module to be used to create the default location for Spring Data named queries. diff --git a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java index 93bb399141..76c0fdbbfd 100644 --- a/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java +++ b/src/main/java/org/springframework/data/repository/core/support/DefaultRepositoryInformation.java @@ -22,6 +22,7 @@ import java.io.Serializable; import java.lang.reflect.GenericDeclaration; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.util.Collections; @@ -184,6 +185,7 @@ public Set getQueryMethods() { */ private boolean isQueryMethodCandidate(Method method) { return !method.isBridge() && !ReflectionUtils.isDefaultMethod(method) // + && !Modifier.isStatic(method.getModifiers()) // && (isQueryAnnotationPresentOn(method) || !isCustomMethod(method) && !isBaseClassMethod(method)); } @@ -243,6 +245,12 @@ Method getTargetClassMethod(Method method, Class baseClass) { return method; } + Method result = findMethod(baseClass, method.getName(), method.getParameterTypes()); + + if (result != null) { + return result; + } + for (Method baseClassMethod : baseClass.getMethods()) { // Wrong name diff --git a/src/main/java/org/springframework/data/repository/core/support/EventPublishingRepositoryProxyPostProcessor.java b/src/main/java/org/springframework/data/repository/core/support/EventPublishingRepositoryProxyPostProcessor.java new file mode 100644 index 0000000000..90200607d5 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/EventPublishingRepositoryProxyPostProcessor.java @@ -0,0 +1,244 @@ +/* + * Copyright 2016 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.core.support; + +import lombok.RequiredArgsConstructor; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.AfterDomainEventPublication; +import org.springframework.data.domain.DomainEvents; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.util.AnnotationDetectionMethodCallback; +import org.springframework.util.Assert; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ReflectionUtils; + +/** + * {@link RepositoryProxyPostProcessor} to register a {@link MethodInterceptor} to intercept the + * {@link CrudRepository#save(Object)} method and publish events potentially exposed via a method annotated with + * {@link DomainEvents}. If no such method can be detected on the aggregate root, no interceptor is added. Additionally, + * the aggregate root can expose a method annotated with {@link AfterDomainEventPublication}. If present, the method + * will be invoked after all events have been published. + * + * @author Oliver Gierke + * @since 1.13 + * @soundtrack Henrik Freischlader Trio - Master Plan (Openness) + */ +@RequiredArgsConstructor +public class EventPublishingRepositoryProxyPostProcessor implements RepositoryProxyPostProcessor { + + private final ApplicationEventPublisher publisher; + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryProxyPostProcessor#postProcess(org.springframework.aop.framework.ProxyFactory, org.springframework.data.repository.core.RepositoryInformation) + */ + @Override + public void postProcess(ProxyFactory factory, RepositoryInformation repositoryInformation) { + + EventPublishingMethod method = EventPublishingMethod.of(repositoryInformation.getDomainType()); + + if (method == null) { + return; + } + + factory.addAdvice(new EventPublishingMethodInterceptor(method, publisher)); + } + + /** + * {@link MethodInterceptor} to publish events exposed an aggregate on calls to a save method on the repository. + * + * @author Oliver Gierke + * @since 1.13 + */ + @RequiredArgsConstructor(staticName = "of") + static class EventPublishingMethodInterceptor implements MethodInterceptor { + + private final EventPublishingMethod eventMethod; + private final ApplicationEventPublisher publisher; + + /* + * (non-Javadoc) + * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) + */ + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + + if (!invocation.getMethod().getName().equals("save")) { + return invocation.proceed(); + } + + for (Object argument : invocation.getArguments()) { + eventMethod.publishEventsFrom(argument, publisher); + } + + return invocation.proceed(); + } + } + + /** + * Abstraction of a method on the aggregate root that exposes the events to publish. + * + * @author Oliver Gierke + * @since 1.13 + */ + @RequiredArgsConstructor + static class EventPublishingMethod { + + private static Map, EventPublishingMethod> CACHE = new ConcurrentReferenceHashMap, EventPublishingMethod>(); + private static EventPublishingMethod NONE = new EventPublishingMethod(null, null); + + private final Method publishingMethod; + private final Method clearingMethod; + + /** + * Creates an {@link EventPublishingMethod} for the given type. + * + * @param type must not be {@literal null}. + * @return an {@link EventPublishingMethod} for the given type or {@literal null} in case the given type does not + * expose an event publishing method. + */ + public static EventPublishingMethod of(Class type) { + + Assert.notNull(type, "Type must not be null!"); + + EventPublishingMethod eventPublishingMethod = CACHE.get(type); + + if (eventPublishingMethod != null) { + return eventPublishingMethod.orNull(); + } + + AnnotationDetectionMethodCallback publishing = new AnnotationDetectionMethodCallback( + DomainEvents.class); + ReflectionUtils.doWithMethods(type, publishing); + + // TODO: Lazify this as the inspection might not be needed if the publishing callback didn't find an annotation in + // the first place + + AnnotationDetectionMethodCallback clearing = new AnnotationDetectionMethodCallback( + AfterDomainEventPublication.class); + ReflectionUtils.doWithMethods(type, clearing); + + EventPublishingMethod result = from(publishing, clearing); + + CACHE.put(type, result); + + return result.orNull(); + } + + /** + * Publishes all events in the given aggregate root using the given {@link ApplicationEventPublisher}. + * + * @param object can be {@literal null}. + * @param publisher must not be {@literal null}. + */ + public void publishEventsFrom(Object object, ApplicationEventPublisher publisher) { + + if (object == null) { + return; + } + + for (Object aggregateRoot : asCollection(object)) { + for (Object event : asCollection(ReflectionUtils.invokeMethod(publishingMethod, aggregateRoot))) { + publisher.publishEvent(event); + } + } + + if (clearingMethod != null) { + ReflectionUtils.invokeMethod(clearingMethod, object); + } + } + + /** + * Returns the current {@link EventPublishingMethod} or {@literal null} if it's the default value. + * + * @return + */ + private EventPublishingMethod orNull() { + return this == EventPublishingMethod.NONE ? null : this; + } + + /** + * Creates a new {@link EventPublishingMethod} using the given pre-populated + * {@link AnnotationDetectionMethodCallback} looking up an optional clearing method from the given callback. + * + * @param publishing must not be {@literal null}. + * @param clearing must not be {@literal null}. + * @return + */ + private static EventPublishingMethod from(AnnotationDetectionMethodCallback publishing, + AnnotationDetectionMethodCallback clearing) { + + if (!publishing.hasFoundAnnotation()) { + return EventPublishingMethod.NONE; + } + + Method eventMethod = publishing.getMethod(); + ReflectionUtils.makeAccessible(eventMethod); + + return new EventPublishingMethod(eventMethod, getClearingMethod(clearing)); + } + + /** + * Returns the {@link Method} supposed to be invoked for event clearing or {@literal null} if none is found. + * + * @param clearing must not be {@literal null}. + * @return + */ + private static Method getClearingMethod(AnnotationDetectionMethodCallback clearing) { + + if (!clearing.hasFoundAnnotation()) { + return null; + } + + Method method = clearing.getMethod(); + ReflectionUtils.makeAccessible(method); + + return method; + } + + /** + * Returns the given source object as collection, i.e. collections are returned as is, objects are turned into a + * one-element collection, {@literal null} will become an empty collection. + * + * @param source can be {@literal null}. + * @return + */ + @SuppressWarnings("unchecked") + private static Collection asCollection(Object source) { + + if (source == null) { + return Collections.emptyList(); + } + + if (Collection.class.isInstance(source)) { + return (Collection) source; + } + + return Arrays.asList(source); + } + } +} diff --git a/src/main/java/org/springframework/data/repository/core/support/FactoryBeanTypePredictingBeanPostProcessor.java b/src/main/java/org/springframework/data/repository/core/support/FactoryBeanTypePredictingBeanPostProcessor.java deleted file mode 100644 index b2afe0574c..0000000000 --- a/src/main/java/org/springframework/data/repository/core/support/FactoryBeanTypePredictingBeanPostProcessor.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright 2016 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.core.support; - -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.PropertyValue; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.FactoryBean; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter; -import org.springframework.beans.factory.config.TypedStringValue; -import org.springframework.core.Ordered; -import org.springframework.core.PriorityOrdered; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; - -/** - * {@link InstantiationAwareBeanPostProcessorAdapter} to predict the bean type for {@link FactoryBean} implementations - * by interpreting a configured property of the {@link BeanDefinition} as type to be created eventually. - * - * @author Oliver Gierke - * @since 1.12 - * @soundtrack Ron Spielmann - Lock Me Up (Electric Tales) - */ -public class FactoryBeanTypePredictingBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter - implements BeanFactoryAware, PriorityOrdered { - - private static final Logger LOGGER = LoggerFactory.getLogger(FactoryBeanTypePredictingBeanPostProcessor.class); - - private final Map> cache = new ConcurrentHashMap>(); - private final Class factoryBeanType; - private final List properties; - private ConfigurableListableBeanFactory context; - - /** - * Creates a new {@link FactoryBeanTypePredictingBeanPostProcessor} predicting the type created by the - * {@link FactoryBean} of the given type by inspecting the {@link BeanDefinition} and considering the value for the - * given property as type to be created eventually. - * - * @param factoryBeanType must not be {@literal null}. - * @param properties must not be {@literal null} or empty. - */ - public FactoryBeanTypePredictingBeanPostProcessor(Class factoryBeanType, String... properties) { - - Assert.notNull(factoryBeanType, "FactoryBean type must not be null!"); - Assert.isTrue(FactoryBean.class.isAssignableFrom(factoryBeanType), "Given type is not a FactoryBean type!"); - Assert.notEmpty(properties, "Properties must not be empty!"); - - for (String property : properties) { - Assert.hasText(property, "Type property must not be null!"); - } - - this.factoryBeanType = factoryBeanType; - this.properties = Arrays.asList(properties); - } - - /* - * (non-Javadoc) - * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory) - */ - public void setBeanFactory(BeanFactory beanFactory) { - - if (beanFactory instanceof ConfigurableListableBeanFactory) { - this.context = (ConfigurableListableBeanFactory) beanFactory; - } - } - - /* - * (non-Javadoc) - * @see org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter#predictBeanType(java.lang.Class, java.lang.String) - */ - @Override - public Class predictBeanType(Class beanClass, String beanName) { - - if (null == context || !factoryBeanType.isAssignableFrom(beanClass)) { - return null; - } - - Class resolvedBeanClass = cache.get(beanName); - - if (resolvedBeanClass != null) { - return resolvedBeanClass == Void.class ? null : resolvedBeanClass; - } - - BeanDefinition definition = context.getBeanDefinition(beanName); - - try { - - for (String property : properties) { - - PropertyValue value = definition.getPropertyValues().getPropertyValue(property); - resolvedBeanClass = getClassForPropertyValue(value, beanName); - - if (Void.class.equals(resolvedBeanClass)) { - continue; - } - - return resolvedBeanClass; - } - - return null; - - } finally { - cache.put(beanName, resolvedBeanClass); - } - } - - /** - * Returns the class which is configured in the given {@link PropertyValue}. In case it is not a - * {@link TypedStringValue} or the value contained cannot be interpreted as {@link Class} it will return {@link Void}. - * - * @param propertyValue can be {@literal null}. - * @param beanName must not be {@literal null}. - * @return - */ - private Class getClassForPropertyValue(PropertyValue propertyValue, String beanName) { - - if (propertyValue == null) { - return Void.class; - } - - Object value = propertyValue.getValue(); - String className = null; - - if (value instanceof TypedStringValue) { - className = ((TypedStringValue) value).getValue(); - } else if (value instanceof String) { - className = (String) value; - } else if (value instanceof Class) { - return (Class) value; - } else if (value instanceof String[]) { - - String[] values = (String[]) value; - - if (values.length == 0) { - return Void.class; - } else { - className = values[0]; - } - - } else { - return Void.class; - } - - try { - return ClassUtils.resolveClassName(className, context.getBeanClassLoader()); - } catch (IllegalArgumentException ex) { - LOGGER.warn( - String.format("Couldn't load class %s referenced as repository interface in bean %s!", className, beanName)); - return Void.class; - } - } - - /* - * (non-Javadoc) - * @see org.springframework.core.Ordered#getOrder() - */ - public int getOrder() { - return Ordered.LOWEST_PRECEDENCE - 1; - } -} diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java index 12ef4656cd..aa17dd4977 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2015 the original author or authors. + * Copyright 2008-2016 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. @@ -24,7 +24,8 @@ import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.beans.factory.annotation.Required; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.repository.Repository; @@ -47,13 +48,14 @@ * @author Oliver Gierke * @author Thomas Darimont */ -public abstract class RepositoryFactoryBeanSupport, S, ID extends Serializable> implements - InitializingBean, RepositoryFactoryInformation, FactoryBean, BeanClassLoaderAware, BeanFactoryAware { +public abstract class RepositoryFactoryBeanSupport, S, ID extends Serializable> + implements InitializingBean, RepositoryFactoryInformation, FactoryBean, BeanClassLoaderAware, + BeanFactoryAware, ApplicationEventPublisherAware { - private RepositoryFactorySupport factory; + private final Class repositoryInterface; + private RepositoryFactorySupport factory; private Key queryLookupStrategyKey; - private Class repositoryInterface; private Class repositoryBaseClass; private Object customImplementation; private NamedQueries namedQueries; @@ -62,20 +64,20 @@ public abstract class RepositoryFactoryBeanSupport, private BeanFactory beanFactory; private boolean lazyInit = false; private EvaluationContextProvider evaluationContextProvider = DefaultEvaluationContextProvider.INSTANCE; + private ApplicationEventPublisher publisher; private T repository; private RepositoryMetadata repositoryMetadata; /** - * Setter to inject the repository interface to implement. + * Creates a new {@link RepositoryFactoryBeanSupport} for the given repository interface. * - * @param repositoryInterface the repository interface to set + * @param repositoryInterface must not be {@literal null}. */ - @Required - public void setRepositoryInterface(Class repositoryInterface) { + protected RepositoryFactoryBeanSupport(Class repositoryInterface) { - Assert.notNull(repositoryInterface); + Assert.notNull(repositoryInterface, "Repository interface must not be null!"); this.repositoryInterface = repositoryInterface; } @@ -162,7 +164,15 @@ public void setBeanClassLoader(ClassLoader classLoader) { @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; + } + /* + * (non-Javadoc) + * @see org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher(org.springframework.context.ApplicationEventPublisher) + */ + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; } /* @@ -217,9 +227,8 @@ public T getObject() { * (non-Javadoc) * @see org.springframework.beans.factory.FactoryBean#getObjectType() */ - @SuppressWarnings("unchecked") public Class getObjectType() { - return (Class) (null == repositoryInterface ? Repository.class : repositoryInterface); + return repositoryInterface; } /* @@ -236,8 +245,6 @@ public boolean isSingleton() { */ public void afterPropertiesSet() { - Assert.notNull(repositoryInterface, "Repository interface must not be null on initialization!"); - this.factory = createRepositoryFactory(); this.factory.setQueryLookupStrategyKey(queryLookupStrategyKey); this.factory.setNamedQueries(namedQueries); @@ -246,6 +253,10 @@ public void afterPropertiesSet() { this.factory.setBeanClassLoader(classLoader); this.factory.setBeanFactory(beanFactory); + if (publisher != null) { + this.factory.addRepositoryProxyPostProcessor(new EventPublishingRepositoryProxyPostProcessor(publisher)); + } + this.repositoryMetadata = this.factory.getRepositoryMetadata(repositoryInterface); if (!lazyInit) { diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java index b45ec1757f..d2917e0dc8 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java @@ -203,7 +203,8 @@ public T getRepository(Class repositoryInterface, Object customImplementa result.setTarget(target); result.setInterfaces(new Class[] { repositoryInterface, Repository.class }); - result.addAdvice(ExposeInvocationInterceptor.INSTANCE); + result.addAdvice(SurroundingTransactionDetectorMethodInterceptor.INSTANCE); + result.addAdvisor(ExposeInvocationInterceptor.ADVISOR); if (TRANSACTION_PROXY_TYPE != null) { result.addInterface(TRANSACTION_PROXY_TYPE); diff --git a/src/main/java/org/springframework/data/repository/core/support/SurroundingTransactionDetectorMethodInterceptor.java b/src/main/java/org/springframework/data/repository/core/support/SurroundingTransactionDetectorMethodInterceptor.java new file mode 100644 index 0000000000..081ed56007 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/core/support/SurroundingTransactionDetectorMethodInterceptor.java @@ -0,0 +1,62 @@ +/* + * Copyright 2016 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.core.support; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * {@link MethodInterceptor} detecting whether a transaction is already running and exposing that fact via + * {@link #isSurroundingTransactionActive()}. Useful in case subsequent interceptors might create transactions + * themselves but downstream components have to find out whether there was one running before the call entered the + * proxy. + * + * @author Oliver Gierke + * @since 1.13 + * @soundtrack Hendrik Freischlader Trio - Openness (Openness) + */ +public enum SurroundingTransactionDetectorMethodInterceptor implements MethodInterceptor { + + INSTANCE; + + private final ThreadLocal SURROUNDING_TX_ACTIVE = new ThreadLocal(); + + /** + * Returns whether a transaction was active before the method call entered the repository proxy. + * + * @return + */ + public boolean isSurroundingTransactionActive() { + return Boolean.TRUE == SURROUNDING_TX_ACTIVE.get(); + } + + /* + * (non-Javadoc) + * @see org.aopalliance.intercept.MethodInterceptor#invoke(org.aopalliance.intercept.MethodInvocation) + */ + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + + SURROUNDING_TX_ACTIVE.set(TransactionSynchronizationManager.isActualTransactionActive()); + + try { + return invocation.proceed(); + } finally { + SURROUNDING_TX_ACTIVE.remove(); + } + } +} diff --git a/src/main/java/org/springframework/data/repository/core/support/TransactionalRepositoryFactoryBeanSupport.java b/src/main/java/org/springframework/data/repository/core/support/TransactionalRepositoryFactoryBeanSupport.java index 9d98d6ff40..6120026563 100644 --- a/src/main/java/org/springframework/data/repository/core/support/TransactionalRepositoryFactoryBeanSupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/TransactionalRepositoryFactoryBeanSupport.java @@ -40,6 +40,15 @@ public abstract class TransactionalRepositoryFactoryBeanSupport repositoryInterface) { + super(repositoryInterface); + } + /** * Setter to configure which transaction manager to be used. We have to use the bean name explicitly as otherwise the * qualifier of the {@link org.springframework.transaction.annotation.Transactional} annotation is used. By explicitly diff --git a/src/main/java/org/springframework/data/repository/history/RevisionRepository.java b/src/main/java/org/springframework/data/repository/history/RevisionRepository.java index 448b4ebed3..b9aa241e36 100755 --- a/src/main/java/org/springframework/data/repository/history/RevisionRepository.java +++ b/src/main/java/org/springframework/data/repository/history/RevisionRepository.java @@ -23,6 +23,7 @@ import org.springframework.data.history.RevisionSort; import org.springframework.data.history.Revisions; import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.Repository; /** * A repository which can access entities held in a variety of {@link Revisions}. @@ -31,7 +32,8 @@ * @author Philipp Huegelmeyer */ @NoRepositoryBean -public interface RevisionRepository> { +public interface RevisionRepository> + extends Repository { /** * Returns the revision of the entity it was last changed in. diff --git a/src/main/java/org/springframework/data/repository/query/QueryMethod.java b/src/main/java/org/springframework/data/repository/query/QueryMethod.java index 3e876764d9..65784283c4 100644 --- a/src/main/java/org/springframework/data/repository/query/QueryMethod.java +++ b/src/main/java/org/springframework/data/repository/query/QueryMethod.java @@ -173,8 +173,22 @@ public Class getReturnedObjectType() { */ public boolean isCollectionQuery() { - return !(isPageQuery() || isSliceQuery()) - && org.springframework.util.ClassUtils.isAssignable(Iterable.class, unwrappedReturnType) + if (isPageQuery() || isSliceQuery()) { + return false; + } + + Class returnType = method.getReturnType(); + + if (QueryExecutionConverters.supports(returnType) && !QueryExecutionConverters.isSingleValue(returnType)) { + return true; + } + + if (QueryExecutionConverters.supports(unwrappedReturnType) + && QueryExecutionConverters.isSingleValue(unwrappedReturnType)) { + return false; + } + + return org.springframework.util.ClassUtils.isAssignable(Iterable.class, unwrappedReturnType) || unwrappedReturnType.isArray(); } diff --git a/src/main/java/org/springframework/data/repository/query/ReturnedType.java b/src/main/java/org/springframework/data/repository/query/ReturnedType.java index 8ff15d351e..09dbadb96e 100644 --- a/src/main/java/org/springframework/data/repository/query/ReturnedType.java +++ b/src/main/java/org/springframework/data/repository/query/ReturnedType.java @@ -191,7 +191,9 @@ public List getInputProperties() { List properties = new ArrayList(); for (PropertyDescriptor descriptor : information.getInputProperties()) { - properties.add(descriptor.getName()); + if (!properties.contains(descriptor.getName())) { + properties.add(descriptor.getName()); + } } return properties; diff --git a/src/main/java/org/springframework/data/repository/util/JavaslangCollections.java b/src/main/java/org/springframework/data/repository/util/JavaslangCollections.java new file mode 100644 index 0000000000..6ee4b42210 --- /dev/null +++ b/src/main/java/org/springframework/data/repository/util/JavaslangCollections.java @@ -0,0 +1,149 @@ +/* + * Copyright 2016 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.util; + +import javaslang.collection.Traversable; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.repository.util.QueryExecutionConverters.WrapperType; +import org.springframework.util.ReflectionUtils; + +/** + * Converter implementations to map from and to Javaslang collections. + * + * @author Oliver Gierke + * @since 1.13 + */ +class JavaslangCollections { + + public enum ToJavaConverter implements Converter { + + INSTANCE; + + public WrapperType getWrapperType() { + return WrapperType.multiValue(javaslang.collection.Traversable.class); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + + if (source instanceof javaslang.collection.Seq) { + return ((javaslang.collection.Seq) source).toJavaList(); + } + + if (source instanceof javaslang.collection.Map) { + return ((javaslang.collection.Map) source).toJavaMap(); + } + + if (source instanceof javaslang.collection.Set) { + return ((javaslang.collection.Set) source).toJavaSet(); + } + + throw new IllegalArgumentException("Unsupported Javaslang collection " + source); + } + } + + public enum FromJavaConverter implements ConditionalGenericConverter { + + INSTANCE { + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes() + */ + @Override + public java.util.Set getConvertibleTypes() { + return CONVERTIBLE_PAIRS; + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.ConditionalConverter#matches(org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor) + */ + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + + // Prevent collections to be mapped to maps + if (sourceType.isCollection() && javaslang.collection.Map.class.isAssignableFrom(targetType.getType())) { + return false; + } + + // Prevent maps to be mapped to collections + if (sourceType.isMap() && !(javaslang.collection.Map.class.isAssignableFrom(targetType.getType()) + || targetType.getType().equals(Traversable.class))) { + return false; + } + + return true; + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.GenericConverter#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor) + */ + @Override + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + + if (source instanceof List) { + return ReflectionUtils.invokeMethod(LIST_FACTORY_METHOD, null, source); + } + + if (source instanceof java.util.Set) { + return ReflectionUtils.invokeMethod(SET_FACTORY_METHOD, null, source); + } + + if (source instanceof java.util.Map) { + return ReflectionUtils.invokeMethod(MAP_FACTORY_METHOD, null, source); + } + + return source; + } + }; + + private static final Set CONVERTIBLE_PAIRS; + private static final Method LIST_FACTORY_METHOD; + private static final Method SET_FACTORY_METHOD; + private static final Method MAP_FACTORY_METHOD; + + static { + + Set pairs = new HashSet(); + pairs.add(new ConvertiblePair(Collection.class, javaslang.collection.Traversable.class)); + pairs.add(new ConvertiblePair(Map.class, javaslang.collection.Traversable.class)); + + CONVERTIBLE_PAIRS = Collections.unmodifiableSet(pairs); + + MAP_FACTORY_METHOD = ReflectionUtils.findMethod(javaslang.collection.LinkedHashMap.class, "ofAll", Map.class); + LIST_FACTORY_METHOD = ReflectionUtils.findMethod(javaslang.collection.List.class, "ofAll", Iterable.class); + SET_FACTORY_METHOD = ReflectionUtils.findMethod(javaslang.collection.LinkedHashSet.class, "ofAll", + Iterable.class); + } + } +} diff --git a/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java b/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java index e8f57d04d9..c9a19d1237 100644 --- a/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java +++ b/src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java @@ -15,15 +15,22 @@ */ package org.springframework.data.repository.util; +import javaslang.collection.Traversable; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.Value; import scala.Function0; import scala.Option; import scala.runtime.AbstractFunction0; +import java.lang.reflect.Method; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; +import java.util.function.Supplier; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; @@ -33,6 +40,7 @@ import org.springframework.scheduling.annotation.AsyncResult; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.concurrent.ListenableFuture; import com.google.common.base.Optional; @@ -43,10 +51,11 @@ *
    *
  • {@code java.util.Optional}
  • *
  • {@code com.google.common.base.Optional}
  • - *
  • {@code scala.Option}
  • + *
  • {@code scala.Option} - as of 1.12
  • *
  • {@code java.util.concurrent.Future}
  • *
  • {@code java.util.concurrent.CompletableFuture}
  • *
  • {@code org.springframework.util.concurrent.ListenableFuture<}
  • + *
  • {@code javaslang.control.Option} - as of 1.13
  • *
* * @author Oliver Gierke @@ -55,24 +64,22 @@ */ public abstract class QueryExecutionConverters { - private static final boolean SPRING_4_2_PRESENT = ClassUtils.isPresent( - "org.springframework.core.annotation.AnnotationConfigurationException", - QueryExecutionConverters.class.getClassLoader()); - private static final boolean GUAVA_PRESENT = ClassUtils.isPresent("com.google.common.base.Optional", QueryExecutionConverters.class.getClassLoader()); private static final boolean JDK_8_PRESENT = ClassUtils.isPresent("java.util.Optional", QueryExecutionConverters.class.getClassLoader()); private static final boolean SCALA_PRESENT = ClassUtils.isPresent("scala.Option", QueryExecutionConverters.class.getClassLoader()); + private static final boolean JAVASLANG_PRESENT = ClassUtils.isPresent("javaslang.control.Option", + QueryExecutionConverters.class.getClassLoader()); - private static final Set> WRAPPER_TYPES = new HashSet>(); + private static final Set WRAPPER_TYPES = new HashSet(); private static final Set> UNWRAPPERS = new HashSet>(); static { - WRAPPER_TYPES.add(Future.class); - WRAPPER_TYPES.add(ListenableFuture.class); + WRAPPER_TYPES.add(WrapperType.singleValue(Future.class)); + WRAPPER_TYPES.add(WrapperType.singleValue(ListenableFuture.class)); if (GUAVA_PRESENT) { WRAPPER_TYPES.add(NullableWrapperToGuavaOptionalConverter.getWrapperType()); @@ -84,7 +91,7 @@ public abstract class QueryExecutionConverters { UNWRAPPERS.add(Jdk8OptionalUnwrapper.INSTANCE); } - if (JDK_8_PRESENT && SPRING_4_2_PRESENT) { + if (JDK_8_PRESENT) { WRAPPER_TYPES.add(NullableWrapperToCompletableFutureConverter.getWrapperType()); } @@ -92,6 +99,14 @@ public abstract class QueryExecutionConverters { WRAPPER_TYPES.add(NullableWrapperToScalaOptionConverter.getWrapperType()); UNWRAPPERS.add(ScalOptionUnwrapper.INSTANCE); } + + if (JAVASLANG_PRESENT) { + + WRAPPER_TYPES.add(NullableWrapperToJavaslangOptionConverter.getWrapperType()); + WRAPPER_TYPES.add(JavaslangCollections.ToJavaConverter.INSTANCE.getWrapperType()); + + UNWRAPPERS.add(JavaslangOptionUnwrapper.INSTANCE); + } } private QueryExecutionConverters() {} @@ -106,8 +121,8 @@ public static boolean supports(Class type) { Assert.notNull(type, "Type must not be null!"); - for (Class candidate : WRAPPER_TYPES) { - if (candidate.isAssignableFrom(type)) { + for (WrapperType candidate : WRAPPER_TYPES) { + if (candidate.getType().isAssignableFrom(type)) { return true; } } @@ -115,6 +130,17 @@ public static boolean supports(Class type) { return false; } + public static boolean isSingleValue(Class type) { + + for (WrapperType candidate : WRAPPER_TYPES) { + if (candidate.getType().isAssignableFrom(type)) { + return candidate.isSingleValue(); + } + } + + return false; + } + /** * Registers converters for wrapper types found on the classpath. * @@ -124,6 +150,8 @@ public static void registerConvertersIn(ConfigurableConversionService conversion Assert.notNull(conversionService, "ConversionService must not be null!"); + conversionService.removeConvertible(Collection.class, Object.class); + if (GUAVA_PRESENT) { conversionService.addConverter(new NullableWrapperToGuavaOptionalConverter(conversionService)); } @@ -137,6 +165,11 @@ public static void registerConvertersIn(ConfigurableConversionService conversion conversionService.addConverter(new NullableWrapperToScalaOptionConverter(conversionService)); } + if (JAVASLANG_PRESENT) { + conversionService.addConverter(new NullableWrapperToJavaslangOptionConverter(conversionService)); + conversionService.addConverter(JavaslangCollections.FromJavaConverter.INSTANCE); + } + conversionService.addConverter(new NullableWrapperToFutureConverter(conversionService)); } @@ -258,8 +291,8 @@ protected Object wrap(Object source) { return Optional.of(source); } - public static Class getWrapperType() { - return Optional.class; + public static WrapperType getWrapperType() { + return WrapperType.singleValue(Optional.class); } } @@ -288,8 +321,8 @@ protected Object wrap(Object source) { return java.util.Optional.of(source); } - public static Class getWrapperType() { - return java.util.Optional.class; + public static WrapperType getWrapperType() { + return WrapperType.singleValue(java.util.Optional.class); } } @@ -344,8 +377,8 @@ protected Object wrap(Object source) { return source instanceof CompletableFuture ? source : CompletableFuture.completedFuture(source); } - public static Class getWrapperType() { - return CompletableFuture.class; + public static WrapperType getWrapperType() { + return WrapperType.singleValue(CompletableFuture.class); } } @@ -370,8 +403,53 @@ protected Object wrap(Object source) { return Option.apply(source); } - public static Class getWrapperType() { - return Option.class; + public static WrapperType getWrapperType() { + return WrapperType.singleValue(Option.class); + } + } + + /** + * Converter to convert from {@link NullableWrapper} into JavaSlang's {@link javaslang.control.Option}. + * + * @author Oliver Gierke + * @since 1.13 + */ + private static class NullableWrapperToJavaslangOptionConverter extends AbstractWrapperTypeConverter { + + private static final Method OF_METHOD; + private static final Method NONE_METHOD; + + static { + OF_METHOD = ReflectionUtils.findMethod(javaslang.control.Option.class, "of", Object.class); + NONE_METHOD = ReflectionUtils.findMethod(javaslang.control.Option.class, "none"); + } + + /** + * Creates a new {@link NullableWrapperToJavaslangOptionConverter} using the given {@link ConversionService}. + * + * @param conversionService must not be {@literal null}. + */ + public NullableWrapperToJavaslangOptionConverter(ConversionService conversionService) { + super(conversionService, createEmptyOption(), javaslang.control.Option.class); + } + + public static WrapperType getWrapperType() { + return WrapperType.singleValue(javaslang.control.Option.class); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.util.QueryExecutionConverters.AbstractWrapperTypeConverter#wrap(java.lang.Object) + */ + @Override + @SuppressWarnings("unchecked") + protected Object wrap(Object source) { + return (javaslang.control.Option) ReflectionUtils.invokeMethod(OF_METHOD, null, source); + } + + @SuppressWarnings("unchecked") + private static javaslang.control.Option createEmptyOption() { + return (javaslang.control.Option) ReflectionUtils.invokeMethod(NONE_METHOD, null); } } @@ -420,7 +498,7 @@ public Object convert(Object source) { * * @author Oliver Gierke * @author Mark Paluch - * @author 1.13 + * @since 1.12 */ private static enum ScalOptionUnwrapper implements Converter { @@ -447,4 +525,61 @@ public Object convert(Object source) { return source instanceof Option ? ((Option) source).getOrElse(alternative) : source; } } + + /** + * Converter to unwrap Javaslang {@link javaslang.control.Option} instances. + * + * @author Oliver Gierke + * @since 1.13 + */ + private static enum JavaslangOptionUnwrapper implements Converter { + + INSTANCE; + + private static final Supplier NULL_SUPPLIER = new Supplier() { + + /* + * (non-Javadoc) + * @see java.util.function.Supplier#get() + */ + public Object get() { + return null; + } + }; + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + @SuppressWarnings("unchecked") + public Object convert(Object source) { + + if (source instanceof javaslang.control.Option) { + return ((javaslang.control.Option) source).getOrElse(NULL_SUPPLIER); + } + + if (source instanceof Traversable) { + return JavaslangCollections.ToJavaConverter.INSTANCE.convert(source); + } + + return source; + } + } + + @Value + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + public static class WrapperType { + + Class type; + boolean singleValue; + + public static WrapperType singleValue(Class type) { + return new WrapperType(type, true); + } + + public static WrapperType multiValue(Class type) { + return new WrapperType(type, false); + } + } } diff --git a/src/main/java/org/springframework/data/util/ClassTypeInformation.java b/src/main/java/org/springframework/data/util/ClassTypeInformation.java index a5b1604554..7ae71d7b8e 100644 --- a/src/main/java/org/springframework/data/util/ClassTypeInformation.java +++ b/src/main/java/org/springframework/data/util/ClassTypeInformation.java @@ -92,7 +92,8 @@ public static ClassTypeInformation from(Class type) { public static TypeInformation fromReturnTypeOf(Method method) { Assert.notNull(method, "Method must not be null!"); - return new ClassTypeInformation(method.getDeclaringClass()).createInfo(method.getGenericReturnType()); + return (TypeInformation) ClassTypeInformation.from(method.getDeclaringClass()) + .createInfo(method.getGenericReturnType()); } /** diff --git a/src/main/java/org/springframework/data/util/TypeDiscoverer.java b/src/main/java/org/springframework/data/util/TypeDiscoverer.java index a7050ff604..52cdc05967 100644 --- a/src/main/java/org/springframework/data/util/TypeDiscoverer.java +++ b/src/main/java/org/springframework/data/util/TypeDiscoverer.java @@ -35,13 +35,16 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.springframework.beans.BeanUtils; import org.springframework.core.GenericTypeResolver; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; /** @@ -51,6 +54,22 @@ */ class TypeDiscoverer implements TypeInformation { + private static final Iterable> MAP_TYPES; + + static { + + ClassLoader classLoader = TypeDiscoverer.class.getClassLoader(); + + Set> mapTypes = new HashSet>(); + mapTypes.add(Map.class); + + try { + mapTypes.add(ClassUtils.forName("javaslang.collection.Map", classLoader)); + } catch (ClassNotFoundException o_O) {} + + MAP_TYPES = Collections.unmodifiableSet(mapTypes); + } + private final Type type; private final Map, Type> typeVariableMap; private final Map fieldTypes = new ConcurrentHashMap(); @@ -103,7 +122,7 @@ protected TypeInformation createInfo(Type fieldType) { } if (fieldType instanceof Class) { - return new ClassTypeInformation((Class) fieldType); + return ClassTypeInformation.from((Class) fieldType); } Class resolveType = resolveType(fieldType); @@ -329,7 +348,14 @@ public TypeInformation getActualType() { * @see org.springframework.data.util.TypeInformation#isMap() */ public boolean isMap() { - return Map.class.isAssignableFrom(getType()); + + for (Class mapType : MAP_TYPES) { + if (mapType.isAssignableFrom(getType())) { + return true; + } + } + + return false; } /* @@ -349,7 +375,7 @@ public TypeInformation getMapValueType() { protected TypeInformation doGetMapValueType() { if (isMap()) { - return getTypeArgument(Map.class, 1); + return getTypeArgument(getBaseType(MAP_TYPES), 1); } List> arguments = getTypeArguments(); @@ -399,7 +425,7 @@ protected TypeInformation doGetComponentType() { } if (isMap()) { - return getTypeArgument(Map.class, 0); + return getTypeArgument(getBaseType(MAP_TYPES), 0); } if (Iterable.class.isAssignableFrom(rawType)) { @@ -525,6 +551,17 @@ private TypeInformation getTypeArgument(Class bound, int index) { return createInfo(arguments[index]); } + private Class getBaseType(Iterable> candidates) { + + for (Class candidate : candidates) { + if (candidate.isAssignableFrom(getType())) { + return candidate; + } + } + + throw new IllegalArgumentException(String.format("Type %s not contained in candidates %s!", getType(), candidates)); + } + /* * (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) diff --git a/src/main/java/org/springframework/data/web/config/SpringDataWebConfigurationMixin.java b/src/main/java/org/springframework/data/web/AllowedSortProperties.java similarity index 54% rename from src/main/java/org/springframework/data/web/config/SpringDataWebConfigurationMixin.java rename to src/main/java/org/springframework/data/web/AllowedSortProperties.java index 15b615245d..b0d8939940 100644 --- a/src/main/java/org/springframework/data/web/config/SpringDataWebConfigurationMixin.java +++ b/src/main/java/org/springframework/data/web/AllowedSortProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2016 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.web.config; +package org.springframework.data.web; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -21,21 +21,23 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.web.config.EnableSpringDataWebSupport.SpringDataWebConfigurationImportSelector; - /** - * Annotation to be able to scan for additional Spring Data configuration classes to contribute to the web integration. + * Annotation to define allowed sort properties for {@link org.springframework.data.domain.Sort} and + * {@link org.springframework.data.domain.Pageable}. * - * @author Oliver Gierke - * @since 1.10 - * @soundtrack Selah Sue - This World (Selah Sue) - * @see SpringDataJacksonConfiguration - * @see SpringDataWebConfigurationImportSelector + * @author Kazuki Shimizu + * @since 1.13 */ -@Retention(RetentionPolicy.RUNTIME) @Documented -@Configuration -@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE }) -public @interface SpringDataWebConfigurationMixin { +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface AllowedSortProperties { + + /** + * Specify allowed sort properties. + * + * @return + */ + String[] value(); + } diff --git a/src/main/java/org/springframework/data/web/HateoasSortHandlerMethodArgumentResolver.java b/src/main/java/org/springframework/data/web/HateoasSortHandlerMethodArgumentResolver.java index 547a332786..4c3784bd80 100644 --- a/src/main/java/org/springframework/data/web/HateoasSortHandlerMethodArgumentResolver.java +++ b/src/main/java/org/springframework/data/web/HateoasSortHandlerMethodArgumentResolver.java @@ -40,7 +40,7 @@ public class HateoasSortHandlerMethodArgumentResolver extends SortHandlerMethodA UriComponentsContributor { /** - * Returns the tempalte variables for the sort parameter. + * Returns the template variables for the sort parameter. * * @param parameter must not be {@literal null}. * @return diff --git a/src/main/java/org/springframework/data/web/PageableArgumentResolver.java b/src/main/java/org/springframework/data/web/PageableArgumentResolver.java new file mode 100644 index 0000000000..2a57d0f949 --- /dev/null +++ b/src/main/java/org/springframework/data/web/PageableArgumentResolver.java @@ -0,0 +1,51 @@ +/* + * Copyright 2016 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.web; + +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * Argument resolver to extract a {@link Pageable} object from a {@link NativeWebRequest} for a particular + * {@link MethodParameter}. A {@link PageableArgumentResolver} can either resolve {@link Pageable} itself or wrap + * another {@link PageableArgumentResolver} to post-process {@link Pageable}. {@link Pageable} resolution yields either + * in a {@link Pageable} object or {@literal null} if {@link Pageable} cannot be resolved. + * + * @author Mark Paluch + * @since 1.13 + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver + */ +public interface PageableArgumentResolver extends HandlerMethodArgumentResolver { + + /** + * Resolves a {@link Pageable} method parameter into an argument value from a given request. + * + * @param parameter the method parameter to resolve. This parameter must have previously been passed to + * {@link #supportsParameter} which must have returned {@code true}. + * @param mavContainer the ModelAndViewContainer for the current request + * @param webRequest the current request + * @param binderFactory a factory for creating {@link WebDataBinder} instances + * @return the resolved argument value, or {@code null} + */ + @Override + Pageable resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, + WebDataBinderFactory binderFactory); +} diff --git a/src/main/java/org/springframework/data/web/PageableHandlerMethodArgumentResolver.java b/src/main/java/org/springframework/data/web/PageableHandlerMethodArgumentResolver.java index b2bb978800..8d0b8c6c07 100644 --- a/src/main/java/org/springframework/data/web/PageableHandlerMethodArgumentResolver.java +++ b/src/main/java/org/springframework/data/web/PageableHandlerMethodArgumentResolver.java @@ -28,7 +28,6 @@ import org.springframework.util.StringUtils; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; /** @@ -39,9 +38,11 @@ * @since 1.6 * @author Oliver Gierke * @author Nick Williams + * @author Mark Paluch */ -public class PageableHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { +public class PageableHandlerMethodArgumentResolver implements PageableArgumentResolver { + private static final SortHandlerMethodArgumentResolver DEFAULT_SORT_RESOLVER = new SortHandlerMethodArgumentResolver(); private static final String INVALID_DEFAULT_PAGE_SIZE = "Invalid default page size configured for method %s! Must not be less than one!"; private static final String DEFAULT_PAGE_PARAMETER = "page"; @@ -52,7 +53,7 @@ public class PageableHandlerMethodArgumentResolver implements HandlerMethodArgum static final Pageable DEFAULT_PAGE_REQUEST = new PageRequest(0, 20); private Pageable fallbackPageable = DEFAULT_PAGE_REQUEST; - private SortHandlerMethodArgumentResolver sortResolver; + private SortArgumentResolver sortResolver; private String pageParameterName = DEFAULT_PAGE_PARAMETER; private String sizeParameterName = DEFAULT_SIZE_PARAMETER; private String prefix = DEFAULT_PREFIX; @@ -64,16 +65,26 @@ public class PageableHandlerMethodArgumentResolver implements HandlerMethodArgum * Constructs an instance of this resolved with a default {@link SortHandlerMethodArgumentResolver}. */ public PageableHandlerMethodArgumentResolver() { - this(null); + this((SortArgumentResolver) null); } /** * Constructs an instance of this resolver with the specified {@link SortHandlerMethodArgumentResolver}. * - * @param sortResolver The sort resolver to use + * @param sortResolver the sort resolver to use */ public PageableHandlerMethodArgumentResolver(SortHandlerMethodArgumentResolver sortResolver) { - this.sortResolver = sortResolver == null ? new SortHandlerMethodArgumentResolver() : sortResolver; + this((SortArgumentResolver) sortResolver); + } + + /** + * Constructs an instance of this resolver with the specified {@link SortArgumentResolver}. + * + * @param sortResolver the sort resolver to use + * @since 1.13 + */ + public PageableHandlerMethodArgumentResolver(SortArgumentResolver sortResolver) { + this.sortResolver = sortResolver == null ? DEFAULT_SORT_RESOLVER : sortResolver; } /** diff --git a/src/main/java/org/springframework/data/web/SortArgumentResolver.java b/src/main/java/org/springframework/data/web/SortArgumentResolver.java new file mode 100644 index 0000000000..9b59971bbe --- /dev/null +++ b/src/main/java/org/springframework/data/web/SortArgumentResolver.java @@ -0,0 +1,51 @@ +/* + * Copyright 2016 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.web; + +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.Sort; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * Argument resolver to extract a {@link Sort} object from a {@link NativeWebRequest} for a particular + * {@link MethodParameter}. A {@link SortArgumentResolver} can either resolve {@link Sort} itself or wrap another + * {@link SortArgumentResolver} to post-process {@link Sort}. {@link Sort} resolution yields either in a {@link Sort} + * object or {@literal null} if {@link Sort} cannot be resolved. + * + * @author Mark Paluch + * @since 1.13 + * @see org.springframework.web.method.support.HandlerMethodArgumentResolver + */ +public interface SortArgumentResolver extends HandlerMethodArgumentResolver { + + /** + * Resolves a {@link Sort} method parameter into an argument value from a given request. + * + * @param parameter the method parameter to resolve. This parameter must have previously been passed to + * {@link #supportsParameter} which must have returned {@code true}. + * @param mavContainer the ModelAndViewContainer for the current request + * @param webRequest the current request + * @param binderFactory a factory for creating {@link WebDataBinder} instances + * @return the resolved argument value, or {@code null} + */ + @Override + Sort resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, + WebDataBinderFactory binderFactory); +} diff --git a/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolver.java b/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolver.java index 870bd4916d..8a1efc7064 100644 --- a/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolver.java +++ b/src/main/java/org/springframework/data/web/SortHandlerMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-2016 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,8 +16,11 @@ package org.springframework.data.web; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.MethodParameter; @@ -40,8 +43,10 @@ * @author Oliver Gierke * @author Thomas Darimont * @author Nick Williams + * @author Mark Paluch + * @author Kazuki Shimizu */ -public class SortHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { +public class SortHandlerMethodArgumentResolver implements SortArgumentResolver { private static final String DEFAULT_PARAMETER = "sort"; private static final String DEFAULT_PROPERTY_DELIMITER = ","; @@ -118,7 +123,10 @@ public Sort resolveArgument(MethodParameter parameter, ModelAndViewContainer mav return getDefaultFromAnnotationOrFallback(parameter); } - return parseParameterIntoSort(directionParameter, propertyDelimiter); + AllowedSortProperties annotatedAllowedSortProperties = parameter + .getParameterAnnotation(AllowedSortProperties.class); + + return parseParameterIntoSort(directionParameter, propertyDelimiter, annotatedAllowedSortProperties); } /** @@ -197,12 +205,21 @@ protected String getSortParameter(MethodParameter parameter) { * Parses the given sort expressions into a {@link Sort} instance. The implementation expects the sources to be a * concatenation of Strings using the given delimiter. If the last element can be parsed into a {@link Direction} it's * considered a {@link Direction} and a simple property otherwise. - * + * * @param source will never be {@literal null}. * @param delimiter the delimiter to be used to split up the source elements, will never be {@literal null}. + * @param annotatedAllowedSortProperties annotation that indicate allowed sort properties * @return */ - Sort parseParameterIntoSort(String[] source, String delimiter) { + Sort parseParameterIntoSort(String[] source, String delimiter, AllowedSortProperties annotatedAllowedSortProperties) { + + Set allowedSortProperties = null; + if (annotatedAllowedSortProperties != null) { + if (annotatedAllowedSortProperties.value().length == 0) { + return null; + } + allowedSortProperties = new HashSet(Arrays.asList(annotatedAllowedSortProperties.value())); + } List allOrders = new ArrayList(); @@ -227,6 +244,10 @@ Sort parseParameterIntoSort(String[] source, String delimiter) { continue; } + if (allowedSortProperties != null && !allowedSortProperties.contains(property)) { + continue; + } + allOrders.add(new Order(direction, property)); } } @@ -260,7 +281,7 @@ protected List foldIntoExpressions(Sort sort) { builder.add(order.getProperty()); } - return builder == null ? Collections. emptyList() : builder.dumpExpressionIfPresentInto(expressions); + return builder == null ? Collections.emptyList() : builder.dumpExpressionIfPresentInto(expressions); } /** @@ -290,7 +311,7 @@ protected List legacyFoldExpressions(Sort sort) { builder.add(order.getProperty()); } - return builder == null ? Collections. emptyList() : builder.dumpExpressionIfPresentInto(expressions); + return builder == null ? Collections.emptyList() : builder.dumpExpressionIfPresentInto(expressions); } /** diff --git a/src/main/java/org/springframework/data/web/config/EnableSpringDataWebSupport.java b/src/main/java/org/springframework/data/web/config/EnableSpringDataWebSupport.java index 3acf2f5650..d895f8d8a9 100644 --- a/src/main/java/org/springframework/data/web/config/EnableSpringDataWebSupport.java +++ b/src/main/java/org/springframework/data/web/config/EnableSpringDataWebSupport.java @@ -23,16 +23,14 @@ import java.util.ArrayList; import java.util.List; -import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.EnvironmentAware; import org.springframework.context.ResourceLoaderAware; -import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.data.querydsl.QueryDslUtils; import org.springframework.data.web.PageableHandlerMethodArgumentResolver; import org.springframework.util.ClassUtils; @@ -124,15 +122,8 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { : SpringDataWebConfiguration.class.getName()); if (JACKSON_PRESENT) { - - ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); - provider.setEnvironment(environment); - provider.setResourceLoader(resourceLoader); - provider.addIncludeFilter(new AnnotationTypeFilter(SpringDataWebConfigurationMixin.class)); - - for (BeanDefinition definition : provider.findCandidateComponents("org.springframework.data")) { - imports.add(definition.getBeanClassName()); - } + imports.addAll( + SpringFactoriesLoader.loadFactoryNames(SpringDataJacksonModules.class, resourceLoader.getClassLoader())); } return imports.toArray(new String[imports.size()]); diff --git a/src/main/java/org/springframework/data/web/config/SpringDataJacksonConfiguration.java b/src/main/java/org/springframework/data/web/config/SpringDataJacksonConfiguration.java index 3fd412e500..5d9d603343 100644 --- a/src/main/java/org/springframework/data/web/config/SpringDataJacksonConfiguration.java +++ b/src/main/java/org/springframework/data/web/config/SpringDataJacksonConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 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. @@ -23,8 +23,7 @@ * * @author Oliver Gierke */ -@SpringDataWebConfigurationMixin -public class SpringDataJacksonConfiguration { +public class SpringDataJacksonConfiguration implements SpringDataJacksonModules { @Bean public GeoModule jacksonGeoModule() { diff --git a/src/main/java/org/springframework/data/web/config/SpringDataJacksonModules.java b/src/main/java/org/springframework/data/web/config/SpringDataJacksonModules.java new file mode 100644 index 0000000000..ef030a84b1 --- /dev/null +++ b/src/main/java/org/springframework/data/web/config/SpringDataJacksonModules.java @@ -0,0 +1,27 @@ +/* + * Copyright 2016 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.web.config; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Marker interface to describe configuration classes that ship Jackson modules that are supposed to be added to the + * Jackson {@link ObjectMapper} configured for {@link EnableSpringDataWebSupport}. + * + * @author Oliver Gierke + * @since 1.13 + */ +public interface SpringDataJacksonModules {} diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..5b6d502f70 --- /dev/null +++ b/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.data.web.config.SpringDataJacksonModules=org.springframework.data.web.config.SpringDataJacksonConfiguration diff --git a/src/main/resources/changelog.txt b/src/main/resources/changelog.txt index cd2c034a3a..dd52d99576 100644 --- a/src/main/resources/changelog.txt +++ b/src/main/resources/changelog.txt @@ -1,6 +1,73 @@ Spring Data Commons Changelog ============================= +Changes in version 1.12.6.RELEASE (2016-12-21) +---------------------------------------------- +* DATACMNS-963 - ReturnedType.getInputProperties() does not guarantee distinct properties. +* DATACMNS-962 - Doc spelling mistake 'walking thought'. +* DATACMNS-958 - Use ExposeInvocationInterceptor.ADVISOR over the advice instance. +* DATACMNS-956 - Ensure consistent usage of ClassTypeInformation.from(…). +* DATACMNS-953 - GenericPropertyMatchers.exact() should use exact matching. +* DATACMNS-943 - Redeclared save(Iterable) results in wrong method overload to be invoked eventually. +* DATACMNS-939 - Static interface methods should not be considered query methods. +* DATACMNS-934 - BasicPersistentEntity.addAssociations(…) must not add null values to the collection of associations. +* DATACMNS-932 - Release 1.12.6 (Hopper SR6). + + +Changes in version 1.13.0.RC1 (2016-12-21) +------------------------------------------ +* DATACMNS-963 - ReturnedType.getInputProperties() does not guarantee distinct properties. +* DATACMNS-962 - Doc spelling mistake 'walking thought'. +* DATACMNS-961 - Upgrade to a newer JDK version on TravisCI. +* DATACMNS-960 - RevisionRepository should extend Repository. +* DATACMNS-959 - Register repository interceptor to allow detecting a surrounding transaction. +* DATACMNS-958 - Use ExposeInvocationInterceptor.ADVISOR over the advice instance. +* DATACMNS-956 - Ensure consistent usage of ClassTypeInformation.from(…). +* DATACMNS-953 - GenericPropertyMatchers.exact() should use exact matching. +* DATACMNS-952 - Switch to less expensive lookup of Spring Data web support configuration. +* DATACMNS-951 - Add Converters for JSR-310 Duration and Period. +* DATACMNS-943 - Redeclared save(Iterable) results in wrong method overload to be invoked eventually. +* DATACMNS-941 - QuerydslBindings not working with inheritance. +* DATACMNS-940 - Support for Javaslang collection types as query method return values. +* DATACMNS-939 - Static interface methods should not be considered query methods. +* DATACMNS-937 - Support for Javaslang's Option as return type of query methods. +* DATACMNS-934 - BasicPersistentEntity.addAssociations(…) must not add null values to the collection of associations. +* DATACMNS-929 - PageableHandlerMethodArgumentResolver.isFallbackPageable() throws NullPointerException if default is configured to be null. +* DATACMNS-928 - Support for exposing domain events from aggregate roots as Spring application events. +* DATACMNS-925 - Improve memory consumption of Parameter and Parameters. +* DATACMNS-923 - Remove obsolete code in DefaultRepositoryInformation. +* DATACMNS-921 - ResultProcessor should create approximate collection instead of exact one. +* DATACMNS-920 - Expose minInclusive and maxInclusive of org.springframework.data.domain.Range. +* DATACMNS-918 - Provide interfaces for Pageable and Sort method argument resolvers. +* DATACMNS-917 - Query methods returning Maps shouldn't return null if no result is present. +* DATACMNS-916 - Generated PropertyAccessor fails lookup of setter accepting a primitive type. +* DATACMNS-912 - Unable to write custom implementation of CRUD method with generic parameters. +* DATACMNS-910 - Remove Spring 4.2 build profile for Travis. +* DATACMNS-909 - Exclude decoratedClass from JSON links, created from Projection and received with Spring @RestController. +* DATACMNS-908 - Allow creating an Order with a different property name. +* DATACMNS-903 - Fix typo in reference documentation. +* DATACMNS-902 - Fix attribute name in example showing how to define a custom repository base class. +* DATACMNS-900 - ExampleMatcher.PropertySpecifiers does not implement equals/hashCode. +* DATACMNS-899 - ParameterizedTypeInformation.getMapValueType for non-map types causes StackOverflowError. +* DATACMNS-896 - ClassTypeInformation computes incorrect TypeVariable mappings for recursive generics. +* DATACMNS-892 - Expose repository interface via attribute on bean definition for repository factory beans. +* DATACMNS-891 - Switch constructor injection of the repository interface for repository factory beans. +* DATACMNS-889 - Release 1.13 RC1 (Ingalls). +* DATACMNS-888 - Add dedicated RevisionSort to easily capture the desire to sort by revision number. +* DATACMNS-875 - Add support for exists projection in repository query derivation. + + +Changes in version 2.0.0.M1 (2016-11-23) +---------------------------------------- +* DATACMNS-939 - Static interface methods should not be considered query methods. +* DATACMNS-937 - Support JavaSlang and other custom wrappers as return types of query methods. +* DATACMNS-935 - Avoid hard dependency on Project Reactor in reactive support. +* DATACMNS-933 - Release 2.0 M1 (Kay). +* DATACMNS-919 - Remove DomainClassPropertyEditorRegistrar. +* DATACMNS-836 - Add components to support reactive repositories. +* DATACMNS-168 - Allow customizing the bean name for repository beans. + + Changes in version 1.12.5.RELEASE (2016-11-03) ---------------------------------------------- * DATACMNS-929 - PageableHandlerMethodArgumentResolver.isFallbackPageable() throws NullPointerException if default is configured to be null. diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt index 97e1666cd0..0337ecfe58 100644 --- a/src/main/resources/notice.txt +++ b/src/main/resources/notice.txt @@ -1,4 +1,4 @@ -Spring Data Commons 1.13 M1 +Spring Data Commons 1.13 RC1 Copyright (c) [2010-2015] Pivotal Software, Inc. This product is licensed to you under the Apache License, Version 2.0 (the "License"). diff --git a/src/test/java/org/springframework/data/convert/Jsr310ConvertersUnitTests.java b/src/test/java/org/springframework/data/convert/Jsr310ConvertersUnitTests.java index 3c6c4aadb6..41aa1f5e23 100644 --- a/src/test/java/org/springframework/data/convert/Jsr310ConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/convert/Jsr310ConvertersUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014 the original author or authors. + * Copyright 2014-2016 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,26 +19,43 @@ import static org.junit.Assert.*; import java.text.SimpleDateFormat; +import java.time.Duration; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.Period; import java.time.ZoneId; +import java.util.Arrays; +import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.convert.Jsr310ConvertersUnitTests.CommonTests; +import org.springframework.data.convert.Jsr310ConvertersUnitTests.DurationConversionTests; +import org.springframework.data.convert.Jsr310ConvertersUnitTests.PeriodConversionTests; /** * Unit tests for {@link Jsr310Converters}. * * @author Oliver Gierke + * @author Barak Schoster */ +@RunWith(Suite.class) +@SuiteClasses({ CommonTests.class, DurationConversionTests.class, PeriodConversionTests.class }) public class Jsr310ConvertersUnitTests { static final Date NOW = new Date(); @@ -55,95 +72,150 @@ public class Jsr310ConvertersUnitTests { CONVERSION_SERVICE = conversionService; } - /** - * @see DATACMNS-606 - */ - @Test - public void convertsDateToLocalDateTime() { - assertThat(CONVERSION_SERVICE.convert(NOW, LocalDateTime.class).toString(), - is(format(NOW, "yyyy-MM-dd'T'HH:mm:ss.SSS"))); - } + public static class CommonTests { - /** - * @see DATACMNS-606 - */ - @Test - public void convertsLocalDateTimeToDate() { + /** + * @see DATACMNS-606 + */ + @Test + public void convertsDateToLocalDateTime() { + assertThat(CONVERSION_SERVICE.convert(NOW, LocalDateTime.class).toString(), + is(format(NOW, "yyyy-MM-dd'T'HH:mm:ss.SSS"))); + } - LocalDateTime now = LocalDateTime.now(); - assertThat(format(CONVERSION_SERVICE.convert(now, Date.class), "yyyy-MM-dd'T'HH:mm:ss.SSS"), is(now.toString())); - } + /** + * @see DATACMNS-606 + */ + @Test + public void convertsLocalDateTimeToDate() { - /** - * @see DATACMNS-606 - */ - @Test - public void convertsDateToLocalDate() { - assertThat(CONVERSION_SERVICE.convert(NOW, LocalDate.class).toString(), is(format(NOW, "yyyy-MM-dd"))); - } + LocalDateTime now = LocalDateTime.now(); + assertThat(format(CONVERSION_SERVICE.convert(now, Date.class), "yyyy-MM-dd'T'HH:mm:ss.SSS"), is(now.toString())); + } - /** - * @see DATACMNS-606 - */ - @Test - public void convertsLocalDateToDate() { + /** + * @see DATACMNS-606 + */ + @Test + public void convertsDateToLocalDate() { + assertThat(CONVERSION_SERVICE.convert(NOW, LocalDate.class).toString(), is(format(NOW, "yyyy-MM-dd"))); + } - LocalDate now = LocalDate.now(); - assertThat(format(CONVERSION_SERVICE.convert(now, Date.class), "yyyy-MM-dd"), is(now.toString())); - } + /** + * @see DATACMNS-606 + */ + @Test + public void convertsLocalDateToDate() { - /** - * @see DATACMNS-606 - */ - @Test - public void convertsDateToLocalTime() { - assertThat(CONVERSION_SERVICE.convert(NOW, LocalTime.class).toString(), is(format(NOW, "HH:mm:ss.SSS"))); - } + LocalDate now = LocalDate.now(); + assertThat(format(CONVERSION_SERVICE.convert(now, Date.class), "yyyy-MM-dd"), is(now.toString())); + } - /** - * @see DATACMNS-606 - */ - @Test - public void convertsLocalTimeToDate() { + /** + * @see DATACMNS-606 + */ + @Test + public void convertsDateToLocalTime() { + assertThat(CONVERSION_SERVICE.convert(NOW, LocalTime.class).toString(), is(format(NOW, "HH:mm:ss.SSS"))); + } - LocalTime now = LocalTime.now(); - assertThat(format(CONVERSION_SERVICE.convert(now, Date.class), "HH:mm:ss.SSS"), is(now.toString())); - } + /** + * @see DATACMNS-606 + */ + @Test + public void convertsLocalTimeToDate() { - /** - * @see DATACMNS-623 - */ - @Test - public void convertsDateToInstant() { + LocalTime now = LocalTime.now(); + assertThat(format(CONVERSION_SERVICE.convert(now, Date.class), "HH:mm:ss.SSS"), is(now.toString())); + } - Date now = new Date(); - assertThat(CONVERSION_SERVICE.convert(now, Instant.class), is(now.toInstant())); - } + /** + * @see DATACMNS-623 + */ + @Test + public void convertsDateToInstant() { + + Date now = new Date(); + assertThat(CONVERSION_SERVICE.convert(now, Instant.class), is(now.toInstant())); + } - /** - * @see DATACMNS-623 - */ - @Test - public void convertsInstantToDate() { + /** + * @see DATACMNS-623 + */ + @Test + public void convertsInstantToDate() { - Date now = new Date(); - assertThat(CONVERSION_SERVICE.convert(now.toInstant(), Date.class), is(now)); + Date now = new Date(); + assertThat(CONVERSION_SERVICE.convert(now.toInstant(), Date.class), is(now)); + } + + @Test + public void convertsZoneIdToStringAndBack() { + + Map ids = new HashMap(); + ids.put("Europe/Berlin", ZoneId.of("Europe/Berlin")); + ids.put("+06:00", ZoneId.of("+06:00")); + + for (Entry entry : ids.entrySet()) { + assertThat(CONVERSION_SERVICE.convert(entry.getValue(), String.class), is(entry.getKey())); + assertThat(CONVERSION_SERVICE.convert(entry.getKey(), ZoneId.class), is(entry.getValue())); + } + } + + private static String format(Date date, String format) { + return new SimpleDateFormat(format).format(date); + } } - @Test - public void convertsZoneIdToStringAndBack() { + @RunWith(Parameterized.class) + public static class DurationConversionTests extends ConversionTest { + + /** + * @see DATACMNS-951 + */ + @Parameters + public static Collection data() { + + return Arrays.asList(new Object[][] { // + { "PT240H", Duration.ofDays(10) }, // + { "PT2H", Duration.ofHours(2) }, // + { "PT3M", Duration.ofMinutes(3) }, // + { "PT4S", Duration.ofSeconds(4) }, // + { "PT0.005S", Duration.ofMillis(5) }, // + { "PT0.000000006S", Duration.ofNanos(6) } // + }); + } + } - Map ids = new HashMap(); - ids.put("Europe/Berlin", ZoneId.of("Europe/Berlin")); - ids.put("+06:00", ZoneId.of("+06:00")); + public static class PeriodConversionTests extends ConversionTest { - for (Entry entry : ids.entrySet()) { - assertThat(CONVERSION_SERVICE.convert(entry.getValue(), String.class), is(entry.getKey())); - assertThat(CONVERSION_SERVICE.convert(entry.getKey(), ZoneId.class), is(entry.getValue())); + /** + * @see DATACMNS-951 + */ + @Parameters + public static Collection data() { + + return Arrays.asList(new Object[][] { // + { "P2D", Period.ofDays(2) }, // + { "P21D", Period.ofWeeks(3) }, // + { "P4M", Period.ofMonths(4) }, // + { "P5Y", Period.ofYears(5) }, // + }); } } - private static String format(Date date, String format) { - return new SimpleDateFormat(format).format(date); + @RunWith(Parameterized.class) + public static class ConversionTest { + + public @Parameter(0) String string; + public @Parameter(1) T target; + + @Test + public void convertsPeriodToStringAndBack() { + + ResolvableType type = ResolvableType.forClass(ConversionTest.class, this.getClass()); + assertThat(CONVERSION_SERVICE.convert(target, String.class), is(string)); + assertThat(CONVERSION_SERVICE.convert(string, type.getGeneric(0).getRawClass()), is((Object) target)); + } } } diff --git a/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java b/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java index 5330b73bee..48a52df928 100644 --- a/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java @@ -27,6 +27,7 @@ import java.util.List; import org.hamcrest.CoreMatchers; +import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -39,11 +40,13 @@ import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.TypeAlias; +import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentEntitySpec; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.Person; +import org.springframework.data.mapping.SimpleAssociationHandler; import org.springframework.data.mapping.context.SampleMappingContext; import org.springframework.data.mapping.context.SamplePersistentProperty; import org.springframework.data.util.ClassTypeInformation; @@ -275,6 +278,24 @@ public void invalidBeanAccessCreatesDescriptiveErrorMessage() { entity.getPropertyAccessor(new Object()); } + /** + * @see DATACMNS-934 + */ + @Test + public void doesNotThrowAnExceptionForNullAssociation() { + + BasicPersistentEntity entity = createEntity(Entity.class); + entity.addAssociation(null); + + entity.doWithAssociations(new SimpleAssociationHandler() { + + @Override + public void doWithAssociation(Association> association) { + Assert.fail("Expected the method to never be called!"); + } + }); + } + private BasicPersistentEntity createEntity(Class type) { return createEntity(type, null); } diff --git a/src/test/java/org/springframework/data/querydsl/QueryDslUtilsUnitTests.java b/src/test/java/org/springframework/data/querydsl/QueryDslUtilsUnitTests.java index da33c453ee..88e57fd93b 100644 --- a/src/test/java/org/springframework/data/querydsl/QueryDslUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/querydsl/QueryDslUtilsUnitTests.java @@ -17,6 +17,7 @@ import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.springframework.data.querydsl.QueryDslUtils.*; import org.junit.Test; @@ -32,6 +33,17 @@ public class QueryDslUtilsUnitTests { */ @Test public void rendersDotPathForPathTraversalContainingAnyExpression() { - assertThat(QueryDslUtils.toDotPath(QUser.user.addresses.any().street), is("addresses.street")); + assertThat(toDotPath(QUser.user.addresses.any().street), is("addresses.street")); + } + + /** + * @see DATACMNS-941 + */ + @Test + public void skipsIntermediateDelegates() { + + assertThat(toDotPath(QUser.user.as(QSpecialUser.class).as(QSpecialUser.class).specialProperty), + is("specialProperty")); + assertThat(toDotPath(QUser.user.as(QSpecialUser.class).specialProperty), is("specialProperty")); } } diff --git a/src/test/java/org/springframework/data/querydsl/User.java b/src/test/java/org/springframework/data/querydsl/User.java index 0e523f53d4..3a4d63bcfb 100644 --- a/src/test/java/org/springframework/data/querydsl/User.java +++ b/src/test/java/org/springframework/data/querydsl/User.java @@ -45,3 +45,18 @@ public User(String firstname, String lastname, Address address) { this.address = address; } } + +@QueryEntity +class SpecialUser extends User { + + public String specialProperty; + + public SpecialUser(String firstname, String lastname, Address address) { + super(firstname, lastname, address); + } +} + +@QueryEntity +class UserWrapper { + public User user; +} diff --git a/src/test/java/org/springframework/data/querydsl/binding/QuerydslBindingsFactoryUnitTests.java b/src/test/java/org/springframework/data/querydsl/binding/QuerydslBindingsFactoryUnitTests.java index 4e2d3228d9..105ff77e9b 100644 --- a/src/test/java/org/springframework/data/querydsl/binding/QuerydslBindingsFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/querydsl/binding/QuerydslBindingsFactoryUnitTests.java @@ -26,7 +26,6 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; -import org.springframework.data.mapping.PropertyPath; import org.springframework.data.querydsl.QUser; import org.springframework.data.querydsl.SimpleEntityPathResolver; import org.springframework.data.querydsl.User; @@ -76,7 +75,7 @@ public void createBindingsShouldHonorQuerydslBinderCustomizerHookWhenPresent() { QuerydslBindings bindings = factory.createBindingsFor(null, USER_TYPE); MultiValueBinding, Object> binding = bindings - .getBindingForPath(PropertyPath.from("firstname", User.class)); + .getBindingForPath(PropertyPathInformation.of("firstname", User.class)); assertThat(binding.bind((Path) QUser.user.firstname, Collections.singleton("rand")), is((Predicate) QUser.user.firstname.contains("rand"))); @@ -97,7 +96,7 @@ public void shouldReuseExistingQuerydslBinderCustomizer() { QuerydslBindings bindings = factory.createBindingsFor(SpecificBinding.class, USER_TYPE); MultiValueBinding, Object> binding = bindings - .getBindingForPath(PropertyPath.from("firstname", User.class)); + .getBindingForPath(PropertyPathInformation.of("firstname", User.class)); assertThat(binding.bind((Path) QUser.user.firstname, Collections.singleton("rand")), is((Predicate) QUser.user.firstname.eq("RAND"))); diff --git a/src/test/java/org/springframework/data/querydsl/binding/QuerydslBindingsUnitTests.java b/src/test/java/org/springframework/data/querydsl/binding/QuerydslBindingsUnitTests.java index 6a9bd23c0c..808f2224ba 100644 --- a/src/test/java/org/springframework/data/querydsl/binding/QuerydslBindingsUnitTests.java +++ b/src/test/java/org/springframework/data/querydsl/binding/QuerydslBindingsUnitTests.java @@ -21,7 +21,7 @@ import org.junit.Before; import org.junit.Test; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.querydsl.QSpecialUser; import org.springframework.data.querydsl.QUser; import org.springframework.data.querydsl.SimpleEntityPathResolver; import org.springframework.data.querydsl.User; @@ -71,7 +71,10 @@ public void rejectsNullPath() { */ @Test public void returnsNullIfNoBindingRegisteredForPath() { - assertThat(bindings.getBindingForPath(PropertyPath.from("lastname", User.class)), nullValue()); + + PathInformation path = PropertyPathInformation.of("lastname", User.class); + + assertThat(bindings.getBindingForPath(path), nullValue()); } /** @@ -82,8 +85,9 @@ public void returnsRegisteredBindingForSimplePath() { bindings.bind(QUser.user.firstname).first(CONTAINS_BINDING); - assertAdapterWithTargetBinding(bindings.getBindingForPath(PropertyPath.from("firstname", User.class)), - CONTAINS_BINDING); + PathInformation path = PropertyPathInformation.of("firstname", User.class); + + assertAdapterWithTargetBinding(bindings.getBindingForPath(path), CONTAINS_BINDING); } /** @@ -94,8 +98,9 @@ public void getBindingForPathShouldReturnSpeficicBindingForNestedElementsWhenAva bindings.bind(QUser.user.address.street).first(CONTAINS_BINDING); - assertAdapterWithTargetBinding(bindings.getBindingForPath(PropertyPath.from("address.street", User.class)), - CONTAINS_BINDING); + PathInformation path = PropertyPathInformation.of("address.street", User.class); + + assertAdapterWithTargetBinding(bindings.getBindingForPath(path), CONTAINS_BINDING); } /** @@ -106,8 +111,9 @@ public void getBindingForPathShouldReturnSpeficicBindingForTypes() { bindings.bind(String.class).first(CONTAINS_BINDING); - assertAdapterWithTargetBinding(bindings.getBindingForPath(PropertyPath.from("address.street", User.class)), - CONTAINS_BINDING); + PathInformation path = PropertyPathInformation.of("address.street", User.class); + + assertAdapterWithTargetBinding(bindings.getBindingForPath(path), CONTAINS_BINDING); } /** @@ -118,7 +124,9 @@ public void propertyNotExplicitlyIncludedAndWithoutTypeBindingIsInvisible() { bindings.bind(String.class).first(CONTAINS_BINDING); - assertThat(bindings.getBindingForPath(PropertyPath.from("inceptionYear", User.class)), nullValue()); + PathInformation path = PropertyPathInformation.of("inceptionYear", User.class); + + assertThat(bindings.getBindingForPath(path), nullValue()); } /** @@ -261,7 +269,7 @@ public void aliasesBinding() { bindings.bind(QUser.user.address.city).as("city").first(CONTAINS_BINDING); - PropertyPath path = bindings.getPropertyPath("city", ClassTypeInformation.from(User.class)); + PathInformation path = bindings.getPropertyPath("city", ClassTypeInformation.from(User.class)); assertThat(path, is(notNullValue())); assertThat(bindings.isPathAvailable("city", User.class), is(true)); @@ -279,14 +287,14 @@ public void explicitlyIncludesOriginalBindingDespiteAlias() { bindings.including(QUser.user.address.city); bindings.bind(QUser.user.address.city).as("city").first(CONTAINS_BINDING); - PropertyPath path = bindings.getPropertyPath("city", ClassTypeInformation.from(User.class)); + PathInformation path = bindings.getPropertyPath("city", ClassTypeInformation.from(User.class)); assertThat(path, is(notNullValue())); assertThat(bindings.isPathAvailable("city", User.class), is(true)); assertThat(bindings.isPathAvailable("address.city", User.class), is(true)); - PropertyPath propertyPath = bindings.getPropertyPath("address.city", ClassTypeInformation.from(User.class)); + PathInformation propertyPath = bindings.getPropertyPath("address.city", ClassTypeInformation.from(User.class)); assertThat(propertyPath, is(notNullValue())); assertAdapterWithTargetBinding(bindings.getBindingForPath(propertyPath), CONTAINS_BINDING); @@ -300,17 +308,42 @@ public void registedAliasWithNullBinding() { bindings.bind(QUser.user.address.city).as("city").withDefaultBinding(); - PropertyPath path = bindings.getPropertyPath("city", ClassTypeInformation.from(User.class)); + PathInformation path = bindings.getPropertyPath("city", ClassTypeInformation.from(User.class)); assertThat(path, is(notNullValue())); MultiValueBinding, Object> binding = bindings.getBindingForPath(path); assertThat(binding, is(nullValue())); } + /** + * @see DATACMNS-941 + */ + @Test + public void registersBindingForPropertyOfSubtype() { + + bindings.bind(QUser.user.as(QSpecialUser.class).specialProperty).first(ContainsBinding.INSTANCE); + + assertThat(bindings.isPathAvailable("specialProperty", User.class), is(true)); + } + private static

, S> void assertAdapterWithTargetBinding(MultiValueBinding binding, SingleValueBinding, ?> expected) { assertThat(binding, is(instanceOf(QuerydslBindings.MultiValueBindingAdapter.class))); assertThat(ReflectionTestUtils.getField(binding, "delegate"), is((Object) expected)); } + + enum ContainsBinding implements SingleValueBinding { + + INSTANCE; + + /* + * (non-Javadoc) + * @see org.springframework.data.querydsl.binding.SingleValueBinding#bind(com.querydsl.core.types.Path, java.lang.Object) + */ + @Override + public Predicate bind(StringPath path, String value) { + return path.contains(value); + } + } } diff --git a/src/test/java/org/springframework/data/querydsl/binding/QuerydslPredicateBuilderUnitTests.java b/src/test/java/org/springframework/data/querydsl/binding/QuerydslPredicateBuilderUnitTests.java index 5b75d8b388..46098be98b 100644 --- a/src/test/java/org/springframework/data/querydsl/binding/QuerydslPredicateBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/querydsl/binding/QuerydslPredicateBuilderUnitTests.java @@ -27,7 +27,9 @@ import org.joda.time.format.DateTimeFormatter; import org.junit.Before; import org.junit.Test; +import org.springframework.data.querydsl.QSpecialUser; import org.springframework.data.querydsl.QUser; +import org.springframework.data.querydsl.QUserWrapper; import org.springframework.data.querydsl.SimpleEntityPathResolver; import org.springframework.data.querydsl.User; import org.springframework.data.querydsl.Users; @@ -219,4 +221,40 @@ public void automaticallyInsertsAnyStepInCollectionReference() { assertThat(predicate, is((Predicate) QUser.user.addresses.any().street.eq("VALUE"))); } + + /** + * @see DATACMNS-941 + */ + @Test + public void buildsPredicateForBindingUsingDowncast() { + + values.add("specialProperty", "VALUE"); + + QuerydslBindings bindings = new QuerydslBindings(); + bindings.bind(QUser.user.as(QSpecialUser.class).specialProperty)// + .first(QuerydslBindingsUnitTests.ContainsBinding.INSTANCE); + + Predicate predicate = builder.getPredicate(USER_TYPE, values, bindings); + + assertThat(predicate, is((Predicate) QUser.user.as(QSpecialUser.class).specialProperty.contains("VALUE"))); + } + + /** + * @see DATACMNS-941 + */ + @Test + public void buildsPredicateForBindingUsingNestedDowncast() { + + values.add("user.specialProperty", "VALUE"); + + QUserWrapper $ = QUserWrapper.userWrapper; + + QuerydslBindings bindings = new QuerydslBindings(); + bindings.bind($.user.as(QSpecialUser.class).specialProperty)// + .first(QuerydslBindingsUnitTests.ContainsBinding.INSTANCE); + + Predicate predicate = builder.getPredicate(USER_TYPE, values, bindings); + + assertThat(predicate, is((Predicate) $.user.as(QSpecialUser.class).specialProperty.contains("VALUE"))); + } } diff --git a/src/test/java/org/springframework/data/repository/config/RepositoryBeanNameGeneratorUnitTests.java b/src/test/java/org/springframework/data/repository/config/RepositoryBeanNameGeneratorUnitTests.java index 10ae062e28..d9d0a00010 100644 --- a/src/test/java/org/springframework/data/repository/config/RepositoryBeanNameGeneratorUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/RepositoryBeanNameGeneratorUnitTests.java @@ -62,7 +62,7 @@ public void usesAnnotationValueIfAnnotationPresent() { private BeanDefinition getBeanDefinitionFor(Class repositoryInterface) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(RepositoryFactoryBeanSupport.class); - builder.addPropertyValue("repositoryInterface", repositoryInterface.getName()); + builder.addConstructorArgValue(repositoryInterface.getName()); return builder.getBeanDefinition(); } diff --git a/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupportUnitTests.java b/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupportUnitTests.java index 5bee74d04e..82603609e3 100644 --- a/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupportUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupportUnitTests.java @@ -23,12 +23,7 @@ import java.util.Collections; import org.junit.Test; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.annotation.Primary; -import org.springframework.core.env.StandardEnvironment; -import org.springframework.core.io.DefaultResourceLoader; -import org.springframework.core.type.AnnotationMetadata; -import org.springframework.core.type.StandardAnnotationMetadata; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactorySupport; @@ -65,26 +60,6 @@ public void considersRepositoryInterfaceExtendingStoreInterfaceStrictMatch() { assertThat(extension.isStrictRepositoryCandidate(ExtendingInterface.class), is(true)); } - /** - * @see DATACMNS-609 - */ - @Test - public void registersRepositoryInterfaceAwareBeanPostProcessorOnlyOnceForMultipleConfigurations() { - - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - AnnotationMetadata annotationMetadata = new StandardAnnotationMetadata(SampleConfiguration.class, true); - - DefaultResourceLoader resourceLoader = new DefaultResourceLoader(); - StandardEnvironment environment = new StandardEnvironment(); - AnnotationRepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource( - annotationMetadata, EnableRepositories.class, resourceLoader, environment); - - extension.registerBeansForRoot(beanFactory, configurationSource); - extension.registerBeansForRoot(beanFactory, configurationSource); - - assertThat(beanFactory.getBeanDefinitionCount(), is(1)); - } - static class SampleRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { @Override diff --git a/src/test/java/org/springframework/data/repository/core/support/DefaultRepositoryInformationUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/DefaultRepositoryInformationUnitTests.java index 875d654751..909cfafc19 100644 --- a/src/test/java/org/springframework/data/repository/core/support/DefaultRepositoryInformationUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/DefaultRepositoryInformationUnitTests.java @@ -18,6 +18,8 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import lombok.experimental.Delegate; + import java.io.Serializable; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -254,6 +256,22 @@ public void discoversCustomlyImplementedCrudMethodWithGenericParameters() throws assertThat(information.isCustomMethod(customBaseRepositoryMethod), is(true)); } + /** + * @see DATACMNS-943 + * @throws Exception + */ + @Test + public void usesCorrectSaveOverload() throws Exception { + + RepositoryMetadata metadata = new DefaultRepositoryMetadata(DummyRepository.class); + RepositoryInformation information = new DefaultRepositoryInformation(metadata, DummyRepositoryImpl.class, null); + + Method method = DummyRepository.class.getMethod("save", Iterable.class); + + assertThat(information.getTargetClassMethod(method), + is(DummyRepositoryImpl.class.getMethod("save", Iterable.class))); + } + private static Method getMethodFrom(Class type, String name) { for (Method method : type.getMethods()) { @@ -303,7 +321,7 @@ static class Boss implements Iterable { @Override public Iterator iterator() { - return Collections.emptySet().iterator(); + return Collections. emptySet().iterator(); } } @@ -366,4 +384,15 @@ public S save(S entity) { } static class Sample {} + + interface DummyRepository extends CrudRepository { + + @Override + List save(Iterable entites); + } + + static class DummyRepositoryImpl implements CrudRepository { + + private @Delegate CrudRepository delegate; + } } diff --git a/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactoryBean.java b/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactoryBean.java index 146cc4f025..10f7fff6c8 100644 --- a/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactoryBean.java +++ b/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2013 the original author or authors. + * Copyright 2012-2016 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. @@ -25,22 +25,17 @@ /** * @author Oliver Gierke */ -public class DummyRepositoryFactoryBean, S, ID extends Serializable> extends - RepositoryFactoryBeanSupport { +public class DummyRepositoryFactoryBean, S, ID extends Serializable> + extends RepositoryFactoryBeanSupport { private T repository; - public DummyRepositoryFactoryBean() { - setMappingContext(new SampleMappingContext()); - } + public DummyRepositoryFactoryBean(Class repositoryInterface) { + + super(repositoryInterface); - /* (non-Javadoc) - * @see org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#setRepositoryInterface(java.lang.Class) - */ - @Override - public void setRepositoryInterface(Class repositoryInterface) { this.repository = mock(repositoryInterface); - super.setRepositoryInterface(repositoryInterface); + setMappingContext(new SampleMappingContext()); } /* diff --git a/src/test/java/org/springframework/data/repository/core/support/EventPublishingRepositoryProxyPostProcessorUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/EventPublishingRepositoryProxyPostProcessorUnitTests.java new file mode 100644 index 0000000000..a4953a65bd --- /dev/null +++ b/src/test/java/org/springframework/data/repository/core/support/EventPublishingRepositoryProxyPostProcessorUnitTests.java @@ -0,0 +1,226 @@ +/* + * Copyright 2016 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.core.support; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.*; + +import lombok.Getter; +import lombok.Value; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; +import java.util.UUID; + +import org.aopalliance.aop.Advice; +import org.aopalliance.intercept.MethodInvocation; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.data.domain.DomainEvents; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.support.EventPublishingRepositoryProxyPostProcessor.EventPublishingMethod; +import org.springframework.data.repository.core.support.EventPublishingRepositoryProxyPostProcessor.EventPublishingMethodInterceptor; + +/** + * Unit tests for {@link EventPublishingRepositoryProxyPostProcessor} and contained classes. + * + * @author Oliver Gierke + * @soundtrack Henrik Freischlader Trio - Nobody Else To Blame (Openness) + */ +@RunWith(MockitoJUnitRunner.class) +public class EventPublishingRepositoryProxyPostProcessorUnitTests { + + @Mock ApplicationEventPublisher publisher; + @Mock MethodInvocation invocation; + + /** + * @see DATACMNS-928 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNullAggregateTypes() { + EventPublishingMethod.of(null); + } + + /** + * @see DATACMNS-928 + */ + @Test + public void publishingEventsForNullIsNoOp() { + EventPublishingMethod.of(OneEvent.class).publishEventsFrom(null, publisher); + } + + /** + * @see DATACMNS-928 + */ + @Test + public void exposesEventsExposedByEntityToPublisher() { + + SomeEvent first = new SomeEvent(); + SomeEvent second = new SomeEvent(); + MultipleEvents entity = MultipleEvents.of(Arrays.asList(first, second)); + + EventPublishingMethod.of(MultipleEvents.class).publishEventsFrom(entity, publisher); + + verify(publisher).publishEvent(eq(first)); + verify(publisher).publishEvent(eq(second)); + } + + /** + * @see DATACMNS-928 + */ + @Test + public void exposesSingleEventByEntityToPublisher() { + + SomeEvent event = new SomeEvent(); + OneEvent entity = OneEvent.of(event); + + EventPublishingMethod.of(OneEvent.class).publishEventsFrom(entity, publisher); + + verify(publisher, times(1)).publishEvent(event); + } + + /** + * @see DATACMNS-928 + */ + @Test + public void doesNotExposeNullEvent() { + + OneEvent entity = OneEvent.of(null); + + EventPublishingMethod.of(OneEvent.class).publishEventsFrom(entity, publisher); + + verify(publisher, times(0)).publishEvent(any()); + } + + /** + * @see DATACMNS-928 + */ + @Test + public void doesNotCreatePublishingMethodIfNoAnnotationDetected() { + assertThat(EventPublishingMethod.of(Object.class), is(nullValue())); + } + + /** + * @see DATACMNS-928 + */ + @Test + public void interceptsSaveMethod() throws Throwable { + + doReturn(SampleRepository.class.getMethod("save", Object.class)).when(invocation).getMethod(); + + SomeEvent event = new SomeEvent(); + MultipleEvents sample = MultipleEvents.of(Arrays.asList(event)); + doReturn(new Object[] { sample }).when(invocation).getArguments(); + + EventPublishingMethodInterceptor// + .of(EventPublishingMethod.of(MultipleEvents.class), publisher)// + .invoke(invocation); + + verify(publisher).publishEvent(event); + } + + /** + * @see DATACMNS-928 + */ + @Test + public void doesNotInterceptNonSaveMethod() throws Throwable { + + doReturn(SampleRepository.class.getMethod("findOne", Serializable.class)).when(invocation).getMethod(); + + EventPublishingMethodInterceptor// + .of(EventPublishingMethod.of(MultipleEvents.class), publisher)// + .invoke(invocation); + + verify(publisher, never()).publishEvent(any()); + } + + /** + * @see DATACMNS-928 + */ + @Test + public void registersAdviceIfDomainTypeExposesEvents() { + + RepositoryInformation information = new DummyRepositoryInformation(SampleRepository.class); + RepositoryProxyPostProcessor processor = new EventPublishingRepositoryProxyPostProcessor(publisher); + + ProxyFactory factory = mock(ProxyFactory.class); + + processor.postProcess(factory, information); + + verify(factory).addAdvice(any(EventPublishingMethodInterceptor.class)); + } + + /** + * @see DATACMNS-928 + */ + @Test + public void doesNotAddAdviceIfDomainTypeDoesNotExposeEvents() { + + RepositoryInformation information = new DummyRepositoryInformation(CrudRepository.class); + RepositoryProxyPostProcessor processor = new EventPublishingRepositoryProxyPostProcessor(publisher); + + ProxyFactory factory = mock(ProxyFactory.class); + + processor.postProcess(factory, information); + + verify(factory, never()).addAdvice(any(Advice.class)); + } + + /** + * @see DATACMNS-928 + */ + @Test + public void publishesEventsForCallToSaveWithIterable() throws Throwable { + + SomeEvent event = new SomeEvent(); + MultipleEvents sample = MultipleEvents.of(Arrays.asList(event)); + doReturn(new Object[] { Arrays.asList(sample) }).when(invocation).getArguments(); + + doReturn(SampleRepository.class.getMethod("save", Iterable.class)).when(invocation).getMethod(); + + EventPublishingMethodInterceptor// + .of(EventPublishingMethod.of(MultipleEvents.class), publisher)// + .invoke(invocation); + + verify(publisher).publishEvent(any(SomeEvent.class)); + } + + @Value(staticConstructor = "of") + static class MultipleEvents { + @Getter(onMethod = @__(@DomainEvents)) Collection events; + } + + @Value(staticConstructor = "of") + static class OneEvent { + @Getter(onMethod = @__(@DomainEvents)) Object event; + } + + @Value + static class SomeEvent { + UUID id = UUID.randomUUID(); + } + + interface SampleRepository extends CrudRepository {} +} diff --git a/src/test/java/org/springframework/data/repository/core/support/ExampleSpecificationAccessorUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/ExampleSpecificationAccessorUnitTests.java index 945fe1f747..44625a83c5 100644 --- a/src/test/java/org/springframework/data/repository/core/support/ExampleSpecificationAccessorUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/ExampleSpecificationAccessorUnitTests.java @@ -38,7 +38,6 @@ * @author Mark Paluch * @soundtrack Ron Spielman Trio - Fretboard Highway (Electric Tales) */ -@SuppressWarnings("unused") public class ExampleSpecificationAccessorUnitTests { Person person; @@ -322,6 +321,19 @@ public void hasPropertySpecifiersReturnsTrueWhenAtLeastOneIsSet() { assertThat(exampleSpecificationAccessor.hasPropertySpecifiers(), is(true)); } + /** + * @see DATACMNS-953 + */ + @Test + public void exactMatcherUsesExactMatching() { + + ExampleMatcher matcher = ExampleMatcher.matching()// + .withMatcher("firstname", exact()); + + assertThat(new ExampleMatcherAccessor(matcher).getPropertySpecifier("firstname").getStringMatcher(), + is(StringMatcher.EXACT)); + } + static class Person { String firstname; } diff --git a/src/test/java/org/springframework/data/repository/core/support/FactoryBeanTypePredictingPostProcessorIntegrationTests.java b/src/test/java/org/springframework/data/repository/core/support/FactoryBeanTypePredictingPostProcessorIntegrationTests.java deleted file mode 100644 index 49f852fb86..0000000000 --- a/src/test/java/org/springframework/data/repository/core/support/FactoryBeanTypePredictingPostProcessorIntegrationTests.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2013-2016 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.core.support; - -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; - -import java.util.Arrays; - -import org.junit.Before; -import org.junit.Test; -import org.springframework.beans.factory.BeanFactoryUtils; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.data.querydsl.User; -import org.springframework.data.repository.Repository; - -/** - * Integration test to make sure Spring Data repository factory beans are found without the factories already - * instantiated. - * - * @see SPR-10517 - * @author Oliver Gierke - */ -public class FactoryBeanTypePredictingPostProcessorIntegrationTests { - - DefaultListableBeanFactory factory; - - @Before - public void setUp() { - - factory = new DefaultListableBeanFactory(); - - // Register factory bean for repository - BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(DummyRepositoryFactoryBean.class); - builder.addPropertyValue("repositoryInterface", UserRepository.class); - factory.registerBeanDefinition("repository", builder.getBeanDefinition()); - - // Register predicting BeanPostProcessor - FactoryBeanTypePredictingBeanPostProcessor processor = new FactoryBeanTypePredictingBeanPostProcessor( - RepositoryFactoryBeanSupport.class, "repositoryInterface"); - processor.setBeanFactory(factory); - factory.addBeanPostProcessor(processor); - } - - @Test - public void lookupBeforeInstantiation() { - - String[] strings = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(factory, RepositoryFactoryInformation.class, - false, false); - assertThat(Arrays.asList(strings), hasItem("&repository")); - } - - @Test - public void lookupAfterInstantiation() { - - factory.getBean(UserRepository.class); - - String[] strings = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(factory, RepositoryFactoryInformation.class, - false, false); - assertThat(Arrays.asList(strings), hasItem("&repository")); - } - - interface UserRepository extends Repository {} -} diff --git a/src/test/java/org/springframework/data/repository/core/support/FactoryBeanTypePredictingPostProcessorUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/FactoryBeanTypePredictingPostProcessorUnitTests.java deleted file mode 100644 index 2ee1da5655..0000000000 --- a/src/test/java/org/springframework/data/repository/core/support/FactoryBeanTypePredictingPostProcessorUnitTests.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2008-2016 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.core.support; - -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; - -import java.io.Serializable; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.data.repository.Repository; -import org.springframework.jndi.JndiObjectFactoryBean; - -/** - * Unit tests for {@link RepositoryInterfaceAwareBeanPostProcessor}. - * - * @author Oliver Gierke - */ -@RunWith(MockitoJUnitRunner.class) -public class FactoryBeanTypePredictingPostProcessorUnitTests { - - private static final Class FACTORY_CLASS = RepositoryFactoryBeanSupport.class; - private static final String BEAN_NAME = "foo"; - private static final String DAO_INTERFACE_PROPERTY = "repositoryInterface"; - - FactoryBeanTypePredictingBeanPostProcessor processor; - BeanDefinition beanDefinition; - - @Mock ConfigurableListableBeanFactory beanFactory; - - @Before - public void setUp() { - - BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(FACTORY_CLASS) - .addPropertyValue(DAO_INTERFACE_PROPERTY, UserDao.class); - this.beanDefinition = builder.getBeanDefinition(); - this.processor = new FactoryBeanTypePredictingBeanPostProcessor(FACTORY_CLASS, "repositoryInterface"); - - when(beanFactory.getBeanDefinition(BEAN_NAME)).thenReturn(beanDefinition); - } - - @Test - public void returnsDaoInterfaceClassForFactoryBean() throws Exception { - - processor.setBeanFactory(beanFactory); - assertEquals(UserDao.class, processor.predictBeanType(FACTORY_CLASS, BEAN_NAME)); - } - - @Test - public void doesNotResolveInterfaceForNonFactoryClasses() throws Exception { - - processor.setBeanFactory(beanFactory); - assertNotTypeDetected(BeanFactory.class); - } - - @Test - public void doesNotResolveInterfaceForUnloadableClass() throws Exception { - - BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(FACTORY_CLASS) - .addPropertyValue(DAO_INTERFACE_PROPERTY, "com.acme.Foo"); - - when(beanFactory.getBeanDefinition(BEAN_NAME)).thenReturn(builder.getBeanDefinition()); - - assertNotTypeDetected(FACTORY_CLASS); - } - - @Test - public void doesNotResolveTypeForPlainBeanFactory() throws Exception { - - BeanFactory beanFactory = mock(BeanFactory.class); - processor.setBeanFactory(beanFactory); - - assertNotTypeDetected(FACTORY_CLASS); - } - - @Test(expected = IllegalArgumentException.class) - public void rejectsNonFactoryBeanType() { - new FactoryBeanTypePredictingBeanPostProcessor(Object.class, "property"); - } - - /** - * @see DATACMNS-821 - */ - @Test - public void usesFirstValueIfPropertyIsOfArrayType() { - - BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(JndiObjectFactoryBean.class); - builder.addPropertyValue("proxyInterfaces", - new String[] { Serializable.class.getName(), Iterable.class.getName() }); - - when(beanFactory.getBeanDefinition(BEAN_NAME)).thenReturn(builder.getBeanDefinition()); - - processor = new FactoryBeanTypePredictingBeanPostProcessor(JndiObjectFactoryBean.class, "proxyInterface", - "proxyInterfaces"); - processor.setBeanFactory(beanFactory); - - assertThat(processor.predictBeanType(JndiObjectFactoryBean.class, BEAN_NAME), - is(typeCompatibleWith(Serializable.class))); - } - - private void assertNotTypeDetected(Class beanClass) { - assertThat(processor.predictBeanType(beanClass, BEAN_NAME), is(nullValue())); - } - - private class User {} - - private interface UserDao extends Repository {} -} diff --git a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupportUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupportUnitTests.java index c06a62c5fd..ffbb9af73f 100644 --- a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupportUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactoryBeanSupportUnitTests.java @@ -44,10 +44,9 @@ public void setsConfiguredClassLoaderOnRepositoryFactory() { ClassLoader classLoader = mock(ClassLoader.class); - RepositoryFactoryBeanSupport factoryBean = new DummyRepositoryFactoryBean(); + RepositoryFactoryBeanSupport factoryBean = new DummyRepositoryFactoryBean(SampleRepository.class); factoryBean.setBeanClassLoader(classLoader); factoryBean.setLazyInit(true); - factoryBean.setRepositoryInterface(SampleRepository.class); factoryBean.afterPropertiesSet(); Object factory = ReflectionTestUtils.getField(factoryBean, "factory"); @@ -58,14 +57,13 @@ public void setsConfiguredClassLoaderOnRepositoryFactory() { * @see DATACMNS-432 */ @Test - @SuppressWarnings("rawtypes") + @SuppressWarnings({ "rawtypes", "unchecked" }) public void initializationFailsWithMissingRepositoryInterface() { exception.expect(IllegalArgumentException.class); exception.expectMessage("Repository interface"); - RepositoryFactoryBeanSupport factoryBean = new DummyRepositoryFactoryBean(); - factoryBean.afterPropertiesSet(); + new DummyRepositoryFactoryBean(null); } interface SampleRepository extends Repository {} diff --git a/src/test/java/org/springframework/data/repository/core/support/SurroundingTransactionDetectorMethodInterceptorUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/SurroundingTransactionDetectorMethodInterceptorUnitTests.java new file mode 100644 index 0000000000..0f9c83ecfe --- /dev/null +++ b/src/test/java/org/springframework/data/repository/core/support/SurroundingTransactionDetectorMethodInterceptorUnitTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2016 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.core.support; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.*; + +import org.junit.Test; +import org.springframework.aop.framework.ReflectiveMethodInvocation; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Unit tests for {@link SurroundingTransactionDetectorMethodInterceptor}. + * + * @author Oliver Gierke + * @soundtrack Hendrik Freischlader Trio - Openness (Openness) + */ +public class SurroundingTransactionDetectorMethodInterceptorUnitTests { + + /** + * @see DATACMNS-959 + */ + @Test + public void registersActiveSurroundingTransaction() throws Throwable { + + TransactionSynchronizationManager.setActualTransactionActive(true); + + INSTANCE.invoke(new StubMethodInvocation(true)); + } + + /** + * @see DATACMNS-959 + */ + @Test + public void registersNoSurroundingTransaction() throws Throwable { + + TransactionSynchronizationManager.setActualTransactionActive(false); + + INSTANCE.invoke(new StubMethodInvocation(false)); + } + + static class StubMethodInvocation extends ReflectiveMethodInvocation { + + boolean transactionActive; + + StubMethodInvocation(boolean expectTransactionActive) throws Exception { + super(null, null, Object.class.getMethod("toString"), null, null, null); + this.transactionActive = expectTransactionActive; + } + + /* + * (non-Javadoc) + * @see org.aopalliance.intercept.Joinpoint#proceed() + */ + public Object proceed() throws Throwable { + + assertThat(INSTANCE.isSurroundingTransactionActive(), is(transactionActive)); + + return null; + } + } +} diff --git a/src/test/java/org/springframework/data/repository/core/support/TransactionRepositoryFactoryBeanSupportUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/TransactionRepositoryFactoryBeanSupportUnitTests.java index 13d6599680..9f7e597872 100644 --- a/src/test/java/org/springframework/data/repository/core/support/TransactionRepositoryFactoryBeanSupportUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/TransactionRepositoryFactoryBeanSupportUnitTests.java @@ -88,7 +88,7 @@ static class SampleTransactionalRepositoryFactoryBean private final CrudRepository repository = mock(CrudRepository.class); public SampleTransactionalRepositoryFactoryBean() { - setRepositoryInterface((Class) CrudRepository.class); + super((Class) CrudRepository.class); } @Override diff --git a/src/test/java/org/springframework/data/repository/query/QueryMethodUnitTests.java b/src/test/java/org/springframework/data/repository/query/QueryMethodUnitTests.java index 5a8a0f1fc8..402eca5908 100644 --- a/src/test/java/org/springframework/data/repository/query/QueryMethodUnitTests.java +++ b/src/test/java/org/springframework/data/repository/query/QueryMethodUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2015 the original author or authors. + * Copyright 2008-2016 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,6 +19,9 @@ import static org.junit.Assert.*; import static org.junit.Assume.*; +import javaslang.collection.Seq; +import javaslang.control.Option; + import java.io.Serializable; import java.lang.reflect.Method; import java.util.List; @@ -226,6 +229,42 @@ public void doesNotRejectFutureQueryForEntityCollection() throws Exception { assertThat(new QueryMethod(method, repositoryMetadata, factory).isCollectionQuery(), is(true)); } + /** + * @see DATACMNS-940 + */ + @Test + public void detectsCustomCollectionReturnType() throws Exception { + + RepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(SampleRepository.class); + Method method = SampleRepository.class.getMethod("returnsSeq"); + + assertThat(new QueryMethod(method, repositoryMetadata, factory).isCollectionQuery(), is(true)); + } + + /** + * @see DATACMNS-940 + */ + @Test + public void detectsWrapperWithinWrapper() throws Exception { + + RepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(SampleRepository.class); + Method method = SampleRepository.class.getMethod("returnsFutureOfSeq"); + + assertThat(new QueryMethod(method, repositoryMetadata, factory).isCollectionQuery(), is(true)); + } + + /** + * @see DATACMNS-940 + */ + @Test + public void detectsSinglValueWrapperWithinWrapper() throws Exception { + + RepositoryMetadata repositoryMetadata = new DefaultRepositoryMetadata(SampleRepository.class); + Method method = SampleRepository.class.getMethod("returnsFutureOfOption"); + + assertThat(new QueryMethod(method, repositoryMetadata, factory).isCollectionQuery(), is(false)); + } + interface SampleRepository extends Repository { String pagingMethodWithInvalidReturnType(Pageable pageable); @@ -269,6 +308,12 @@ interface SampleRepository extends Repository { * @see DATACMNS-716 */ Future> returnsFutureForEntityCollection(); + + Seq returnsSeq(); + + Future> returnsFutureOfSeq(); + + Future> returnsFutureOfOption(); } class User { diff --git a/src/test/java/org/springframework/data/repository/query/ReturnedTypeUnitTests.java b/src/test/java/org/springframework/data/repository/query/ReturnedTypeUnitTests.java index f3d3cbaa28..384f24e835 100644 --- a/src/test/java/org/springframework/data/repository/query/ReturnedTypeUnitTests.java +++ b/src/test/java/org/springframework/data/repository/query/ReturnedTypeUnitTests.java @@ -179,6 +179,20 @@ public void considersInterfaceImplementedByDomainTypeNotProjecting() throws Exce assertThat(type.isProjecting(), is(false)); } + /** + * @see DATACMNS-963 + */ + @Test + public void detectsDistinctInputProperties() { + + ReturnedType type = ReturnedType.of(Child.class, Object.class, new SpelAwareProxyProjectionFactory()); + + List properties = type.getInputProperties(); + + assertThat(properties, hasSize(1)); + assertThat(properties, contains("firstname")); + } + private static ReturnedType getReturnedType(String methodName, Class... parameters) throws Exception { return getQueryMethod(methodName, parameters).getResultProcessor().getReturnedType(); } @@ -262,4 +276,12 @@ interface OpenProjection { @Value("#{target.firstname + ' ' + target.lastname}") String getFullName(); } + + static interface Parent { + String getFirstname(); + } + + static interface Child extends Parent { + String getFirstname(); + } } diff --git a/src/test/java/org/springframework/data/repository/sample/SampleConfiguration.java b/src/test/java/org/springframework/data/repository/sample/SampleConfiguration.java index 3293697c39..19617f927e 100644 --- a/src/test/java/org/springframework/data/repository/sample/SampleConfiguration.java +++ b/src/test/java/org/springframework/data/repository/sample/SampleConfiguration.java @@ -24,18 +24,15 @@ public Repositories repositories() { @Bean public RepositoryFactoryBeanSupport, User, Long> userRepositoryFactory() { - DummyRepositoryFactoryBean, User, Long> factory = new DummyRepositoryFactoryBean, User, Long>(); - factory.setRepositoryInterface(UserRepository.class); - - return factory; + return new DummyRepositoryFactoryBean, User, Long>(UserRepository.class); } @Bean public RepositoryFactoryBeanSupport, Product, Long> productRepositoryFactory( ProductRepository productRepository) { - DummyRepositoryFactoryBean, Product, Long> factory = new DummyRepositoryFactoryBean, Product, Long>(); - factory.setRepositoryInterface(ProductRepository.class); + DummyRepositoryFactoryBean, Product, Long> factory = new DummyRepositoryFactoryBean, Product, Long>( + ProductRepository.class); factory.setCustomImplementation(productRepository); return factory; diff --git a/src/test/java/org/springframework/data/repository/support/DomainClassConverterUnitTests.java b/src/test/java/org/springframework/data/repository/support/DomainClassConverterUnitTests.java index f62a189021..b96525b30f 100644 --- a/src/test/java/org/springframework/data/repository/support/DomainClassConverterUnitTests.java +++ b/src/test/java/org/springframework/data/repository/support/DomainClassConverterUnitTests.java @@ -214,7 +214,7 @@ public void toIdConverterDoesNotMatchIfTargetTypeIsAssignableFromSource() throws private ApplicationContext initContextWithRepo() { BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(DummyRepositoryFactoryBean.class); - builder.addPropertyValue("repositoryInterface", UserRepository.class); + builder.addConstructorArgValue(UserRepository.class); DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); factory.registerBeanDefinition("provider", builder.getBeanDefinition()); diff --git a/src/test/java/org/springframework/data/repository/support/DomainClassPropertyEditorRegistrarUnitTests.java b/src/test/java/org/springframework/data/repository/support/DomainClassPropertyEditorRegistrarUnitTests.java index 3522a9035a..71633d70a8 100644 --- a/src/test/java/org/springframework/data/repository/support/DomainClassPropertyEditorRegistrarUnitTests.java +++ b/src/test/java/org/springframework/data/repository/support/DomainClassPropertyEditorRegistrarUnitTests.java @@ -51,7 +51,7 @@ public class DomainClassPropertyEditorRegistrarUnitTests { public void setup() { BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(DummyRepositoryFactoryBean.class); - builder.addPropertyValue("repositoryInterface", EntityRepository.class); + builder.addConstructorArgValue(EntityRepository.class); DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); factory.registerBeanDefinition("provider", builder.getBeanDefinition()); diff --git a/src/test/java/org/springframework/data/repository/support/RepositoriesUnitTests.java b/src/test/java/org/springframework/data/repository/support/RepositoriesUnitTests.java index 7b32bc57d0..3aac0bbd46 100644 --- a/src/test/java/org/springframework/data/repository/support/RepositoriesUnitTests.java +++ b/src/test/java/org/springframework/data/repository/support/RepositoriesUnitTests.java @@ -73,7 +73,7 @@ public void setUp() { private AbstractBeanDefinition getRepositoryBeanDefinition(Class repositoryInterface) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(DummyRepositoryFactoryBean.class); - builder.addPropertyValue("repositoryInterface", repositoryInterface); + builder.addConstructorArgValue(repositoryInterface); return builder.getBeanDefinition(); } diff --git a/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java b/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java index 228762ccd4..c5ad8d8f02 100644 --- a/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/repository/util/QueryExecutionConvertersUnitTests.java @@ -17,18 +17,26 @@ import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; -import static org.junit.Assume.*; +import static org.springframework.data.repository.util.QueryExecutionConverters.*; +import javaslang.collection.HashMap; +import javaslang.collection.HashSet; +import javaslang.collection.Traversable; import scala.Option; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import org.junit.Before; import org.junit.Test; -import org.springframework.core.SpringVersion; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.data.util.Version; +import org.springframework.util.ReflectionUtils; import org.springframework.util.concurrent.ListenableFuture; import com.google.common.base.Optional; @@ -41,9 +49,6 @@ */ public class QueryExecutionConvertersUnitTests { - private static final Version SPRING_VERSION = Version.parse(SpringVersion.getVersion()); - private static final Version FOUR_DOT_TWO = new Version(4, 2); - DefaultConversionService conversionService; @Before @@ -64,6 +69,7 @@ public void registersWrapperTypes() { assertThat(QueryExecutionConverters.supports(Future.class), is(true)); assertThat(QueryExecutionConverters.supports(ListenableFuture.class), is(true)); assertThat(QueryExecutionConverters.supports(Option.class), is(true)); + assertThat(QueryExecutionConverters.supports(javaslang.control.Option.class), is(true)); } /** @@ -71,9 +77,6 @@ public void registersWrapperTypes() { */ @Test public void registersCompletableFutureAsWrapperTypeOnSpring42OrBetter() { - - assumeThat(SPRING_VERSION.isGreaterThanOrEqualTo(FOUR_DOT_TWO), is(true)); - assertThat(QueryExecutionConverters.supports(CompletableFuture.class), is(true)); } @@ -172,4 +175,143 @@ public void unwrapsScalaOption() { public void unwrapsEmptyScalaOption() { assertThat(QueryExecutionConverters.unwrap(Option.empty()), is((Object) null)); } + + /** + * @see DATACMNS-937 + */ + @Test + public void turnsNullIntoJavaslangOption() { + assertThat(conversionService.convert(new NullableWrapper(null), javaslang.control.Option.class), + is((Object) optionNone())); + } + + /** + * @see DATACMNS-937 + */ + @Test + public void wrapsValueIntoJavaslangOption() { + + javaslang.control.Option result = conversionService.convert(new NullableWrapper("string"), + javaslang.control.Option.class); + + assertThat(result.isEmpty(), is(false)); + assertThat(result.get(), is((Object) "string")); + } + + /** + * @see DATACMNS-937 + */ + @Test + public void unwrapsEmptyJavaslangOption() { + assertThat(QueryExecutionConverters.unwrap(optionNone()), is(nullValue())); + } + + /** + * @see DATACMNS-937 + */ + @Test + public void unwrapsJavaslangOption() { + assertThat(QueryExecutionConverters.unwrap(option("string")), is((Object) "string")); + } + + /** + * @see DATACMNS-940 + */ + @Test + public void conversListToJavaslang() { + + assertThat(conversionService.canConvert(List.class, javaslang.collection.Traversable.class), is(true)); + assertThat(conversionService.canConvert(List.class, javaslang.collection.List.class), is(true)); + assertThat(conversionService.canConvert(List.class, javaslang.collection.Set.class), is(true)); + assertThat(conversionService.canConvert(List.class, javaslang.collection.Map.class), is(false)); + + List integers = Arrays.asList(1, 2, 3); + + Traversable result = conversionService.convert(integers, Traversable.class); + + assertThat(result, is(instanceOf(javaslang.collection.List.class))); + } + + /** + * @see DATACMNS-940 + */ + @Test + public void convertsSetToJavaslang() { + + assertThat(conversionService.canConvert(Set.class, javaslang.collection.Traversable.class), is(true)); + assertThat(conversionService.canConvert(Set.class, javaslang.collection.Set.class), is(true)); + assertThat(conversionService.canConvert(Set.class, javaslang.collection.List.class), is(true)); + assertThat(conversionService.canConvert(Set.class, javaslang.collection.Map.class), is(false)); + + Set integers = Collections.singleton(1); + + Traversable result = conversionService.convert(integers, Traversable.class); + + assertThat(result, is(instanceOf(javaslang.collection.Set.class))); + } + + /** + * @see DATACMNS-940 + */ + @Test + public void convertsMapToJavaslang() { + + assertThat(conversionService.canConvert(Map.class, javaslang.collection.Traversable.class), is(true)); + assertThat(conversionService.canConvert(Map.class, javaslang.collection.Map.class), is(true)); + assertThat(conversionService.canConvert(Map.class, javaslang.collection.Set.class), is(false)); + assertThat(conversionService.canConvert(Map.class, javaslang.collection.List.class), is(false)); + + Map map = Collections.singletonMap("key", "value"); + + Traversable result = conversionService.convert(map, Traversable.class); + + assertThat(result, is(instanceOf(javaslang.collection.Map.class))); + } + + /** + * @see DATACMNS-940 + */ + @Test + public void unwrapsJavaslangCollectionsToJavaOnes() { + + assertThat(unwrap(javaslangList(1, 2, 3)), is(instanceOf(List.class))); + assertThat(unwrap(javaslangSet(1, 2, 3)), is(instanceOf(Set.class))); + assertThat(unwrap(javaslangMap("key", "value")), is(instanceOf(Map.class))); + } + + @SuppressWarnings("unchecked") + private static javaslang.control.Option optionNone() { + + Method method = ReflectionUtils.findMethod(javaslang.control.Option.class, "none"); + return (javaslang.control.Option) ReflectionUtils.invokeMethod(method, null); + } + + @SuppressWarnings("unchecked") + private static javaslang.control.Option option(T source) { + + Method method = ReflectionUtils.findMethod(javaslang.control.Option.class, "of", Object.class); + return (javaslang.control.Option) ReflectionUtils.invokeMethod(method, null, source); + } + + @SuppressWarnings("unchecked") + private static javaslang.collection.List javaslangList(T... values) { + + Method method = ReflectionUtils.findMethod(javaslang.collection.List.class, "ofAll", Iterable.class); + return (javaslang.collection.List) ReflectionUtils.invokeMethod(method, null, Arrays.asList(values)); + } + + @SuppressWarnings("unchecked") + private static javaslang.collection.Set javaslangSet(T... values) { + + Method method = ReflectionUtils.findMethod(HashSet.class, "ofAll", Iterable.class); + return (javaslang.collection.Set) ReflectionUtils.invokeMethod(method, null, Arrays.asList(values)); + } + + @SuppressWarnings("unchecked") + private static javaslang.collection.Map javaslangMap(K key, V value) { + + Method method = ReflectionUtils.findMethod(HashMap.class, "ofAll", Map.class); + return (javaslang.collection.Map) ReflectionUtils.invokeMethod(method, null, + Collections.singletonMap(key, value)); + } } diff --git a/src/test/java/org/springframework/data/util/ClassTypeInformationUnitTests.java b/src/test/java/org/springframework/data/util/ClassTypeInformationUnitTests.java index c4a4af0fa2..2d1405efa2 100644 --- a/src/test/java/org/springframework/data/util/ClassTypeInformationUnitTests.java +++ b/src/test/java/org/springframework/data/util/ClassTypeInformationUnitTests.java @@ -19,6 +19,8 @@ import static org.junit.Assert.*; import static org.springframework.data.util.ClassTypeInformation.*; +import javaslang.collection.Traversable; + import java.lang.reflect.Method; import java.util.Calendar; import java.util.Collection; @@ -106,7 +108,7 @@ public void discoversArraysAndCollections() { property = information.getProperty("rawSet"); assertEquals(Set.class, property.getType()); - assertThat(property.getComponentType().getType(), is(Matchers.>equalTo(Object.class))); + assertThat(property.getComponentType().getType(), is(Matchers.> equalTo(Object.class))); assertNull(property.getMapValueType()); } @@ -413,6 +415,29 @@ public void prefersLocalTypeMappingOverNestedWithSameGenericType() { assertThat(information.getProperty("field").getType(), is(typeCompatibleWith(Nested.class))); } + /** + * @see DATACMNS-940 + */ + @Test + public void detectsJavaslangTraversableComponentType() { + + ClassTypeInformation information = ClassTypeInformation.from(SampleTraversable.class); + + assertThat(information.getComponentType().getType(), is(typeCompatibleWith(Integer.class))); + } + + /** + * @see DATACMNS-940 + */ + @Test + public void detectsJavaslangMapComponentAndValueType() { + + ClassTypeInformation information = ClassTypeInformation.from(SampleMap.class); + + assertThat(information.getComponentType().getType(), is(typeCompatibleWith(String.class))); + assertThat(information.getMapValueType().getType(), is(typeCompatibleWith(Integer.class))); + } + static class StringMapContainer extends MapContainer { } @@ -611,4 +636,8 @@ static class SomeType { static class Nested extends SomeType {} static class Concrete extends SomeType {} + + static interface SampleTraversable extends Traversable {} + + static interface SampleMap extends javaslang.collection.Map {} } diff --git a/src/test/java/org/springframework/data/web/PageableHandlerMethodArgumentResolverUnitTests.java b/src/test/java/org/springframework/data/web/PageableHandlerMethodArgumentResolverUnitTests.java index 94ebea5348..63bf70a4e2 100644 --- a/src/test/java/org/springframework/data/web/PageableHandlerMethodArgumentResolverUnitTests.java +++ b/src/test/java/org/springframework/data/web/PageableHandlerMethodArgumentResolverUnitTests.java @@ -25,6 +25,7 @@ import org.springframework.core.MethodParameter; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.web.SortDefault.SortDefaults; import org.springframework.mock.web.MockHttpServletRequest; @@ -36,6 +37,7 @@ * * @author Oliver Gierke * @author Nick Williams + * @author Kazuki Shimizu */ public class PageableHandlerMethodArgumentResolverUnitTests extends PageableDefaultUnitTests { @@ -283,6 +285,86 @@ public void detectsFallbackPageableIfNullOneIsConfigured() { assertThat(resolver.isFallbackPageable(new PageRequest(0, 10)), is(false)); } + /** + * @see DATACMNS-966 + */ + @Test + public void allAllowSortProperty() throws Exception { + + PageableHandlerMethodArgumentResolver resolver = getResolver(); + resolver.setOneIndexedParameters(true); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", "id,title"); + + MethodParameter parameter = new MethodParameter( + Sample.class.getMethod("annotatedAllowedSortProperties", Pageable.class), 0); + + Pageable result = resolver.resolveArgument(parameter, null, new ServletWebRequest(request), null); + + assertThat(result.getSort(), is(new Sort("id", "title"))); + } + + /** + * @see DATACMNS-966 + */ + @Test + public void containsInvalidSortProperty() throws Exception { + + PageableHandlerMethodArgumentResolver resolver = getResolver(); + resolver.setOneIndexedParameters(true); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", "id,aaaa"); + + MethodParameter parameter = new MethodParameter( + Sample.class.getMethod("annotatedAllowedSortProperties", Pageable.class), 0); + + Pageable result = resolver.resolveArgument(parameter, null, new ServletWebRequest(request), null); + + assertThat(result.getSort(), is(new Sort("id"))); + } + + /** + * @see DATACMNS-966 + */ + @Test + public void allInvalidSortProperty() throws Exception { + + PageableHandlerMethodArgumentResolver resolver = getResolver(); + resolver.setOneIndexedParameters(true); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", "bbbb,aaaa"); + + MethodParameter parameter = new MethodParameter( + Sample.class.getMethod("annotatedAllowedSortProperties", Pageable.class), 0); + + Pageable result = resolver.resolveArgument(parameter, null, new ServletWebRequest(request), null); + + assertThat(result.getSort(), nullValue()); + } + + /** + * @see DATACMNS-966 + */ + @Test + public void ignoreSortProperty() throws Exception { + + PageableHandlerMethodArgumentResolver resolver = getResolver(); + resolver.setOneIndexedParameters(true); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", "id,title"); + + MethodParameter parameter = new MethodParameter( + Sample.class.getMethod("annotatedAllowedSortPropertiesIsEmpty", Pageable.class), 0); + + Pageable result = resolver.resolveArgument(parameter, null, new ServletWebRequest(request), null); + + assertThat(result.getSort(), nullValue()); + } + @Override protected PageableHandlerMethodArgumentResolver getResolver() { PageableHandlerMethodArgumentResolver resolver = new PageableHandlerMethodArgumentResolver(); @@ -322,5 +404,10 @@ void simpleDefaultWithContaineredExternalSort(@PageableDefault(size = PAGE_SIZE, void validQualifier(@Qualifier("foo") Pageable pageable); void noQualifiers(Pageable first, Pageable second); + + void annotatedAllowedSortProperties(@AllowedSortProperties({ "id", "title" }) Pageable pageable); + + void annotatedAllowedSortPropertiesIsEmpty(@AllowedSortProperties({}) Pageable pageable); + } } diff --git a/src/test/java/org/springframework/data/web/SortDefaultUnitTests.java b/src/test/java/org/springframework/data/web/SortDefaultUnitTests.java index aa373234be..7e44242761 100644 --- a/src/test/java/org/springframework/data/web/SortDefaultUnitTests.java +++ b/src/test/java/org/springframework/data/web/SortDefaultUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-2016 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. @@ -63,7 +63,7 @@ public void parsesSimpleSortStringCorrectly() { private static void assertSortStringParsedInto(Sort expected, String... source) { SortHandlerMethodArgumentResolver resolver = new SortHandlerMethodArgumentResolver(); - Sort sort = resolver.parseParameterIntoSort(source, ","); + Sort sort = resolver.parseParameterIntoSort(source, ",", null); assertThat(sort, is(expected)); } diff --git a/src/test/java/org/springframework/data/web/SortHandlerMethodArgumentResolverUnitTests.java b/src/test/java/org/springframework/data/web/SortHandlerMethodArgumentResolverUnitTests.java index 5a9fe062d5..0815ab212b 100644 --- a/src/test/java/org/springframework/data/web/SortHandlerMethodArgumentResolverUnitTests.java +++ b/src/test/java/org/springframework/data/web/SortHandlerMethodArgumentResolverUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-2016 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. @@ -41,6 +41,7 @@ * @author Oliver Gierke * @author Thomas Darimont * @author Nick Williams + * @author Kazuki Shimizu */ public class SortHandlerMethodArgumentResolverUnitTests extends SortDefaultUnitTests { @@ -211,6 +212,55 @@ public void doesNotReturnNullWhenAnnotatedWithSortDefault() throws Exception { assertThat(resolveSort(request, getParameterOfMethod("containeredDefault")), is(new Sort("foo", "bar"))); } + /** + * @see DATACMNS-966 + */ + @Test + public void allAllowSortProperty() throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", "id,title"); + + assertThat(resolveSort(request, getParameterOfMethod("annotatedAllowedSortProperties")), + is(new Sort("id", "title"))); + } + + /** + * @see DATACMNS-966 + */ + @Test + public void containsInvalidSortProperty() throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", "id,aaaa"); + + assertThat(resolveSort(request, getParameterOfMethod("annotatedAllowedSortProperties")), is(new Sort("id"))); + } + + /** + * @see DATACMNS-966 + */ + @Test + public void allInvalidSortProperty() throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", "bbbb,aaaa"); + + assertThat(resolveSort(request, getParameterOfMethod("annotatedAllowedSortProperties")), nullValue()); + } + + /** + * @see DATACMNS-966 + */ + @Test + public void ignoreSortProperty() throws Exception { + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter("sort", "id,title"); + + assertThat(resolveSort(request, getParameterOfMethod("annotatedAllowedSortPropertiesIsEmpty")), nullValue()); + } + private static Sort resolveSort(HttpServletRequest request, MethodParameter parameter) throws Exception { SortHandlerMethodArgumentResolver resolver = new SortHandlerMethodArgumentResolver(); @@ -271,5 +321,10 @@ void simpleDefaultWithDirection( void containeredDefault(@SortDefaults(@SortDefault({ "foo", "bar" })) Sort sort); void invalid(@SortDefaults(@SortDefault({ "foo", "bar" })) @SortDefault({ "bar", "foo" }) Sort sort); + + void annotatedAllowedSortProperties(@AllowedSortProperties({ "id", "title" }) Sort sort); + + void annotatedAllowedSortPropertiesIsEmpty(@AllowedSortProperties({}) Sort sort); + } } diff --git a/src/test/java/org/springframework/data/web/config/SampleMixin.java b/src/test/java/org/springframework/data/web/config/SampleMixin.java index 26e8001b35..019e73d74f 100644 --- a/src/test/java/org/springframework/data/web/config/SampleMixin.java +++ b/src/test/java/org/springframework/data/web/config/SampleMixin.java @@ -20,8 +20,7 @@ /** * @author Oliver Gierke */ -@SpringDataWebConfigurationMixin -public class SampleMixin { +public class SampleMixin implements SpringDataJacksonModules { @Bean String sampleBean() { diff --git a/src/test/resources/META-INF/spring.factories b/src/test/resources/META-INF/spring.factories new file mode 100644 index 0000000000..81d1c0f829 --- /dev/null +++ b/src/test/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.data.web.config.SpringDataJacksonModules=org.springframework.data.web.config.SampleMixin diff --git a/template.mf b/template.mf index 2d453512ee..605b08db64 100644 --- a/template.mf +++ b/template.mf @@ -13,6 +13,7 @@ Import-Template: com.google.common.*;version="${guava:[=.=.=,+1.0.0)}";resolution:=optional, com.jayway.jsonpath.*;version="${jsonpath:[=.=.=,+1.0.0]}";resolution:=optional, com.querydsl.*;version="${querydsl:[=.=.=,+1.0.0)}";resolution:=optional, + javaslang.*;version="${javaslang:[=.=.=,+1.0.0)}";resolution:=optional, javax.enterprise.*;version="${cdi:[=.=.=,+1.0.0)}";resolution:=optional, javax.inject.*;version="[1.0.0,2.0.0)";resolution:=optional, javax.servlet.*;version="[2.5.0, 4.0.0)";resolution:=optional,